强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

第 6 章:坐标变换

第 6 章:坐标变换

坐标变换是 3D 图形编程的数学基础。通过模型、视图、投影三个矩阵,我们将 3D 世界中的物体投影到 2D 屏幕上。


6.1 坐标空间变换链

从模型的本地坐标到屏幕像素,数据经过 5 个坐标空间:

模型空间 (Local/Object Space)
    │  Model Matrix (模型矩阵)
    ▼
世界空间 (World Space)
    │  View Matrix (视图矩阵)
    ▼
观察空间 (View/Camera Space)
    │  Projection Matrix (投影矩阵)
    ▼
裁剪空间 (Clip Space)
    │  透视除法 (gl_Position.xyz / gl_Position.w)
    ▼
NDC (Normalized Device Coordinates): [-1, 1]³
    │  视口变换
    ▼
屏幕空间 (Screen Space): [0, width] × [0, height]
空间坐标范围说明
模型空间物体定义以物体中心为原点
世界空间全局场景物体在世界中的位置
观察空间以相机为原点Z 轴指向相机后方
裁剪空间[-w, w]超出范围的被裁剪
NDC[-1, 1]透视除法后的结果
屏幕空间像素坐标最终显示位置

6.2 向量基础

6.2.1 向量运算

向量加法:        向量缩放:         点积:              叉积:
  v1 + v2          k * v            v1 · v2            v1 × v2
  ╱╲   ╱╲         ╱╲    ╱╲         |v1||v2|cos θ       垂直于两向量的平面
 ╱  ╲+╱  ╲  →   ╱  ╲ →╱  ╲        返回标量            返回向量 (仅 vec3)
运算公式用途
加法(a.x+b.x, a.y+b.y, ...)位移
缩放(k*a.x, k*a.y, ...)放大/缩小
点积a.x*b.x + a.y*b.y + ...角度、投影
叉积(a.y*b.z-a.z*b.y, ...)法线、朝向
归一化`v /v

6.3 矩阵基础

6.3.1 齐次坐标

3D 变换中的平移无法用 3×3 矩阵表示,因此引入齐次坐标:

3D 点:  (x, y, z)  →  齐次坐标:  (x, y, z, 1)
3D 向量: (x, y, z)  →  齐次坐标:  (x, y, z, 0)

💡 向量的 w=0,因此平移不会影响向量(这是正确的——向量只有方向,没有位置)。

6.3.2 4×4 变换矩阵结构

┌ R R R Tx ┐     R = 旋转 (3×3)
│ R R R Ty │     T = 平移 (3×1)
│ R R R Tz │     [0 0 0 1] = 齐次行
│ 0 0 0  1 ┘

列主序存储 (OpenGL):
[0] [4] [8]  [12]    ← m[0] 是第一列
[1] [5] [9]  [13]
[2] [6] [10] [14]
[3] [7] [11] [15]

6.4 基本变换

6.4.1 平移(Translation)

平移矩阵 T(tx, ty, tz):
┌ 1  0  0  tx ┐     ┌ x + tx ┐
│ 0  1  0  ty │ × │ y │ = │ y + ty │
│ 0  0  1  tz │     │ z │     │ z + tz │
│ 0  0  0   1 ┘     │ 1 ┘     │   1    ┘

6.4.2 缩放(Scale)

缩放矩阵 S(sx, sy, sz):
┌ sx 0  0  0 ┐     ┌ x*sx ┐
│ 0  sy 0  0 │ × │ y │ = │ y*sy │
│ 0  0  sz 0 │     │ z │     │ z*sz │
│ 0  0  0  1 ┘     │ 1 ┘     │  1   ┘

6.4.3 旋转(Rotation)

绕 Z 轴旋转 θ:
┌ cos θ  -sin θ  0  0 ┐
│ sin θ   cos θ  0  0 │
│   0       0    1  0 │
│   0       0    0  1 ┘

绕 X 轴旋转 θ:
┌ 1    0       0    0 ┐
│ 0  cos θ  -sin θ  0 │
│ 0  sin θ   cos θ  0 │
│ 0    0       0    1 ┘

绕 Y 轴旋转 θ:
┌  cos θ  0  sin θ  0 ┐
│    0    1    0    0 │
│ -sin θ  0  cos θ  0 │
│    0    0    0    1 ┘

⚠️ 矩阵乘法顺序:先缩放 → 再旋转 → 最后平移。组合矩阵 M = T × R × S。GLM 中按代码编写的逆序相乘:model = translate * rotate * scale


6.5 GLM 数学库

6.5.1 基本用法

#include <glm/glm.hpp>
#include <glm/gtc/matrix_transform.hpp>
#include <glm/gtc/type_ptr.hpp>

// 向量
glm::vec3 position(1.0f, 2.0f, 3.0f);
glm::vec3 direction = glm::normalize(glm::vec3(1.0f, 0.0f, -1.0f));

// 矩阵变换
glm::mat4 model = glm::mat4(1.0f);  // 单位矩阵
model = glm::translate(model, glm::vec3(0.0f, -1.0f, 0.0f));
model = glm::rotate(model, glm::radians(45.0f), glm::vec3(0.0f, 1.0f, 0.0f));
model = glm::scale(model, glm::vec3(0.5f, 0.5f, 0.5f));

// 传递给着色器
glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));

6.5.2 GLM 常用函数

函数说明
glm::translate(mat4, vec3)平移
glm::rotate(mat4, angle, axis)旋转(弧度)
glm::scale(mat4, vec3)缩放
glm::radians(degrees)角度转弧度
glm::normalize(vec)归一化
glm::cross(a, b)叉积
glm::dot(a, b)点积
glm::length(vec)向量长度
glm::inverse(mat)矩阵求逆
glm::value_ptr(mat)获取数据指针
glm::perspective(fov, aspect, near, far)透视投影
glm::ortho(l, r, b, t, near, far)正交投影
glm::lookAt(eye, center, up)视图矩阵

6.6 模型矩阵(Model Matrix)

模型矩阵将物体从模型空间变换到世界空间

// 创建模型矩阵:先缩放 → 再旋转 → 最后平移
glm::mat4 model = glm::mat4(1.0f);
model = glm::translate(model, worldPosition);           // 世界位置
model = glm::rotate(model, glm::radians(rotation), axis); // 旋转
model = glm::scale(model, glm::vec3(scaleFactor));      // 缩放
同一个立方体,不同模型矩阵:
┌─────────┐   模型矩阵 A    ┌─────────┐
│  立方体  │  ──────────▶   │ 位于(2,0,0)
│ (原点)   │                │ 旋转45°  │
└─────────┘                │ 缩放0.5x │
                           └─────────┘

6.7 视图矩阵(View Matrix)

视图矩阵将世界空间变换到以相机为中心的观察空间:

6.7.1 相机三要素

要素说明示例
位置 (Position)相机在世界空间的位置vec3(0, 2, 5)
目标 (Target)相机看向的点vec3(0, 0, 0)
上方向 (Up)相机的"头顶"方向vec3(0, 1, 0)

6.7.2 lookAt 矩阵

glm::vec3 cameraPos   = glm::vec3(0.0f, 2.0f, 5.0f);
glm::vec3 cameraTarget = glm::vec3(0.0f, 0.0f, 0.0f);
glm::vec3 cameraUp    = glm::vec3(0.0f, 1.0f, 0.0f);

glm::mat4 view = glm::lookAt(cameraPos, cameraTarget, cameraUp);
           Up (0,1,0)
            │
            │  ┌─── 相机位置 (0,2,5)
            ▼  ▼
           [C]──────→ 目标方向
          ╱
         ╱ Right
        ↙

6.7.3 FPS 相机实现

// 第一人称相机
glm::vec3 cameraPos   = glm::vec3(0.0f, 0.0f, 3.0f);
glm::vec3 cameraFront = glm::vec3(0.0f, 0.0f, -1.0f);
glm::vec3 cameraUp    = glm::vec3(0.0f, 1.0f, 0.0f);

float yaw   = -90.0f;  // 偏航角
float pitch =  0.0f;   // 俯仰角
float speed =  2.5f;

// 鼠标回调:更新朝向
void mouse_callback(GLFWwindow* window, double xpos, double ypos) {
    float xoffset = xpos - lastX;
    float yoffset = lastY - ypos;
    lastX = xpos;
    lastY = ypos;

    float sensitivity = 0.1f;
    xoffset *= sensitivity;
    yoffset *= sensitivity;

    yaw   += xoffset;
    pitch += yoffset;
    pitch = glm::clamp(pitch, -89.0f, 89.0f);

    glm::vec3 front;
    front.x = cos(glm::radians(yaw)) * cos(glm::radians(pitch));
    front.y = sin(glm::radians(pitch));
    front.z = sin(glm::radians(yaw)) * cos(glm::radians(pitch));
    cameraFront = glm::normalize(front);
}

// 每帧更新视图矩阵
view = glm::lookAt(cameraPos, cameraPos + cameraFront, cameraUp);

6.8 投影矩阵(Projection Matrix)

6.8.1 透视投影

透视投影模拟人眼视角——近大远小:

float fov    = glm::radians(45.0f);  // 视场角
float aspect = 800.0f / 600.0f;       // 宽高比
float near   = 0.1f;                  // 近裁剪面
float far    = 100.0f;                // 远裁剪面

glm::mat4 projection = glm::perspective(fov, aspect, near, far);
透视投影视锥体 (Frustum):
         far plane
      ┌──────────────┐
     ╱                ╲
    ╱                  ╲
   ╱     视锥体         ╲
  ╱    (Frustum)         ╲
 ╱  ┌──────────────┐      ╲
╱  ╱   near plane    ╲      ╲
│╱       [相机]        ╲│

6.8.2 正交投影

正交投影没有透视效果——平行线保持平行,常用于 2D 渲染和 CAD:

float left   = -1.0f;
float right  =  1.0f;
float bottom = -1.0f;
float top    =  1.0f;
float near   =  0.1f;
float far    = 100.0f;

glm::mat4 projection = glm::ortho(left, right, bottom, top, near, far);
特性透视投影正交投影
远近大小近大远小等大
平行线汇聚于消失点保持平行
用途3D 游戏、场景渲染2D UI、CAD、等距视角

6.8.3 近裁剪面的注意事项

⚠️ near 值不能设为 0:当 near→0 时,深度缓冲的精度急剧下降(Z-fighting)。建议 near = 0.1,far = 100~1000。

nearfar深度精度分布
0.0011000几乎所有精度集中在 0~1 范围
0.1100合理分布
1.01000稍远处精度更高

6.9 MVP 矩阵组合

// 顶点着色器
uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main() {
    gl_Position = projection * view * model * vec4(aPos, 1.0);
}
// C++ 端
glm::mat4 model = glm::mat4(1.0f);
glm::mat4 view = glm::lookAt(...);
glm::mat4 projection = glm::perspective(...);

shader.setMat4("model", model);
shader.setMat4("view", view);
shader.setMat4("projection", projection);

6.9.1 矩阵应用顺序

代码书写顺序:    projection → view → model → vertex
实际变换顺序:    vertex → model → view → projection
                  局部坐标  世界坐标  相机坐标  裁剪坐标

矩阵乘法:  gl_Position = P * V * M * vec4(aPos, 1.0);
               ←── 从右往左应用 ──┘

6.10 法线变换

当模型经过非均匀缩放时,法线向量不能直接用模型矩阵变换,需要使用法线矩阵(Normal Matrix):

glm::mat3 normalMatrix = glm::mat3(glm::transpose(glm::inverse(model)));
// 顶点着色器
uniform mat3 normalMatrix;

void main() {
    vNormal = normalMatrix * aNormal;
}

💡 法线矩阵 = 模型矩阵左上 3×3 的逆转置。均匀缩放时可以直接用模型矩阵的左上 3×3。


6.11 完整示例:旋转的立方体

// 核心渲染代码
while (!glfwWindowShouldClose(window)) {
    float time = glfwGetTime();

    // 模型矩阵:持续旋转
    glm::mat4 model = glm::mat4(1.0f);
    model = glm::rotate(model, time * glm::radians(50.0f), glm::vec3(0.5f, 1.0f, 0.0f));

    // 视图矩阵:固定相机
    glm::mat4 view = glm::lookAt(
        glm::vec3(0.0f, 2.0f, 3.0f),  // 相机位置
        glm::vec3(0.0f, 0.0f, 0.0f),  // 看向原点
        glm::vec3(0.0f, 1.0f, 0.0f)   // 上方向
    );

    // 投影矩阵
    glm::mat4 projection = glm::perspective(
        glm::radians(45.0f), 800.0f / 600.0f, 0.1f, 100.0f
    );

    // 设置 Uniform
    shader.use();
    shader.setMat4("model", model);
    shader.setMat4("view", view);
    shader.setMat4("projection", projection);

    // 绘制
    glBindVertexArray(cubeVAO);
    glDrawArrays(GL_TRIANGLES, 0, 36);
}

6.12 注意事项

⚠️ 矩阵乘法不满足交换律A × B ≠ B × A。在 OpenGL 中,变换顺序是投影 × 视图 × 模型 × 顶点。

⚠️ 弧度 vs 角度:GLM 的 glm::rotate 接受弧度。使用 glm::radians() 转换。

⚠️ 每帧重新创建矩阵:不要缓存 MVP 矩阵。每帧重新计算以响应相机移动和物体动画。

⚠️ far 值不要太大:far = 10000 会严重降低深度精度,导致 Z-fighting。使用对数深度缓冲或减小 far 值。


6.13 业务场景

场景 1:建筑漫游

FPS 相机 + 透视投影实现第一人称建筑漫游。

场景 2:CAD 软件

正交投影 + 轨道相机(Orbit Camera)实现无透视变形的技术制图。

场景 3:小地图

主视图使用透视投影渲染 3D 场景,右下角用正交投影渲染俯视小地图。


6.14 扩展阅读

资源说明
3D Math Primer for Graphics图形学数学入门
GLM 文档GLM 库完整文档
Song Ho 的 OpenGL 教程变换矩阵详解
线性代数的本质3Blue1Brown 视频

本章小结

  • 3D→2D 变换链:模型空间 → 世界空间 → 观察空间 → 裁剪空间 → 屏幕空间
  • 齐次坐标 (x,y,z,w) 使平移可以用矩阵乘法表示
  • MVP 矩阵:gl_Position = Projection × View × Model × aPos
  • GLM 是 OpenGL 最常用的数学库,API 风格与 GLSL 一致
  • 透视投影模拟人眼(3D 游戏),正交投影无透视效果(2D/CAD)
  • 法线需要使用模型矩阵逆转置来正确变换

上一章第 5 章:纹理映射 下一章第 7 章:光照与阴影