坐标系统
在OpenGL中,顶点从3D空间变换到2D屏幕需要经过多个坐标空间的转换。理解这些坐标系统及其变换关系是3D图形编程的核心。
坐标空间概览
顶点数据经过以下坐标空间的变换:
局部空间 ──→ 世界空间 ──→ 观察空间 ──→ 裁剪空间 ──→ 屏幕空间
│ │ │ │ │
模型矩阵 视图矩阵 投影矩阵 透视除法 视口变换
五个坐标空间
1. 局部空间(Local Space)
局部空间是物体自身的坐标系,也称为模型空间或物体空间。
特点:
- 原点通常在物体中心或某个角落
- 坐标值相对于物体自身
- 便于建模和复用
// 一个立方体的局部坐标
float vertices[] = {
-0.5f, -0.5f, -0.5f, // 最小角
0.5f, 0.5f, 0.5f // 最大角
};
2. 世界空间(World Space)
世界空间是场景的全局坐标系,所有物体共享同一个世界空间。
特点:
- 定义物体在场景中的位置
- 通过模型矩阵从局部空间变换而来
- 物体之间的相对位置关系
// 将物体放置在世界坐标(2, 0, 0)处
glm::mat4 model = glm::translate(glm::mat4(1.0f), glm::vec3(2.0f, 0.0f, 0.0f));
3. 观察空间(View Space)
观察空间是以摄像机为原点的坐标系,也称为摄像机空间或眼空间。
特点:
- 摄像机位于原点
- 摄像机看向-Z方向
- 通过视图矩阵从世界空间变换而来
// 创建视图矩阵
glm::mat4 view = glm::lookAt(
glm::vec3(0.0f, 0.0f, 3.0f), // 摄像机位置
glm::vec3(0.0f, 0.0f, 0.0f), // 观察目标
glm::vec3(0.0f, 1.0f, 0.0f) // 上方向
);
4. 裁剪空间(Clip Space)
裁剪空间是投影变换后的坐标空间,用于裁剪不可见的几何体。
特点:
- 坐标范围:x、y、z都在[-w, w]范围内
- 通过投影矩阵从观察空间变换而来
- 透视除法后变为标准化设备坐标
// 透视投影
glm::mat4 projection = glm::perspective(
glm::radians(45.0f), // 视野角度
800.0f / 600.0f, // 宽高比
0.1f, // 近平面
100.0f // 远平面
);
// 正交投影
glm::mat4 ortho = glm::ortho(
-10.0f, 10.0f, // 左右
-10.0f, 10.0f, // 上下
0.1f, 100.0f // 近远
);
5. 屏幕空间(Screen Space)
屏幕空间是最终的像素坐标,通过视口变换从标准化设备坐标转换而来。
特点:
- 坐标单位是像素
- 原点在窗口左下角
- 由OpenGL自动处理
// 设置视口
glViewport(0, 0, 800, 600);
变换矩阵详解
模型矩阵(Model Matrix)
模型矩阵将局部坐标变换到世界坐标,包含平移、旋转、缩放的组合:
glm::mat4 model = glm::mat4(1.0f);
// 先缩放
model = glm::scale(model, glm::vec3(0.5f));
// 再旋转
model = glm::rotate(model, glm::radians(45.0f), glm::vec3(0.0f, 1.0f, 0.0f));
// 最后平移
model = glm::translate(model, glm::vec3(1.0f, 2.0f, 3.0f));
视图矩阵(View Matrix)
视图矩阵将世界坐标变换到观察坐标,本质上是将场景相对于摄像机进行变换:
// glm::lookAt函数创建视图矩阵
glm::mat4 view = glm::lookAt(
cameraPos, // 摄像机位置
cameraTarget, // 观察目标
cameraUp // 上方向向量
);
视图矩阵的数学原理:
其中:
- R = 右向量
- U = 上向量
- D = 方向向量(摄像机看向的方向)
- P = 摄像机位置
投影矩阵(Projection Matrix)
投影矩阵将观察坐标变换到裁剪坐标,分为透视投影和正交投影。
透视投影
透视投影模拟人眼的视觉效果,远处的物体看起来更小:
glm::mat4 projection = glm::perspective(
glm::radians(fov), // 垂直视野角度
aspectRatio, // 宽高比
nearPlane, // 近平面距离
farPlane // 远平面距离
);
透视投影矩阵:
正交投影
正交投影没有透视效果,平行线保持平行:
glm::mat4 ortho = glm::ortho(
left, right, // 左右边界
bottom, top, // 上下边界
near, far // 近远平面
);
正交投影矩阵:
透视除法
透视除法将裁剪坐标转换为标准化设备坐标(NDC):
这个过程由OpenGL自动完成。NDC的范围是[-1, 1]。
视口变换
视口变换将NDC转换为屏幕坐标:
glViewport(x, y, width, height);
变换公式:
在着色器中应用变换
顶点着色器
#version 330 core
layout (location = 0) in vec3 aPos;
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;
void main() {
// 注意顺序:projection * view * model
gl_Position = projection * view * model * vec4(aPos, 1.0);
}
C++代码
// 模型矩阵
glm::mat4 model = glm::mat4(1.0f);
model = glm::rotate(model, glm::radians(-55.0f), glm::vec3(1.0f, 0.0f, 0.0f));
// 视图矩阵
glm::mat4 view = glm::mat4(1.0f);
view = glm::translate(view, glm::vec3(0.0f, 0.0f, -3.0f));
// 投影矩阵
glm::mat4 projection = glm::perspective(glm::radians(45.0f), 800.0f / 600.0f, 0.1f, 100.0f);
// 传递给着色器
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view));
glUniformMatrix4fv(projectionLoc, 1, GL_FALSE, glm::value_ptr(projection));
3D盒子示例
#include <glad/glad.h>
#include <GLFW/glfw3.h>
#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>
#include <iostream>
float vertices[] = {
// 前面
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 1.0f,
0.5f, 0.5f, 0.5f, 1.0f, 1.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
// 后面
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 0.0f,
// 左面
-0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
-0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
-0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
// 右面
0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
// 底面
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
0.5f, -0.5f, -0.5f, 1.0f, 1.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f,
0.5f, -0.5f, 0.5f, 1.0f, 0.0f,
-0.5f, -0.5f, 0.5f, 0.0f, 0.0f,
-0.5f, -0.5f, -0.5f, 0.0f, 1.0f,
// 顶面
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f,
-0.5f, 0.5f, 0.5f, 0.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
0.5f, 0.5f, 0.5f, 1.0f, 0.0f,
0.5f, 0.5f, -0.5f, 1.0f, 1.0f,
-0.5f, 0.5f, -0.5f, 0.0f, 1.0f
};
int main() {
// 初始化代码...
glEnable(GL_DEPTH_TEST);
// 创建VAO、VBO...
// 渲染循环
while (!glfwWindowShouldClose(window)) {
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
// 模型矩阵
glm::mat4 model = glm::mat4(1.0f);
model = glm::rotate(model, (float)glfwGetTime(), glm::vec3(0.5f, 1.0f, 0.0f));
// 视图矩阵
glm::mat4 view = glm::mat4(1.0f);
view = glm::translate(view, glm::vec3(0.0f, 0.0f, -3.0f));
// 投影矩阵
glm::mat4 projection = glm::perspective(glm::radians(45.0f), 800.0f / 600.0f, 0.1f, 100.0f);
// 设置uniform
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view));
glUniformMatrix4fv(projectionLoc, 1, GL_FALSE, glm::value_ptr(projection));
// 绘制
glBindVertexArray(VAO);
glDrawArrays(GL_TRIANGLES, 0, 36);
glfwSwapBuffers(window);
glfwPollEvents();
}
glfwTerminate();
return 0;
}
多个物体
绘制多个物体时,只需修改模型矩阵:
// 绘制第一个物体
glm::mat4 model1 = glm::mat4(1.0f);
model1 = glm::translate(model1, glm::vec3(-1.0f, 0.0f, 0.0f));
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model1));
glDrawArrays(GL_TRIANGLES, 0, 36);
// 绘制第二个物体
glm::mat4 model2 = glm::mat4(1.0f);
model2 = glm::translate(model2, glm::vec3(1.0f, 0.0f, 0.0f));
model2 = glm::scale(model2, glm::vec3(0.5f));
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model2));
glDrawArrays(GL_TRIANGLES, 0, 36);
深度测试
在3D渲染中,深度测试用于确定哪些片段应该被显示:
glEnable(GL_DEPTH_TEST);
// 渲染循环中
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
深度测试函数:
glDepthFunc(GL_LESS); // 默认:如果新片段深度更小则通过
// 其他选项:GL_ALWAYS, GL_NEVER, GL_EQUAL, GL_LEQUAL, GL_GREATER, GL_NOTEQUAL, GL_GEQUAL
小结
坐标系统是3D图形编程的基础:
- 局部空间:物体自身的坐标
- 世界空间:场景中的全局坐标
- 观察空间:以摄像机为原点的坐标
- 裁剪空间:投影变换后的坐标
- 屏幕空间:最终的像素坐标
变换顺序:投影矩阵 × 视图矩阵 × 模型矩阵 × 顶点坐标
下一章我们将学习 摄像机,实现可交互的视角控制。
调试建议
如果物体不显示,检查以下几点:
- 摄像机位置是否正确(是否在物体内部或太远)
- 近/远平面设置是否合理
- 深度测试是否启用
- 模型矩阵是否正确应用