四元数插值 Interpolating Quaternions

整理 Nlerp、Slerp、四元数指数形式和双覆盖问题。

四元数插值 Interpolating Quaternions

对四元数进行 插值,本质上是在多个 旋转姿态 做 平滑过渡 或 权重混合,四元数插值有两种常见做法

方法全称主要特点典型用途
SlerpSpherical Linear Interpolation(球面线性插值)1. 在四维单位球面上走最短圆弧,角速度恒定。
2. 产生的旋转序列“扭矩最小”(torque-minimal),不会出现忽快忽慢的转动。
3. 计算量稍大(需要三角函数)。
4. 不具备交换律:Slerp(q₀→q₁, t) ≠ Slerp(q₁→q₀, t)。
需要高物理正确性和匀速旋转的场景,如刚体动力学、摄像机跟随。
NlerpNormalized Linear Interpolation(归一化线性插值)1. 先对四元数做线性插值(Lerp),再把结果单位化;计算只用乘加、一次归一化,速度快、代码简单。
2. 插值路径不是严格的球面大圆弧,角速度 不恒定(靠近端点时更慢)。
3. 具备交换律:Nlerp(q₀→q₁, t) = Nlerp(q₁→q₀, 1−t)。
4. 仍然是扭矩最小插值,但精度略逊于 Slerp。
角色动画、UI 过渡等对精度要求不高,追求性能的场合。

这里所说的 “扭矩最小”(torque-minimal 或 minimum-torque) 是从 “刚体动力学” 角度来评价一条旋转插值曲线的物理性质:在理想均匀刚体、忽略外力的前提下,这条插值轨迹所需的外加扭矩 τ\tau(或其二范数的时间积分)是所有可行路径里最小的

Nlerp 归一化线性插值

对任何 标量 或 向量 做 lerp 线性插值 都是

a+t(b−a),t∈[0,1]\mathbf{a} + t(\mathbf{b} - \mathbf{a}), \quad t \in[0,1]

四元数也不例外,只是 a, b\mathbf{a}, \ \mathbf{b} 必须保持单位长度,而进行 lerp 之后,长度一般不再等于 11,所以我们可以再最后进行一次 归一化 Normalize,这就是 Nlerp 归一化线性插值

可能不是最短路径

因为 单位四元数 和它的 相反数 −q-q 在 三维空间 中表示 同一姿态,但却位于 四维超球面 的 对立半球,当我们直接执行 lerp 时,如果起点和终点分属两侧,会沿半个大圆以外的 “长弧” 进行插值,角度将多转 180∘180^\circ,为了避免这种情况,我们可以用 点乘 判断是否同属一个半球,如果

a⋅b<0\mathbf{a} \cdot \mathbf{b} < 0

那么说明它们分属 四维超球面 的两侧,我们需要将终点进行 取反

b←−b\mathbf{b} \leftarrow -\mathbf{b}

典型代码

Quaternion Nlerp(Quaternion start, Quaternion end, float t)
{
    if(Dot(start,end) < 0.0f)         // ① 取最短弧
        end = Negate(end);
    Quaternion res = start + (end - start) * t;   // ② 线性插值
    return Normalize(res);            // ③ 单位化
}

Nlerp 非常快速,只有加减乘+一次 normalize

带权混合 Mixing

动画系统常用另一种 等价形式 来表示 lerp 和 Nlerp,即用 权重混合 来把两个(或多个)Pose/骨骼姿态叠加,对于四元数,也就是按 权重 对姿态求平均

mix(a,b; w)=Normalize((1−w)a+wb)\mathrm{mix}(\mathbf{a},\mathbf{b}; \ w) = \mathrm{Normalize}((1-w)\mathbf{a} + w\mathbf{b})
  • 去掉 Normalize\mathrm{Normalize} 就是普通的 lerp 带权混合形式

当然,我们仍然需要用 点乘 判断是否同属一个半球用以取反来保证插值按 最短弧 进行

Quaternion Nlerp(Quaternion start, Quaternion end, float t)
{
    if(Dot(start,end) < 0.0f)         // ① 取最短弧
        end = Negate(end);
    Quaternion res =  a*(1-t) + b*t;   // ② 线性插值
    return Normalize(res);            // ③ 单位化
}

这在 动画层叠 / additive blending 里特别常用:你可以给若干动作片段各分配 0.3、0.5、0.2…… 的权重,把它们 “加权平均” 后再归一化,得到最终骨骼姿态

Slerp 球面线性插值

我们先来看看 三维向量 的 Slerp,其完全基于 大圆(测地线) 上做等速运动 的 几何约束

首先,若有 夹角 为 θ\theta 的 q⃗0, q⃗1\vec{q}_0, \ \vec{q}_1 均是 三维单位球体 上的点,那么它们生成的 二维子空间 P=span{q⃗0, q⃗1}P = \mathrm{span}\{\vec{q}_0, \ \vec{q}_1 \} 与 原点 相交于一个 单位圆,整条 最短弧 都落在这个平面里,也就是说我们将问题降维到一个 二维单位圆

我们首先需要构造 正交基,因为 q⃗0, q⃗1\vec{q}_0, \ \vec{q}_1 都是 三维单位球体 上的点,所以它们都是 单位向量,那么

e1=q⃗0\mathbf{e}_1 = \vec{q}_0

而要构造 e2\mathbf{e}_2,我们先使用 Gram-Schmidt 正交化 q⃗1−(q⃗1⋅q⃗0)q⃗0\vec{q}_1 - (\vec{q}_1 \cdot \vec{q}_0)\vec{q}_0,然后再对其执行归一化,也就是除以它的长度 ∥q⃗1−(q⃗1⋅q⃗0)q⃗0∥\| \vec{q}_1 - (\vec{q}_1 \cdot \vec{q}_0)\vec{q}_0 \|

e2=q⃗1−(q⃗1⋅q⃗0)q⃗0∥q⃗1−(q⃗1⋅q⃗0)q⃗0∥\mathbf{e}_2 = \frac{\vec{q}_1 - (\vec{q}_1 \cdot \vec{q}_0)\vec{q}_0}{\| \vec{q}_1 - (\vec{q}_1 \cdot \vec{q}_0)\vec{q}_0 \|}

我们知道 点乘 可以写为

q⃗1⋅q⃗0=∥q⃗1∥⋅∥q⃗0∥cos⁡θ=cos⁡θ\vec{q}_1 \cdot \vec{q}_0 = \|\vec{q}_1\| \cdot \|\vec{q}_0\| \cos{\theta} = \cos{\theta}
  • q⃗0, q⃗1\vec{q}_0, \ \vec{q}_1 都是 单位向量

所以 分子 可以写为

q⃗1−(q⃗1⋅q⃗0)q⃗0=q⃗1−cos⁡θq⃗0\vec{q}_1 - (\vec{q}_1 \cdot \vec{q}_0)\vec{q}_0 = \vec{q}_1 - \cos{\theta}\vec{q}_0

而 分母 也可以进行修改

∥q⃗1−(q⃗1⋅q⃗0)q⃗0∥2=∥q⃗1∥2+cos⁡2θ∥q⃗0∥2−2cos⁡θ(q⃗1⋅q⃗0)=1+cos⁡2θ−2cos⁡2θ∵∥q⃗1∥2=∥q⃗0∥2=1, q⃗1⋅q⃗0=cos⁡θ=1−cos⁡2θ=sin⁡2θ∴ ∥q⃗1−(q⃗1⋅q⃗0)q⃗0∥=sin⁡θ\begin{align*} \| \vec{q}_1 - (\vec{q}_1 \cdot \vec{q}_0)\vec{q}_0 \|^2 &= \|\vec{q}_1 \|^2 + \cos^2{\theta}\|\vec{q}_0\|^2 - 2\cos{\theta}(\vec{q}_1 \cdot \vec{q}_0) \\ &= 1 + \cos^2{\theta} - 2\cos^2{\theta} \quad \because \|\vec{q}_1\|^2 = \|\vec{q}_0\|^2 = 1, \ \vec{q}_1 \cdot \vec{q}_0 = \cos{\theta} \\ &= 1 - \cos^2{\theta} \\ &= \sin^2{\theta} \\ \therefore \ \| \vec{q}_1 - (\vec{q}_1 \cdot \vec{q}_0)\vec{q}_0 \| &= \sin{\theta} \end{align*}

所以我们得到最终的 正交基

{e1=q⃗0e2=q⃗1−cos⁡θ q⃗0sin⁡θ  ⟹  P=span{e1, e2}\begin{cases} \mathbf{e}_1 = \vec{q}_0 \\[6pt] \mathbf{e}_2 = \dfrac{\vec{q}_1 - \cos{\theta} \ \vec{q}_0}{\sin{\theta}} \end{cases} \implies P = \mathrm{span}\{\mathbf{e}_1,\ \mathbf{e}_2 \}

e1, e2\mathbf{e}_1,\ \mathbf{e}_2 在平面 PP 内 正交归一,因此任何 插值点 都可写成

q⃗(t)=x(t)e1+y(t)e2\vec{q}(t) = x(t)\mathbf{e}_1 + y(t)\mathbf{e}_2

从 p⃗0→p⃗1\vec{p}_0 \to \vec{p}_1 的 插值点 必须满足以下 约束条件

  1. 端点条件

    q⃗(0)=q⃗0=e1  ⟹  x(0)=1, y(0)=0q⃗(1)=q⃗1=cos⁡θe1+sin⁡θe2  ⟹  x(1)=cos⁡θ, y(1)=sin⁡θ\begin{align*} \vec{q}(0) &= \vec{q}_0 = \mathbf{e}_1 &\implies x(0) = 1, \ y(0) = 0 \quad \quad \quad \\ \vec{q}(1) &= \vec{q}_1 = \cos{\theta}\mathbf{e}_1 + \sin{\theta}\mathbf{e}_2 &\implies x(1) = \cos{\theta}, \ y(1) = \sin{\theta} \end{align*}
    • q⃗1=cos⁡θe1+sin⁡θe2\vec{q}_1 = \cos{\theta}\mathbf{e}_1 + \sin{\theta}\mathbf{e}_2 是因为 q⃗1⋅e1=q⃗1⋅q⃗0=cos⁡θq⃗1⋅e2=q⃗1⋅q⃗1−cos⁡θ q⃗0sin⁡θ=q⃗1⋅q⃗1−(q⃗1⋅q⃗0)(q⃗1⋅q⃗0)sin⁡θ=1−cos⁡2θsin⁡θ=sin⁡2θsin⁡θ=sin⁡θ\begin{align*} \vec{q}_1 \cdot \mathbf{e}_1 &= \vec{q}_1 \cdot \vec{q}_0 = \cos{\theta} \\ \vec{q}_1 \cdot \mathbf{e}_2 &= \vec{q}_1 \cdot \frac{\vec{q}_1 - \cos{\theta} \ \vec{q}_0}{\sin{\theta}} \\ &= \frac{\vec{q}_1\cdot\vec{q}_1 - (\vec{q}_1\cdot \vec{q}_0)(\vec{q}_1\cdot \vec{q}_0)}{\sin{\theta}} \\ &= \frac{1 - \cos^2{\theta}}{\sin{\theta}} \\ &= \frac{\sin^2{\theta}}{\sin{\theta}} \\ &= \sin{\theta} \end{align*}
  2. 保持单位长度
    插值向量 的长度需要保持为 11

    ∥q⃗(t)∥2=x(t)2+y(t)2=1\|\vec{q}(t)\|^2 = x(t)^2 + y(t)^2 = 1
  3. 角度线性(匀速)
    我们知道 单位向量 uu 和 vv 的 点乘 与 夹角 关系是

    u⃗⋅v⃗=cos⁡[∠(u⃗, v⃗)]\vec{u} \cdot \vec{v} = \cos{[\angle(\vec{u}, \ \vec{v})]}

    我们还知道 q⃗0\vec{q}_0 和 q⃗1\vec{q}_1 的 完整夹角 是 θ\theta,而 插值向量 q⃗(t)\vec{q}(t) 与 初始向量 q⃗0\vec{q}_0 的 夹角 是 ∠(q⃗0, q⃗(t))\angle(\vec{q}_0,\ \vec{q}(t)),我们希望它随着插值的进行,一路 线性 的增长到 θ\theta,也就是

    ∠(q⃗0, q⃗(t))=tθ,t∈[0,1]\angle(\vec{q}_0,\ \vec{q}(t)) = t\theta,\quad t\in [0,1]

    有了这个结论,再结合 单位向量 的 点乘 与 夹角 关系,可得

    q⃗0⋅q⃗(t)=cos⁡tθ\vec{q}_0 \cdot \vec{q}(t) = \cos{t\theta}

    我们之前还建立了 正交基 e1,e2\mathbf{e}_1,\mathbf{e}_2,并用它表示了 插值向量 q⃗(t)\vec{q}(t),我们知道

    e1=q⃗0q⃗(t)=x(t)e1+y(t)e2\begin{align*} \mathbf{e}_1 &= \vec{q}_0 \\ \vec{q}(t) &= x(t)\mathbf{e}_1 + y(t)\mathbf{e}_2 \end{align*}

    所以有

    q⃗0⋅q⃗(t)=e1⋅[x(t)e1+y(t)e2]=x(t)e1⋅e1+y(t)e2⋅e1=x(t)⋅1+y(t)⋅0=x(t)\begin{align*} \vec{q}_0 \cdot \vec{q}(t) &= \mathbf{e}_1 \cdot [x(t)\mathbf{e}_1 + y(t)\mathbf{e}_2] \\ &= x(t)\mathbf{e}_1\cdot\mathbf{e}_1 + y(t)\mathbf{e}_2\cdot\mathbf{e}_1 \\ &= x(t)\cdot 1 + y(t)\cdot 0 \\ &= x(t) \end{align*}

    回代,最终得到

    x(t)=cos⁡tθx(t) = \cos{t\theta}

由 角度线性 约束,我们已经得到了 x(t)x(t),接下来我们只需要结合 保持单位长度 约束就可以得到 y(t)y(t)

x(t)2+y(t)2=1  ⟹  y(t)=±1−cos⁡2(tθ)=±sin⁡(tθ)x(t)^2 + y(t)^2 = 1 \implies y(t) = \pm \sqrt{1 - \cos^2(t\theta)} = \pm\sin(t\theta)

沿 最短弧 时 y(t)≥0y(t) \geq 0 (与 e2\mathbf{e}_2 同向),故取正号,得到

y(t)=sin⁡tθy(t) = \sin{t\theta}

所以我们得出

q⃗(t)=cos⁡tθ e1+sin⁡tθ e2\vec{q}(t) = \cos{t\theta} \ \mathbf{e}_1 + \sin{t\theta} \ \mathbf{e}_2

然后我们将 e1, e2\mathbf{e}_1, \ \mathbf{e}_2 展开写回为 q⃗0, q⃗1\vec{q}_0, \ \vec{q}_1

{e1=q⃗0e2=q⃗1−cos⁡θ q⃗0sin⁡θq⃗(t)=cos⁡tθ e1+sin⁡tθ e2=cos⁡tθ q⃗0+sin⁡tθ q⃗1−cos⁡θ q⃗0sin⁡θ=cos⁡tθ q⃗0+sin⁡tθsin⁡θq⃗1−cos⁡θsin⁡tθsin⁡θq⃗0=[cos⁡tθ−cos⁡θsin⁡tθsin⁡θ]q⃗0+sin⁡tθsin⁡θq⃗1=sin⁡θcos⁡tθ−cos⁡θsin⁡tθsin⁡θq⃗0+sin⁡tθsin⁡θq⃗1\begin{align*} & \begin{cases} \mathbf{e}_1 = \vec{q}_0 \\[6pt] \mathbf{e}_2 = \dfrac{\vec{q}_1 - \cos{\theta} \ \vec{q}_0}{\sin{\theta}} \end{cases} \\ \\ \vec{q}(t) &= \cos{t\theta} \ \mathbf{e}_1 + \sin{t\theta} \ \mathbf{e}_2 \\ &= \cos{t\theta} \ \vec{q}_0 + \sin{t\theta} \ \frac{\vec{q}_1 - \cos{\theta} \ \vec{q}_0}{\sin{\theta}} \\ &= \cos{t\theta} \ \vec{q}_0 + \frac{\sin{t\theta}}{\sin{\theta}}\vec{q}_1 - \frac{\cos{\theta}\sin{t\theta}}{\sin{\theta}}\vec{q}_0 \\ &= \left[\cos{t\theta} - \frac{\cos{\theta}\sin{t\theta}}{\sin{\theta}} \right]\vec{q}_0 + \frac{\sin{t\theta}}{\sin{\theta}}\vec{q}_1\\ &= \frac{\sin{\theta}\cos{t\theta} - \cos{\theta}\sin{t\theta}}{\sin{\theta}}\vec{q}_0 + \frac{\sin{t\theta}}{\sin{\theta}}\vec{q}_1 \end{align*}

我们可以利用 三角恒等式

sin⁡(A−B)=sin⁡Acos⁡B−cos⁡Asin⁡B\sin(A - B) = \sin{A}\cos{B} - \cos{A}\sin{B}

所以我们有

sin⁡(θ−tθ)=sin⁡θcos⁡tθ−cos⁡θsin⁡tθ\sin{(\theta - t\theta)} = \sin{\theta}\cos{t\theta} - \cos{\theta}\sin{t\theta}

回代,得到

q⃗(t)=sin⁡θcos⁡tθ−cos⁡θsin⁡tθsin⁡θq⃗0+sin⁡tθsin⁡θq⃗1=sin⁡(θ−tθ)sin⁡θq⃗0+sin⁡tθsin⁡θq⃗1=sin⁡[(1−t)θ]sin⁡θq⃗0+sin⁡tθsin⁡θq⃗1\begin{align*} \vec{q}(t) &= \frac{\sin{\theta}\cos{t\theta} - \cos{\theta}\sin{t\theta}}{\sin{\theta}}\vec{q}_0 + \frac{\sin{t\theta}}{\sin{\theta}}\vec{q}_1 \\ &= \frac{\sin{(\theta - t\theta)}}{\sin{\theta}}\vec{q}_0 + \frac{\sin{t\theta}}{\sin{\theta}}\vec{q}_1 \\ &= \boxed{ \frac{\sin{[(1 - t)\theta]} }{\sin{\theta}}\vec{q}_0 + \frac{\sin{t\theta}}{\sin{\theta}}\vec{q}_1 } \end{align*}

这正是 正弦权重 版的 Slerp 公式,整条 插值曲线 因此可以视为在以 q⃗0, q⃗1\vec{q}_0, \ \vec{q}_1 “按弦长正弦比” 进行的匀速运动,当 θ→0\theta\to0 时,sin⁡θ≈θ\sin{\theta} \approx \theta,此时公式滑退化为普通 lerp 线性插值 q⃗0+(q⃗1−q⃗0)t\vec{q}_0 + (\vec{q}_1 - \vec{q}_0)t

四元数 Slerp

要推导 四元数 q0q_0 到 q1q_1 的 Slerp,核心思想是在 四维单位球 S3S^3 上寻找经过 q0→q1q_0 \to q_1 的最短大圆弧,并要求匀速(角速度恒定)地走这条弧线

单位四元数 对应 三维旋转,其集合构成 三维流形 S3S^3 (半径 11 的 四维超球面),两个 四元数 点 q0, q1∈S3q_0,\ q_1 \in S^3 间的测地线就是穿过两点的 大圆(Great Circle),设

Ω=arccos(q0⋅q1),0<Ω≤π\Omega = \mathrm{arccos}(q_0\cdot q_1), \quad 0 < \Omega \leq \pi

是两点在 S3S^3 上的 夹角 (用四元数点积即可求得),在大圆坐标系里,想要 匀速 行走意味着 插值点 必须满足

θ(t)=(1−t)0+tΩ=tΩ\theta(t) = (1 - t) 0 + t\Omega = t\Omega

即 插值角度 θ(t)\theta(t) 与参数 tt 成 线性 关系

根据我们前面的推导,在二维圆上我们有以下 slerp 插值公式

q⃗(t)=sin⁡[(1−t)θ]sin⁡θq⃗0+sin⁡tθsin⁡θq⃗1\vec{q}(t) = \frac{\sin{[(1 - t)\theta]} }{\sin{\theta}}\vec{q}_0 + \frac{\sin{t\theta}}{\sin{\theta}}\vec{q}_1

我们将 p⃗0\vec{p}_0 和 p⃗1\vec{p}_1 换成 四元数 q0, q1q_0,\ q_1 就得到

q(t)=sin⁡[(1−t)Ω]sin⁡Ωq0+sin⁡tΩsin⁡Ωq1q(t) = \boxed{ \frac{\sin{ [(1-t)\Omega] }}{\sin{\Omega}}q_0 + \frac{\sin{t\Omega}}{\sin{\Omega}}q_1 }

四元数 Slerp - 指数形式

我们知道,四元数的相乘表示旋转的叠加,并且我们已知

  • 起点姿态 q0q_0
  • 终点姿态 q1q_1

我们需要找到一个 “中间旋转” Δ\Delta 使得

q0Δ=q1q_0\Delta = q_1

可得

Δ=q0−1q1=q0∗q1\Delta = q_0^{-1}q_1 = q_0^\ast q_1

可以认为我们先用 q0−1q_0^{-1} 把当前姿态 “复原” 到基准坐标,再用 q1q_1 把它转到目标姿态,整个组合就等价于一次旋转 Δ\Delta

  • 注意,Δ\Delta 是 “姿态到姿态” 的 差旋转 输入/输出都是完整的 四元数,包含 朝向 + 自旋,而我们前面章节介绍的,输入 from 向量 和 to 向量 来构造 from-to 四元数 是 “方向到方向” 的最短旋转,输入仅仅是两个 3D 向量,输出一个把第一个向量转到第二支向量的 四元数,不含轴向自旋
    • 旋转差 3 DoF 自由度:Δ=q0−1q1\Delta = q_0^{-1}q_1 完整 SO(3) 旋转;既包含 “把坐标轴指向哪儿”,也包含 “绕该方向自旋了多少”
    • from-to 2 DoF 自由度:只能把一根方向矢量对齐到另一根;无法决定 绕该方向自旋(roll)

由于 Δ\Delta 也是表示旋转的 单位四元数,所以我们可以将它写成 轴-角 表示以及等价的 指数表示

Δ=(cos⁡Ω2, n^sin⁡Ω2)=en^Ω2\Delta = (\cos{\frac{\Omega}{2}},\ \hat{n}\sin{\frac{\Omega}{2}} ) = e^{\hat{n}\frac{\Omega}{2}}

如此一来,我们执行插值将会变的容易,我们可以取 对数 以后对角度按 tt 进行 线性插值

log⁡Δ=n^Ω2  ⟹  tlog⁡Δ=n^tΩ2\log{\Delta} = \hat{n}\frac{\Omega}{2} \implies t\log{\Delta} = \hat{n}\frac{t\Omega}{2}

如果我们再对左右取 指数,可得

etlog⁡Δ=en^tΩ2=(cos⁡tΩ2, n^sin⁡tΩ2)=(en^Ω2)t=Δte^{t\log{\Delta}} = e^{\hat{n}\frac{t\Omega}{2}} = (\cos{\frac{t\Omega}{2}},\ \hat{n}\sin{\frac{t\Omega}{2}} ) = (e^{\hat{n}\frac{\Omega}{2}})^t = \Delta^t

也就是说,要对 Δ\Delta 进行 t∈[0,1]t \in [0,1] 且保持旋转 轴 不变,仅对 角度 进行 线性插值,我们只需要对原四元数 Δ\Delta 作 实数幂 操作 Δt\Delta^t,所以 四元数幂 的实现是

Quaternion Pow(Quaternion q, float t)
{
    //---------------------------------------
    // 0)  若 q 不是严格单位四元数,先归一化
    //---------------------------------------
    q.normalize();   // 可省略,但最好保守处理
 
    //---------------------------------------
    // 1)  提取半角 θ/2
    //---------------------------------------
    float cosHalf = q.w;              // w = cos(θ/2)
    // 若 |cosHalf| ≈ 1 → 角度极小,需要特殊处理
    if (fabs(cosHalf) > 0.9999f)
    {
        // 与 Lerp 近似:q^t ≈ (1-t) + t*q
        return Quaternion::Lerp(Quaternion::Identity(), q, t).normalized();
    }
    float halfAngle = acos(cosHalf);  // θ/2  ∈ (0,π)
 
    //---------------------------------------
    // 2)  求旋转轴 n̂
    //---------------------------------------
    float sinHalf = sqrtf(1.0f - cosHalf * cosHalf);   // = |v|, > 0 因为上面排除了极小角
    Vector3 axis(q.x / sinHalf, q.y / sinHalf, q.z / sinHalf);  // 已单位化
 
    //---------------------------------------
    // 3)  把角度乘 t,再算新的 sin/cos
    //---------------------------------------
    float newHalf = t * halfAngle;     // = tθ/2
    float newCos = cosf(newHalf);
    float newSin = sinf(newHalf);
 
    //---------------------------------------
    // 4)  组合回四元数
    //---------------------------------------
    return Quaternion(axis * newSin, newCos);   // (x,y,z,w)
}
  • 若平台有 sincosf() 或 SIMD sin_cos(), 可以一次性同时取 sin 和 cos,更省性能

最终,我们可以得到四元数 Slerp 的 指数幂形式

q(t)=q0Δt=q0(q0−1q1)t\boxed{ q(t) = q_0\Delta^t = q_0(q_0^{-1}q_1)^t }

它与 三角函数 写法完全等价,只需将 (q0−1q1)t(q_0^{-1}q_1)^t 展开成 (cos⁡Ω2, n^sin⁡Ω2)(\cos{\frac{\Omega}{2}},\ \hat{n}\sin{\frac{\Omega}{2}}) 并用 倍角公式 即可,但是 指数 写法更紧凑,且与实现 Pow() 函数(幂运算)天然耦合

struct Quaternion { float x, y, z, w; /* …ctor & ops… */ };
 
Quaternion Slerp(const Quaternion& a_, const Quaternion& b_, float t)
{
    Quaternion a = a_, b = b_;
 
    // ① 最短弧:同半球
    if (Dot(a, b) < 0.0f)
        b = -b;
 
    // ② 差旋转
    Quaternion delta = b * Inverse(a);
 
    // ④ 小角退化(利用 delta.w ≈ cos(θ/2))
    if (fabs(delta.w) > 0.9999f)
        return Normalize(Lerp(a, b, t));   // Nlerp 近似 
 
    // ③ Δ^t —— 直接用四元数 Pow() 函数
    Quaternion deltaPow = Pow(delta, t);
 
    // ⑤ 左乘起点
    return deltaPow * a;
}
 

以上是 指数形式 实现的 四元数 Slerp,但是不管是 指数形式 还是 三角函数形式,都无法避免 acosf,所以四元数的 Slerp 存在一定的性能消耗

四元数 Slerp vs 向量 Slerp

维度四元数 Slerp三维向量 Slerp
插值对象单位四元数 q∈S3q\in S^3(表示完整姿态/旋转)单位向量 v∈S2\mathbf v\in S^2(仅表示方向;不含绕该方向的自旋)
几何空间四维单位球面 S3S^3 上的最短大圆弧三维单位球面 S2S^2 上的最短大圆弧
“距离”定义李群 SO(3) 的测地距离:Ω=2arccos⁡(q0 ⁣⋅q1)\displaystyle\Omega=2\arccos(q_0\!\cdot q_1)球面夹角:θ=arccos⁡(v0 ⁣⋅v1)\displaystyle\theta=\arccos(\mathbf v_0\!\cdot\mathbf v_1)
公式形态q(t)=q0 (q0−1q1)tq(t)=q_0\,(q_0^{-1}q_1)^{t}
或 q(t)=sin⁡((1−t)Ω)sin⁡Ω q0+sin⁡(tΩ)sin⁡Ω q1\displaystyle q(t)=\frac{\sin((1-t)\Omega)}{\sin\Omega}\,q_0+\frac{\sin(t\Omega)}{\sin\Omega}\,q_1
v(t)=sin⁡((1−t)θ)sin⁡θ v0+sin⁡(tθ)sin⁡θ v1\displaystyle \mathbf v(t)=\frac{\sin((1-t)\theta)}{\sin\theta}\,\mathbf v_0+\frac{\sin(t\theta)}{\sin\theta}\,\mathbf v_1
是否“扭矩最小 / 匀速”是。角速度恒定、扭矩最小是。角速度恒定(但只对方向向量有意义)
双覆盖问题qq 与 −q-q 表示同一旋转;如果需要最短路径,则插值前必须先把终点翻到同一半球v\mathbf v 与 −v-\mathbf v 表示相反方向,语义不同,不做翻转
可组合性闭合于乘法:q(t1) q(t2)=q(t1+t2)q(t_1)\,q(t_2)=q(t_1+t_2)(当基准相同)不具备群结构,难以“相乘”组合
应用场景1. 刚体姿态、关节骨骼动画
2. 摄像机朝向 + roll
3. 任意两旋转的自然过渡
1. 纯方向插值(朝向一束射线、法线指向)
2. 只关心“看向哪里”,不关心自转
额外自由度包含“绕朝向轴”的自旋;Slerp 会正确插值该自旋没有轴向转动概念
实现成本需要四元数乘法、逆、幂;稍重只需三维向量运算;更轻

四元数保存了 “绕方向自转” 的信息;为了保持 匀速自旋,就得处理 q0−1q1q_0^{-1}q_1 再做幂运算,而向量只关心方向角度,公式更简单,如果只旋转镜头指向,而不想处理 roll,向量 Slerp 就够;要连同 roll 一起平滑过渡,必须用 四元数 Slerp

双覆盖问题

在上面的分析中我们提到,在两个四元数之间进行插值时,插值可以采用两者之间最短或最长的圆弧,因为这两个四元数都代表 同一姿态

Quaternion double cover animation comparing the solid short path to negative q1 with the translucent long path to q1 before flipping.
同一姿态可由 q 与 -q 表示;实心箭头展示翻转后的最短 lift,紫色虚影展示未翻转时从 q₀ 走向 q₁ 的长路径。

我们一般会想让插值沿着 最短路径 进行,如果你曾经见过游戏中角色的弯曲错误,但动作却正确,那么很可能是没有沿着 最短路径 执行插值,我们在上面的代码中已经加入了 半球检测

// ① 最短弧:同半球
if (Dot(a, b) < 0.0f)
    b = -b;