3、基础数学知识(下):矩阵运算、欧拉角与四元数、刚体变换

好,咱们接着聊。上一节我们把向量和点积、叉积这些基本功过了一遍。这一节要啃的,是真正让碰撞检测跑起来的核心数学工具——矩阵、欧拉角、四元数,以及它们怎么组合成刚体变换。

说实话,我刚入行那会儿,觉得矩阵就是用来算坐标的,欧拉角就是转几个角度,四元数?那玩意儿看着就头疼。直到我在一个物理沙盒项目里,被万向锁坑得死去活来……嗯,从那以后,我才老老实实把这几样东西的脾气摸透了。

3.1 矩阵运算:坐标变换的骨架

矩阵在碰撞检测里,说白了就是描述“怎么把一个点或向量,从一个地方弄到另一个地方”。最常见的,就是旋转矩阵和平移矩阵。

3.1.1 旋转矩阵

二维旋转大家可能都熟,绕原点转个θ角:

// 二维旋转矩阵
R(θ) = | cosθ  -sinθ |
       | sinθ   cosθ |

三维里就复杂一点,得分别绕X、Y、Z轴转。我习惯把它们记成“哪个轴不动,哪一行/列就特殊”:

旋转轴 旋转矩阵(右手系,逆时针为正)
绕X轴 | 1   0   0 |
| 0   cosθ   -sinθ |
| 0   sinθ   cosθ |
绕Y轴 | cosθ   0   sinθ |
| 0   1   0 |
| -sinθ   0   cosθ |
绕Z轴 | cosθ   -sinθ   0 |
| sinθ   cosθ   0 |
| 0   0   1 |
我的小习惯: 写代码时,我从来不会手算这些矩阵。直接用数学库(比如 glm、Eigen、Unity 的 Matrix4x4)的静态方法生成。但你必须能看懂它们,尤其是在调试的时候——矩阵里一个符号错了,碰撞检测就全歪了。

3.1.2 平移矩阵与齐次坐标

旋转是线性变换,但平移不是。为了把平移也塞进矩阵里,我们引入了齐次坐标。说白了,就是给二维点加一个1变成三维,给三维点加一个1变成四维。

// 齐次坐标下的平移矩阵(4x4)
T(tx, ty, tz) = | 1   0   0   tx |
                | 0   1   0   ty |
                | 0   0   1   tz |
                | 0   0   0   1  |

你想想看,一个点 P = (x, y, z, 1),左乘这个矩阵,结果就是 (x+tx, y+ty, z+tz, 1)。干净利落。

我曾经踩过的坑: 矩阵乘法不满足交换律!先旋转再平移,和先平移再旋转,结果完全不同。在组合变换矩阵时,一定要想清楚顺序:通常是 先缩放 → 再旋转 → 最后平移。

3.2 欧拉角:直观但危险

欧拉角用三个角度(pitch、yaw、roll)来描述旋转。直观,好理解,调参数时很方便。

  • Pitch(俯仰):绕X轴转,点头摇头。
  • Yaw(偏航):绕Y轴转,左右看。
  • Roll(滚转):绕Z轴转,歪头。

但欧拉角有个臭名昭著的问题——万向锁。当pitch达到±90°时,yaw和roll的旋转轴会重合,丢失一个自由度。我在一个飞行模拟器项目里遇到过,飞机翻转到90°俯仰时,控制直接乱套了。

结论: 欧拉角适合做用户输入和调试显示,但绝不适合做插值和内部物理计算。内部计算,请交给四元数。

3.3 四元数:旋转的正确姿势

四元数听起来玄乎,其实就是一个实部加三个虚部:q = w + xi + yj + zk。在碰撞检测里,我们只用单位四元数(模长为1)来表示旋转。

3.3.1 为什么用四元数?

  • 无万向锁:这是最大的优点。
  • 插值平滑:用球面线性插值(Slerp)可以在两个旋转之间平滑过渡。
  • 组合高效:两个四元数相乘就能合成旋转,比矩阵乘法快。

3.3.2 核心操作

我个人觉得,记住下面几个操作就够了:

// 1. 用轴角构造四元数
// 绕单位轴 (ax, ay, az) 旋转 θ 弧度
q.w = cos(θ/2)
q.x = ax * sin(θ/2)
q.y = ay * sin(θ/2)
q.z = az * sin(θ/2)

// 2. 用四元数旋转向量 v
// 公式:v' = q * v * q_conjugate
// 注意:v 要当成纯四元数 (0, v.x, v.y, v.z)

// 3. 四元数乘法(组合旋转)
q_result = q1 * q2  // 先应用 q2,再应用 q1

// 4. 球面线性插值(Slerp)
// 数学库通常提供现成函数,比如 glm::slerp()
避坑指南: 四元数乘法顺序和矩阵乘法顺序是对应的。如果你习惯用行向量左乘矩阵,那四元数乘法顺序就是 q_result = q1 * q2(先q2后q1)。别搞反了,我曾经因为这个调了一下午的bug。

3.4 刚体变换:把一切串起来

一个刚体在空间中的位置和朝向,用一个变换矩阵或一对 (位置, 旋转) 来描述。这就是刚体变换。

3.4.1 变换矩阵形式

一个4x4的刚体变换矩阵长这样:

M = | R   t |
    | 0   1 |

其中:
R 是 3x3 旋转矩阵(正交,行列式为+1)
t 是 3x1 平移向量
最后一行是 [0, 0, 0, 1]

这个矩阵可以把一个点从局部坐标系变换到世界坐标系。反过来,求逆矩阵就能从世界坐标回到局部坐标。对于刚体变换,逆矩阵很好求:

M_inv = | R^T   -R^T * t |
        | 0     1       |

因为旋转矩阵的逆就是它的转置,省去了求逆的麻烦。

3.4.2 在碰撞检测中的应用

碰撞检测里,刚体变换无处不在:

  • 将碰撞体顶点从局部坐标变换到世界坐标:用于空间划分和粗略检测。
  • 将另一个物体的顶点变换到当前物体的局部坐标:用于精确的分离轴检测。
  • 更新刚体的位置和旋转:每帧根据速度和角速度积分。
核心思路: 尽量在局部坐标系下做碰撞检测的几何计算(因为碰撞体形状是固定的),只在需要比较位置时才变换到世界坐标系。这样可以减少计算量,也更容易维护。

3.5 知识体系总览

下面这张图,把这一节的核心逻辑串起来了。你可以把它当成一个思维导图来记:

基础数学知识体系:矩阵、欧拉角、四元数与刚体变换 刚体变换 矩阵运算 旋转矩阵 (3x3) 平移矩阵 + 齐次坐标 (4x4) 组合变换:缩放→旋转→平移 欧拉角 Pitch / Yaw / Roll ⚠ 万向锁问题 仅用于输入/显示 四元数 单位四元数 (w, x, y, z) 轴角构造 / 向量旋转 Slerp 平滑插值 碰撞检测中的应用 局部坐标 ↔ 世界坐标 变换

从这张图能看出来,矩阵是底层工具,欧拉角和四元数是两种不同的旋转表达方式,而刚体变换是它们的最终应用。我个人建议,你在写物理引擎时,内部统一用四元数+向量表示刚体状态,只在需要显示或序列化时转成矩阵或欧拉角。

再强调一次: 不要在生产代码里用欧拉角做插值或积分。我见过一个物理引擎因为用了欧拉角做角速度积分,结果刚体在高转速下直接飞出了宇宙。换成四元数后,一切正常。