Quaternion 与 三维空间旋转

无论是做游戏开发 (Unity3D / Unreal 5)、无人机,还是无人驾驶,相信很多人都听说过 Quaternion (四元数)。

举个例子,无人机如果用 欧拉角 计算旋转姿态,就会像下面左边那样坠机;而使用 Quaternion 的话受到扰动后依旧能恢复平稳。

Record_2024_11_03_17_02_41_917.mp4 [video-to-gif output image]

再举个例子,下面是 Unity3D 和 Unreal 游戏引擎的 API, 只要是关于旋转的 API 都能发现 Quaternion。

# Unity3D
public static Quaternion Slerp(Quaternion a, Quaternion b, float t);

# Unreal
class unreal.Rotator(roll, pitch, yaw).quaternion()

假如你依旧没有听说过 Quaternion,至少听说过欧拉角,例如一架飞机的姿态可以用 roll, pitch, yaw 表示:

img

虽然欧拉角的 roll, pitch, yaw 看起来非常直观,但是计算旋转矩阵会非常痛苦。不信的话,我们一起欣赏一下欧拉角最简单的绕 x, y, z 轴旋转矩阵的矩阵。如果是绕任意向量旋转,还会更加复杂。

Rx(ϕ)Ry(θ)Rz(ψ)=[cos(ϕ)sin(ϕ)0sin(ϕ)cos(ϕ)0001][cos(θ)0sin(θ)010sin(θ)0cos(θ)][cos(ψ)sin(ψ)0sin(ψ)cos(ψ)0001]=[cos(ψ)cos(θ)cos(ψ)sin(θ)sin(ϕ)sin(ψ)cos(ϕ)cos(ψ)sin(θ)cos(ϕ)sin(ψ)sin(ϕ)sin(ψ)cos(θ)sin(ψ)sin(θ)sin(ϕ)+cos(ψ)cos(ϕ)sin(ψ)sin(θ)cos(ϕ)+cos(ψ)sin(ϕ)sin(θ)sin(ϕ)cos(θ)cos(θ)cos(ϕ)]

另一方面,使用欧拉角会碰到著名的 Gimbal Lock 问题,也就是下图这样:如果使用旋转矩阵,每当飞机的姿态与某个轴重合的时候,就会突然绕另一个轴旋转,如果无人机出现这个问题当然就是坠机了。

Record_2024_11_04_19_11_16_497.mp4 [optimize output image]

完蛋,一看到虚数,很多人可能就头大,不禁回想起本科 一元分析、多元分析、复分析 的恐惧。于是决定写这篇文章,用动画和图片的方式,直白地演示 Quaternion 怎么用虚数表示三维空间的姿态。

2维虚数: i

首先,我们从最简单的 2维 平面开始,提到虚数很多人第一印象可能就是下面的公式。

i2=1

都说 x2 平方大于等于 0,怎么还会有 i2=1 啊?其实我们只需要换个角度,就豁然开朗了。

image-20241103173903678

1i=i

ii=1

这样是不是理解,为什么 i2=1 了,每次和 i 相乘就是旋转 90°。其实就是先从 1 旋转 90°到 i 轴,再旋转一次就成了 -1 负轴。

假设我们旋转任意角度 θ,那么单位圆上的任一点就都能用复数表示了:

z=cosθ+isinθ

这样用旋转来理解虚数 i,相信容易了不少,上面的 z 我们就可以理解成逆时针旋转 θ

image-20241103175834400

那么如果我们先旋转 θ1,再旋转 θ2,会怎么样呢?相信大家都知道最终角度是 θ1+θ2,但是我们需要证明一下。

image-20241103181612734

如果我们把复数理解为旋转,两次旋转 θ1+θ2 则是把两个复数 z1z2 相乘:

z1z2=(cosθ1+isinθ1)(cosθ2+isinθ2)=(cosθ1cosθ2sinθ1sinθ2)+i(cosθ1sinθ2+sinθ1cosθ2)=cos(θ1+θ2)+isin(θ1+θ2)

于是,两个复数相乘 z1z2,其实就是旋转角度相加 θ1+θ2。一提到乘法等于加法,你可能立马想起来指数 e 不就是这样吗:

eaeb=ea+b

这就是为什么也有人用指数 eiθ 表示复数旋转:
eiθ=cosθ+isinθ

比如我们先旋转 eiθ1,再旋转 eiθ2,那么最后的位置就是 ei(θ1+θ2)
eiθ1eiθ2=eiθ1+θ2)=cos(θ1+θ2)+isin(θ1+θ2)

到这里位置,相信你不再觉得复数 i 很离谱,很头大了,其实它就是代表二维平面的旋转

z=eiθ=cosθ+isinθ

Record_2024_11_04_19_27_14_705.mp4 [optimize output image]

四维 Quaternion: i, j, k

前面提到 Quaternion 是四维的,可能一下从二维跳到四维太突兀了,我们先看看三维的情况:

q=cosθ+sinθ(ai+bj)

这里说是三维,其实不过是多了个虚轴 j 罢了,如果我们俯视这个三维的球体,会发现改变 i,j 的参数,其实就是在旋转一个二维的单位向量 ai+bj,其中  a2+b2=1 表示单位圆上的点。

Record_2024_11_04_19_34_46_65.mp4 [video-to-gif output image]

那么我们固定这个二维的向量 ai+bj,再改变 θ 是什么效果呢?注意看下面动画圈出来的红色向量,其实改变的就是与这个二维向量的夹角 θ 。如果我们切换到与 ai+bj 垂直的正视图,又看到一个单位圆的旋转了。

q=cosθ+sinθ(ai+bj)

Record_2024_11_04_19_58_29_2.mp4 [optimize output image]

相信现在你已经理解了 2维、3维的空间旋转,但是别忘了,我们还可以自转呢。于是我们再增加一个维度就有了 Quaternion,其实就是先找到一个三维向量 ai+bj+ck,再绕着这个向量旋转 θ

q=cosθ+sinθ(ai+bj+ck)

还是注意红色箭头圈出来的向量,其实我们改变 (i,j,k) 就是改变这个三维向量在空间里的位置,最后不出意外的话,剩下的 θ 就是与这个三维向量的夹角了。

Record_2024_11_04_20_08_03_356.mp4 [optimize output image]

当然,我们还是需要改变 θ 看 Quaternion 是如何绕三维向量 ai+bj+ck 旋转的。

Record_2024_11_04_20_13_45_604.mp4 [optimize output image]

可以看到,果然是绕三维向量 ai+bj+ck 自转。相信你现在已经理解 Quaternion 是怎么表示三维空间姿态的了,以及为什么我们需要四个维度。

q=cosθ+sinθ(ai+bj+ck)

当然,你也可以自己去这个网站上体验一下 Quaternion 的乐趣。

最后,我们看看 Quaternion 的旋转公式,一个向量 p 绕任意三维向量 q 旋转 θ 后的向量 p,公式相比欧拉角的旋转矩阵要简单多了,而且也不会有文章开头 Gimbal Lock 的问题,因为 Quaternion 维度更高有四维:

p=qpq¯

q=cosθ2+usinθ2

u=uxi+uyj+uzk

如果你想问,为什么是 cosθ2+usinθ2,而不是 θ 可以在这里找到证明过程:Euler-Rodrigues-Hamilton。

https://www.math.stonybrook.edu/~oleg/courses/mat150-spr16/lecture-5.pdf

总结

现在你应当理解什么是虚数,为什么 i2=1 了,以及我们为什么需要四维的 Quaternion 来表示三维空间的旋转了。Quaternion 不仅旋转公式简单,还不会有 Gimbal Lock 的问题

另外,也有人用下面的公式表示 Quaternion,只要你还记得单位球的旋转动画,应该能猜到:w2+a2+b2+c2=1

q=w+ai+bj+ck

最后顺便一提,Quaternion 的乘法遵循 ii=jj=kk=1,ij=ji=k,ijk=1,如果我们把两个 w=0 的 Quaternion 相乘会发现很有意思的事情:
q1=a1i+b1j+c1kq2=a2i+b2j+c2k

q1q2=(a1i+b1j+c1k)(a2i+b2j+c2k)=(a1a2+b1b2+c1c2)+(b1c2b2c1)i(a1c2a2c1)j+(a1b2a2b1)k=q1q2+q1×q2

现在你可能理解为什么向量的内积是一个数值 (实部),向量的外积是一个向量 (虚部 i,j,k) 了,其实内积和外积,是源自于更高维度的 Quaternion 乘法。