TT Lab
开始
学习 学习路径 课程

3D 数学与软件光栅化器

一个点抵达屏幕之前

在 TT Lab 中继续学习

一句话总结

顶点从模型坐标出发,依次经过世界坐标、视图坐标和裁剪坐标,再除以 w,才变成屏幕坐标。透视感就来自这一次除法。

为什么需要它

创建物体的人会把它放在原点附近建模。布置场景的人会把它放在世界中的某个位置。手持相机的人以自己的位置和朝向观察世界。负责绘制到屏幕上的一方只认识像素坐标。这四方的基准各不相同,所以需要逐级切换坐标系的步骤。

关键在于,每一步都可以用一个矩阵表示。把模型矩阵、视图矩阵和投影矩阵预先相乘,合成一个 MVP,每个顶点只需做一次 4x4 乘法即可。顶点着色器所做的事,实际上就是这一行。

工作原理

视图矩阵把相机移到原点,并让相机的观察方向对准 -z 轴。可以理解为不是移动相机,而是把世界反向移动。构造方法是求出相机的三个轴,再用它们组成行向量。

f = normalize(target - eye)      # 보는 방향
s = normalize(cross(f, up))      # 오른쪽
u = cross(s, f)                  # 진짜 위쪽 (up 은 대충 준 값이라 다시 구한다)

view = |  s.x   s.y   s.z   -dot(s, eye) |
       |  u.x   u.y   u.z   -dot(u, eye) |
       | -f.x  -f.y  -f.z    dot(f, eye) |
       |   0     0     0          1      |

第三行带负号,是因为在右手坐标系中约定相机朝 -z 方向观察。如果违背这个约定,物体会落在相机后方,什么也看不到。

投影矩阵把视锥体(frustum)内的空间压缩进一个立方体。透视投影矩阵的最后两行是完成这件事的关键。

t = 1 / tan(fov/2)

proj = | t/aspect  0        0                 0             |
       | 0         t        0                 0             |
       | 0         0   -(f+n)/(f-n)   -2*f*n/(f-n)          |
       | 0         0       -1                 0             |

注意最后一行是 (0, 0, -1, 0)。正是这一行让结果的 w 分量变成 -z,也就是到相机的距离。接着把 x、y、z 都除以这个 w,越远的点就越被拉向屏幕中央。这就是透视除法(perspective divide),透视感全部来自这里。仅靠矩阵乘法产生不了透视——矩阵无法表示除法,所以采用的迂回做法是:先把距离放进 w,之后再做除法。

除法之后的坐标称为归一化设备坐标(NDC),范围是 -1 到 1。最后再对它施加视口变换,转换成像素坐标。翻转 y 也是在这一步。

示意图:顶点从模型坐标依次经过世界坐标、视图坐标和裁剪坐标,先裁掉视锥体之外的部分,再除以 w 得到 NDC,最后经视口变换成为屏幕像素

sx = (ndc_x * 0.5 + 0.5) * 화면폭
sy = (1 - (ndc_y * 0.5 + 0.5)) * 화면높이

正交投影不会改动 w。它的最后一行是 (0, 0, 0, 1),所以除法等同于恒等操作,物体大小与距离无关。工程图纸和 2D UI 使用的就是它。

必须在除法之前裁剪的原因也出在这里。w 为 0 时除法会出错,w 为负则说明该点位于相机后方。如果不加处理地直接相除,符号会翻转,这个点会以一个看似正常的坐标出现在屏幕的另一侧。跨在相机前后的三角形在屏幕上被拉得很长,就是这个缺陷造成的。因此,流水线在除法之前,于裁剪坐标状态下裁掉视锥体之外的部分。裁剪坐标这个名字就由此而来。

视口变换中还包括深度。这一步把 NDC 的 z 映射到深度缓冲使用的范围:OpenGL 默认是 -1..1,Direct3D 和 Vulkan 则是 0..1。由于这个差异,投影矩阵的第三行因 API 而异,如果直接照搬别人的代码,深度判断会被颠倒,或者有一半被裁掉。

在现场相遇的样子

如果把 near 平面设得太小(例如 0.001),深度缓冲的精度就会集中在近处,远处的物体之间会出现 z-fighting。原因是深度值与 1/z 成比例分布,这一点从上面矩阵的第三行就能直接读出来。实际工作中的解决办法不是缩小 far,而是增大 near。

另一个常见问题是宽高比。窗口大小变了,却没有更新投影矩阵的 aspect,物体就会被拉伸。这就是需要在窗口大小变化的回调中重新构造投影矩阵的原因。

下一项实验要做什么

亲手构造视图矩阵以及透视和正交投影矩阵,经过透视除法和视口变换,把立方体的十二条棱画到屏幕上。分别用透视和正交方式绘制同一个立方体并比较,同时改变视场角,用数字和图像一起确认物体在屏幕上占据的范围有多大。