四元数和三维旋转(三)

本文最后更新于:2025年4月17日 晚上

四元数插值

假设有两个旋转变换 q0=[cos(θ0),sin(θ0)u0]q_0 = [\cos(\theta_{0}), \sin(\theta_{0})\pmb{u_0}]q1=[cos(θ1),sin(θ1)u1]q_1 = [\cos(\theta_{1}), \sin(\theta_{1})\pmb{u_1}],我们希望找出一些中间变换 qtq_{t},让初始变换 q0q_{0} 能够平滑地过渡到最终变换 q1q_{1}tt 的取值可以是 t[0,1]t \in [0, 1],当 t=0t = 0qtq_{t} 等同于初始变换 q0q_{0},而 t=1t = 1qtq_{t} 等同于最终变换 q1q_{1}

由于插值的对象是两个变换,想象起来可能非常困难,我们不妨假设 3D 空间中有任意一个向量 vv,那么 q0q_{0} 会将 vv 变换到 v0=q0vq0v_{0} = q_{0}vq_{0}^*,而 q1q_{1} 会将 vv 变换到 v1=q1vq1v_{1} = q_{1}vq_{1}^*。我们需要找出中间向量 vt=qtvqtv_{t} = q_{t}vq_{t}^* 所对应的变换 qtq_{t},使 vv 旋转到 v0v_{0}v1v_{1} 中间的某个位置 vtv_{t}:

我们可以看到,这个旋转的变化量其实对应的仍是一个旋转。它将由 q0q_{0} 变换到 v0v_{0} 的向量进一步旋转到 vtv_{t}。这个旋转拥有某一个固定的旋转轴 ut\pmb{u_{t}},我们只需要缩放这个变换所对应的角度 ϕ\phi 就能够达到插值的目的了。

那么,现在的问题是,我们该怎么获得这个旋转的变化量。我们不妨考虑一下什么变换 Δq\Delta q 能将已经旋转到 v0v_{0} 的向量 vv 直接变换到 v1v_{1}。这其实就是一个旋转的复合,我们先进行 q0q_{0} 变换,再进行 Δq\Delta q 变换,它们复合的结果需要等于 q1q_{1} 变换,也就是说:

Δqq0=q1\Delta qq_{0} = q_{1}

那么,

Δqq0q01=q1q01Δq=q1q01\begin{aligned} \Delta qq_{0}q_{0}^{-1} &= q_{1}q_{0}^{-1}\\ \\ \Delta q &= q_{1}q_{0}^{-1}\\ \end{aligned}

因为所有的旋转 qq 都是单位四元数,q1=qq^{-1} = q^*,它又可以写成:

Δq=q1q0\Delta q = q_{1}q_{0}^*

如果我们对 Δq\Delta qtt 次方,(Δq)t(\Delta q)^t 就能缩放这个旋转所对应的角度了。所以,我们就能得出插值的公式:

qt=Slerp(q0,q1;t)=(q1q0)tq0q_t = Slerp(q_0, q_1; t) = (q_1q_0^*)^tq_0

可以发现,当 t=0t = 0 时,qt=q0q_t = q_0;而当 t=1t = 1 时,qt=q1q_t = q_1;如果 tt 为中间值,比如说 t=0.4t = 0.4 时,qt=(q1q0)0.4q0q_t = (q_{1}q_{0}^*)^{0.4}q_{0},它会先进行 q0q_0 变换将 vv 变换到 v0v_0,并在此基础上向 v1v_1 旋转 40%.

这个插值方法称为Slerp,但它涉及到多个四元数的乘法,而且还包含幂运算,实际应用中效率很低。为了理解它我们还需要研究一下 3D 空间的旋转与四元数的 4D 向量空间之间的关系

3D空间旋转变化量 vs. 四元数4D向量空间夹角

为了探讨这个关系,我们来实际计算一下 Δq\Delta q。由于这个关系和角度有关,我们只需要关心 Δq\Delta 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),]\begin{aligned} \Delta q &= q_{1}q_{0}^*\\ &= [\cos(\theta_{1}), \sin(\theta_{1})\pmb{u_1}][\cos(\theta_{0}), -\sin(\theta_{0})\pmb{u_0}]\\ &= [\cos(\theta_{0})\cos(\theta_{1}) - (\sin(\theta_{1})\pmb{u_1})\cdot(-\sin(\theta_{0})\pmb{u_0}), \dots]\\ &= [\cos(\theta_{0})\cos(\theta_{1}) + (\sin(\theta_{1})\pmb{u_1})\cdot(\sin(\theta_{0})\pmb{u_0}), \dots]\\ \end{aligned}

如果将 q0q_{0}q1q_{1} 看作是两个四维向量,我们可以发现 Δq\Delta q 的实部正好就是 q0q_{0}q1q_{1} 点乘的结果 q0q1q_{0} \cdot q_{1},因为 q0q_{0}q1q_{1} 都是单位四元数,q0q1q_{0} \cdot q_{1} 正好是这两个四元数在 4D 空间中夹角的余弦值,我们将这个夹角称为 θ\theta,那么 q0q1=cos(θ)q_{0} \cdot q_{1} = \cos(\theta)

我们又知道,Δq\Delta q 表示的也是一个旋转,而如果它代表的旋转的角度是 2ϕ2\phi,那么 Δq\Delta q 的实数部为 Δq=[cos(ϕ),]\Delta q = [\cos(\phi), \dots]。所以

Δq=[cos(ϕ),]=[cos(θ),]cos(ϕ)=cos(θ)\Delta q = [\cos(\phi), \dots] = [\cos(\theta), \dots]\\ \cos(\phi) = \cos(\theta)

因为 ϕ\phiθ\theta 都是夹角,ϕθ[0,π]\phi,\theta \in [0, \pi],所以这个方程有唯一的解

ϕ=θ\phi = \theta

这也就是说,q0q_{0}q1q_{1} 作为向量在 4D 四元数空间中的夹角 θ\theta,正好是它们旋转变化量 Δq\Delta q 的所代表旋转的角度的一半,即 θ=12(2ϕ)\theta = \frac{1}{2}(2\phi)。所以,我们可以直接用插值向量的方法对旋转进行插值。

为了更直观的理解这层关系,请看下面这两幅图。虽然四元数是出于四维空间之内的,但是因为只有两个四元数,我们可以将它们投影到一个二维的圆上来,也就是左图。右侧则是 3D 空间中发生的旋转改变

可以看到,当 q1q_{1}q0q_{0} 之间的夹角为 θ\theta 时,旋转的变化量正好是 2θ2\theta。如果我们在圆上有一个单位四元数 qtq_{t},使得它与 q0q_{0} 的夹角为 tθt\theta,与 q1q_{1} 的夹角为 (1t)θ(1 - t)\theta,那么我们就能保证在 3D 空间中,它相对于 q0q_{0} 的旋转变化量为 2tθ2t\theta,相对于 q1q_{1} 的旋转变化量为 2(1t)θ2(1 - t)\theta

现在,两个单位四元数的插值就被我们简化为了一个圆上(其实是超球面的一部分)两个向量的插值,我们能直接套用向量的插值公式对两个四元数进行插值。接下来,我们就来讨论如何对两个向量进行插值。

Lerp, Nlerp, Slerp

不论是哪种插值方法,我们都希望将中间向量 vtv_{t} 写为初始向量 v0v_{0} 和最终向量 v1v_{1} 的线性组合,也就是说:

vt=αv0+βv1v_{t} = \alpha v_{0} + \beta v_{1}

其中,系数 α\alphaβ\beta 都是 tt 的函数。不同的插值方法只是拥有不同的系数而已。

Lerp

首先是两个向量插值最简单的一种形式:线性插值,也叫做“Lerp”。Lerp 会沿着一条直线进行插值,如果将 v0\pmb{v_{0}}v1\pmb{v_{1}} 看做是三角形的两个边,那么 vt\pmb{v_{t}} 会指向三角形的第三条边。

我们能将 vt\pmb{v_{t}} 写为两个向量的和。

vt=Lerp(v0,v1,t)=v0+t(v1v0)=(1t)v0+tv1\begin{aligned} \pmb{v_{t}} = Lerp(\pmb{v_{0}}, \pmb{v_{1}}, t) &= \pmb{v_{0}} + t(\pmb{v_{1}} - \pmb{v_{0}})\\ &= (1 - t)\pmb{v_{0}} + t\pmb{v_{1}} \end{aligned}

如果将 Lerp 的结果应用到单位四元数上,我们可以得到:

qt=Lerp(q0,q1,t)=(1t)q0+tq1q_{t} = Lerp(q_{0}, q_{1}, t) = (1 - t)q_{0} + tq_{1}

当然,因为我们是沿着一条直线(也就是圆上的一个弦)进行插值的,这样插值出来的四元数并不是单位四元数。

Nlerp

虽然这样插值出来的 qtq_{t} 并不是单位四元数,但只要将 qtq_{t} 除以他的模长 qt\lVert q_{t} \rVert 就能够将其转化为一个单位四元数了。

我们将这种先对向量进行插值,再进行正规化的插值方法称为正规化线性插值,或者“Nlerp”。与 Lerp 不同,Nlerp 的两个输入向量必须是单位向量,否则插值出来的结果不会经过初始和最终向量。下面分别是向量和四元数的 Nlerp 公式。

vt=Nlerp(v0,v1,t)=(1t)v0+tv1(1t)v0+tv1qt=Nlerp(q0,q1,t)=(1t)q0+tq1(1t)q0+tq1v_{t} = Nlerp(\pmb{v_{0}}, \pmb{v_{1}}, t) = \frac{(1 - t)\pmb{v_{0}} + t\pmb{v_{1}}}{\lVert (1 - t)\pmb{v_{0}} + t\pmb{v_{1}} \rVert}\\ \\ q_{t} = Nlerp(q_{0}, q_{1}, t) = \frac{(1 - t)q_{0} + tq_{1}}{\lVert (1 - t)q_{0} + tq_{1} \rVert}

Nlerp 插值仍然存在有一定的问题,当需要插值的弧比较大时,vt\pmb{v_{t}} 的角速度会有显著的变化.我们可以来看一个例子:

这五个 tt 值将整个弧和弦分割成了四个部分.虽然弦上的四段是等长的,但是四个弧是完全不相等的。t=0t = 0t=0.25t = 0.25 之间的弧(红色)明显比 t=0.25t = 0.25t=0.50t = 0.50 的弧(蓝色)要短了不少.

这也就是说,在同等时间内,vt\pmb{v_{t}} 扫过的角度是不同的。vt\pmb{v_{t}} 扫过的速度(或者说角速度)首先会不断地增加,到 t=0.50t = 0.50 之后会开始减速,所以 Nlerp 插值不能保证均匀的角速度.

Slerp

为了解决这个问题,我们可以转而对角度进行线性插值。也就是说,如果 v1\pmb{v_{1}}v2\pmb{v_{2}} 之间的夹角为 θ\theta,那么

θt=(1t)0+tθ=tθ\theta_{t} = (1 - t)\cdot 0 + t\theta = t\theta

因为对角度线性插值直接是让向量在球面上的一个弧上旋转,所以又称球面线性插值,或者“Slerp”。

上面的公式并没有涉及到任何的向量,我们希望将 vt\pmb{v_{t}} 写为 v0\pmb{v_{0}}v1\pmb{v_{1}} 的线性组合

vt=αv0+βv1(1)\pmb{v_{t}} = \alpha\pmb{v_{0}} + \beta\pmb{v_{1}} \tag{1}

这里的 v0\pmb{v_{0}}v1\pmb{v_{1}} 仍是单位向量,为了求出这其中的 α\alphaβ\beta,我们需要借助图像来找出一些关系:

我们可以先对(1)式的两边同时点乘 v0\pmb{v_{0}}

v0vt=v0(αv0+βv1)v0vt=α(v0v0)+β(v0v1)\begin{aligned} \pmb{v_{0}}\cdot\pmb{v_{t}} &= \pmb{v_{0}}\cdot(\alpha\pmb{v_{0}} + \beta\pmb{v_{1}})\\ \pmb{v_{0}}\cdot\pmb{v_{t}} &= \alpha(\pmb{v_{0}}\cdot\pmb{v_{0}}) + \beta(\pmb{v_{0}}\cdot\pmb{v_{1}}) \end{aligned}

我们知道,v0\pmb{v_{0}}vt\pmb{v_{t}} 之间的夹角是 tθt\thetav0\pmb{v_{0}} 与它自身之间的夹角为 0,v0\pmb{v_{0}}v1\pmb{v_{1}} 之间的夹角是 θ\theta,而且所有的向量都是单位向量,所以

cos(tθ)=α+βcos(θ)(2)\cos(t\theta) = \alpha + \beta\cos(\theta) \tag{2}

同理,我们将(1)式的两边同时点乘 v1\pmb{v_{1}},构造第二个方程

v1vt=v1(αv0+βv1)v1vt=α(v1v0)+β(v1v1)\begin{aligned} \pmb{v_{1}}\cdot\pmb{v_{t}} &= \pmb{v_{1}}\cdot(\alpha\pmb{v_{0}} + \beta\pmb{v_{1}})\\ \pmb{v_{1}}\cdot\pmb{v_{t}} &= \alpha(\pmb{v_{1}}\cdot\pmb{v_{0}}) + \beta(\pmb{v_{1}}\cdot\pmb{v_{1}}) \end{aligned}

cos((1t)θ)=αcos(θ)+β(3)\cos((1 - t)\theta) = \alpha\cos(\theta) + \beta \tag{3}

现在我们可以用(2)式和(3)式求出 α\alphaβ\beta 了。

由(2)式可以得到

α=cos(tθ)βcos(θ)(4)\alpha = \cos(t\theta) - \beta\cos(\theta) \tag{4}

将(4)式代入(3)式

cos((1t)θ)=(cos(tθ)βcos(θ))cos(θ)+βcos((1t)θ)=cos(tθ)cos(θ)βcos2(θ)+ββ(1cos2(θ))=cos((1t)θ)cos(tθ)cos(θ)β=cos(θtθ)cos(tθ)cos(θ)sin2(θ)β=cos(θ)cos(tθ)+sin(θ)sin(tθ)cos(tθ)cos(θ)sin2(θ)β=sin(θ)sin(tθ)sin2(θ)β=sin(tθ)sin(θ)\begin{aligned} \cos((1 - t)\theta) &= (\cos(t\theta) - \beta\cos(\theta))\cos(\theta) + \beta\\ \cos((1 - t)\theta) &= \cos(t\theta)\cos(\theta) - \beta\cos^2(\theta) + \beta\\ \beta(1 - \cos^2(\theta)) &= \cos((1 - t)\theta) - \cos(t\theta)\cos(\theta)\\ \beta &= \frac{\cos(\theta - t\theta) - \cos(t\theta)\cos(\theta)}{\sin^2(\theta)}\\ \beta &= \frac{\cos(\theta)\cos(t\theta) + \sin(\theta)\sin(t\theta) - \cos(t\theta)\cos(\theta)}{\sin^2(\theta)}\\ \beta &= \frac{\sin(\theta)\sin(t\theta)}{\sin^2(\theta)}\\ \beta &= \frac{\sin(t\theta)}{\sin(\theta)} \end{aligned}

β\beta 代入(4)式解出 α\alpha

α=cos(tθ)(sin(tθ)sin(θ))cos(θ)=cos(tθ)sin(θ)sin(tθ)cos(θ)sin(θ)=sin((1t)θ)sin(θ)\begin{aligned} \alpha &= \cos(t\theta) - (\frac{\sin(t\theta)}{\sin(\theta)})\cos(\theta)\\ &= \frac{\cos(t\theta)\sin(\theta) - \sin(t\theta)\cos(\theta)}{\sin(\theta)}\\ &= \frac{\sin((1 - t)\theta)}{\sin(\theta)} \end{aligned}

α\alphaβ\beta 代回(1)式,我们可以得到向量的Slerp公式

vt=Slerp(v0,v1,t)=sin((1t)θ)sin(θ)v0+sin(tθ)sin(θ)v1\pmb{v_{t}} = Slerp(\pmb{v_{0}}, \pmb{v_{1}}, t) = \frac{\sin((1 - t)\theta)}{\sin(\theta)}\pmb{v_{0}} + \frac{\sin(t\theta)}{\sin(\theta)}\pmb{v_{1}}

类似地,我们有四元数的 Slerp 公式:

qt=Slerp(q0,q1,t)=sin((1t)θ)sin(θ)q0+sin(tθ)sin(θ)q1q_{t} = Slerp(q_{0}, q_{1}, t) = \frac{\sin((1 - t)\theta)}{\sin(\theta)}q_{0} + \frac{\sin(t\theta)}{\sin(\theta)}q_{1}

其中 q0q_{0}q1q_{1} 之间的夹角 θ\theta 可以直接使用它们点乘的结果来得出,即

θ=arccos(q0q1)\theta = \arccos(q_{0} \cdot q_{1})

这里导出的公式会比之前利用幂运算的公式要高效很多,但是它仍然涉及到三个三角函数以及一个反三角函数的运算,所以还是会比 Nlerp 要慢一点。如果要插值的角度比较小的话,Nlerp 其实相对于 Slerp 的误差并没有那么大。为了提高效率,我们经常会使用 Nlerp 来代替 Slerp。我们也能用一些数值分析的方法来近似并优化四元数的 Slerp。

除了效率问题之外,我们在实现 Slerp 时要注意,如果单位四元数之间的夹角 θ\theta 非常小,那么 sin(θ)\sin(\theta) 可能会由于浮点数的误差被近似为 0.0,从而导致除以 0 的错误.所以,我们在实施 Slerp 之前,需要检查两个四元数的夹角是否过小(或者完全相同)。一旦发现这种问题,我们就必须改用 Nlerp 对两个四元数进行插值,这时候 Nlerp 的误差非常小所以基本不会与真正的 Slerp 有什么区别。

双倍覆盖带来的问题

如果你还记得,两个不同的单位四元数 qqq-q 对应的其实是同一个旋转,这个特性显然会对我们的插值造成一些影响.虽然 qqq-q 对向量变换的最终效果是完全相同的,但是它们作为向量相差了 π\pi 弧度:

可以看到,虽然我们能够将 q0q_{0} 向左插值至 q1q_{1}(蓝色的弧),但这会将 3D 空间中的向量旋转接近 360 度,而实际上这两个旋转相差并没有那么多,它并不是 3D 空间中的弧面最短路径。而如果我们将 q0q_{0} 向右插值至等价的 q1-q_{1}(红色的弧),它的旋转变化量就会比插值到 q1q_{1} 要小很多,所以 q0q_{0} 插值到 q1-q_{1} 才是插值的最短路径。

这也就告诉我们,在对两个单位四元数进行插值之前,我们需要先检测 q0q_{0}q1q_{1} 之间是否是钝角,即检测它们点积的结果 q0q1q_{0} \cdot q_{1} 是否为负数。如果 q0q1<0q_{0} \cdot q_{1} < 0 ,那么我们就反转其中的一个四元数,比如说将 q1q_{1} 改为 q1-q_{1},并使用 q0q_{0}q1-q_{1} 之间新的夹角来进行插值,这样才能保证插值的路径是最短的。

Squad

Slerp已经是我们理想中的插值方式了:它直接对角度插值,插值角度匀速变化,运算效率尚可。但是它还有一个小问题:角度变化的速率等于夹角,即 dθtdt=ddt(tθ)=θ\frac{d\theta_{t}}{dt} = \frac{d}{dt}(t\theta) = \theta,这就意味着,当我们在多个角速度之间插值的时候,当在不同的四元数之间插值的时候,速率会发生突变,或者说在切断点处不可导。从数学上讲,函数 ff 连续并不意味着函数的一阶导连续(前者称为 C0C^0 连续,后者称为 C1C^1 连续)。

为此,我们希望能以牺牲固定角速度为条件,让插值的曲线能够在高阶导处也连续,下面介绍的Squad(Spherical and quadrangle)就是一种解决方法。

Bézier 曲线

假设我们有一个向量的序列 v0\pmb{v_{0}}v1\pmb{v_{1}}\dotsvn\pmb{v_{n}},如果我们想对这个序列进行插值,那么我们可以分别对每一对向量 vi\pmb{v_{i}}vi+1\pmb{v_{i+1}} 进行插值,然后将插值的曲线连接起来,也就是我们所说的样条(Spline).如果直接使用 Lerp 的话,我们会得到这样的结果(假设我们只有五个向量需要插值 v0\pmb{v_{0}}v1\pmb{v_{1}}v2\pmb{v_{2}}v3\pmb{v_{3}}v4\pmb{v_{4}}):

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

为了解决这个问题,我们可以分段对每两个向量 vi\pmb{v_{i}}vi+1\pmb{v_{i+1}} 之间使用 Bézier曲线进行插值,之后将所有的曲线(样条)连接起来.因为我们需要让曲线的一阶导数(或者说曲线的趋势)连续,我们还需要知道它们的前一个向量 vi1\pmb{v_{i-1}} 和后一个向量 vi+2\pmb{v_{i+2}},并且用它们生成两个控制点 si\pmb{s_{i}}si+1\pmb{s_{i+1}} 来控制曲线的趋势.我们会使用 vi\pmb{v_{i}}vi+1\pmb{v_{i+1}} 作为端点(曲线会经过这两个点),si\pmb{s_{i}}si+1\pmb{s_{i+1}} 作为中间的控制点,使用一个三次 Bézier 曲线(Cubic Bézier Curve,四个点)来近似这个两个向量之间的插值。

在我们的例子中,因为我们一共有四对向量(v0v1\pmb{v_{0}}\pmb{v_{1}}v1v2\pmb{v_{1}}\pmb{v_{2}}v2v3\pmb{v_{2}}\pmb{v_{3}}v3v4\pmb{v_{3}}\pmb{v_{4}}),我们会使用四个三次 Bézier 曲线对这五个点进行插值.我们知道,对于三次Bézier 曲线所产生的样条,如果想让最终的插值曲线达到 C1C^1 连续,则需要让前一个样条在 vi\pmb{v_{i}} 的控制点与当前样条在 vi\pmb{v_{i}} 的控制点分别处于最终曲线在 vi\pmb{v_{i}} 处切线对等的两侧:

在上面的曲线中,蓝色的线就是曲线在点 vi\pmb{v_{i}} 处的切线,红色的点就是三次 Bézier 曲线的控制点,分别处于切线对等的两侧.对于两个端点 v0\pmb{v_{0}}v4\pmb{v_{4}},我们直接将这两个向量的控制点取为它们本身(这不是唯一的做法,但这样是可行的),最终得到一个平滑的曲线。我们希望将类似的逻辑带到四元数的超球面上,得到四元数序列的插值的方法,但在此之前我们需要了解如何构造一个三次 Bézier 曲线。

de Casteljau 算法

Bézier 曲线的构造有个著名的递归算法叫做 de Casteljau 算法(de Casteljau’s Algorithm),它对任意次方的 Bézier 曲线都是成立的,但是这里我们只关注三次 Bézier 曲线的情况。

这个算法最基本的思想就是线性插值的嵌套。假设我们有四个向量 v0\pmb{v_{0}}v1\pmb{v_{1}}v2\pmb{v_{2}}v3\pmb{v_{3}},那么我们可以这样子获得最终的三次 Bézier 曲线:

首先,我们对每一对向量 v0v1\pmb{v_{0}}\pmb{v_{1}}v1v2\pmb{v_{1}}\pmb{v_{2}}v2v3\pmb{v_{2}}\pmb{v_{3}} 进行线性插值,获得 v01\pmb{v_{01}}v12\pmb{v_{12}}v23\pmb{v_{23}}

v01=Lerp(v0,v1;t)v12=Lerp(v1,v2;t)v23=Lerp(v2,v3;t)\pmb{v_{01}} = Lerp(\pmb{v_{0}}, \pmb{v_{1}}; t)\\ \pmb{v_{12}} = Lerp(\pmb{v_{1}}, \pmb{v_{2}}; t)\\ \pmb{v_{23}} = Lerp(\pmb{v_{2}}, \pmb{v_{3}}; t)\\

之后,我们对 v01v12\pmb{v_{01}}\pmb{v_{12}}v12v23\pmb{v_{12}}\pmb{v_{23}} 这两对向量进行线性插值,获得 v012\pmb{v_{012}}v123\pmb{v_{123}}

v012=Lerp(v01,v12;t)v123=Lerp(v12,v23;t)\pmb{v_{012}} = Lerp(\pmb{v_{01}}, \pmb{v_{12}}; t)\\ \pmb{v_{123}} = Lerp(\pmb{v_{12}}, \pmb{v_{23}}; t)\\

最后,对 v012\pmb{v_{012}}v123\pmb{v_{123}} 进行线性插值获得 v0123\pmb{v_{0123}},这个向量就是我们想要的最终结果,它就是三次 Bézier 曲线上的点:

v0123=Lerp(v012,v123;t)\pmb{v_{0123}} = Lerp(\pmb{v_{012}}, \pmb{v_{123}}; t)\\

虽然这个算法看起来很繁琐,但是我们可以通过一张图来理解它(取 tt = 0.4):

可以看到,虽然我们一直在使用线性插值,最终获得的却是一条三次 Bézier 曲线(黑色的线)。

如果将这些式子合并起来,我们就能得到三次 Bézier 曲线的递归公式。因为这个式子太长了,我将 Lerp(vi,vi+1;t)Lerp(\pmb{v_{i}}, \pmb{v_{i+1}}; t) 简写为 L(vi,vi+1;t)L(\pmb{v_{i}}, \pmb{v_{i+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)Bézier(\pmb{v_{0}}, \pmb{v_{1}}, \pmb{v_{2}}, \pmb{v_{3}}; t) = L(L(L(\pmb{v_{0}}, \pmb{v_{1}}; t), L(\pmb{v_{1}}, \pmb{v_{2}}; t); t), L(L(\pmb{v_{1}}, \pmb{v_{2}}; t), L(\pmb{v_{2}}, \pmb{v_{3}}; t); t); t)

如果将 Lerp 的定义 Lerp(vi,vi+1;t)=(1t)vi+tvi+1Lerp(\pmb{v_{i}}, \pmb{v_{i+1}}; t) = (1 - t)\pmb{v_{i}} + t\pmb{v_{i+1}} 不断代入并展开的话,我们就能获得这样一个式子:

Beˊzier(v0,v1,v2,v3;t)=(1t)3v0+3(1t)2tv1+3(1t)t2v2+t3v3Bézier(\pmb{v_{0}}, \pmb{v_{1}}, \pmb{v_{2}}, \pmb{v_{3}}; t) = (1 - t)^3\pmb{v_{0}} + 3(1 - t)^2t\pmb{v_{1}} + 3(1 - t)t^2\pmb{v_{2}} + t^3\pmb{v_{3}}

因为每项的次数都是 3,所以我们说它是一个三次 Bézier 曲线。

我们可以直接将递归的公式运用到四元数上,得到四元数的球面 Bézier 曲线公式,但因为球面的线性插值不是 Lerp 而是 Slerp,我们需要将公式中所有的 Lerp 全部换成 Slerp(你可以想象一下,将四个向量形成的四边形看作是一个网格(Mesh),之后将这个网格贴在球面上)。同样因为公式太长,我会将 Slerp(qi,qi+1;t)Slerp(q_{i}, q_{i+1}; t) 简写为 S(qi,qi+1;t)S(q_{i}, q_{i+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)SBézier(q_{0}, q_{1}, q_{2}, q_{3}; t) = S(S(S(q_{0}, q_{1}; t), S(q_{1}, q_{2}; t);t),S(S(q_{1}, q_{2}; t), S(q_{2}, q_{3}; t); t); t)

很明显这个方法实在是太复杂了。仅仅是一个 Slerp 就要使用四个三角函数,而我们这里一共有 7个 Slerp,如果真的要使用它进行插值会对性能产生非常大的影响。

Squad

三次 Bézier 曲线实际上是嵌套了三层一次(one-order)插值,而 Squad 则使用的是一层二次插值嵌套了一层一次插值。

我们首先是分别对 v0v3\pmb{v_{0}}\pmb{v_{3}}v1v2\pmb{v_{1}}\pmb{v_{2}} 进行插值,得到 v03\pmb{v_{03}}v12\pmb{v_{12}}

v03=Lerp(v0,v3;t)v12=Lerp(v1,v2;t)\pmb{v_{03}} = Lerp(\pmb{v_{0}}, \pmb{v_{3}}; t)\\ \pmb{v_{12}} = Lerp(\pmb{v_{1}}, \pmb{v_{2}}; t)\\

之后,我们使用 2t(1t)2t(1 - t) 为参数,对 v03v12\pmb{v_{03}}\pmb{v_{12}} 进行二次插值,得到最终的 v0312\pmb{v_{0312}}

v0312=Lerp(v03,v12;2t(1t))\pmb{v_{0312}} = Lerp(\pmb{v_{03}}, \pmb{v_{12}}; 2t(1 - t))\\

上述过程可以通过下图阐明(t=0.4t = 0.4),黑色曲线就是生成的插值曲线:

当然,我们也可以把它写成递归形式:

Squad(v0,v1,v2,v3;t)=Lerp(Lerp(v0,v3;t),Lerp(v1,v2;t);2t(1t))Squad(\pmb{v_{0}}, \pmb{v_{1}}, \pmb{v_{2}}, \pmb{v_{3}}; t) = Lerp(Lerp(\pmb{v_{0}}, \pmb{v_{3}}; t), Lerp(\pmb{v_{1}}, \pmb{v_{2}}; t); 2t(1 - t))

可以看到,这样的插值要比三次 Bézier 曲线简单很多,将七次 Lerp 减少到了三次.虽然最终的曲线与三次 Bézier 曲线不完全相同,但是已经很近似了。我们可以看几个对比。下图中,左边是三次 Bézier 曲线,右边是 Squad 曲线:

如果利用 Lerp 的定义 Lerp(vi,vi+1;t)=(1t)vi+tvi+1Lerp(\pmb{v_{i}}, \pmb{v_{i+1}}; t) = (1 - t)\pmb{v_{i}} + t\pmb{v_{i+1}} 将递归式展开的话,我们能得到这样的式子

Squad(v0,v1,v2,v3;t)=(2t22t+1)(1t)v0+2(1t)2tv1+2(1t)t2v2+t(2t22t+1)v3Squad(\pmb{v_{0}}, \pmb{v_{1}}, \pmb{v_{2}}, \pmb{v_{3}}; t) = (2t^2 - 2t + 1)(1 - t)\pmb{v_{0}} + 2(1 - t)^2t\pmb{v_{1}} + 2(1 - t)t^2\pmb{v_{2}} + t(2t^2 - 2t + 1)\pmb{v_{3}}

它仍是一个三次曲线,只不过系数有所不同。

如果我们将这个递归公式用于球面,就能得到四元数的 Squad 公式。

Squad(q0,q1,q2,q3;t)=Slerp(Slerp(q0,q3;t),Slerp(q1,q2;t);2t(1t))Squad(q_{0}, q_{1}, q_{2}, q_{3}; t) = Slerp(Slerp(q_{0}, q_{3}; t), Slerp(q_{1}, q_{2}; t); 2t(1 - t))

我们知道 Slerp(qi,qi+1;t)=(qi+1qi)tqiSlerp(q_{i}, q_{i+1}; t) = (q_{i+1}q_{i}^*)^tq_{i},所以我们可以将 Squad 写成指数形式:

Squad(q0,q1,q2,q3;t)=(Slerp(q1,q2;t)(Slerp(q0,q3;t)))2t(1t)Slerp(q0,q3;t)Squad(q_{0}, q_{1}, q_{2}, q_{3}; t) = (Slerp(q_{1}, q_{2}; t)(Slerp(q_{0}, q_{3}; t))^*)^{2t(1-t)}Slerp(q_{0}, q_{3}; t)

Squad 应用

接下来,我们回到本章最初的主题,对多个单位四元数进行插值。如果我们有一个四元数序列 q0q_{0}q1q_{1}\dotsqnq_{n},我们希望对每一对四元数 qiq_{i}qi+1q_{i+1} 都使用 Squad 进行插值,所以我们有

Squad(qi,si,si+1,qi+1;t)=Slerp(Slerp(qi,qi+1;t),Slerp(si,si+1;t);2t(1t))Squad(q_{i}, s_{i}, s_{i+1}, q_{i+1}; t) = Slerp(Slerp(q_{i}, q_{i+1}; t), Slerp(s_{i}, s_{i+1}; t); 2t(1 - t))

现在,留下来的问题就是找出中间的控制点 sis_{i}si+1s_{i+1} 了。类似于 Bézier 曲线的样条,我们同样需要前一个四元数 qi1q_{i-1} 以及 qi+2q_{i+2} 的信息。

sis_{i} 的推导还是比较复杂的,但是它最基本的理念非常简单:让 Squad 在切换点可导,从而达到 C1C^1 连续。也就是说,我们希望 qi1qiq_{i-1}q_{i} 插值时在 t=1t = 1 处的导数,与 qiqi+1q_{i}q_{i+1} 插值时在 t=0t = 0 处的导数相等:

Squad(qi1,si1,si,qi;1)=Squad(qi,si,si+1,qi+1;0)Squad'(q_{i-1}, s_{i-1}, s_{i}, q_{i}; 1) = Squad'(q_{i}, s_{i}, s_{i+1}, q_{i+1}; 0)

如果我们想要从这里继续推导下去的话,就需要用到单位四元数导数的定义(它和 q=[cos(θ),sin(θ)u]=euθq = [\cos(\theta), \sin(\theta)\pmb{u}] = e^{u\theta} 有关)。最终得到

si=qiexp(log(qiqi1)+log(qiqi+1)4)s_{i} = q_{i}exp(-\frac{\log(q_{i}^*q_{i-1}) + \log(q_{i}^*q_{i+1})}{4})

注意,和 Bézier 曲线的样条不同的是,这里的 sis_{i} 在对 qi1qiq_{i-1}q_{i} 插值时和对 qiqi+1q_{i}q_{i+1} 插值时都是相同的,不像之前是处于切线的两端不同的两个向量。

与两个四元数之间的插值一样,Squad 同样会受到双重覆盖的影响。我们在计算中间控制点和插值之前,需要先选中一个四元数,比如说 qiq_{i},检测它与其它三个四元数之间的夹角,如果是钝角就翻转,将插值的路线减到最小。


四元数和三维旋转(三)
http://example.com/posts/四元数和三维旋转(三)/
作者
祭零小白
发布于
2022年7月20日
许可协议