简介:《四元数详细讲解.pdf》以几何与线性代数视角解析三维旋转,面向Unity开发者、计算机图形学初学者,可帮助规避欧拉角带来的万向节死锁问题。文档从复数与2D旋转的关系切入,逐步过渡到四元数定义、共轭及旋转公式,通过直观推导替代繁复的代数证明,降低理解门槛。资源为单个PDF文件,共439KB,便于离线查阅;目前已有3375人学习下载,是入门四元数与三维旋转的实用参考。内容还结合Unity引擎,演示Quaternion.Euler创建四元数、RotateTowards平滑旋转和Lerp插值等常见操作,使读者不仅掌握数学原理,也能直接应用到实际项目。
1. 四元数到底是什么:从欧拉角万向锁说起
做三维姿态解算的开发者,十有八九被欧拉角坑过:pitch 超过 90° 后,yaw 和 roll 突然失控,物体像被“锁”住一样乱转,这就是万向锁。四元数是一种用四个数字表达三维旋转的数学工具,它不依赖固定的旋转顺序,天然没有万向锁问题,插值也比欧拉角平滑得多。
像《四元数详细讲解.pdf》这类文档,核心就是讲清楚复数如何从平面扩展到三维空间,让你在几何层面理解它,而不是只在代码里机械调库。适合做无人机、机械臂、三维渲染、惯性导航的开发者。读完它,旋转表达、坐标变换、姿态更新这三类高频工程问题都能自己上手解决。
2. 先把四元数看懂:定义、乘法表与几何直觉
很多资料把四元数定义成“一个实部加三个虚部”,这没错,但不够。你要建立的第一直觉是:四元数是复数在三维空间的推广。复数 a + bi 可以用一个实轴和一个虚轴表示平面旋转,四元数则引入三条互相正交的虚轴 i、j、k,从而表达三维旋转。
2.1 从复数到四元数:i、j、k 的定义与性质
复数 a + bi 中,i² = -1,乘以 e^{iθ} 相当于在平面内旋转 θ。四元数的形式是 q = w + xi + yj + zk,其中 w 是实部,x、y、z 是虚部系数。它最关键的定义式是:
i² = j² = k² = ijk = -1
这个式子不是随便写的,它决定了 i、j、k 之间所有乘法结果。由 ijk = -1 可以推出 ij = k、jk = i、ki = j,同时 ji = -k、kj = -i、ik = -j。也就是说,i×j 不等于 j×i,虚部之间的乘法不满足交换律。这正是四元数能表示三维旋转、而普通复数只能表示平面旋转的根本原因。
具体乘出来看更直观。下面这张乘法表建议抄在笔记本上:
| × | 1 | i | j | k |
|---|---|---|---|---|
| 1 | 1 | i | j | k |
| i | i | -1 | k | -j |
| j | j | -k | -1 | i |
| k | k | j | -i | -1 |
读表时注意两点。第一,主对角线全是 -1,说明 i² = j² = k² = -1。第二,i 行 j 列是 k,j 行 i 列是 -k,方向相反。这个非交换性跟三维向量叉积的右手定则完全对应:i 指向 X 轴,j 指向 Y 轴,i×j 沿 Z 轴正向,j×i 沿 Z 轴负向。
有人会问:三维空间为什么不直接搞两个虚数单位?原因是二维复数的旋转本质上是在一个平面里转,三维空间需要三个独立平面(XY、YZ、ZX),三个虚数单位分别对应这三个平面的法向,才能完整表达空间旋转。这套说辞不需要背,但能解释为什么非交换性是躲不掉的。
把乘法表记住之后,建议自己手动算一遍 (0,1,0,0) ⊗ (0,0,1,0),按 i×j = k 的规则,结果应该是 (0,0,0,1)。这个计算只需要三步,但能帮你把抽象定义焊死在脑子里。
2.2 几何直觉:旋转轴与旋转角的表达
单位四元数可以写成极坐标形式:
q = cos(θ/2) + (uₓi + u_y j + u_z k) sin(θ/2)
这里 u = (uₓ, u_y, u_z) 是单位旋转轴向量,θ 是绕该轴的旋转角。注意是 θ/2,不是 θ。为什么是半角?因为后面向量旋转的标准公式是 v' = q ⊗ v ⊗ q*,q 作用一次、q* 再作用一次,两次叠加后实际旋转角度才是 θ。如果构造四元数时直接写 cosθ + u sinθ,最终旋转角度会翻倍。
纯四元数指实部为零的四元数,写成 q = (0, v)。它正好对应三维向量 v。把向量塞进四元数公式做变换,得到的纯四元数的虚部就是旋转后的向量。范数的定义是 ||q|| = √(w² + x² + y² + z²),单位四元数满足 ||q|| = 1。旋转用到的四元数必须是单位四元数,否则变换还会附带缩放效果。
三个最常用的特殊四元数值得直接记住:绕 X 轴转 90° 对应 q = (√2/2, √2/2, 0, 0);绕 Y 轴转 90° 对应 q = (√2/2, 0, √2/2, 0);绕 Z 轴转 90° 对应 q = (√2/2, 0, 0, √2/2)。做验证时先用这三个,错了马上能看出来。
拿到一个四元数时怎么看它代表什么旋转?一个快速办法是计算等效旋转轴 u = (x, y, z) / √(x² + y² + z²),旋转角 θ = 2 · atan2(√(x² + y² + z²), w)。这个转换比欧拉角直观,是我调试四元数时最常用的手段,很多文档会把这组公式藏在正文里,值得专门抄出来。
从自由度看,三维旋转只有 3 个自由度,四元数却有 4 个分量,多的那个约束正是单位范数 ||q|| = 1。这跟欧拉角用 3 个角度、旋转矩阵用 9 个元素相比,四元数是在冗余与数值稳定性之间取了比较好的平衡:它比欧拉角多一个约束,但比矩阵少 5 个冗余量。
到这一步,你应该能在脑子里把四元数对应到一根旋转轴和一圈旋转角。有了这个几何图像,后面所有公式都不需要死记。
3. 四元数核心运算:乘法、共轭、逆与向量旋转公式
理解定义只是第一步,真正干活靠的是四个运算:乘法、共轭、范数、逆。这一章把每个运算的公式、用途、注意事项都过一遍,然后给出三维向量旋转的标准公式以及复合旋转的顺序规则。
3.1 哈密顿积:非交换的乘法规则
两个四元数 q₁ = (w₁, x₁, y₁, z₁) 和 q₂ = (w₂, x₂, y₂, z₂) 的哈密顿积写成 q₁ ⊗ q₂。用向量形式记 q₁ = (w₁, v₁),q₂ = (w₂, v₂),展开式为:
q₁ ⊗ q₂ = (w₁w₂ - v₁·v₂, w₁v₂ + w₂v₁ + v₁×v₂)
实部是 w₁w₂ 减去两个虚部向量的点积,虚部是三项合成:w₁v₂、w₂v₁、v₁×v₂。这个公式比记忆整张四元数乘法表更通用,因为你不必逐个展开 x、y、z,向量点积和叉积可以直接套现成的三维向量代码。
| 符号 | 含义 | 注意 |
|---|---|---|
| w | 实部分量 | 也叫标量部分 |
| x, y, z | 虚部分量 | 对应 X/Y/Z 轴 |
| ⊗ | 哈密顿积 | 不满足交换律 |
| v₁·v₂ | 三维点积 | 结果是一个标量 |
| v₁×v₂ | 三维叉积 | 结果是一个向量 |
实际展开时尤其注意叉积项 v₁×v₂ 的方向。举一个具体数值:q₁ 取 i,q₂ 取 j,即 q₁ = (0,1,0,0),q₂ = (0,0,1,0)。代入公式,w = 0 × 0 - (1,0,0)·(0,1,0) = 0,虚部 = 0×(0,1,0) + 0×(1,0,0) + (1,0,0)×(0,1,0) = (0,0,1),所以结果是 (0,0,0,1),也就是 k,和乘法表一致。一旦把叉积方向写反,这类结果就会多一个负号,旋转也跟着反向。
3.2 共轭、范数、逆:三个易混淆的运算
共轭:q* = (w, -v),也就是虚部全部取反。范数:||q|| = √(w² + x² + y² + z²)。逆:q⁻¹ = q* / ||q||²。单位四元数满足 ||q|| = 1,所以 q⁻¹ = q*。这个结论几乎天天用,但要记得它只在单位四元数下成立。如果你忘记归一化就直接取共轭当逆用,旋转结果会附带缩放。
逆的几何意义是反方向旋转。绕轴 u 转 θ 的四元数,它的逆等价于绕轴 u 转 -θ。在很多场景里,你会用 q 表达“从局部坐标到世界坐标”的旋转,用 q⁻¹ 表达“从世界坐标到局部坐标”的旋转,成对出现。
这三个运算很容易混:共轭是“虚部取反”,范数是“长度”,逆是“反向旋转”。调试时如果发现旋转方向对但角度不对,先查是不是把共轭和逆用混了;如果发现物体在缩放,先查范数是否为 1。
3.3 向量旋转的标准公式:v' = q ⊗ v ⊗ q*
单位四元数 q 对三维向量 v 的旋转公式是:
v' = q ⊗ v ⊗ q*
其中 v 写成纯四元数 (0, v),v' 的虚部就是旋转后的向量。展开流程分成三步:第一步,把向量 v 放进四元数实部为零;第二步,计算 q ⊗ v;第三步,把结果再右乘 q*。
复合旋转的顺序是这块最容易出错的地方。如果先应用 q₁、再应用 q₂,合成四元数是:
q = q₂ ⊗ q₁
注意不是 q₁ ⊗ q₂。把两次旋转代入公式验证:v' = q₂ ⊗ (q₁ ⊗ v ⊗ q₁*) ⊗ q₂* = (q₂ ⊗ q₁) ⊗ v ⊗ (q₂ ⊗ q₁)*。所以先写的 q₂ 反而后乘进去。很多文档里会画一条时间轴说明这点,自己动手推一遍比背口诀可靠。
3.4 为什么矩阵、欧拉角都替代不了它
旋转矩阵是 9 个数、3 个自由度,冗余 6 个;欧拉角是 3 个数,但有万向锁和旋转顺序依赖;四元数是 4 个数加 1 个约束,冗余刚好 1 个。这个折中让它具备三个工程优势:插值平滑、无万向锁、存储和计算开销都小于矩阵。
代价是四元数不够直观,调试时通常要再转回轴角或欧拉角来观察,所以它从来不是替代品,而是团队里配合使用的底层表达。项目里做数学库选型时,我一般会让上层只暴露轴角和欧拉角接口,内部统一用四元数存储,这样外部接口友好、内部数值稳定。
4. 把讲解文档变成工程能力:姿态更新与坐标变换的标准步骤
读四元数文档不能停在“看懂了公式”,要落到三个工程操作:四元数与欧拉角互转、陀螺仪角速度积分更新姿态、四元数与旋转矩阵互转。这三块覆盖了飞行器、机械臂、VR/AR 里最常见的四元数使用场景。
4.1 四元数与欧拉角的互转公式
调试时人眼读不懂四元数,通常要转成欧拉角显示。以下公式基于右手坐标系、ZYX 内旋顺序,单位四元数 q = (w, x, y, z):
| 欧拉角 | 公式 | 注意事项 |
|---|---|---|
| roll | atan2(2(wx + yz), 1 - 2(x² + y²)) | 注意 atan2 的参数顺序 |
| pitch | asin(2(wy - zx)) | 参数先 clamp 到 [-1, 1] |
| yaw | atan2(2(wz + xy), 1 - 2(y² + z²)) | 结果在 [-π, π] |
写代码时最容易翻车的是 asin 那条:当 pitch 接近 ±90° 时,2(wy - zx) 可能因为浮点误差跑到 [-1, 1] 之外,asin 直接返回 NaN。常见做法是先做 clamp:angle = asin(max(-1.0, min(1.0, 2(wy - zx))))。至于为什么 pitch 用 asin 而不是 atan2,是因为 pitch 的取值范围只有 [-π/2, π/2],不需要 atan2 的象限判断。
提示:pitch 接近 ±90° 时,asin 的自变量可能因浮点误差超出 [-1, 1],务必先 clamp 再传参。
反过来的转换也经常用:给定欧拉角转四元数。按 ZYX 顺序,绕 Z 轴 yaw、绕 Y 轴 pitch、绕 X 轴 roll,可以分别构造三个四元数再按顺序相乘。这里再次提醒:如果欧拉角旋转顺序约定跟文档不一致,转换结果会差很多,所以拿到公式第一件事是确认坐标系和旋转顺序,不是直接改参数。
4.2 陀螺仪积分更新姿态:四元数微分方程
姿态解算里最经典的操作是用陀螺仪角速度更新四元数。陀螺仪输出机体坐标系下的角速度 ω = (ωx, ωy, ωz),单位通常用 rad/s。四元数随时间的变化满足微分方程:
q_dot = 0.5 q ⊗ ω
其中 ω 写成纯四元数 (0, ωx, ωy, ωz)。工程上常见做法是用一阶龙格-库塔做离散更新,流程如下:
| 步骤 | 操作 | 公式 | 说明 |
|---|---|---|---|
| 1 | 读角速度并去零偏 | ω = gyro - bias | 零偏在静止时采样取平均 |
| 2 | 构造纯四元数 | ω = (0, ωx, ωy, ωz) | 单位要统一成 rad/s |
| 3 | 计算导数 | q_dot = 0.5 * q ⊗ ω | 四元数乘法,注意顺序 |
| 4 | 一阶积分 | q_new = q + q_dot * dt | dt 是控制周期 |
| 5 | 归一化 | q_new = q_new / | |
| 6 | 更新当前姿态 | q = q_new | 丢弃更新前的值 |
参数上,dt 一般取控制循环周期,比如 100Hz 控制率对应 dt = 0.01s。积分步长太大或数据有毛刺时,一阶方法的误差会明显累积,这时优先检查去零偏和低通滤波,而不是盲目减小 dt,因为减 dt 会同时增加 CPU 负担。
归一化这一步值得多说一句:四元数微分方程在数学上自动保持单位范数,但离散数值积分会产生漂移,一两步看不出来,几千步之后范数可能变成 1.002,旋转结果就会出现肉眼可见的缩放。常见做法是每周期做一次 q_new /= sqrt(w² + x² + y² + z²)。这个除法开销很小,但能把长期漂移挡住。
4.3 四元数与旋转矩阵互转
渲染管线或刚体动力学里经常需要把四元数展开成旋转矩阵。单位四元数 q = (w, x, y, z) 对应的旋转矩阵为:
| 第1列 | 第2列 | 第3列 | |
|---|---|---|---|
| 第1行 | 1 - 2(y²+z²) | 2(xy - wz) | 2(xz + wy) |
| 第2行 | 2(xy + wz) | 1 - 2(x²+z²) | 2(yz - wx) |
| 第3行 | 2(xz - wy) | 2(yz + wx) | 1 - 2(x²+y²) |
这个矩阵用于列向量左乘,也就是 v_world = R · v_body。如果项目用行向量,矩阵需要转置,这是另一个高频翻车点。由旋转矩阵提取四元数时同样有讲究:直接解方程可能遇到接近 0 的 w,导致数值不稳定。常见做法是比较矩阵迹和各个对角元,选择最大的那一项作为主元,再反解出其他三个分量。
从文档到工程,我的建议是把这些公式写成一个独立的转换层,不要散落在业务代码里。上层只管传四元数、欧拉角、矩阵三种表达,底层统一维护约定,这样排查问题时只需要改一处。
5. 四元数使用避坑:五个常见问题与排查方法
公式会背不代表代码能跑对。这里整理五个实际项目里踩过或帮人排查过的四元数问题,按“现象→原因→解决”的方式写,给你一个可以直接对号入座的清单。
5.1 坑一:没做归一化,旋转漂移越积越狠
现象:物体连续旋转一段时间后,姿态缓慢偏移,甚至模型越变越大或越缩越小。
原因:旋转四元数要求单位范数。很多实现把 q_new = q + q_dot * dt 算完就直接用,忽略了归一化。离散积分的误差让范数一点点偏离 1,四元数里的尺度信息被当成旋转参与计算,于是出现缩放。
解决:在每次更新四元数之后强制归一化:q /= ||q||。如果项目里同时存在多个模块都往 q 上叠加旋转,把所有写入入口收拢到一个函数里,统一做归一化,别让调用方自己负责。排查时打印 (w, x, y, z) 的范数曲线,画出来看是不是随时间单调增长,如果是,基本就是这条。
5.2 坑二:乘法顺序写反,旋转方向跟预期相反
现象:绕 X 轴正向旋转 30°,物体实际转了 -30°;或者先绕 X 再绕 Y,结果却是先执行了 Y。
原因:一个是四元数乘法不满足交换律,另一个是把向量旋转公式 v' = q v q* 里的 q 和 q* 位置写反。用 q* v q 替换正确公式,旋转方向就反了。
解决:写一个最小验证:向量 (1, 0, 0) 绕 Z 轴转 90°,期望结果是 (0, 1, 0)。如果得到 (0, -1, 0),说明右乘顺序或虚部符号有问题。这个小用例 5 分钟能跑完,值得作为所有四元数函数的单元测试。
5.3 坑三:欧拉角转换显示时的角度跳变
现象:yaw 在 170° 和 -170° 之间来回跳,日志和可视化曲线出现锯齿;用差值判断旋转幅度时也类似。
原因:atan2 返回结果限定在 [-π, π],所以 180° 附近会从正区间跳到负区间。这不是四元数错了,而是欧拉角的周期表示问题,直接比较两个 yaw 数值没有意义。
解决:姿态比较和差值计算尽量在四元数空间做,用完四元数点积再转成角度;欧拉角只用于人机交互显示。如果必须要连续输出,对 yaw 做“退卷”处理:当相邻差值超过 π 时加减 2π,把角度拉成连续曲线。
5.4 坑四:线性插值导致旋转路径扭曲
现象:两个姿态之间补帧,动画路径不是最短旋转,中间姿态还可能出现速度不均或轻微形变。
原因:直接把四个分量做线性插值(Lerp)后,结果不是单位四元数,中间状态偏离旋转流形,等价于一个带缩放的错误旋转。
解决:需要精确路径时用 slerp,公式和实现见第 6 章;对性能敏感的场合可以用 nlerp——先线性插值再归一化,代价小,大多数游戏场景够用。插值前先检查点积是否为负,为负时把其中一个四元数取反,保证走的是最短路径。
5.5 坑五:半角搞错,旋转角度直接翻倍
现象:设定期望旋转 90°,实际旋转 180°,且旋转轴方向可能同时异常。
原因:构造旋转四元数时,角度参数直接用了 θ,而公式要求的是 θ/2。这个错误在“把轴角转成四元数”的函数里非常普遍,尤其是复制粘贴已有代码时。
解决:构造函数写清楚:q.w = cos(angle * 0.5),q.x = axis.x * sin(angle * 0.5),其他轴同理。更好的做法是把参数名直接命名为 half_angle,从命名上提醒自己。写完先用 90° 绕 X 轴的特例验证:把 (0, 1, 0) 旋转到 (0, 0, 1)。如果得到 (0, -1, 0) 或 (0, 0, -1),优先怀疑这条。
6. 从会用到用对:验证套路、插值技巧与调试习惯
6.1 一个通用的验证套路
不管新接入谁的四元数代码,我都先跑一组固定用例,确认没有原则性错误再上真实数据。推荐下面这组:
| 输入 | 旋转 | 期望输出 |
|---|---|---|
| (1, 0, 0) | 绕 Z 轴 90° | (0, 1, 0) |
| (0, 1, 0) | 绕 Z 轴 90° | (-1, 0, 0) |
| (1, 0, 0) | 绕 Y 轴 180° | (-1, 0, 0) |
| (1, 0, 0) | 先绕 Z 90° 再绕 X 90° | (0, 0, -1) |
第四行复合旋转是重点:它能同时暴露乘法顺序、左右乘约定、坐标系手性三处问题。如果期望输出对不上,先检查旋转顺序,再检查 q 和 q* 的位置,最后确认坐标系定义。
6.2 插值技巧:slerp 与 nlerp
slerp 的标准公式是:
q(t) = [sin((1-t)Ω) q₁ + sin(tΩ) q₂] / sinΩ
其中 Ω 是 q₁ 和 q₂ 之间的夹角,由 cosΩ = q₁ · q₂ 求出。实现时注意三点:先归一化输入;点积为负时把其中一个取反;sinΩ 接近 0 时改用线性插值避免除零。nlerp 则是先 q(t) = (1-t)q₁ + tq₂ 再归一化,速度快但路径略偏,视觉差异在大多数场景下不明显。
我现在的调试习惯是:四元数一律用轴角打印,比如转成“绕 (x, y, z) 旋转 θ°”再输出到日志;每次改动算法先跑上面的表格,再灌真实传感器数据。第一次做姿态解算时,因为半角错误,所有旋转角度都翻了一倍,查了整整一天。从那以后,固定验证用例就成了我接任何旋转代码的第一道关。希望帮到你。
本文还有配套的精品资源,点击获取