本文最后更新于:2025年4月17日 晚上
四元数插值
假设有两个旋转变换 q0=[cos(θ0),sin(θ0)u0] 和 q1=[cos(θ1),sin(θ1)u1],我们希望找出一些中间变换 qt,让初始变换 q0 能够平滑地过渡到最终变换 q1,t 的取值可以是 t∈[0,1],当 t=0 时 qt 等同于初始变换 q0,而 t=1 时 qt 等同于最终变换 q1。
由于插值的对象是两个变换,想象起来可能非常困难,我们不妨假设 3D 空间中有任意一个向量 v,那么 q0 会将 v 变换到 v0=q0vq0∗,而 q1 会将 v 变换到 v1=q1vq1∗。我们需要找出中间向量 vt=qtvqt∗ 所对应的变换 qt,使 v 旋转到 v0 与 v1 中间的某个位置 vt:

我们可以看到,这个旋转的变化量其实对应的仍是一个旋转。它将由 q0 变换到 v0 的向量进一步旋转到 vt。这个旋转拥有某一个固定的旋转轴 ut,我们只需要缩放这个变换所对应的角度 ϕ 就能够达到插值的目的了。
那么,现在的问题是,我们该怎么获得这个旋转的变化量。我们不妨考虑一下什么变换 Δq 能将已经旋转到 v0 的向量 v 直接变换到 v1。这其实就是一个旋转的复合,我们先进行 q0 变换,再进行 Δq 变换,它们复合的结果需要等于 q1 变换,也就是说:
Δqq0=q1
那么,
Δqq0q0−1Δq=q1q0−1=q1q0−1
因为所有的旋转 q 都是单位四元数,q−1=q∗,它又可以写成:
Δq=q1q0∗
如果我们对 Δq 取 t 次方,(Δq)t 就能缩放这个旋转所对应的角度了。所以,我们就能得出插值的公式:
qt=Slerp(q0,q1;t)=(q1q0∗)tq0
可以发现,当 t=0 时,qt=q0;而当 t=1 时,qt=q1;如果 t 为中间值,比如说 t=0.4 时,qt=(q1q0∗)0.4q0,它会先进行 q0 变换将 v 变换到 v0,并在此基础上向 v1 旋转 40%.
这个插值方法称为Slerp,但它涉及到多个四元数的乘法,而且还包含幂运算,实际应用中效率很低。为了理解它我们还需要研究一下 3D 空间的旋转与四元数的 4D 向量空间之间的关系
3D空间旋转变化量 vs. 四元数4D向量空间夹角
为了探讨这个关系,我们来实际计算一下 Δq。由于这个关系和角度有关,我们只需要关心 Δq 的实部就可以了:
Δq=q1q0∗=[cos(θ1),sin(θ1)u1][cos(θ0),−sin(θ0)u0]=[cos(θ0)cos(θ1)−(sin(θ1)u1)⋅(−sin(θ0)u0),…]=[cos(θ0)cos(θ1)+(sin(θ1)u1)⋅(sin(θ0)u0),…]
如果将 q0 和 q1 看作是两个四维向量,我们可以发现 Δq 的实部正好就是 q0 和 q1 点乘的结果 q0⋅q1,因为 q0 和 q1 都是单位四元数,q0⋅q1 正好是这两个四元数在 4D 空间中夹角的余弦值,我们将这个夹角称为 θ,那么 q0⋅q1=cos(θ)
我们又知道,Δq 表示的也是一个旋转,而如果它代表的旋转的角度是 2ϕ,那么 Δq 的实数部为 Δq=[cos(ϕ),…]。所以
Δq=[cos(ϕ),…]=[cos(θ),…]cos(ϕ)=cos(θ)
因为 ϕ 和 θ 都是夹角,ϕ,θ∈[0,π],所以这个方程有唯一的解
ϕ=θ
这也就是说,q0 与 q1 作为向量在 4D 四元数空间中的夹角 θ,正好是它们旋转变化量 Δq 的所代表旋转的角度的一半,即 θ=21(2ϕ)。所以,我们可以直接用插值向量的方法对旋转进行插值。
为了更直观的理解这层关系,请看下面这两幅图。虽然四元数是出于四维空间之内的,但是因为只有两个四元数,我们可以将它们投影到一个二维的圆上来,也就是左图。右侧则是 3D 空间中发生的旋转改变

可以看到,当 q1 与 q0 之间的夹角为 θ 时,旋转的变化量正好是 2θ。如果我们在圆上有一个单位四元数 qt,使得它与 q0 的夹角为 tθ,与 q1 的夹角为 (1−t)θ,那么我们就能保证在 3D 空间中,它相对于 q0 的旋转变化量为 2tθ,相对于 q1 的旋转变化量为 2(1−t)θ。
现在,两个单位四元数的插值就被我们简化为了一个圆上(其实是超球面的一部分)两个向量的插值,我们能直接套用向量的插值公式对两个四元数进行插值。接下来,我们就来讨论如何对两个向量进行插值。
Lerp, Nlerp, Slerp
不论是哪种插值方法,我们都希望将中间向量 vt 写为初始向量 v0 和最终向量 v1 的线性组合,也就是说:
vt=αv0+βv1
其中,系数 α 与 β 都是 t 的函数。不同的插值方法只是拥有不同的系数而已。
Lerp
首先是两个向量插值最简单的一种形式:线性插值,也叫做“Lerp”。Lerp 会沿着一条直线进行插值,如果将 v0 和 v1 看做是三角形的两个边,那么 vt 会指向三角形的第三条边。

我们能将 vt 写为两个向量的和。
vt=Lerp(v0,v1,t)=v0+t(v1−v0)=(1−t)v0+tv1
如果将 Lerp 的结果应用到单位四元数上,我们可以得到:
qt=Lerp(q0,q1,t)=(1−t)q0+tq1
当然,因为我们是沿着一条直线(也就是圆上的一个弦)进行插值的,这样插值出来的四元数并不是单位四元数。

Nlerp
虽然这样插值出来的 qt 并不是单位四元数,但只要将 qt 除以他的模长 ∥qt∥ 就能够将其转化为一个单位四元数了。

我们将这种先对向量进行插值,再进行正规化的插值方法称为正规化线性插值,或者“Nlerp”。与 Lerp 不同,Nlerp 的两个输入向量必须是单位向量,否则插值出来的结果不会经过初始和最终向量。下面分别是向量和四元数的 Nlerp 公式。
vt=Nlerp(v0,v1,t)=∥(1−t)v0+tv1∥(1−t)v0+tv1qt=Nlerp(q0,q1,t)=∥(1−t)q0+tq1∥(1−t)q0+tq1
Nlerp 插值仍然存在有一定的问题,当需要插值的弧比较大时,vt 的角速度会有显著的变化.我们可以来看一个例子:

这五个 t 值将整个弧和弦分割成了四个部分.虽然弦上的四段是等长的,但是四个弧是完全不相等的。t=0 到 t=0.25 之间的弧(红色)明显比 t=0.25 到 t=0.50 的弧(蓝色)要短了不少.
这也就是说,在同等时间内,vt 扫过的角度是不同的。vt 扫过的速度(或者说角速度)首先会不断地增加,到 t=0.50 之后会开始减速,所以 Nlerp 插值不能保证均匀的角速度.
Slerp
为了解决这个问题,我们可以转而对角度进行线性插值。也就是说,如果 v1 和 v2 之间的夹角为 θ,那么
θt=(1−t)⋅0+tθ=tθ
因为对角度线性插值直接是让向量在球面上的一个弧上旋转,所以又称球面线性插值,或者“Slerp”。
上面的公式并没有涉及到任何的向量,我们希望将 vt 写为 v0 和 v1 的线性组合
vt=αv0+βv1(1)
这里的 v0 和 v1 仍是单位向量,为了求出这其中的 α 和 β,我们需要借助图像来找出一些关系:

我们可以先对(1)式的两边同时点乘 v0
v0⋅vtv0⋅vt=v0⋅(αv0+βv1)=α(v0⋅v0)+β(v0⋅v1)
我们知道,v0 和 vt 之间的夹角是 tθ,v0 与它自身之间的夹角为 0,v0 和 v1 之间的夹角是 θ,而且所有的向量都是单位向量,所以
cos(tθ)=α+βcos(θ)(2)
同理,我们将(1)式的两边同时点乘 v1,构造第二个方程
v1⋅vtv1⋅vt=v1⋅(αv0+βv1)=α(v1⋅v0)+β(v1⋅v1)
cos((1−t)θ)=αcos(θ)+β(3)
现在我们可以用(2)式和(3)式求出 α 和 β 了。
由(2)式可以得到
α=cos(tθ)−βcos(θ)(4)
将(4)式代入(3)式
cos((1−t)θ)cos((1−t)θ)β(1−cos2(θ))ββββ=(cos(tθ)−βcos(θ))cos(θ)+β=cos(tθ)cos(θ)−βcos2(θ)+β=cos((1−t)θ)−cos(tθ)cos(θ)=sin2(θ)cos(θ−tθ)−cos(tθ)cos(θ)=sin2(θ)cos(θ)cos(tθ)+sin(θ)sin(tθ)−cos(tθ)cos(θ)=sin2(θ)sin(θ)sin(tθ)=sin(θ)sin(tθ)
将 β 代入(4)式解出 α
α=cos(tθ)−(sin(θ)sin(tθ))cos(θ)=sin(θ)cos(tθ)sin(θ)−sin(tθ)cos(θ)=sin(θ)sin((1−t)θ)
将 α 和 β 代回(1)式,我们可以得到向量的Slerp公式
vt=Slerp(v0,v1,t)=sin(θ)sin((1−t)θ)v0+sin(θ)sin(tθ)v1
类似地,我们有四元数的 Slerp 公式:
qt=Slerp(q0,q1,t)=sin(θ)sin((1−t)θ)q0+sin(θ)sin(tθ)q1
其中 q0 与 q1 之间的夹角 θ 可以直接使用它们点乘的结果来得出,即
θ=arccos(q0⋅q1)
这里导出的公式会比之前利用幂运算的公式要高效很多,但是它仍然涉及到三个三角函数以及一个反三角函数的运算,所以还是会比 Nlerp 要慢一点。如果要插值的角度比较小的话,Nlerp 其实相对于 Slerp 的误差并没有那么大。为了提高效率,我们经常会使用 Nlerp 来代替 Slerp。我们也能用一些数值分析的方法来近似并优化四元数的 Slerp。
除了效率问题之外,我们在实现 Slerp 时要注意,如果单位四元数之间的夹角 θ 非常小,那么 sin(θ) 可能会由于浮点数的误差被近似为 0.0,从而导致除以 0 的错误.所以,我们在实施 Slerp 之前,需要检查两个四元数的夹角是否过小(或者完全相同)。一旦发现这种问题,我们就必须改用 Nlerp 对两个四元数进行插值,这时候 Nlerp 的误差非常小所以基本不会与真正的 Slerp 有什么区别。
双倍覆盖带来的问题
如果你还记得,两个不同的单位四元数 q 与 −q 对应的其实是同一个旋转,这个特性显然会对我们的插值造成一些影响.虽然 q 与 −q 对向量变换的最终效果是完全相同的,但是它们作为向量相差了 π 弧度:

可以看到,虽然我们能够将 q0 向左插值至 q1(蓝色的弧),但这会将 3D 空间中的向量旋转接近 360 度,而实际上这两个旋转相差并没有那么多,它并不是 3D 空间中的弧面最短路径。而如果我们将 q0 向右插值至等价的 −q1(红色的弧),它的旋转变化量就会比插值到 q1 要小很多,所以 q0 插值到 −q1 才是插值的最短路径。
这也就告诉我们,在对两个单位四元数进行插值之前,我们需要先检测 q0 与 q1 之间是否是钝角,即检测它们点积的结果 q0⋅q1 是否为负数。如果 q0⋅q1<0 ,那么我们就反转其中的一个四元数,比如说将 q1 改为 −q1,并使用 q0 与 −q1 之间新的夹角来进行插值,这样才能保证插值的路径是最短的。
Squad
Slerp已经是我们理想中的插值方式了:它直接对角度插值,插值角度匀速变化,运算效率尚可。但是它还有一个小问题:角度变化的速率等于夹角,即 dtdθt=dtd(tθ)=θ,这就意味着,当我们在多个角速度之间插值的时候,当在不同的四元数之间插值的时候,速率会发生突变,或者说在切断点处不可导。从数学上讲,函数 f 连续并不意味着函数的一阶导连续(前者称为 C0 连续,后者称为 C1 连续)。
为此,我们希望能以牺牲固定角速度为条件,让插值的曲线能够在高阶导处也连续,下面介绍的Squad(Spherical and quadrangle)就是一种解决方法。
Bézier 曲线
假设我们有一个向量的序列 v0, v1,…,vn,如果我们想对这个序列进行插值,那么我们可以分别对每一对向量 vi 和 vi+1 进行插值,然后将插值的曲线连接起来,也就是我们所说的样条(Spline).如果直接使用 Lerp 的话,我们会得到这样的结果(假设我们只有五个向量需要插值 v0,v1,v2,v3,v4):

很明显,这个曲线虽然是连续的,但是它的一阶导数(切线)在切换插值向量时都不是连续的。为了解决这个问题,我们最常使用的就是 Bézier 曲线。我们一开始的想法可能会是将中间的 v1,v2,v3 作为控制点,直接使用一个四次 Bézier 曲线(因为有五个点)来生成这个近似曲线。但是 Bézier 曲线只会经过初始点与最终点(插值),一般不会经过中间的控制点(近似),所以这样求出来的曲线虽然是可导的,但是插值曲线不会经过中间的三个向量:

为了解决这个问题,我们可以分段对每两个向量 vi 和 vi+1 之间使用 Bézier曲线进行插值,之后将所有的曲线(样条)连接起来.因为我们需要让曲线的一阶导数(或者说曲线的趋势)连续,我们还需要知道它们的前一个向量 vi−1 和后一个向量 vi+2,并且用它们生成两个控制点 si 和 si+1 来控制曲线的趋势.我们会使用 vi 和 vi+1 作为端点(曲线会经过这两个点),si 和 si+1 作为中间的控制点,使用一个三次 Bézier 曲线(Cubic Bézier Curve,四个点)来近似这个两个向量之间的插值。
在我们的例子中,因为我们一共有四对向量(v0v1、v1v2、v2v3、v3v4),我们会使用四个三次 Bézier 曲线对这五个点进行插值.我们知道,对于三次Bézier 曲线所产生的样条,如果想让最终的插值曲线达到 C1 连续,则需要让前一个样条在 vi 的控制点与当前样条在 vi 的控制点分别处于最终曲线在 vi 处切线对等的两侧:

在上面的曲线中,蓝色的线就是曲线在点 vi 处的切线,红色的点就是三次 Bézier 曲线的控制点,分别处于切线对等的两侧.对于两个端点 v0 和 v4,我们直接将这两个向量的控制点取为它们本身(这不是唯一的做法,但这样是可行的),最终得到一个平滑的曲线。我们希望将类似的逻辑带到四元数的超球面上,得到四元数序列的插值的方法,但在此之前我们需要了解如何构造一个三次 Bézier 曲线。
de Casteljau 算法
Bézier 曲线的构造有个著名的递归算法叫做 de Casteljau 算法(de Casteljau’s Algorithm),它对任意次方的 Bézier 曲线都是成立的,但是这里我们只关注三次 Bézier 曲线的情况。
这个算法最基本的思想就是线性插值的嵌套。假设我们有四个向量 v0,v1,v2,v3,那么我们可以这样子获得最终的三次 Bézier 曲线:
首先,我们对每一对向量 v0v1、v1v2、v2v3 进行线性插值,获得 v01、v12、v23:
v01=Lerp(v0,v1;t)v12=Lerp(v1,v2;t)v23=Lerp(v2,v3;t)
之后,我们对 v01v12 和 v12v23 这两对向量进行线性插值,获得 v012 和 v123
v012=Lerp(v01,v12;t)v123=Lerp(v12,v23;t)
最后,对 v012 和 v123 进行线性插值获得 v0123,这个向量就是我们想要的最终结果,它就是三次 Bézier 曲线上的点:
v0123=Lerp(v012,v123;t)
虽然这个算法看起来很繁琐,但是我们可以通过一张图来理解它(取 t = 0.4):

可以看到,虽然我们一直在使用线性插值,最终获得的却是一条三次 Bézier 曲线(黑色的线)。
如果将这些式子合并起来,我们就能得到三次 Bézier 曲线的递归公式。因为这个式子太长了,我将 Lerp(vi,vi+1;t) 简写为 L(vi,vi+1;t):
Beˊzier(v0,v1,v2,v3;t)=L(L(L(v0,v1;t),L(v1,v2;t);t),L(L(v1,v2;t),L(v2,v3;t);t);t)
如果将 Lerp 的定义 Lerp(vi,vi+1;t)=(1−t)vi+tvi+1 不断代入并展开的话,我们就能获得这样一个式子:
Beˊzier(v0,v1,v2,v3;t)=(1−t)3v0+3(1−t)2tv1+3(1−t)t2v2+t3v3
因为每项的次数都是 3,所以我们说它是一个三次 Bézier 曲线。
我们可以直接将递归的公式运用到四元数上,得到四元数的球面 Bézier 曲线公式,但因为球面的线性插值不是 Lerp 而是 Slerp,我们需要将公式中所有的 Lerp 全部换成 Slerp(你可以想象一下,将四个向量形成的四边形看作是一个网格(Mesh),之后将这个网格贴在球面上)。同样因为公式太长,我会将 Slerp(qi,qi+1;t) 简写为 S(qi,qi+1;t):
SBeˊzier(q0,q1,q2,q3;t)=S(S(S(q0,q1;t),S(q1,q2;t);t),S(S(q1,q2;t),S(q2,q3;t);t);t)
很明显这个方法实在是太复杂了。仅仅是一个 Slerp 就要使用四个三角函数,而我们这里一共有 7个 Slerp,如果真的要使用它进行插值会对性能产生非常大的影响。
Squad
三次 Bézier 曲线实际上是嵌套了三层一次(one-order)插值,而 Squad 则使用的是一层二次插值嵌套了一层一次插值。
我们首先是分别对 v0v3 和 v1v2 进行插值,得到 v03 和 v12:
v03=Lerp(v0,v3;t)v12=Lerp(v1,v2;t)
之后,我们使用 2t(1−t) 为参数,对 v03v12 进行二次插值,得到最终的 v0312 :
v0312=Lerp(v03,v12;2t(1−t))
上述过程可以通过下图阐明(t=0.4),黑色曲线就是生成的插值曲线:

当然,我们也可以把它写成递归形式:
Squad(v0,v1,v2,v3;t)=Lerp(Lerp(v0,v3;t),Lerp(v1,v2;t);2t(1−t))
可以看到,这样的插值要比三次 Bézier 曲线简单很多,将七次 Lerp 减少到了三次.虽然最终的曲线与三次 Bézier 曲线不完全相同,但是已经很近似了。我们可以看几个对比。下图中,左边是三次 Bézier 曲线,右边是 Squad 曲线:

如果利用 Lerp 的定义 Lerp(vi,vi+1;t)=(1−t)vi+tvi+1 将递归式展开的话,我们能得到这样的式子
Squad(v0,v1,v2,v3;t)=(2t2−2t+1)(1−t)v0+2(1−t)2tv1+2(1−t)t2v2+t(2t2−2t+1)v3
它仍是一个三次曲线,只不过系数有所不同。
如果我们将这个递归公式用于球面,就能得到四元数的 Squad 公式。
Squad(q0,q1,q2,q3;t)=Slerp(Slerp(q0,q3;t),Slerp(q1,q2;t);2t(1−t))
我们知道 Slerp(qi,qi+1;t)=(qi+1qi∗)tqi,所以我们可以将 Squad 写成指数形式:
Squad(q0,q1,q2,q3;t)=(Slerp(q1,q2;t)(Slerp(q0,q3;t))∗)2t(1−t)Slerp(q0,q3;t)
Squad 应用
接下来,我们回到本章最初的主题,对多个单位四元数进行插值。如果我们有一个四元数序列 q0,q1,…,qn,我们希望对每一对四元数 qi 和 qi+1 都使用 Squad 进行插值,所以我们有
Squad(qi,si,si+1,qi+1;t)=Slerp(Slerp(qi,qi+1;t),Slerp(si,si+1;t);2t(1−t))
现在,留下来的问题就是找出中间的控制点 si 和 si+1 了。类似于 Bézier 曲线的样条,我们同样需要前一个四元数 qi−1 以及 qi+2 的信息。
si 的推导还是比较复杂的,但是它最基本的理念非常简单:让 Squad 在切换点可导,从而达到 C1 连续。也就是说,我们希望 qi−1qi 插值时在 t=1 处的导数,与 qiqi+1 插值时在 t=0 处的导数相等:
Squad′(qi−1,si−1,si,qi;1)=Squad′(qi,si,si+1,qi+1;0)
如果我们想要从这里继续推导下去的话,就需要用到单位四元数导数的定义(它和 q=[cos(θ),sin(θ)u]=euθ 有关)。最终得到
si=qiexp(−4log(qi∗qi−1)+log(qi∗qi+1))
注意,和 Bézier 曲线的样条不同的是,这里的 si 在对 qi−1qi 插值时和对 qiqi+1 插值时都是相同的,不像之前是处于切线的两端不同的两个向量。
与两个四元数之间的插值一样,Squad 同样会受到双重覆盖的影响。我们在计算中间控制点和插值之前,需要先选中一个四元数,比如说 qi,检测它与其它三个四元数之间的夹角,如果是钝角就翻转,将插值的路线减到最小。