跳到主要内容

坐标系统

在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 // 上方向向量
);

视图矩阵的数学原理:

View=[RxRyRzRPUxUyUzUPDxDyDzDP0001]View = \begin{bmatrix} R_x & R_y & R_z & -\vec{R} \cdot \vec{P} \\ U_x & U_y & U_z & -\vec{U} \cdot \vec{P} \\ D_x & D_y & D_z & -\vec{D} \cdot \vec{P} \\ 0 & 0 & 0 & 1 \end{bmatrix}

其中:

  • R = 右向量
  • U = 上向量
  • D = 方向向量(摄像机看向的方向)
  • P = 摄像机位置

投影矩阵(Projection Matrix)

投影矩阵将观察坐标变换到裁剪坐标,分为透视投影和正交投影。

透视投影

透视投影模拟人眼的视觉效果,远处的物体看起来更小:

glm::mat4 projection = glm::perspective(
glm::radians(fov), // 垂直视野角度
aspectRatio, // 宽高比
nearPlane, // 近平面距离
farPlane // 远平面距离
);

透视投影矩阵:

Perspective=[1aspecttan(fov/2)00001tan(fov/2)0000far+nearfarnear2farnearfarnear0010]Perspective = \begin{bmatrix} \frac{1}{aspect \cdot \tan(fov/2)} & 0 & 0 & 0 \\ 0 & \frac{1}{\tan(fov/2)} & 0 & 0 \\ 0 & 0 & -\frac{far+near}{far-near} & -\frac{2 \cdot far \cdot near}{far-near} \\ 0 & 0 & -1 & 0 \end{bmatrix}

正交投影

正交投影没有透视效果,平行线保持平行:

glm::mat4 ortho = glm::ortho(
left, right, // 左右边界
bottom, top, // 上下边界
near, far // 近远平面
);

正交投影矩阵:

Ortho=[2rightleft00right+leftrightleft02topbottom0top+bottomtopbottom002farnearfar+nearfarnear0001]Ortho = \begin{bmatrix} \frac{2}{right-left} & 0 & 0 & -\frac{right+left}{right-left} \\ 0 & \frac{2}{top-bottom} & 0 & -\frac{top+bottom}{top-bottom} \\ 0 & 0 & -\frac{2}{far-near} & -\frac{far+near}{far-near} \\ 0 & 0 & 0 & 1 \end{bmatrix}

透视除法

透视除法将裁剪坐标转换为标准化设备坐标(NDC):

[xndcyndczndc]=[xclip/wyclip/wzclip/w]\begin{bmatrix} x_{ndc} \\ y_{ndc} \\ z_{ndc} \end{bmatrix} = \begin{bmatrix} x_{clip}/w \\ y_{clip}/w \\ z_{clip}/w \end{bmatrix}

这个过程由OpenGL自动完成。NDC的范围是[-1, 1]。

视口变换

视口变换将NDC转换为屏幕坐标:

glViewport(x, y, width, height);

变换公式:

xscreen=xndc+12width+xx_{screen} = \frac{x_{ndc} + 1}{2} \cdot width + x yscreen=yndc+12height+yy_{screen} = \frac{y_{ndc} + 1}{2} \cdot height + y

在着色器中应用变换

顶点着色器

#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图形编程的基础:

  • 局部空间:物体自身的坐标
  • 世界空间:场景中的全局坐标
  • 观察空间:以摄像机为原点的坐标
  • 裁剪空间:投影变换后的坐标
  • 屏幕空间:最终的像素坐标

变换顺序:投影矩阵 × 视图矩阵 × 模型矩阵 × 顶点坐标

下一章我们将学习 摄像机,实现可交互的视角控制。

调试建议

如果物体不显示,检查以下几点:

  1. 摄像机位置是否正确(是否在物体内部或太远)
  2. 近/远平面设置是否合理
  3. 深度测试是否启用
  4. 模型矩阵是否正确应用