
图中给出了光栅化之前发生的各种坐标名称和操作的高级概述,下面详细介绍下图中展示的渲染管线中坐标变换的流程。这是从顶点着色器到光栅化之前的关键步骤。让我们逐步分析:
- 预光栅化着色器阶段 (Pre-rasterization Shader Stage) 这个阶段通常在顶点着色器中进行,处理模型的原始顶点数据。例如一个立方体的顶点坐标可能是:
(-1, -1, -1) // 左下后
(1, -1, -1) // 右下后
(-1, 1, -1) // 左上后
// ... 其他顶点
- 裁剪坐标 (Clip Coordinates)
- 通过模型视图投影矩阵 (MVP) 变换后的坐标
- 这是一个四维齐次坐标系统 (x, y, z, w)
- 示例: (-0.5, 0.8, 0.3, 1.0)
- 此时顶点还未进行透视除法
- 透视除法 (Perspective Division)
- 将裁剪坐标的 x、y、z 分量除以 w 分量
- 转换公式: (x/w, y/w, z/w)
- 例如上面的坐标会变成: (-0.5, 0.8, 0.3)
- 这步操作实现了透视效果,使远处物体看起来更小
- 标准化设备坐标 (Normalized Device Coordinates, NDC)
- 经过透视除法后的坐标系统
- x、y、z 分量都被归一化到 [-1, 1] 范围内
- 示例: (0.3, -0.4, 0.5)
- 这是一个设备无关的标准化 3D 空间
- 视口变换 (Viewport Transformation)
- 将 NDC 坐标转换到屏幕空间
- 考虑了视口的位置和大小
- 对深度值进行调整:
- Pz = maxDepth - minDepth (深度范围)
- Qz = minDepth (最小深度值)
- 最终深度 = Pz * Zndc + Qz
- 帧缓冲坐标 (Framebuffer Coordinates)
- 最终的像素坐标系统
- x、y 是整数屏幕坐标
- z 是 [0,1] 范围内的深度值
- 例如在 1920×1080 分辨率下:
- (960, 540, 0.5) 表示屏幕中心
- (0, 0, 0) 表示左上角
- (1919, 1079, 1) 表示右下角
这个转换过程确保了 3D 场景能够正确地投影到 2D 屏幕上,同时保持适当的深度信息用于后续的深度测试。每一步转换都服务于特定的目的: 裁剪坐标便于裁剪操作,NDC 提供了标准化的空间,而最终的帧缓冲坐标直接对应于屏幕像素。
具体公式
让我详细介绍每个阶段的具体变换公式:
- 从模型空间到裁剪空间 (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 |
- 透视除法 - 从裁剪空间到 NDC (Normalized Device Coordinates)
x_ndc = x_clip / w_clip
y_ndc = y_clip / w_clip
z_ndc = z_clip / w_clip
结果范围:所有坐标都在 [-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:远平面距离
- 深度值变换: 如图中所示:
Pz = maxDepth - minDepth
Qz = minDepth
z_final = Pz * z_ndc + Qz
举个具体例子: 假设有一个顶点在模型空间的坐标是 (2, 3, -1)
- 经过 MVP 变换后可能得到:(1.5, 2.0, -0.5, 2.0)
- 透视除法后:(0.75, 1.0, -0.25)
- 假设视口是 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
让我详细解释每个矩阵的构建过程:
- 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 是平移距离
- 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 |
- 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;注意事项:
- 矩阵乘法顺序很重要,必须是 P * V * M
- OpenGL 和 DirectX 的投影矩阵略有不同,主要是在深度值的映射上
- 通常这些矩阵在 CPU 端计算好后传递给着色器
- 现代图形 API 中,往往会分别传递这三个矩阵,而不是直接传递组合后的 MVP 矩阵,这样可以在着色器中更灵活地使用