四、四元数(Quaternion)
1. 为什么需要四元数
旋转矩阵可以做任意旋转,但在做插值和组合时有两个致命问题:
- 万向节死锁(Gimbal Lock):用欧拉角(绕 X/Y/Z 轴依次旋转)时,中间轴的 90° 旋转会使得另外两个轴的旋转方向重合,丢失一个自由度。这是欧拉角本身的数学缺陷,不是引擎 bug。
- 旋转插值不自然:两个旋转矩阵之间做线性插值,结果通常不是同角速度的旋转——物体会在旋转过程中加速或减速。
四元数解决了这两个问题:它用一个四维向量表达三维空间中的任意旋转,不存在死锁,而且天然支持球面线性插值(Slerp),保证均匀角速度。
2. 四元数的几何意义
一个四元数 q = (w, x, y, z) 代表绕单位轴 u = (x, y, z) 旋转角度 θ。其中:
w = cos(θ/2)
x = u.x × sin(θ/2)
y = u.y × sin(θ/2)
z = u.z × sin(θ/2)模长为 1(单位四元数)才代表纯旋转;非单位四元数会引入缩放效果,但在图形学中通常只使用单位四元数。
一个有用的直觉:四元数和轴角表示在数学上是等价的,但四元数在运算(特别是组合两个旋转)上比轴角方便得多——两个四元数相乘即得到组合旋转。
3. 四元数运算
- 乘法:两个四元数相乘 = 复合旋转(先旋 q1 再旋 q2)。注意四元数乘法不满足交换律——先绕 X 再绕 Y 不等于先绕 Y 再绕 X。
- 共轭:反转旋转方向(逆时针变顺时针)。
- 旋转向量:
v' = q · v · q⁻¹(用四元数旋转三维向量)。 - Slerp(球面线性插值):在四维超球面上按最短弧插值两个单位四元数,保证旋转过程中角速度恒定。
4. 引擎中的实用建议
- 存储用四元数,编辑用欧拉角。Unity Inspector 里显示的是欧拉角,但
Transform.rotation存的是四元数。原因就是避免 Gimbal Lock 和插值问题。 - 自己拼旋转矩阵的时候,可以用
Quaternion.AngleAxis逐轴指定,比手动推到四元数方便。 - 不要直接修改
Quaternion的 x/y/z/w 分量——你不是在做一个简单的几何旋转,而是在尝试"把四维向量的各分量当做独立自由度调整",这几乎不会得到你想要的结果。用Quaternion.Euler或AngleAxis来构造。
五、投影矩阵
1. 投影的本质
投影矩阵的作用是把三维世界的物体"拍平"到二维屏幕上。它回答了这个问题:"世界空间中用 XYZ 描述的一个点,在屏幕上应该画在哪个像素位置?"
投影分为正交投影和透视投影两种。
2. 正交投影
正交投影相当于扔掉一个深度方向,直接从正面看过去。远处的物体和近处的物体一样大,没有"近大远小"。在工程图纸、2D 游戏、UI 布局、以及编辑器视口中的正交模式下常用。
2D 游戏的摄像机几乎都是正交的。比如《归义军传说》的摄像机就是正交投影,orthographicSize = 5,一个格子在屏幕上固定显示多少像素,不管它在地图上的"前后"位置。
3. 透视投影
透视投影模拟人眼——远处的物体比近处的小,平行线在远方汇聚于灭点。核心参数是视场角(FOV)和近/远裁剪面。
透视投影矩阵的非线性特征(z 值越大 → 投影到屏幕上的步长越小)导致一个问题:深度精度不均匀。近处几厘米内的物体有很高的 z-buffer 精度,而远处几十米的区间可能只被分给 100 个离散的深度值。这就是所谓的 z-fighting 的根源——远处的两个紧邻物体共享同一个 z-buffer 值,引擎无法判断谁在前面。
常用缓解方案:尽量拉近裁剪面、推远远裁剪面(但不是万能的——logarithmic z-buffer 和 reversed z-buffer 是更彻底的方案,现代 PC 引擎中已基本普及)。
4. 透视除法
投影矩阵左乘一个顶点后,顶点进入裁剪空间(Clip Space)。这个矩阵的最后一行的特殊形式使得 w 分量不再是 1,而是携带了深度信息。之后执行透视除法——将 x、y、z 分量除以 w——得到归一化设备坐标(NDC,Normalized Device Coordinates),范围 [-1, 1]。
NDC 中的 x/y 再经过视口变换映射到屏幕像素坐标。整个流水线:
模型坐标系 → 世界矩阵 → 观察矩阵 → 投影矩阵 → 裁剪空间 → 透视除法(÷w) → NDC → 视口变换 → 屏幕像素六、总结:什么时候自己写 vs 用引擎的
- 旋转矩阵的推导:知道就行,不需要手写——引擎都提供
Matrix4x4.Rotate/Quaternion。 - 四元数 Slerp:了解原理(为什么球面插值比线性插值好),实际直接用
Quaternion.Slerp。 - 投影矩阵:基本不需要自己构造,除非在写自定义的 shadow map 或非标准投影。了解透视除法和 z-fighting 的根源比记住矩阵的 16 个元素有用得多。
- 最值得自己动手写一遍的:MVP 变换链(世界 → 观察 → 投影 → NDC)。写一遍可以彻底搞懂为什么物体的世界坐标最后会变成屏幕上那几个像素——这对调试渲染问题(比如"某个物体明明在摄像机能看到的范围但没渲染出来")帮助巨大。