旋转表示与插值
全库目录221
本文目录15
旋转问题通常有三个层次:怎么给人编辑、怎么在程序中保存、怎么在两个姿态之间平滑变化。欧拉角、矩阵和四元数各自适合不同层次。
旋转表示
欧拉角
欧拉角把旋转拆成绕三个轴的连续旋转。它容易在 Inspector 中编辑,但结果依赖旋转顺序。例如 XYZ 和 ZYX 并不等价。
它的主要问题:
- 同一姿态可能有多组角度表示;
- 角度跨过 180/360 度时,数值插值可能绕远路;
- 某些姿态会让两个旋转轴重合,失去一个自由度,这就是万向节锁。
欧拉角适合作为编辑器输入,不适合作为所有运行时旋转计算的内部表示。
万向节锁的本质
欧拉角是对旋转群的三角度参数化。全局的三个独立角度坐标无法连续且唯一地覆盖全部旋转而避免坐标奇点。局部三参数表示可以在一块有限范围内正常使用,超出范围时需要换坐标图或其他表示。
以常见的 Y-X-Z 顺序为例:当绕 X 的俯仰角接近正负 90 度时,Y 轴与 Z 轴的旋转效果重合,系统失去一个自由度。表现是飞行器机头竖直时,滚转与偏航变成同一件事。
单位四元数以四个分量和单位长度约束表示旋转,映射到旋转群时是二重覆盖:q 与 -q 表示同一旋转。它避免欧拉角式的万向节锁,仍需处理符号一致性、归一化和插值路径。
在前述Y-X-Z组合中,令R=Ry(yaw) Rx(pitch) Rz(roll)并采用列向量。pitch=90°时可以把矩阵写成
[ cos(yaw-roll), sin(yaw-roll), 0 ]
[ 0, 0, -1 ]
[-sin(yaw-roll), cos(yaw-roll), 0 ]姿态只剩yaw−roll一项,给yaw和roll同时增加10°不会改变最终矩阵。两个编辑参数仍然存在,却产生同一个旋转效果,这就是该点自由度退化的具体含义。改用四元数保存姿态后,若每帧仍先转回这套欧拉角再逐项编辑,仍会重新遇到同一参数化奇点。
旋转矩阵
行列式为+1的3×3正交矩阵表示纯旋转;行列式为−1的正交矩阵还包含反射。它能直接变换向量,也方便与缩放、平移组合成 4x4 矩阵。
纯旋转矩阵的三个基向量互相垂直且长度为 1,逆矩阵等于转置矩阵。不断做浮点运算后,这些基向量可能漂移,需要重新正交化。
正交性漂移
每次矩阵相乘都引入舍入误差。正交性误差随运算次数累积,误差累积后基向量可能不再垂直,物体会被轻微剪切或缩放。出现速度取决于精度、旋转步幅和累积次数,检查正交性比规定一个运算次数更可靠。
重新正交化用 Gram-Schmidt:固定一个基向量,把第二个减去它在第一个方向上的投影并归一化,第三个取前两个的叉积:
Vector3 x = Normalize(m.Column0);
Vector3 y = Normalize(m.Column1 - x * Dot(x, m.Column1));
Vector3 z = Cross(x, y);选择固定哪一列会影响误差分配:通常固定语义上最重要的轴(例如朝向轴)。
四元数
单位四元数可以表示三维旋转:
是单位旋转轴, 是旋转角。这里的四个分量不是四维位置,也不是欧拉角。
四元数的优点:
- 只用四个数保存旋转;
- 组合旋转比较稳定;
- 不会像欧拉角一样出现万向节锁;
- 适合做旋转插值。
和 表示同一个空间旋转。这会影响插值路径:如果两个四元数点积小于 0,通常会翻转其中一个,使插值走单位球面的短弧。
[!warning] 顺序问题 四元数乘法不满足交换律。
qA * qB的应用顺序取决于引擎约定,不要凭记忆跨引擎复制。
角速度积分
物理与动画系统常需要按角速度推进姿态。角速度 (方向为转轴、模长为角速度)对四元数的更新是:
这里统一用 (xyz,w) 排列,角速度位于世界空间,增量从左侧乘入;局部角速度则从右侧乘入。零角速度直接保留姿态。小步长下也能使用一阶近似 ,再归一化。若旋转公式依赖单位长度,累积误差就会使输出偏离纯旋转。
状态插值
Lerp
线性插值:
当 在 0 到 1 之间时,结果位于两端之间。超出范围就是外插。
Lerp 很适合:
- 数值、颜色和位置;
- UV、权重和材质参数;
- 已经在同一线性空间中的数据。
每帧执行 current = lerp(current, target, speed * dt) 不是固定时长动画。它是离散的一阶逼近,越接近目标越慢,speed*dt较大时还会越过目标。若目标在当前步内固定,希望不同时间步得到相同衰减,可以使用t=1-exp(-k*dt),其中k单位为每秒。固定时长应该保存起点和累计时间,再用规范化进度计算。
方向和旋转插值
Nlerp
对两个四元数做 Lerp 后归一化。它便宜、稳定,但角速度不完全恒定。小角度或大量动画混合中经常足够。
Slerp
球面线性插值沿单位球面的弧线前进。设两个单位四元数夹角为 :
它能保持较稳定的角速度。 很小时, 接近零,工程实现通常退化到 Nlerp。
插值路径与角速度
Nlerp 走的是单位球面的弦再归一化,Slerp 走大圆弧线。Nlerp 的角速度在区间内变化,Slerp 在固定端点和线性时间参数下保持恒定角速度。差异是否可见取决于夹角、播放速度和观察部位。大量骨骼混合常选择 Nlerp 节省计算;匀速转向或大角度镜头旋转则更适合 Slerp。
多关键帧可以用Squad等球面样条控制接缝切向。角速度连续依赖符号连续、控制四元数和关键帧时间参数的构造;只把相邻区间各自做Slerp,接缝两侧的旋转轴或速度仍可能突变。
曲线插值
Smoothstep
常见三次形式:
它让区间两端的一阶导数为零,适合做平滑阈值和简单缓动。它改变的是插值参数,不改变 Lerp 本身。
Bezier
Bezier 曲线由控制点决定。三次 Bezier 常用于路径和动画曲线:
控制点不一定在曲线上。参数 等速变化也不保证物体沿曲线匀速移动;匀速需要弧长参数化或近似查表。
Hermite
Hermite 曲线由端点和端点切线控制。动画编辑器中的关键帧切线常可以理解成这种形式。
动画混合中的注意点
- 平移和缩放通常可以线性插值。
- 旋转应使用 Nlerp、Slerp 或专门的四元数混合。
- 直接线性混合旋转矩阵会破坏正交性并产生缩放或剪切。
- 多骨骼姿态混合还要明确局部空间、模型空间和加法动画的基准姿态。
最短弧四元数插值
输入 a、b 是归一化旋转,t 位于 。四元数 与 表示同一个旋转,但直接插值可能绕长弧:
Quaternion SlerpShortest(Quaternion a, Quaternion b, float t)
{
float cosine = Dot(a, b);
if (cosine < 0.0f) { b = -b; cosine = -cosine; }
if (cosine > 0.9995f) return Normalize(Lerp(a, b, t));
float angle = Acos(Clamp(cosine, -1.0f, 1.0f));
return (Sin((1.0f - t) * angle) * a
+ Sin(t * angle) * b) / Sin(angle);
}先根据点积统一符号,再在夹角很小时退化为归一化 Lerp,避免 sin(angle) 接近零。验证时让终点四元数每帧随机取等价符号;结果应保持同一旋转路径,没有突然转一整圈。
实现边界
C++片段使用Quaternion、Vector3和宿主数学函数,尚未作为独立C++程序编译。Gram–Schmidt示例要求前两列非零且不共线;退化输入应拒绝或从可靠参考轴重建,不能直接归一化零向量。叉积生成右手正交基,镜像变换应在纯旋转之外单独保存。
Slerp中的四元数夹角是物体旋转角的一半。身份旋转到绕Z轴90°,中点应把X轴转到约(0.7071,0.7071,0);终点换成等价负四元数后结果相同。这个解析例同时检查符号选择和半角含义,目标引擎测试仍需实际执行。
验证方法
- 画出旋转前后的三个局部轴。
- 测试跨越 180 度时是否绕远路。
- 对比 Lerp、Nlerp 和 Slerp 的角速度。
- 给物体做长时间连续旋转,检查矩阵基是否漂移。
- 不要把 Inspector 显示的欧拉角变化当成内部一定用欧拉角计算。
相关主题
参考资料
- Ken Shoemake, Animating Rotation with Quaternion Curves.
- Eric Lengyel, Foundations of Game Engine Development, Volume 1: Mathematics.
- Unity 6旋转与朝向:引擎的欧拉角与四元数约定,2026-10-22核对。