core 设计

本页说明 Luna-Flow/quaternion 所实现的数学,以及其 API 为何是现在的形态。下文每个公式都是 src/quaternion.mbt 中代码实际计算的公式;凡实现与数学不一致之处,都会明确指出。

设计目标

本包为 MoonBit 提供一个四元数类型 Quaternion[T],服务于两类使用者:

  • 代数代码:把 H\mathbb H 当作精确或近似标量类型上的环,并通过 luna-generic 的 trait 使用它;
  • 几何代码:把单位四元数当作三维旋转——由轴与角或欧拉角构造它们,并对其进行复合、作用、插值与分解。

两者由同一个泛型类型服务。每个运算只要求 T 上它所需的 trait,因此环运算可以在 Int 上精确进行,而需要平方根或三角函数的运算则经由 Double。

数学背景

作为实代数的四元数

四元数 H\mathbb H 是以 1,i,j,k1, i, j, k 为基的 4 维实向量空间,

q=w+x i+y j+z k,w,x,y,z∈R,q = w + x\,i + y\,j + z\,k, \qquad w, x, y, z \in \mathbb R,

其双线性乘法由 Hamilton 关系确定11 W. R. Hamilton,“On quaternions; or on a new system of imaginaries in algebra”,1843 年。这些关系正是他刻在都柏林布鲁姆桥(Brougham Bridge)上的那几个式子。

i2=j2=k2=ijk=−1.i^2 = j^2 = k^2 = ijk = -1 .

记 q=(w,u)q = (w, \mathbf u),其中 ww 为标量部,u=(x,y,z)\mathbf u = (x, y, z) 为向量部,并把 R3\mathbb R^3 等同于纯四元数 {(0,u)}\{(0, \mathbf u)\}。Quaternion[T] 恰好存储这一对:字段 r 存 ww,三元组 vec 存 u\mathbf u。由于乘法是 R\mathbb R-双线性的,实数 (s,0)(s, \mathbf 0) 与一切元素可交换。

基元的乘积

乘积的一切性质都源自这四个关系。在 ijk=−1ijk = -1 两边右乘 kk,并利用 k2=−1k^2 = -1:

ijk k=−k−ij=−kij=k.\begin{aligned} ijk\,k &= -k \\ -ij &= -k \\ ij &= k . \end{aligned}

在 ijk=−1ijk = -1 两边左乘 ii,并利用 i2=−1i^2 = -1:−jk=−i-jk = -i,故 jk=ijk = i。反向的乘积可由这两式得出:

ji=j (jk)=j2k=−k,kj=(ij) j=i j2=−i,ik=i (ij)=i2j=−j,ki=(ij) i=i (ji)=−ik=j.\begin{aligned} ji &= j\,(jk) = j^2 k = -k, \\ kj &= (ij)\,j = i\,j^2 = -i, \\ ik &= i\,(ij) = i^2 j = -j, \\ ki &= (ij)\,i = i\,(ji) = -ik = j . \end{aligned}

因此基元的乘法与标准基的叉积相同,只是对角线上多了 −1-1:

⋅\cdotiijjkk
ii−1-1kk−j-j
jj−k-k−1-1ii
kkjj−i-i−1-1

八个元素 ±1,±i,±j,±k\pm 1, \pm i, \pm j, \pm k 构成四元数群 Q8Q_8,它可以用 2×22\times 2 复矩阵实现。22 例如 1↦I1 \mapsto I、i↦(i00−i)i \mapsto \begin{pmatrix} i & 0 \\ 0 & -i\end{pmatrix}、j↦(01−10)j \mapsto \begin{pmatrix} 0 & 1 \\ -1 & 0\end{pmatrix}、k↦(0ii0)k \mapsto \begin{pmatrix} 0 & i \\ i & 0\end{pmatrix}。这些矩阵满足 Hamilton 关系,而矩阵乘法满足结合律。 Q8Q_8 的乘法满足结合律,由双线性可知 H\mathbb H 的乘积也满足结合律。

Hamilton 积

按乘法表把 p=w1+x1i+y1j+z1kp = w_1 + x_1 i + y_1 j + z_1 k 与 q=w2+x2i+y2j+z2kq = w_2 + x_2 i + y_2 j + z_2 k 逐项展开相乘,得到

pq=(w1w2−x1x2−y1y2−z1z2)+(w1x2+x1w2+y1z2−z1y2) i+(w1y2−x1z2+y1w2+z1x2) j+(w1z2+x1y2−y1x2+z1w2) k.\begin{aligned} pq ={} & (w_1 w_2 - x_1 x_2 - y_1 y_2 - z_1 z_2) \\ &+ (w_1 x_2 + x_1 w_2 + y_1 z_2 - z_1 y_2)\, i \\ &+ (w_1 y_2 - x_1 z_2 + y_1 w_2 + z_1 x_2)\, j \\ &+ (w_1 z_2 + x_1 y_2 - y_1 x_2 + z_1 w_2)\, k . \end{aligned}

同一乘积用标量–向量形式表达更为清晰。对两个纯四元数 a\mathbf a 与 b\mathbf b,基元的平方贡献 −a1b1−a2b2−a3b3-a_1 b_1 - a_2 b_2 - a_3 b_3,混合项两两配对,例如 a1b2 ij+a2b1 ji=(a1b2−a2b1)ka_1 b_2\, ij + a_2 b_1\, ji = (a_1 b_2 - a_2 b_1)k。因此

a b=− a⋅b+a×b.\mathbf a\,\mathbf b = -\,\mathbf a\cdot\mathbf b + \mathbf a\times\mathbf b .

结合双线性以及标量可交换这一事实,

(s1,v1)(s2,v2)=s1s2+s1v2+s2v1+v1v2=(s1s2−v1⋅v2,  s1v2+s2v1+v1×v2).\begin{aligned} (s_1, \mathbf v_1)(s_2, \mathbf v_2) &= s_1 s_2 + s_1\mathbf v_2 + s_2\mathbf v_1 + \mathbf v_1\mathbf v_2 \\ &= \big(s_1 s_2 - \mathbf v_1\cdot\mathbf v_2,\ \ s_1\mathbf v_2 + s_2\mathbf v_1 + \mathbf v_1\times\mathbf v_2\big). \end{aligned}

这正是 Mul 的实现:标量部为 self.r * other.r - dot(self.vec, other.vec),向量部为 cross(self.vec, other.vec) 加上两个缩放后的向量。

交换两个因子只会翻转叉积的符号,因此

pq−qp=2 v1×v2,pq - qp = 2\,\mathbf v_1 \times \mathbf v_2 ,

两个四元数可交换当且仅当它们的向量部平行。特别地,H\mathbb H 不可交换:ij=kij = k 而 ji=−kji = -k。

上述推导用到了分量彼此可交换(s1v2=v2s1s_1\mathbf v_2 = \mathbf v_2 s_1 等)。因此泛型实现假定 T 的乘法可交换,luna-generic 提供的所有数值类型都满足这一点。

共轭与范数

共轭为 qˉ=(w,−u)\bar q = (w, -\mathbf u)。四元数乘以其共轭得到一个实数,因为平行向量的叉积为零:

qqˉ=(w2+u⋅u, −wu+wu−u×u)=(w2+x2+y2+z2, 0)=∣q∣2,q\bar q = \big(w^2 + \mathbf u\cdot\mathbf u,\ -w\mathbf u + w\mathbf u - \mathbf u\times\mathbf u\big) = \big(w^2 + x^2 + y^2 + z^2,\ \mathbf 0\big) = |q|^2 ,

同理 qˉq=∣q∣2\bar q q = |q|^2。Quaternion::square_len 计算的就是这个 ∣q∣2|q|^2,而 Quaternion::dot 是以它为范数的 R4\mathbb R^4 内积。

共轭会反转乘积顺序。设 p=(s1,v1)p = (s_1, \mathbf v_1)、q=(s2,v2)q = (s_2, \mathbf v_2),

pq‾=(s1s2−v1⋅v2, −s1v2−s2v1−v1×v2),qˉ pˉ=(s2s1−v2⋅v1, −s2v1−s1v2+v2×v1),\begin{aligned} \overline{pq} &= \big(s_1 s_2 - \mathbf v_1\cdot\mathbf v_2,\ -s_1\mathbf v_2 - s_2\mathbf v_1 - \mathbf v_1\times\mathbf v_2\big), \\ \bar q\,\bar p &= \big(s_2 s_1 - \mathbf v_2\cdot\mathbf v_1,\ -s_2\mathbf v_1 - s_1\mathbf v_2 + \mathbf v_2\times\mathbf v_1\big), \end{aligned}

由于 v2×v1=−v1×v2\mathbf v_2\times\mathbf v_1 = -\mathbf v_1\times\mathbf v_2,二者相等。所以 pq‾=qˉ pˉ\overline{pq} = \bar q\,\bar p。

范数是可乘的。利用结合律、pq‾=qˉpˉ\overline{pq} = \bar q\bar p,以及实数 ∣q∣2|q|^2 与 pp 可交换这一事实:

∣pq∣2=(pq) (pq)‾=p (qqˉ) pˉ=p ∣q∣2 pˉ=∣q∣2 ppˉ=∣p∣2 ∣q∣2.\begin{aligned} |pq|^2 &= (pq)\,\overline{(pq)} = p\,(q\bar q)\,\bar p \\ &= p\,|q|^2\,\bar p = |q|^2\,p\bar p = |p|^2\,|q|^2 . \end{aligned}

在整数上,这就是欧拉四平方恒等式,教程用 Quaternion[Int] 验证了它。

逆元与两种除法

若 qq 不为零,则 ∣q∣2>0|q|^2 > 0,而 qqˉ=qˉq=∣q∣2q\bar q = \bar q q = |q|^2 表明

q−1=qˉ∣q∣2q^{-1} = \frac{\bar q}{|q|^2}

是双边逆元。这就是 Inverse 的实现:用 one() / square_len() 缩放共轭。每个非零四元数都可逆,因此 H\mathbb H 是一个除环(斜域)。由于不可交换,它不是域。

没有交换律时,“qq 除以 rr”有两种含义,取决于未知数位于哪一侧:

x r=q  ⟺  x=q r−1(right division, q/r),r x=q  ⟺  x=r−1q(left division, q.left_div(r)).\begin{aligned} x\,r = q &\iff x = q\,r^{-1} && \text{(right division, } q / r\text{)}, \\ r\,x = q &\iff x = r^{-1} q && \text{(left division, } q\text{.left\_div}(r)\text{)}. \end{aligned}

实现避免构造 r−1r^{-1},只在最后做一次除法。设 q=(sq,vq)q = (s_q, \mathbf v_q)、r=(sr,vr)r = (s_r, \mathbf v_r)、rˉ=(sr,−vr)\bar r = (s_r, -\mathbf v_r),由标量–向量乘积得

q r−1=q rˉ∣r∣2=(sqsr+vq⋅vr,  srvq−sqvr−vq×vr)∣r∣2,r−1q=rˉ q∣r∣2=(sqsr+vq⋅vr,  srvq−sqvr−vr×vq)∣r∣2,\begin{aligned} q\,r^{-1} &= \frac{q\,\bar r}{|r|^2} = \frac{\big(s_q s_r + \mathbf v_q\cdot\mathbf v_r,\ \ s_r\mathbf v_q - s_q\mathbf v_r - \mathbf v_q\times\mathbf v_r\big)}{|r|^2}, \\ r^{-1} q &= \frac{\bar r\,q}{|r|^2} = \frac{\big(s_q s_r + \mathbf v_q\cdot\mathbf v_r,\ \ s_r\mathbf v_q - s_q\mathbf v_r - \mathbf v_r\times\mathbf v_q\big)}{|r|^2}, \end{aligned}

它们分别是 Div 的实现(其中 c = cross(q.vec, r.vec))与 Quaternion::left_div(其中 c = cross(r.vec, self.vec))。两种商的标量部相同,差为

q r−1−r−1q=−2 vq×vr∣r∣2.q\,r^{-1} - r^{-1} q = -\frac{2\,\mathbf v_q\times\mathbf v_r}{|r|^2}.

示例。 取 q=1+2i+3j+4kq = 1 + 2i + 3j + 4k、r=5+6i+7j+8kr = 5 + 6i + 7j + 8k。则 ∣r∣2=174|r|^2 = 174,sqsr+vq⋅vr=5+12+21+32=70s_q s_r + \mathbf v_q\cdot\mathbf v_r = 5 + 12 + 21 + 32 = 70,srvq−sqvr=(4,8,12)s_r\mathbf v_q - s_q\mathbf v_r = (4, 8, 12),vq×vr=(−4,8,−4)\mathbf v_q\times\mathbf v_r = (-4, 8, -4),所以

q/r=1174(70+8i+0j+16k)≈0.4023+0.0460 i+0.0920 k,q.left_div(r)=1174(70+0i+16j+8k)≈0.4023+0.0920 j+0.0460 k.\begin{aligned} q / r &= \tfrac{1}{174}\big(70 + 8i + 0j + 16k\big) \approx 0.4023 + 0.0460\,i + 0.0920\,k, \\ q.\mathrm{left\_div}(r) &= \tfrac{1}{174}\big(70 + 0i + 16j + 8k\big) \approx 0.4023 + 0.0920\,j + 0.0460\,k . \end{aligned}

API 页面验证了这些数值以及两个定义方程。

单位四元数与旋转

单位四元数(∣q∣=1|q| = 1)可以写成

q=(cos⁡θ2, sin⁡θ2 n),∣n∣=1,q = \Big(\cos\tfrac{\theta}{2},\ \sin\tfrac{\theta}{2}\,\mathbf n\Big), \qquad |\mathbf n| = 1 ,

这正是 from_axis_angle(axis, angle) 的返回值,其中 n\mathbf n 为归一化后的轴。对单位 qq 有 q−1=qˉq^{-1} = \bar q。考虑纯四元数上的映射 v↦q v qˉ\mathbf v \mapsto q\,\mathbf v\,\bar q。记 q=(w,u)q = (w, \mathbf u)。首先,

q v=(−u⋅v, wv+u×v).q\,\mathbf v = \big(-\mathbf u\cdot\mathbf v,\ w\mathbf v + \mathbf u\times\mathbf v\big).

再右乘 qˉ=(w,−u)\bar q = (w, -\mathbf u),标量部为

−w u⋅v+(wv+u×v)⋅u=−w u⋅v+w v⋅u+0=0,-w\,\mathbf u\cdot\mathbf v + (w\mathbf v + \mathbf u\times\mathbf v)\cdot\mathbf u = -w\,\mathbf u\cdot\mathbf v + w\,\mathbf v\cdot\mathbf u + 0 = 0 ,

因此像仍是纯四元数,其向量部为

v′=(u⋅v) u+w (wv+u×v)−(wv+u×v)×u=(u⋅v) u+w2v+2w u×v+u×(u×v)=(w2−∣u∣2) v+2(u⋅v) u+2w u×v,\begin{aligned} \mathbf v' &= (\mathbf u\cdot\mathbf v)\,\mathbf u + w\,(w\mathbf v + \mathbf u\times\mathbf v) - (w\mathbf v + \mathbf u\times\mathbf v)\times\mathbf u \\ &= (\mathbf u\cdot\mathbf v)\,\mathbf u + w^2\mathbf v + 2w\,\mathbf u\times\mathbf v + \mathbf u\times(\mathbf u\times\mathbf v) \\ &= (w^2 - |\mathbf u|^2)\,\mathbf v + 2(\mathbf u\cdot\mathbf v)\,\mathbf u + 2w\,\mathbf u\times\mathbf v , \end{aligned}

这里用到 a×u=−u×a\mathbf a\times\mathbf u = -\mathbf u\times\mathbf a 与 u×(u×v)=(u⋅v)u−∣u∣2v\mathbf u\times(\mathbf u\times\mathbf v) = (\mathbf u\cdot\mathbf v)\mathbf u - |\mathbf u|^2\mathbf v。代入 w=cos⁡θ2w = \cos\frac\theta2、u=sin⁡θ2 n\mathbf u = \sin\frac\theta2\,\mathbf n,并利用半角公式 cos⁡2θ2−sin⁡2θ2=cos⁡θ\cos^2\frac\theta2 - \sin^2\frac\theta2 = \cos\theta、2sin⁡2θ2=1−cos⁡θ2\sin^2\frac\theta2 = 1 - \cos\theta、2sin⁡θ2cos⁡θ2=sin⁡θ2\sin\frac\theta2\cos\frac\theta2 = \sin\theta:

v′=cos⁡θ v+(1−cos⁡θ)(n⋅v) n+sin⁡θ (n×v).\mathbf v' = \cos\theta\,\mathbf v + (1 - \cos\theta)(\mathbf n\cdot\mathbf v)\,\mathbf n + \sin\theta\,(\mathbf n\times\mathbf v).

这就是 Rodrigues 旋转公式:v↦qvq−1\mathbf v \mapsto q\mathbf v q^{-1} 是绕 n\mathbf n 旋转 θ\theta,按右手定则为逆时针方向。由于 ∣qvqˉ∣=∣q∣∣v∣∣qˉ∣=∣v∣|q\mathbf v\bar q| = |q||\mathbf v||\bar q| = |\mathbf v|,该映射是等距映射,正如旋转所应满足的那样。

由此得到三个塑造 API 的结论:

  • 复合即乘法。 p (qvq−1) p−1=(pq) v (pq)−1p\,(q\mathbf v q^{-1})\,p^{-1} = (pq)\,\mathbf v\,(pq)^{-1},因此“先 qq 后 pp”的旋转是 p * q。
  • 双重覆盖。 (−q) v (−q)−1=qvq−1(-q)\,\mathbf v\,(-q)^{-1} = q\mathbf v q^{-1},所以 qq 与 −q-q 是同一个旋转。事实上角度 θ+2π\theta + 2\pi 给出 −q-q。每个旋转恰好对应两个单位四元数,这就是 == 不能用于比较旋转、而 slerp 要翻转某个输入符号的原因。
  • 廉价求值。 Quaternion::rotate 计算 t=2 u×v\mathbf t = 2\,\mathbf u\times\mathbf v 与 v′=v+w t+u×t\mathbf v' = \mathbf v + w\,\mathbf t + \mathbf u\times\mathbf t。展开得 u×t=2(u⋅v)u−2∣u∣2v\mathbf u\times\mathbf t = 2(\mathbf u\cdot\mathbf v)\mathbf u - 2|\mathbf u|^2\mathbf v,所以 v+w t+u×t=(1−2∣u∣2) v+2(u⋅v) u+2w u×v,\mathbf v + w\,\mathbf t + \mathbf u\times\mathbf t = (1 - 2|\mathbf u|^2)\,\mathbf v + 2(\mathbf u\cdot\mathbf v)\,\mathbf u + 2w\,\mathbf u\times\mathbf v , 它与上面的公式相等,当且仅当 1−2∣u∣2=w2−∣u∣21 - 2|\mathbf u|^2 = w^2 - |\mathbf u|^2,即 w2+∣u∣2=1w^2 + |\mathbf u|^2 = 1。这就是 rotate 要求单位四元数的原因:它无需除法,但依赖于范数为 1。

把同一公式作用于基向量,就得到单位四元数的旋转矩阵:

R(q)=(1−2(y2+z2)2(xy−wz)2(xz+wy)2(xy+wz)1−2(x2+z2)2(yz−wx)2(xz−wy)2(yz+wx)1−2(x2+y2)).R(q) = \begin{pmatrix} 1 - 2(y^2 + z^2) & 2(xy - wz) & 2(xz + wy) \\ 2(xy + wz) & 1 - 2(x^2 + z^2) & 2(yz - wx) \\ 2(xz - wy) & 2(yz + wx) & 1 - 2(x^2 + y^2) \end{pmatrix}.

本包不返回这个矩阵,但下文的欧拉角提取会读取它的元素。

欧拉角

记 qX(α)q_X(\alpha)、qY(α)q_Y(\alpha)、qZ(α)q_Z(\alpha) 为绕各坐标轴旋转 α\alpha,RX,RY,RZR_X, R_Y, R_Z 为对应的矩阵。绕轴 A,B,CA, B, C、角度为 a,b,ca, b, c 的三次旋转序列有两种解读:

  • 外旋(external=true):绕固定轴,先 AA,再 BB,最后 CC。后发生的旋转乘在左边:q=qC(c) qB(b) qA(a)q = q_C(c)\,q_B(b)\,q_A(a)。
  • 内旋(external=false):绕随物体转动的轴,先 AA,再转动后的 BB,最后两次转动后的 CC:q=qA(a) qB(b) qC(c)q = q_A(a)\,q_B(b)\,q_C(c)。

同一乘积的两种解读表明:角度为 (a,b,c)(a, b, c) 的内旋 ABCABC 就是角度为 (c,b,a)(c, b, a) 的外旋 CBACBA。实现正是这样做的:每个 to_euler_internal_* 函数都调用逆序顺序的外旋函数,再把三元组反转。

提取:外旋 XYZ。 对 R=RZ(c) RY(b) RX(a)R = R_Z(c)\,R_Y(b)\,R_X(a),把基本矩阵相乘得到

R=(cos⁡bcos⁡c⋯⋯cos⁡bsin⁡c⋯⋯−sin⁡bcos⁡bsin⁡acos⁡bcos⁡a).R = \begin{pmatrix} \cos b\cos c & \cdots & \cdots \\ \cos b\sin c & \cdots & \cdots \\ -\sin b & \cos b\sin a & \cos b\cos a \end{pmatrix}.

与 R(q)R(q) 逐元素比较:

sin⁡b=−R31=2(wy−xz),a=atan2⁡(R32,R33)=atan2⁡(2(wx+yz), 1−2(x2+y2)),c=atan2⁡(R21,R11)=atan2⁡(2(wz+xy), 1−2(y2+z2)),\begin{aligned} \sin b &= -R_{31} = 2(wy - xz), \\ a &= \operatorname{atan2}(R_{32}, R_{33}) = \operatorname{atan2}\big(2(wx + yz),\ 1 - 2(x^2 + y^2)\big), \\ c &= \operatorname{atan2}(R_{21}, R_{11}) = \operatorname{atan2}\big(2(wz + xy),\ 1 - 2(y^2 + z^2)\big), \end{aligned}

在 cos⁡b>0\cos b > 0 时成立(把 atan2 的两个参数同除以 cos⁡b\cos b 不改变角度)。这些就是 to_euler_external_XYZ 中的表达式,其中 b=arcsin⁡(⋅)∈[−π/2,π/2]b = \arcsin(\cdot) \in [-\pi/2, \pi/2]。to_euler_external_YZX 与 to_euler_external_ZYX 是把轴置换后的同一推导;非循环置换会翻转反正弦内以及 atan2 分子的符号。模块在提取前会归一化 q,因此 R(q)R(q) 对角线所用的恒等式 w2+x2+y2+z2=1w^2 + x^2 + y^2 + z^2 = 1 在舍入误差范围内成立。

万向节锁。 当 cos⁡b=0\cos b = 0 时,元素 R32,R33,R21,R11R_{32}, R_{33}, R_{21}, R_{11} 全部为零,第一次与第三次旋转作用于同一个物理轴。在 XYZ 情形下,对 b=π/2b = \pi/2,

RZ(c) RY(π2) RX(a)=RY(π2) RX(a−c),R_Z(c)\,R_Y(\tfrac\pi2)\,R_X(a) = R_Y(\tfrac\pi2)\,R_X(a - c),

因此只有 a−ca - c 是确定的(b=−π/2b = -\pi/2 时为 a+ca + c)。常规做法是令 c=0c = 0,再从剩余元素恢复 aa,此处为 a=atan2⁡(R12,R22)a = \operatorname{atan2}(R_{12}, R_{22})。实现在 ∣sin⁡b∣≥0.9998|\sin b| \ge 0.9998(约 88.85∘88.85^\circ)时切换到特殊分支:打印警告,令 b=±π/2b = \pm\pi/2、c=0c = 0,但对 XYZ 用 atan2⁡(R13,R22)\operatorname{atan2}(R_{13}, R_{22}) 求第一个角,对其他顺序则沿用常规分支的公式(其参数接近零)。两者都没有分离出 a∓ca \mp c,因此该分支目前返回的角不能复现输入;参见已知偏差。

构造。 默认顺序 "XYZ" 下的 from_euler(roll, pitch, yaw) 以闭式展开 qX(roll) qY(pitch) qZ(yaw)q_X(\text{roll})\,q_Y(\text{pitch})\,q_Z(\text{yaw})。记 cr=cos⁡roll2c_r = \cos\frac{\text{roll}}{2}、sr=sin⁡roll2s_r = \sin\frac{\text{roll}}{2} 等,两次应用 Hamilton 积得到

qXqYqZ=(crcpcy−srspsy,  srcpcy+crspsy,  crspcy−srcpsy,  crcpsy+srspcy),q_X q_Y q_Z = \big(c_r c_p c_y - s_r s_p s_y,\ \ s_r c_p c_y + c_r s_p s_y,\ \ c_r s_p c_y - s_r c_p s_y,\ \ c_r c_p s_y + s_r s_p c_y\big),

即源码中的表达式。to_euler_internal_XYZ 是它的逆。

球面线性插值

单位四元数构成三维球面 S3⊂R4S^3 \subset \mathbb R^4,两个姿态之间的最短路径是一段大圆弧。设 q1,q2q_1, q_2 为单位四元数,cos⁡Ω=q1⋅q2\cos\Omega = q_1\cdot q_2,0<Ω<π0 < \Omega < \pi。单位向量

q⊥=q2−cos⁡Ω q1sin⁡Ωq_\perp = \frac{q_2 - \cos\Omega\, q_1}{\sin\Omega}

与 q1q_1 正交,弧为 γ(φ)=cos⁡φ q1+sin⁡φ q⊥\gamma(\varphi) = \cos\varphi\, q_1 + \sin\varphi\, q_\perp。在 φ=tΩ\varphi = t\Omega 处,

γ(tΩ)=sin⁡Ωcos⁡tΩ−cos⁡Ωsin⁡tΩsin⁡Ω q1+sin⁡tΩsin⁡Ω q2=sin⁡((1−t)Ω)sin⁡Ω q1+sin⁡(tΩ)sin⁡Ω q2,\begin{aligned} \gamma(t\Omega) &= \frac{\sin\Omega\cos t\Omega - \cos\Omega\sin t\Omega}{\sin\Omega}\,q_1 + \frac{\sin t\Omega}{\sin\Omega}\,q_2 \\ &= \frac{\sin\big((1-t)\Omega\big)}{\sin\Omega}\,q_1 + \frac{\sin(t\Omega)}{\sin\Omega}\,q_2 , \end{aligned}

这就是 slerp 计算的公式。它以恒定角速度运动,旋转角随 tt 线性增长。由于双重覆盖,当 q1⋅q2<0q_1\cdot q_2 < 0 时 slerp 先把 q2q_2 替换为 −q2-q_2,使 Ω≤π/2\Omega \le \pi/2,旋转走较短的一侧。它在调用 acos 前把 cos⁡Ω\cos\Omega 截断到 [−1,1][-1, 1],因为舍入可能使两个单位四元数的点积略微超过 1。

幂

pow_by_int 利用 q2m=(qm)2q^{2m} = (q^m)^2 与 q2m+1=q (qm)2q^{2m+1} = q\,(q^m)^2,只需要结合律;所有因子都是同一个 qq 的幂,因此彼此可交换。负指数利用 q−n=(q−1)nq^{-n} = (q^{-1})^n。

pow_by_T 使用极形式。每个非零 qq 都可写成 q=∣q∣ (cos⁡φ+n^sin⁡φ)q = |q|\,(\cos\varphi + \hat{\mathbf n}\sin\varphi),其中 φ∈[0,π]\varphi \in [0, \pi],n^\hat{\mathbf n} 为单位向量,而 11 与 n^\hat{\mathbf n} 张成的空间是 C\mathbb C 的一个副本(因为 n^2=−1\hat{\mathbf n}^2 = -1)。于是棣莫弗公式定义了

qt=∣q∣t (cos⁡tφ+n^sin⁡tφ).q^t = |q|^t\,\big(\cos t\varphi + \hat{\mathbf n}\sin t\varphi\big).

实现以 φ=arcsin⁡∣u∣/∣q∣\varphi = \arcsin\lvert\mathbf u\rvert/|q| 计算角度,只在 φ≤π/2\varphi \le \pi/2 时正确,即标量部非负时;参见已知偏差。

设计决策

泛型分量与逐函数约束

问题。 四元数在精确整数上(数论、验证恒等式)和在 Double 上(几何)都很有用,而 luna-generic 生态希望由一个类型同时满足两者。

选项。 仅支持 Double 的类型;由单一“实数”trait 参数化的类型;或对 T 不加约束、每个函数只要求自己所用 trait 的类型。

选择。 最后一种。Quaternion[T] 不加约束;+ 需要 Add,* 需要 Mul + Sub + Add,square_len 需要 Mul + Add,只有涉及平方根或三角函数的函数才要求 DoubleConvert。这遵循 Luna-Flow 的原则——代码依赖能表达其需求的最小 trait 组合,也使 Quaternion[Int] 能精确验证代数恒等式。

标量–向量存储

该类型存储 r : T 与 vec : (T, T, T),而不是四个独立字段。上文的乘积、共轭、逆元与旋转都是以标量–向量形式推导的,因此代码读起来就像推导本身(在 vec 上使用 dot、cross、scale)。类型是抽象的,表示形式因而可以自由改变;代价是目前还没有分量访问器。

为什么是 Ring 而不是 Field

luna-generic 的 Field 是 Ring + Inverse + Div,针对它编写的泛型代码有权依赖交换域的定律,例如 ab=baa b = b a 或 a/b⋅c=ac/ba / b \cdot c = a c / b。四元数满足所有环公理且有逆元,但不满足交换律,因此声明 Field 会让这类代码悄无声息地算出错误结果。所以本包实现 Zero、One、AddMonoid、MulMonoid、Semiring 与 Ring(对交换的 T,它们在 H\mathbb H 上都成立),外加运算 trait Inverse 与 Conjugate,到此为止。除法仍可通过 Div 与 left_div 使用,但泛型代码必须显式要求它。

/ 是右除

问题。 乘法不可交换时,q / r 必须选定一侧。0.2.0 之前它计算 r−1qr^{-1} q。

选择。 自 0.2.0 起,q / r 为 q r−1q\,r^{-1},即 xr=qx r = q 的解。这是除环的通常约定,并让 Div 与 Inverse 保持与 luna-generic 标量类型相同的一致关系:a / b == a * b.inv()。对旋转而言它也读起来自然:to / from 是在 from 之后施加便得到 to 的旋转。另一种商仍以 left_div 提供,名字表明逆元位于哪一侧。这一变更是破坏性的,已记录在变更日志中。

Double 桥接

平方根与三角函数只存在于 Double 上,而本包只依赖 luna-generic,后者没有分析类 trait。DoubleConvert 是一个双方法 trait,把分量转换到 Double 再转换回来,使 magnitude、normalize、slerp、pow_by_T 与欧拉角转换在签名上保持泛型,同时在 Double 中计算。它为 Double(恒等映射)与 Int(from_double 截断)实现。该 trait 声明为 pub 而非 pub(open),因此其他包无法为自己的类型实现它。

精确的结构相等

Eq 与 Hash 由派生得到,对分量做精确比较。“作为旋转”相等(q∼−qq \sim -q)或“在容差内”相等取决于具体应用,因此交由调用者决定(教程给出了一种做法)。精确相等还使 Eq 与 Hash 保持一致。

显式方法提升

MoonBit 0.10 不再把 trait 实现隐式变为方法。src/extends.mbt 提升了运算符、equal、hash、to_string、conjugate、zero、one 与 inv,它们都是四元数上的自然运算。方法形式 not_equal、hash_combine、output、default 与 to_repr 为兼容而保留,但已弃用并隐藏。

正确性与不变量

代数定律

在具有精确算术的交换环 T(Int、BigInt)上,实现满足:

  • 环公理:(H,+,0)(\mathbb H, +, 0) 是阿贝尔群,(H,⋅,1)(\mathbb H, \cdot, 1) 是幺半群,且 ⋅\cdot 对 ++ 满足双侧分配律;
  • pq‾=qˉ pˉ\overline{pq} = \bar q\,\bar p、qˉˉ=q\bar{\bar q} = q、qqˉ=qˉq=∣q∣2q\bar q = \bar q q = |q|^2 以及 ∣pq∣2=∣p∣2∣q∣2|pq|^2 = |p|^2|q|^2;
  • 对精确除法(T 为域,例如有理数):q q−1=q−1q=1q\,q^{-1} = q^{-1}q = 1、(q/r) r=q(q / r)\,r = q 以及 r (q.left_div(r))=qr\,(q.\mathrm{left\_div}(r)) = q。

本包的测试在整数样本上检查环公理、不可交换性以及两个除法恒等式。

浮点误差

在 Double 上,这些定律在舍入误差范围内成立。pqpq 的每个分量是 pp 的分量与 qq 的分量的四个乘积之和,计算时乘积有四次舍入、加法有三次舍入。对这类内积的标准界33 N. J. Higham,Accuracy and Stability of Numerical Algorithms,第 2 版,SIAM 2002,§3.1:对任意求和顺序,∣fl(xTy)−xTy∣≤γn∣x∣T∣y∣\lvert\mathrm{fl}(x^{\mathsf T}y) - x^{\mathsf T}y\rvert \le \gamma_n \lvert x\rvert^{\mathsf T}\lvert y\rvert。 逐分量给出

∣fl(pq)m−(pq)m∣≤γ4∑l=14∣pl∣ ∣qσm(l)∣≤γ4 ∣p∣ ∣q∣,γn=nu1−nu,\lvert \mathrm{fl}(pq)_m - (pq)_m \rvert \le \gamma_4 \sum_{l=1}^{4} \lvert p_l\rvert\,\lvert q_{\sigma_m(l)}\rvert \le \gamma_4\, |p|\,|q|, \qquad \gamma_n = \frac{n u}{1 - n u},

其中 σm\sigma_m 是第 mm 个分量中出现的 qq 的分量置换,第二个不等式即 Cauchy–Schwarz 不等式。对四个分量求和得 ∥fl(pq)−pq∥≤2γ4∣p∣∣q∣\lVert\mathrm{fl}(pq) - pq\rVert \le 2\gamma_4 |p||q|,于是由范数的可乘性

(1−2γ4) ∣p∣ ∣q∣≤∣fl(pq)∣≤(1+2γ4) ∣p∣ ∣q∣.(1 - 2\gamma_4)\,|p|\,|q| \le |\mathrm{fl}(pq)| \le (1 + 2\gamma_4)\,|p|\,|q| .

因此 nn 个单位四元数之积的范数为 1+δ1 + \delta,一阶意义下 ∣δ∣≲8nu|\delta| \lesssim 8nu,其中 u=2−53≈1.1×10−16u = 2^{-53} \approx 1.1 \times 10^{-16};实践中误差会部分抵消,漂移更小。由于 rotate 假定 ∣q∣=1|q| = 1,漂移后的四元数会把向量缩放约 1+2δ1 + 2\delta 倍。长的乘积链应定期调用 normalize;其结果的范数与 11 相差在几个 uu 之内(例如 1+2i+3j+4k1 + 2i + 3j + 4k 归一化后计算得到的范数为 0.99999999999999990.9999999999999999)。

除以零与退化输入

本包不检查输入;退化情形遵循 T 的算术:

输入DoubleInt
除数为零时的 inv、/、left_divNaN 分量运行时陷阱(整数除以零)
零的 normalize原样返回零原样返回零
零的 pow_by_T原样返回零原样返回零
轴为零的 from_axis_angleNaN 向量部运行时陷阱
输入为零的 slerp零输入未经归一化直接使用没有意义

在 Int 上,除非 ∣q∣2=1|q|^2 = 1,inv 均为零,且所有经由 DoubleConvert 的函数都会截断。

阈值

当 cos⁡Ω>0.9995\cos\Omega > 0.9995,即 Ω<0.0316\Omega < 0.0316 rad 时,slerp 退回归一化线性插值。此时 sin⁡Ω<0.032\sin\Omega < 0.032,除以它会放大权重中的舍入误差;而在阈值处,线性路径与圆弧在 S3S^3 上的偏差小于 5.1×10−75.1 \times 10^{-7} rad(约合旋转角 10−610^{-6} rad),低于阈值时更小。

欧拉角转换把 ∣sin⁡b∣≥0.9998|\sin b| \ge 0.9998 视为万向节锁。在此把 bb 取整为 ±π/2\pm\pi/2 会使中间角产生最多 π/2−arcsin⁡0.9998≈0.020\pi/2 - \arcsin 0.9998 \approx 0.020 rad 的误差。

复杂度

每个运算都是对四个分量的 O(1)O(1) 操作:一次 Hamilton 积需要 16 次乘法与 12 次加法,rotate 需要 18 次乘法与 12 次加法。pow_by_int(n) 需要 O(log⁡∣n∣)O(\log |n|) 次乘积和同阶的递归深度。

已知偏差

以下是当前实现与上文数学的偏差。它们在此明确记录而非隐藏,并已报告待修复:

  • "ZYX" 或 "YZX" 下的 from_euler 并不计算轴旋转的乘积;结果不是单位四元数。"ZXY" 按位置取角,而 "YXZ" 按轴取角。
  • to_euler_external_XZY(以及由此而来的 to_euler_internal_YZX)计算的是内旋 X-Y-Z 的提取,而非所指定的序列。
  • 欧拉角提取的万向节锁分支没有分离出自由角(见上文),并用 println 输出警告。
  • pow_by_T 用 asin 计算 φ\varphi,其值不超过 π/2\pi/2;改用 atan2⁡(∣u∣,w)\operatorname{atan2}(\lvert\mathbf u\rvert, w) 即可覆盖 [0,π][0, \pi]。

被否决的方案

  • 实现 Field。 被否决,因为泛型 Field 代码可能依赖交换律(见上文)。
  • 保留 / 为左除。 它与 a / b == a * b.inv() 以及旋转中 to / from 的通常读法相矛盾。
  • 仅支持 Double 的类型。 它会失去 Int 上的精确算术,以及其他标量类型上的 luna-generic 环结构。
  • 存储四个具名字段或旋转矩阵。 四个字段会掩盖公式所用的标量–向量结构;矩阵有九个元素,需要重新正交化而非廉价的归一化,插值也没那么简单。
  • 在 Eq 中按容差比较。 基于容差的 == 不具有传递性,也无法与 Hash 保持一致。

边界

本包刻意不做以下事情:

  • 提供旋转矩阵、指数与对数映射或四元数微积分(对偶四元数、姿态的导数);
  • 检查输入或返回 Result:退化输入遵循 T 的算术,rotate 相信其接收者是单位四元数;
  • 实现 luna-generic 的 Field 或 Num,或让四元数成为有序类型;
  • 依赖 arithmetic、linear-algebra 或 luna-complex;它唯一的 Luna-Flow 依赖是 luna-generic;
  • 选定比较旋转所用的容差;这留给调用者决定。

Footnotes

  1. W. R. Hamilton,“On quaternions; or on a new system of imaginaries in algebra”,1843 年。这些关系正是他刻在都柏林布鲁姆桥(Brougham Bridge)上的那几个式子。 ↩

  2. 例如 1↦I1 \mapsto I、i↦(i00−i)i \mapsto \begin{pmatrix} i & 0 \\ 0 & -i\end{pmatrix}、j↦(01−10)j \mapsto \begin{pmatrix} 0 & 1 \\ -1 & 0\end{pmatrix}、k↦(0ii0)k \mapsto \begin{pmatrix} 0 & i \\ i & 0\end{pmatrix}。这些矩阵满足 Hamilton 关系,而矩阵乘法满足结合律。 ↩

  3. N. J. Higham,Accuracy and Stability of Numerical Algorithms,第 2 版,SIAM 2002,§3.1:对任意求和顺序,∣fl(xTy)−xTy∣≤γn∣x∣T∣y∣\lvert\mathrm{fl}(x^{\mathsf T}y) - x^{\mathsf T}y\rvert \le \gamma_n \lvert x\rvert^{\mathsf T}\lvert y\rvert。 ↩