LabHub
学习 学习路径 课程

3D 数学与软件光栅化器

明明是三维,为什么用 4x4 矩阵

在 LabHub 中继续学习

一句话总结

平移是加法,不是乘法,因此无法放入 3x3 矩阵。把坐标扩展成 (x, y, z, 1),平移也能表示为一次乘法,所以图形变换矩阵是 4x4。

概念图: 表示为一次矩阵乘法 · 齐次坐标(homogeneous coordinates) · 乘法顺序会改变结果。 · 右侧矩阵先应用。

为什么需要它

把一个物体放到屏幕上,需要连续缩放、旋转、移动到指定位置、转换到相机坐标,再进行投影。若把它们写成一连串函数调用,每个顶点要执行五种计算;十万个顶点就是五十万次。

如果这些计算都能表示为一次矩阵乘法,情况就不同了。矩阵乘法满足结合律,可以预先把五个矩阵合成一个,每个顶点只需做一次 4x4 乘法。这就是 GPU 只通过 uniform 接收一个 MVP 矩阵的原因。问题在于平移不是乘法:3x3 矩阵可以旋转与缩放,却无法表达平移。

如何工作

解决办法是增加一个维度。把点 (x, y, z) 写成 (x, y, z, 1) 并乘以 4x4 矩阵,最后一列会乘以 1 再加到结果中。把位移放入这一列,平移就进入了乘法。

| 1 0 0 tx |   | x |   | x + tx |
| 0 1 0 ty | * | y | = | y + ty |
| 0 0 1 tz |   | z |   | z + tz |
| 0 0 0  1 |   | 1 |   |   1    |

这种表示称为齐次坐标(homogeneous coordinates)。第四个分量 w 并非占位符:w 为 1 表示点,为 0 表示方向。方向不应受平移影响,北方无论移动到哪里仍是北方;设为 w=0 后,平移列乘以 0 会被自动忽略。因此,法线向量与光线方向会表示为 (x, y, z, 0)。

乘法顺序会改变结果。 矩阵乘法不满足交换律。T * R 表示“先旋转再平移”,R * T 表示“先平移再旋转”。采用列向量右乘的约定时,右侧矩阵先应用。 对点 (1,0,0) 先绕 z 轴旋转 90 度再沿 x 轴移动 2,前者得到 (2,1,0),后者得到 (0,3,0)。物体是在原地自转还是绕轨道公转,就取决于这里。

旋转矩阵还涉及手性。在右手坐标系中,绕 z 轴旋转会把 x 轴转向 y 轴。

rotate_z(t) = | cos t  -sin t  0  0 |
              | sin t   cos t  0  0 |
              |   0       0    1  0 |
              |   0       0    0  1 |

只写错一个符号,物体就会反向旋转,而且不会产生任何错误消息。

还存在行优先与列优先的约定差异。本实验把矩阵保存为“四个长度为 4 的列表”,点放在右侧相乘。相反,DirectX 系列文档与代码常把点放在左侧作为行向量,此时同一变换的矩阵看起来是转置的,两种约定中的乘法顺序也相反。移植他人代码后结果异常时,应首先怀疑这一点。内存中的存储顺序又是另一个维度,因此实际可以组合出四种约定。

旋转也不只能用矩阵表示。四元数用四个值保存旋转,而且比矩阵更适合在两个旋转之间平滑插值,因为逐元素混合两个矩阵,结果通常不再是旋转矩阵。但传给 GPU 时最终仍要转换为矩阵,因此常见结构是用四元数保存状态,绘制时再构造矩阵。

在实际项目中

在着色器中变换法线时,直接使用模型矩阵会导致照明错误。加入非均匀缩放(例如只把 x 缩小一半)后,原本垂直于表面的向量不再垂直。正确做法是使用模型矩阵逆矩阵的转置;只有亲手操作过矩阵,才容易真正理解这一事实。

另一个问题是浮点误差累积。每帧都把小旋转乘到当前旋转矩阵上,几百帧后正交性会遭破坏,物体逐渐变形。因此,引擎通常用角度或四元数保存状态,并每帧重新构造矩阵,而不把矩阵本身当状态。

还经常忽略逆矩阵的成本。通用 4x4 逆矩阵计算昂贵,但图形学中的大多数变换只有旋转与平移,可以更便宜地求逆。旋转部分是正交矩阵,转置就是逆矩阵;平移取反后再应用逆旋转即可。构造相机矩阵时正是利用了这一性质:前面看到的视图矩阵把相机三条轴作为行,最后一列放入负点积。

最后还要培养选择向量运算的直觉。点积用于角度与投影,叉积用于垂直方向与面积。想知道两个向量是否朝向同侧,看点积符号;想知道朝哪一侧旋转,看叉积符号。这两句话能解释相当一部分图形代码。

下一实验要做什么

亲手实现三维向量运算与 4x4 矩阵乘法,组装平移、缩放与旋转矩阵。通过数字比较乘法顺序变化带来的结果,再把三个变换后的四边形画到同一张 PNG 上。最后用叉积求三角形法线与面积。