图中给出了光栅化之前发生的各种坐标名称和操作的高级概述,下面详细介绍下图中展示的渲染管线中坐标变换的流程。这是从顶点着色器到光栅化之前的关键步骤。让我们逐步分析:

  1. 预光栅化着色器阶段 (Pre-rasterization Shader Stage) 这个阶段通常在顶点着色器中进行,处理模型的原始顶点数据。例如一个立方体的顶点坐标可能是:
(-1, -1, -1)  // 左下后
(1, -1, -1)   // 右下后 
(-1, 1, -1)   // 左上后
// ... 其他顶点
  1. 裁剪坐标 (Clip Coordinates)
  • 通过模型视图投影矩阵 (MVP) 变换后的坐标
  • 这是一个四维齐次坐标系统 (x, y, z, w)
  • 示例: (-0.5, 0.8, 0.3, 1.0)
  • 此时顶点还未进行透视除法
  1. 透视除法 (Perspective Division)
  • 将裁剪坐标的 x、y、z 分量除以 w 分量
  • 转换公式: (x/w, y/w, z/w)
  • 例如上面的坐标会变成: (-0.5, 0.8, 0.3)
  • 这步操作实现了透视效果,使远处物体看起来更小
  1. 标准化设备坐标 (Normalized Device Coordinates, NDC)
  • 经过透视除法后的坐标系统
  • x、y、z 分量都被归一化到 [-1, 1] 范围内
  • 示例: (0.3, -0.4, 0.5)
  • 这是一个设备无关的标准化 3D 空间
  1. 视口变换 (Viewport Transformation)
  • 将 NDC 坐标转换到屏幕空间
  • 考虑了视口的位置和大小
  • 对深度值进行调整:
    • Pz = maxDepth - minDepth (深度范围)
    • Qz = minDepth (最小深度值)
    • 最终深度 = Pz * Zndc + Qz
  1. 帧缓冲坐标 (Framebuffer Coordinates)
  • 最终的像素坐标系统
  • x、y 是整数屏幕坐标
  • z 是 [0,1] 范围内的深度值
  • 例如在 1920×1080 分辨率下:
    • (960, 540, 0.5) 表示屏幕中心
    • (0, 0, 0) 表示左上角
    • (1919, 1079, 1) 表示右下角

这个转换过程确保了 3D 场景能够正确地投影到 2D 屏幕上,同时保持适当的深度信息用于后续的深度测试。每一步转换都服务于特定的目的: 裁剪坐标便于裁剪操作,NDC 提供了标准化的空间,而最终的帧缓冲坐标直接对应于屏幕像素。

具体公式

让我详细介绍每个阶段的具体变换公式:

  1. 从模型空间到裁剪空间 (Clip Space) 这一步通过 MVP 矩阵实现:
| x_clip |   |  MVP00  MVP01  MVP02  MVP03 |   | x_model |
| y_clip | = |  MVP10  MVP11  MVP12  MVP13 | * | y_model |
| z_clip |   |  MVP20  MVP21  MVP22  MVP23 |   | z_model |
| w_clip |   |  MVP30  MVP31  MVP32  MVP33 |   |    1    |
  1. 透视除法 - 从裁剪空间到 NDC (Normalized Device Coordinates)
x_ndc = x_clip / w_clip
y_ndc = y_clip / w_clip
z_ndc = z_clip / w_clip

结果范围:所有坐标都在 [-1, 1] 范围内

  1. 视口变换 - 从 NDC 到屏幕空间
x_screen = (viewport_width / 2) * x_ndc + (viewport_x + viewport_width / 2)
y_screen = -(viewport_height / 2) * y_ndc + (viewport_y + viewport_height / 2)
z_screen = (far - near) / 2 * z_ndc + (near + far) / 2

其中:

  • viewport_width:视口宽度
  • viewport_height:视口高度
  • viewport_x:视口左下角 x 坐标
  • viewport_y:视口左下角 y 坐标
  • near:近平面距离
  • far:远平面距离
  1. 深度值变换: 如图中所示:
Pz = maxDepth - minDepth
Qz = minDepth
z_final = Pz * z_ndc + Qz

举个具体例子: 假设有一个顶点在模型空间的坐标是 (2, 3, -1)

  1. 经过 MVP 变换后可能得到:(1.5, 2.0, -0.5, 2.0)
  2. 透视除法后:(0.75, 1.0, -0.25)
  3. 假设视口是 1920x1080,视口变换后:
x = 1920/2 * 0.75 + 1920/2 = 1680
y = -1080/2 * 1.0 + 1080/2 = 0
z = (1000-0.1)/2 * (-0.25) + (1000+0.1)/2 = 375

这些变换的主要目的是:

  • MVP 变换将场景转换到一个统一的裁剪空间
  • 透视除法实现透视效果
  • 视口变换将标准化坐标映射到实际的屏幕像素
  • 深度变换确保深度测试可以正确进行

所有这些变换都是线性的(除了透视除法),这使得硬件可以高效地实现它们。每个顶点都要经过这些变换,然后才能进行光栅化。

什么是 MVP 矩阵

MVP 矩阵是 Model(模型)、View(视图)和 Projection(投影)三个变换矩阵的组合:MVP = Projection * View * Model

让我详细解释每个矩阵的构建过程:

  1. Model 矩阵(模型矩阵)
  • 用于将模型从局部空间变换到世界空间
  • 包含了缩放 (Scale)、旋转 (Rotation) 和平移 (Translation) 操作
Model = Translation * Rotation * Scale

// 例如,一个包含平移和缩放的模型矩阵:
| Sx  0   0   Tx |
| 0   Sy  0   Ty |
| 0   0   Sz  Tz |
| 0   0   0   1  |

其中:
- Sx,Sy,Sz 是缩放因子
- Tx,Ty,Tz 是平移距离
  1. View 矩阵(视图矩阵)
  • 将世界空间转换到摄像机空间
  • 基于摄像机的位置 (eye)、观察点 (target) 和上方向 (up) 构建
// 构建步骤:
1. 计算摄像机坐标系的三个基向量:
   zaxis = normalize(eye - target)  // 前方向
   xaxis = normalize(cross(up, zaxis))  // 右方向
   yaxis = cross(zaxis, xaxis)  // 上方向

2. 构建视图矩阵:
| xaxis.x    yaxis.x    zaxis.x    0 |
| xaxis.y    yaxis.y    zaxis.y    0 |
| xaxis.z    yaxis.z    zaxis.z    0 |
| -dot(xaxis,eye)  -dot(yaxis,eye)  -dot(zaxis,eye)  1 |
  1. Projection 矩阵(投影矩阵) 可以是透视投影或正交投影:

A. 透视投影矩阵

// 基于视野(fov)、宽高比(aspect)、近平面(near)和远平面(far)
| (1/(aspect*tan(fov/2)))  0   0   0 |
| 0  (1/tan(fov/2))   0   0 |
| 0   0   -(far+near)/(far-near)  -(2*far*near)/(far-near) |
| 0   0   -1   0 |

B. 正交投影矩阵

// 基于视景体的六个面
| 2/(right-left)   0   0   -(right+left)/(right-left) |
| 0   2/(top-bottom)   0   -(top+bottom)/(top-bottom) |
| 0   0   -2/(far-near)   -(far+near)/(far-near) |
| 0   0   0   1 |

实际代码示例(使用 GLM 库):

// 创建模型矩阵
glm::mat4 model = glm::mat4(1.0f);
model = glm::translate(model, glm::vec3(0.0f, 0.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(1.0f, 1.0f, 1.0f));
 
// 创建视图矩阵
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)   // 上方向
);
 
// 创建透视投影矩阵
glm::mat4 projection = glm::perspective(
    glm::radians(45.0f),  // 视野角度
    800.0f/600.0f,        // 宽高比
    0.1f,                 // 近平面
    100.0f                // 远平面
);
 
// 计算MVP矩阵
glm::mat4 mvp = projection * view * model;

注意事项:

  1. 矩阵乘法顺序很重要,必须是 P * V * M
  2. OpenGL 和 DirectX 的投影矩阵略有不同,主要是在深度值的映射上
  3. 通常这些矩阵在 CPU 端计算好后传递给着色器
  4. 现代图形 API 中,往往会分别传递这三个矩阵,而不是直接传递组合后的 MVP 矩阵,这样可以在着色器中更灵活地使用