大家好,这是我的第一篇文章。今后自己的博客文章会不定期进行更新,大部分主题将会是我对近几年 GDC (游戏开发者大会)上的数学专题与编程专题的学习心得(总结)~ 这一篇文章目的是向大家深入介绍四元数,主要的参考文献是2013年 GDC 的 《UnderstandingQuaternions》 。和很多博客不同的是,自己最后会尝试利用一些群论的知识去理解四元数,学习过相关知识的同学可以看看,相互探讨一下。文章篇幅可能有些长,可以直接在目录里面选择自己需要了解的进行阅读。
那么话不多说,先来进入第一部分。
通常将物体绕一个点或者一个轴转动一定角度的操作称为旋转。不妨先考虑 2D 的情形:
旋转示意图那么自然而言便会引申出旋转角的概念:
旋转角示意图显然,旋转角串联起了旋转前后的两个图形。而这种“串联”关系,通常采用矩阵的语言进行描述。
在线性代数里,我们知道: - 一个矩阵代表一种变换(并不一定是旋转变换)。 - 一个旋转矩阵乘以一个向量(左乘或者右乘)将使得该向量被旋转。
其中,2维的旋转矩阵形式如下:
(cosθ−sinθsinθ cosθ) 通过观察上面的旋转角示意图,这样一个矩阵形式是容易理解的。通过对坐标系作逆时针 θ 角度的旋转以后, x 轴上的单位向量(1,0)变成(cosθ,sinθ),而 y 轴上的单位向量(0,1)变成(−sinθ,cosθ),把它们列排即得二维的旋转矩阵。 对于3维的旋转矩阵而言,其矩阵形式是与2维的旋转矩阵类似的:均通过将坐标轴上的单位向量经旋转后的像作为其旋转矩阵的某一列。以绕 x 轴逆时针旋转θ 角度(右手坐标系)为例,其旋转矩阵形式如下: ⎛⎝⎜10 00 cosθ−sinθ0 sinθcosθ⎞⎠⎟ 并且,旋转矩阵有一个非常完美的性质:正交矩阵,即对于旋转矩阵 M 而言,有: - M−1=MT - MTM=E - |M|=1 在算旋转矩阵的逆矩阵的时候,可以直接求其转置矩阵,省下许多计算量。其次,其行列式为1意味着在经过旋转变换以后,图形的大小和形状都不会发生改变(比方说一个圆绕其圆心旋转任意角度,其半径都不会改变)。关于旋转矩阵是正交矩阵的证明可以参见这篇文章: 旋转矩阵(Rotate Matrix)的性质分析 在正交矩阵的前提下,我们可以得到旋转矩阵一个有趣的性质:旋转矩阵对应特征值1的特征向量为其旋转轴。因为旋转矩阵为正交矩阵,故其特征值为 ±1 ,当有不少于1个特征值为1时,说明物体不仅仅是在绕 x 轴(或y,z 轴)旋转。欧拉这样一位划时代的数学大师提出的欧拉角,是一种十分简单的表示旋转的有力工具。 来看看 wikipedia 对欧拉角的定义:
令原始坐标系的三个坐标轴分别为 x,y,z ,旋转后的坐标系的三个坐标轴为 X,Y,Z , N 轴是xOy 平面与 XOY 平面的交线(也可以利用外积将 N 轴定义为N=z×Z)。这样一来,三个欧拉角可定义如下: - α (或 φ ) 是 x 轴和N 轴的夹角; - β (或 θ ) 是 z 轴和Z 轴的夹角; - γ (或 ψ ) 是 N 轴和X 轴的夹角;
欧拉角示意图也就是说,欧拉角将绕一个过原点的旋转轴旋转 θ 分解成了“三步曲”(此时 xyz 坐标系跟随旋转): 1. 绕 z 轴旋转α,使得 x 轴与N 轴重合; 2. 绕 x 轴旋转β,使得 z 轴与Z 轴重合; 3. 绕 z 轴旋转γ,此时 xyz 坐标系将与 XYZ 坐标系重合。
这样的旋转顺序被称为是“ zxz ”顺序的,实际上并不一定非要按照这个顺序进行旋转,这个我们等等会再提及一下,此处先以“ zxz ”顺序为标准继续进行介绍。 看到这里,也许你已经想骂人了:嘿,说好的“欧拉角很简单呢?”。别急,可能这样将坐标系旋转来旋转去确实会令人有些头大(想象不出来如何旋转的同学,可以参看欧拉角- wikipedia 百科里的动图演示)。接下来,我们会证明上述的旋转过程是与下述旋转过程(此时 xyz 坐标系不跟随旋转,注意与上述旋转过程的区别)等价的: 1. 绕 z 轴旋转γ; 2. 绕 x 轴旋转β; 3. 绕 z 轴旋转α。
为了证明上述两种旋转过程等价(即最终的旋转结果相同),可以利用欧拉角对应的旋转矩阵进行证明,证明过程如下:(参考自欧拉角与万向节死锁) 记 - 绕 z 轴旋转α,使得 x 轴与N 轴重合对应的旋转矩阵为 Mz1N ; - 绕 x 轴旋转β,使得 z 轴与Z 轴重合对应的旋转矩阵为 MxN ; - 绕 z 轴旋转γ,此时 xyz 坐标系将与 XYZ 坐标系重合对应的旋转矩阵为 Mz2N ; - 绕 z 轴旋转γ 对应的旋转矩阵为 Mz2 ; - 绕 x 轴旋转β 对应的旋转矩阵为 Mx ; - 绕 z 轴旋转α 对应的旋转矩阵为 Mz1 。
那么问题便转化为求证: Mz1NMxNMz2N=Mz2MxMz1 。 证明: 显然, Mz1N=Mz1 ,因为此时的旋转还不受坐标系改变的影响。 而对于 MxN ,要得到绕 x 轴旋转β,使得 z 轴与Z 轴重合的效果,可以先绕 z 轴旋转α(此时坐标系跟随旋转),然后绕 x 轴旋转β ,最后再绕 z 轴旋转α(脑海里面想象不出来的话可以拿些物体比划比划,我就是这么过来的=。=)。用旋转矩阵的语言描述出来即为:
MxN=M−1z1NMxMz1N. 同理可得: Mz2N=(Mz1NMxN)−1Mz2(Mz1NMxN). 综上所述有: Mz1NMxNMz2N=Mz1NMxN(Mz1NMxN)−1Mz2(Mz1NMxN)=(Mz1NMxN)(Mz1NMxN)−1Mz2(Mz1NMxN)=Mz2(Mz1NMxN)=Mz2(Mz1NM−1z1NMxMz1N)=Mz2MxMz1N=Mz2MxMz1因此,实际上在使用欧拉角表示旋转时,可以直接将其理解为先绕 z 轴旋转γ,再绕 x 轴旋转β ,最后绕 z 轴旋转α 。这样不涉及坐标系的旋转,在理解和运用上便会简单得多。 需要注意的是: 1. 欧拉角的旋转顺序并不一定都是“ zxz ”顺序,你也可以根据自己需要定义为“ xyz ”,“ zyx ”等等,上述性质是不会改变的。只要保证前后定义的旋转顺序一样即可,否则可能会出现无法预料的错误,毕竟不同旋转顺序下旋转效果还是有可能大不相同的。 2. 细心的同学可能会发现,在旋转矩阵中,最先进行的旋转操作对应的旋转矩阵在最右侧与向量相乘,旋转矩阵按照旋转的次序从右向左排列;而在欧拉角中,最先进行的旋转操作(此处说的是坐标系跟随旋转的情形)对应的旋转矩阵在最左側与向量相乘。看到一篇文章,三维旋转:旋转矩阵,欧拉角,四元数,对于这个问题是解释得不错的:
对于前者(旋转矩阵),我们始终是以绝对参考系为参照来的,对于后者(欧拉角),我们每一次旋转的刻画都是基于刚体的坐标系。比如,在欧拉角中的第2步,绕 x 轴旋转β,这里的 x 轴实际上是N 轴了(而不是蓝色的 x 轴)。 为什么旋转参考系的不同会导致旋转矩阵次序的差异呢?细想一下便知,旋转矩阵左乘叠加用以描述三维变换效果的叠加,这本身就是基于绝对坐标系的,所以旋转矩阵一节没有疑问;而对于欧拉角一节的这种旋转方式,这样考虑: 1. 如果有一个“影子坐标系3”与原坐标系重合,然后首先进行了第3步(绕z轴旋转 γ ); 2. 然后有一个“影子坐标系2”也与原坐标系重合,然后与“影子坐标系3”一起(视作同一个刚体)进行了第二步; 3. 最后一个“影子坐标系1”,与前两个坐标系一起进行了第一步。 此时,考察“影子坐标系”1和2,他们就分别落在了欧拉角旋转的两个“快照”上,而“影子坐标系3”就落在旋转后的位置上(红色的)。 而在上述过程中,“影子坐标系3”就是相对于绝对坐标系依次进行了第三步,第二步,和第一步。所以欧拉角的旋转矩阵写成那样,也是行得通的。
说了这么多,先停下来总结一下旋转矩阵和欧拉角: 旋转矩阵 - 优点:学过线性代数的都能很容易地理解; - 缺点:和其它表示旋转的方式相比,其空间消耗较大,一个三维的旋转矩阵需要存储9个元素,而且在作矩阵乘法时也会消耗较多的时间。 - 欧拉角 - 优点:表示简单,仅需要存储三个值, (α,β,γ) ; - 缺点:在编程实现上,通常还是将其转化为旋转矩阵进行计算,其空间复杂度和时间复杂度没有太大的变化;且在使用过程当中可能会出现著名的万向节锁现象,直观理解可以参考这个视频,欧拉旋转。 这个视频比较形象地讲解了何谓万向节锁,万向节锁大体说来便是物体绕某个坐标轴旋转了90度,使得某两个坐标轴平面重合,从而丢失了一个维度。在这种情况下,无论接下来作什么旋转都无法将物体旋转到某个角度,除非打破原先定义的旋转顺序或者同时旋转三个坐标轴。这样一个问题的产生也使得欧拉角在球面平滑插值上“力不从心”。 这样看来,也许四元数的出现正是一个 timing 吧……
提到欧拉角,就不得不提及罗德里格旋转公式。罗德里格旋转公式的出现将欧拉角从较复杂的旋转矩阵计算“解放”了出来。该旋转公式是这样的:
给定旋转轴 r⃗ ,旋转角 θ 以及一个点 p⃗ ,则 p⃗ 绕旋转轴 r⃗ 旋转 θ 后的点的计算公式为:
R(r⃗ ,θ,p⃗ )=p⃗ cosθ+(r⃗ ×p⃗ )sinθ+r⃗ (r⃗ p⃗ )(1−cosθ).该公式是由法国数学家 Benjamin Olinde Rodrigues 提出的,当然这位数学家最著名的工作并非这个旋转公式,而是勒让德多项式……好吧,有点扯远了……关于罗德里格旋转公式的证明在此先简略,有兴趣的同学直接参看罗德里格旋转公式的维基百科即可。( Rodrigues′rotation formula )
罗德里格旋转公式的核心思想是向量分解:不妨先假设三维空间中待旋转的点(向量)为 p⃗ ,将其分解为两个分向量的和,其中一个向量平行于旋转轴,另外一个向量与旋转轴正交。这样,我们便可以使用二维平面上的旋转方法操作 p⃗ 正交于旋转轴的分向量(如下图所示):
罗德里格旋转公式示意图如此一来,我们直接在一个2维平面上旋转 p⃗ 正交于旋转轴的分向量之后,再把它与 p⃗ 平行于旋转轴的分向量相加即得最终的结果。某种程度上,我们将3维的旋转操作转化为了2维的旋转操作,达到“降维”的效果。 有趣的是,罗德里格旋转公式通常还有另外一种形式(先剧透一下,这与后面要介绍的四元数息息相关~):
令 a2+b2+c2+d2=1 ,其中 a=cos(θ/2) ,再设 r=(b,c,d)=sin(θ/2)r⃗ ,那么罗德里格旋转公式还可以写成:
R(a,r,p⃗ )=2a(r×p⃗ )+2(r×(r×p⃗ ))+p⃗ .证明:将 a=cos(θ/2),r=(b,c,d)=sin(θ/2)r⃗ 代入即可,
2a(r×p⃗ )=2cos(θ/2)(sin(θ/2)r⃗ ×p⃗ )=(r⃗ ×p⃗ )sinθ,2(r×(r×p⃗ ))+p⃗ =2(r(rp⃗ ))−p⃗ r2)+p⃗ (向量外积的拉格朗日公式)=2(sin(θ/2))2r⃗ (r⃗ p⃗ )−2(1−(cos(θ/2))2)p⃗ +p⃗ (二倍角公式)=r⃗ (r⃗ p⃗ )(1−cosθ)+p⃗ cosθ.因此, R(r⃗ ,θ,p⃗ )=R(a,r,p⃗ ) ,说明罗德里格旋转公式的两种表示形式是等价的。 罗德里格旋转公式第二种表示形式提出了可以用于创建3维旋转的4个参数。也许对于已经学过四元数的同学,看到这里应该会有似曾相识的感觉吧……别急,咱们继续往下看~(^▽^)
呼呼呼……终于进入第二部分噜,谈及四元数不可避免地需要提及复数。我个人始终认为,两者之间的联系是密不可分的。 复数最初来自于求解一些特定的二次方程,比如 x2+1=0 ,其解为 x=−1−−−√ ,不妨给它一个特定的名字,就让 x=i 吧。 复数便是这样一类数字:
a+bi,a,b∈R,i=−1−−−√, 其中, a 为实部,b 为虚部,当忽略虚部 b 时,就是我们常见的实数了。 通常为了便于理解,我们会用二维平面上的一点来表示一个复数,此时两个坐标轴分别为实数轴和虚数轴:四元数的乘法运算法则为: (a bi)(c di)=(ac−bd) (bc ad)i, 也可以写成: (a,b)(c,d)=(ac−bd,bc+ad). 因此,一个复数也只是一个带有特殊乘法运算的2维向量罢了。 同时,我们可以得到: (x+yi)(cosθ+isinθ)=(xcosθ−ysinθ)+i(xsinθ+ycosθ). 经过旋转矩阵一节的“熏陶”以后,相信对这个形式已经不陌生了。没错,这就是2维旋转的复数形式!!!
第二部分哗地一下就结束了(大雾),主要是因为上过高中的应该都学过复数,因此许多性质也就不再赘述。第二部分仅列出理解四元数需要的一些“前置技能”而已,现终于进入核心章节——四元数了(撒花(^▽^))。
爱尔兰数学家汉密尔顿可以说是“四元数之父”了。他在回忆录当中大概是这样描述四元数的诞生的:有一天他在吃早餐时,他的儿子问他:“爸爸,你可以让两个三维向量相乘得到另外一个三维向量吗?”他遗憾地回答:“不,我只能将两个三维向量相加减得到另外一个三维向量。”
(a0+b0i+c0j)(a1+b1i+c1j)=(a0a1−b0b1−c0c1)+(a0b1+b0a1)i+(a0c1+c0a1)j+b0c1ij+c0b1ji. 那么这里就会出现问题了, ij,ji 应该等于多少?显然这里的乘法运算不能直接定义为向量内积,因为两个向量作内积的结果是一个实数(降维了……)。此外,哈密尔顿还不希望该乘法运算不满足交换律,且两个非零向量作乘法运算以后不能产生零向量(即没有零因子, 想起无零因子环???)当时哈密尔顿无法定义出满足上述条件的乘法运算。显然,在这里叉积也是无法奏效的,因为两个平行向量的叉积为零向量,而且当时的时代也还没有叉积这个概念…… 终于在1843年,当汉密尔顿在都柏林的布鲁姆桥下沿着皇家运河散步时,他突然意识到仅仅三个维度是无法定义出满足上述条件的乘法运算,必须增加一个维度才行!四元数就此诞生!他非常兴奋地将他的成果雕刻在桥上,大致内容如下: 任意一个四元数可以表示成 ω+xi+yj+zk,ω,x,y,z∈R 的形式,其中 i2=j2=k2=ijk=−1,ij=k,jk=i,ki=j,ji=−k,kj=−i,ik=−j, 用乘法表即可表示成:注意,这里的乘法运算不再满足交换律。如果你熟悉叉积,这可能看起来很熟悉。实际上,叉积正是来自四元数。 此外,对于上述定义的乘法运算,每一个非零向量都有其逆元。后面还会提到,这意味着当我们希望撤销某一个旋转时,我们只要取该旋转操作对应的四元数的逆元即可,这是十分方便的。 讲到这里,我们现在已经可以回答前两个问题了(可能有同学已经忘了最初的问题是什么,这里再贴一下):
为什么是四个元? i,j,k 是什么? 为什么旋转的公式是 qpq−1 ?为什么是 θ/2 ?我们应该如何理解 4D ( 4D 可视化)?对于问题1:为什么是四个元? 因为仅仅三个维度是无法定义出满足条件的乘法运算的: 1. 两个向量作乘法运算的结果仍旧是一个向量; 2. 该乘法运算不满足交换律; 3. 两个非零向量作乘法运算以后不能产生零向量。 对于问题2: i,j,k 是什么? 存在于四维空间的三个坐标轴。
某种程度上,四元数是由复数扩展而来的:
a+bi⇒ω+xi+yj+zk 它可以表示四维平面上的一点 (ω,x,y,z) ,或者可以写成 (ω,v),v=(x,y,z) ,这是四元数很常用的一种表示形式,大家可以记一记。 四元数的加减和普通的向量加减是类似的,因此这里不再赘述。我们讲讲四元数的乘法。 取两个四元数 q0=(ω0,v0),q1=(ω1,v1) ,定义四元数之间的乘法运算如下: q1q0=(ω1ω0−v1v0,ω1v0+ω0v1+v1×v0). 需要注意的是,因为包含叉积运算的缘故,故四元数之间的乘法运算一般不满足交换律: q1q0≠q0q1 。 这样,我们容易知道,对于四元数之间的乘法运算和任意四元数 q ,其单位元是e=(1,0,0,0),( eq=qe=q )。当 q 非零元(0,0,0,0)时,q−1存在,使得 qq−1=q−1q=e 成立。与复数类似,单位四元数(记住,这类四元数的模是1!!!)可以表示一个旋转,后面会进行证明。 对于3维旋转,令 \omega=cos(\theta/2),\\(x,y,z)=v=sin(\theta/2)r.
ω=cos(θ/2),(x,y,z)=v=sin(θ/2)r. 看过前面的罗德里格旋转公式的同学,一定对此表示十分熟悉……接下来便会介绍如何利用四元数进行3维向量的旋转。 对于单位四元数 q=(\omega,v)=(cos(\theta/2),sin(\theta/2)r) 而言,我们很容易就可以知道它的逆元(自己拿笔在纸上验证一下即可): q^{-1}=(\omega,v)^{-1}=(cos(-\theta/2),sin(-\theta/2)r)\\=(cos(\theta/2),-sin(\theta/2)r)\\=(\omega,-v)=q^{*}. 易知,单位四元数的逆元是和其共轭相等的。再次强调一下,这个性质仅仅是对单位四元数成立。有了它,四元数的旋转定理(自己命名的,捂脸逃走( /ω\))也就顺理成章地腾空出世了~四元数旋转定理: 已知三维向量p 和单位四元数q,将p 看成一个四元数(为了方便,我依旧命名为p),即p=(0,p),则向量p 经旋转q 操作后的结果为: p'=qpq^{-1}=qpq^{*}.
还可以写成如下形式: p'=p+2\omega(v\times p)+2(v\times(v\times p)). (喂,这不就是罗德里格旋转公式吗……)先来看第二种形式,你们的猜测是没错的,这其实就是罗德里格旋转公式的“变种”,证明方法也与罗德里格旋转公式的证明方法类似,只要先把单位四元数q 拆成\omega 和v 即可,即: 四元数 \omega=cos(\theta/2),(x,y,z)=v=sin(\theta/2)\vec r,\\p'=p 2\omega(v\times p) 2(v\times(v\times p)); 罗德里格旋转公式 a=cos(\theta/2),(b,c,d)=r=sin(\theta/2)\vec r,\\p'=p+2a(r\times p)+2(r\times(r\times p)).
不得不佩服一下,罗德里格真的是一个富有远见的数学家…… 再来看看第一种形式: p'=qpq^{-1}=qpq^{*} ,它的证明可不简单,但我觉得还是有必要写一下,因为它的证明确实是蛮精彩的。(当然,没兴趣的同学就跳过吧……)证明过程参考自博客 三维旋转:旋转矩阵,欧拉角,四元数,不过我个人感觉博主的证明写得有些乱,因此我整理如下: p'=qvq^{-1}=qvq^{*}. 证明:如下图所示, u 为旋转轴上的单位向量,旋转角度为\sigma ,向量 v 旋转到w 处,旋转到 \sigma /2 处为 k ,图中并未标出。 (图片来自参考博客)令四元数q=(cos(\sigma/2),sin(\sigma/2)u),则 q=(cos(\sigma/2),sin(\sigma/2)u)=(\frac{kv}{{{|v|^2}}},v\times k)=\frac{1}{{{|v|^2}}}(0,-k)(0,v), 为了方便,仍旧用四元数 k^* 表示 (0,-k) ,四元数 v 表示(0,v),即 q=\frac{1}{{{|v|^2}}}k^*v. 现在令 w=qvq^*, 如果能证明 w 与v 的夹角是 \sigma ,且 v,k,w 在同一个平面上,则说明 w 是v 绕旋转轴 u 旋转\sigma 得到的,从而命题得证。 不妨来计算一下 wk^* 有: wk^*=(qvq^*)k^*=qvq^{-1}k^*\\ \ =qv(\frac{1}{{{|v|^2}}}k^*v)^{-1}k^*\\ \ \ =|v|^2qvv^{-1}(k^*)^{-1}k^*\\ \quad \quad \quad \ \ \ \ =|v|^2q=|v|^2\frac{1}{{{|v|^2}}}k^*v=k^*v, 对于上式 wk^*=k^*v ,将其拆分为四元数的 \omega 部分和 v 部分进行分析:\omega 部分相等表明 w,k 夹角与 k,v 的夹角相同, v 部分相等表明w,-k 与 -k,v 所处的平面相同,这就说明 w 是v 绕旋转轴 u 旋转\sigma 得到的,从而命题得证。 显然,我们已经解决了问题3:为什么旋转的公式是 qpq^{-1} ?接下来我们尝试解决问题4:为什么是 \theta/2 ? 幸运的是,对于这个问题,我在一篇文章 Understanding \ Quaternions 里面找到了答案。限于篇幅,我就不贴出来了,英文比较好的同学可以看一下,英文比较吃力的同学也可以看一下译文 Understanding\ Quaternions中文翻译《理解四元数》。 想要进行一下直观理解的也可以参看下图:
(图片来自知乎问题 如何形象地理解四元数? Yang\ Eninala 的回答)上图非常直观地回答了问题4:为什么在p'=qpq^{-1}里的q=(cos(\sigma/2),sin(\sigma/2)r) 用的是\theta/2 而非\theta? 直观原因就是q,q^{-1} 做的均是一个\theta/2 的旋转,且qp,pq^{-1} 不一定是一个纯四元数(即第一个分量为0的四元数,这是三维向量的特殊表示形式)。因此,为了保证最终结果为一个纯四元数,我们须作两次旋转:第一次旋转结果qp 跳出了原先的三维超平面,第二次旋转结果pq^{-1} 才又回到了三维超平面的世界。
说完了三维旋转,终于可以讲讲四维旋转这样一种人脑很难想象的操作了。实际上,四元数定义的都是四维旋转,而不是三维旋转。三维旋转仅仅是四维旋转的一种特殊情形,就像二维旋转是三维旋转的一种特殊情形一样。利用四元数进行三维旋转时,其实是在一个四维空间上的某个三维超平面上进行的。 一般来说,单独一个四元数是无法执行四维旋转的,一个四维旋转可以唯一地被拆分为一个左旋转 qL 和一个右旋转 qR ,表达出来就是 p′=qLpqR ,此处的 p,p′ 已经不仅仅是一个纯四元数了。
终于到最后一个问题了:我们都是生活在三维空间内的生物,该如何想象“生活在”在四维空间内的四元数呢? 接下来我们仅仅考虑模不超过1的四元数(容易想象一些……),将其构成的4维超球切成三块,它们都是四维空间的半球在3维空间上的投影(在我们看来就是三维球体了),这个“切”的依据便是四元数 q=(w,v),w∈R,v∈R3 中 w 的大小了。如下图所示:
模不超过1的四元数构成的4维超球“切割”示意图经过投影切割后,我们可以得到两个实心球(w>0 或 w<0 )和一个空心球( w=0 )。在4维空间上,左右两个实心球通过中间的空心球连接在一起。对于任意球体的某个截面而言,四元数“退化”成了一个三维向量,其长度为 |sin(θ/2)| ( q=(w,v)=(cos(θ/2),sin(θ/2)r,|r|=1 )。 当截面上的向量长度等于0时,你就会得到一个单位四元数(此时 sin(θ/2)=0,cos(θ/2)=1,−π≤θ≤π )。 w>0 所属实心超球里的四元数表示 −π 到 π 的旋转,而 w<0 所属实心超球里的四元数同样表示 −π 到 π 的旋转,只不过此时的旋转轴需要被翻转; w=0 所属空心超球里的四元数则代表大小为 π 的旋转。
前面已经介绍过欧拉角的万向节锁问题,正是由于万向节锁导致欧拉角无法很好地用于三维旋转的插值(当出现万向节锁现象时无法按照原定旋转顺序得到预期的旋转,即欧拉角用于三维旋转的插值时并不平滑)。那么。这时候就必须要四元数站出来扛大梁了~以下部分参考文章 Understanding Quaternions 中文翻译《理解四元数》以及四元数插值与均值(姿态平滑),限于篇幅,这里只介绍计算机图形学里表示三维旋转插值常用的 SLERP 方案的球面线性插值。 SLERP 方案可以在两个朝向之间平滑地进行插值,不妨设第一个朝向为 q1=(ω1,x1,y1,z1) ,第二个朝向为 q2=(ω2,x2,y2,z2) ,它们都是单位四元数,它们的夹角是 θ ;被插值前的点为 p ,插值后的点为p′,插值参数为 t∈[0,1] ,当 t=0 时 p′=q1 ,当 t=1 时 p′=q2 。 事实上,球面上的线性插值和一般的线性插值是有一些区别的。一般的线性插值公式为 vt=v1+(v2−v1)t,t∈[0,1] ,我们可以将 vt 理解为起点为原点,终点在 v2−v1 上的插值向量。如下图所示:
(图片来自参考文章)当 t=14,24,34 时, vt 将 v2−v1 等分为四部分,但对应的弦长却并不相同!说明,当 t 均匀变化时,对应的旋转角速度并不是均匀变化的,因此有必要采用更合理的插值公式。 不妨设球面线性插值的计算公式为vt=p(t)v1 q(t)v2,|v1|=|vt|=|v2|=1,现在我们要计算出 p(t),q(t) 。注意到 v1,v2 之间的夹角为 θ , v1,vt 之间的夹角为 tθ , vt,v2 之间的夹角为 (1−t)θ 。如下图所示:
(图片来自参考文章)将 vt=p(t)v1+q(t)v2 与 v1 作内积得:
vtv1=p(t)v1v1+q(t)v2v1,costθ=p(t)+q(t)cosθ, 再将 vt=p(t)v1+q(t)v2 与 v2 作内积得: vtv2=p(t)v1v2+q(t)v2v2,cos(1−t)θ=p(t)cosθ+q(t), 联立求解并用三角函数和差公式化简得: p(t)=costθ−cos((1−t)θ)cosθ1−cos2θ=costθ−cos2θcostθ−sinθsintθcosθsin2θ=sin2θcostθ−sinθsintθcosθsin2θ=sinθcostθ−sintθcosθsinθ=sin((1−t)θ)sinθ,q(t)=cos((1−t)θ)−costθcosθ1−cos2θ=cosθcostθ+sinθsintθ−costθcosθsin2θ=sinθsintθsin2θ=sintθsinθ, 故计算向量的球面线性插值的计算公式如下所示: vt=sin((1−t)θ)sinθv1+sin(tθ)sinθv2. (图片来自参考文章)SLERP 方案将上述公式原封不动地套用到了四元数身上,得到:
qt=sin((1−t)θ)sinθq1+sin(tθ)sinθq2. 注意到,在使用这个公式之前还需要计算 q1,q2 的夹角 θ ,我们可以利用内积公式进行计算得到 θ : cosθ=q1q2|q1||q2|=ω1ω2+x1x2+y1y2+z1z2|q1||q2|,θ=arccos(ω1ω2+x1x2+y1y2+z1z2|q1||q2|). 需要注意的是,当 q1,q2 内积为负数时,直接使用上述计算公式会让整个插值过程“绕远路”,如下图所示: 四元数的球面插值示意图可以看到,当 q1,q2 内积为负数时,从 q1 “走”到 qt 需要绕过半个多球面(你能怎么办?相信你也很绝望吧QAQ)。这时候,我们需要对其中一个取反(即四元数四个分量都取其相反数),这里我们对 q1 取反,可以看到,从 −q1 “走”到 qt 要比从 q1 “走”到 qt 少绕了半个球面!!! 此外,当 sinθ=0 时上述计算公式是无法使用的,因为分母为0整个分式是个未定式。此时,我们只能退而求其次采用平面上的线性插值公式了: qt=q1+(q2−q1)t,t∈[0,1].
本章建议学习过一些抽象代数的同学观看,没有学过的就跳过吧……(丫咩萝,也是花了一些精力在这里的o(╥﹏╥)o)部分内容参考自文章四元数的运算。
四元数除环群旋转四元数的矩阵表示 Q8 群由除环的定义,四元数所构成的集合是一个除环(如果四元数还满足交换律它就构成一个域)。 四元数除环因为它的不可交换性导致了一个很有趣的结果:四元数的 n 阶多项式可以拥有数目多于n 的根(即在四元数除环代数基本定理无法成立)。例如方程 x2+1=0 就有无数多个解。事实上,只要 b2+c2+d2=1,b,c,d∈R ,那么 x=0+bi+cj+dk 便是一个解。
在“四元数和空间转动”的维基百科里提到:
非零四元数的乘法群在R3的实部为零的部分上的共轭作用可以实现转动。单位四元数(绝对值为1的四元数)若实部为 cosθ ,它的共轭作用是一个角度为 2θ 的转动,转轴为虚部的方向。四元数的优点是: - 表达式无奇点(和例如欧拉角之类的表示相比) - 比矩阵更简炼(也更快速) - 单位四元数的对可以表示四维空间中的一个转动。
那么在群论观点下,四元数是如何和三维旋转扯上联系的呢?不妨看看四元数除环的四个基 1,i,j,k ,它们生成群 U(2) ,而 U(2)/U(1)=SU(2) 。 又 SU(2)/Z2≅SO(3),SU(2)≅S3 ,其中, U(n) 是 n 级酉群(即n×n 阶酉矩阵组成的群), SU(2) 是2级特殊酉群, SO(3) 是3级特殊正交群(又称为三维旋转群), S3 是三维单位球面。这说明单位四元数通过群同构能够与三维旋转产生极强的联系,且单位四元数从抽象意义上看是分布在一个三维单位球面上的。 同时, S3 与 SO(3) 之间存在着一个2对1的同态满射,这说明 S3 是 SO(3) 的双重覆盖,说人话就是每两个四元数才能对应一个三维旋转,这样大家也许能对四元数旋转公式 p′=qpq∗ 有一个更深的理解吧……
因为单位四元数构成的三维单位球面 S3 群同构于2级特殊酉群 SU(2) ,因此我们也可以通过研究 SU(2) 去刻画 S3 的性质。给出 S3 到 SU(2) 的同构映射:
S3→SU(2):a+bi+cj+dk→(a−bi−b+cib+cia+di). 而 SU(2) 的四个基为 I=(1001),X=(−i0 0i),Y=(0 −11 1),Z=(0ii1), 故 X2=Y2=Z2=XYZ=−1,(a−bi−b+cib+cia+di)=aI+bX+cY+dZ. 此时 SU(2) 表现出类似四元数的性质,这正是群同构的奇妙之处!!! 同理,我们还可以将单位四元数构成的三维单位球面 S3 映射到一类特殊的四阶实数矩阵上(我暂且把这类特殊的四阶实数矩阵称之为 M(4) ,我暂时还没验证是否是一个群……有点懒……),映射如下所示: S3→M(4):a+bi+cj+dk→⎛⎝⎜⎜⎜ a−b d−c ba−c−d−d c a −b cd ba⎞⎠⎟⎟⎟. 再令 I=⎛⎝⎜⎜⎜1000010000100001⎞⎠⎟⎟⎟,X=⎛⎝⎜⎜⎜ 0001 00 −1 0 0100−1 000⎞⎠⎟⎟⎟,Y=⎛⎝⎜⎜⎜ 0010 0001−1 000 0 −1 00⎞⎠⎟⎟⎟,Z=⎛⎝⎜⎜⎜0−1 001 0 000 0 010 0 −1 0⎞⎠⎟⎟⎟, 则 X2=Y2=Z2=XYZ=−1M=aI+bX+cY+dZ,M∈M(4). 此时 M(4) 同样表现出类似四元数的性质!!! 事实上,上述两种矩阵表示都可以认为是四元数另外的存在形式。 既然可以用2级特殊酉群 SU(2) 来研究单位四元数构成的三维单位球面 S3 群的结构,那么我们是否又可以用单位四元数构成的三维单位球面 S3 群去研究三维旋转群 SO(3) 的结构呢?相信这才是四元数诞生的真正意义。答案当然是肯定的,在群表示论里面常常用不可约表示去研究一个群的结构,由于涉及到太多专业名词,限于篇幅,这里仅仅介绍一个关于如何去研究三维旋转群 SO(3) 结构的定理:设 Vn 是两个不定元 x 和y 的 n 次齐次多项式组成的复线性空间。考虑拓扑群SL(2,C) 到拓扑群 GL(Vn) 的一个映射 ϕn :
ϕn:SL(2,C)→GL(Vn),A↦ϕn(A), 其中 (ϕn(A)f)(x,y)=f((x,y)A)=f(a11x+a21y,a12x+a22y), 这里 f∈Vn,A=(aij) ,则 SU(2) 的上述不可约复表示 ϕn(n=0,1,2,……) 是 SU(2) 的全部不等价的有限不可约复表示。从而 SO(3) 的上述不可约复表示 ϕ2m(m=0,1,2,……) 是 SO(3) 的全部不等价的有限不可约复表示。因为单位四元数构成的三维单位球面 S3 群同构于2级特殊酉群 SU(2) (熟悉的开场白……),我们不妨只取 SU(2) 的部分元素来研究,从而能够解释 S3 群的部分结构。只取以下八个元素:
E=(1001),−E,I=( 01−10),−I,J=(i00i),−J,K=(0ii0),−K. 容易验证,它构成一个群,我们称之为 Q8 。其中, E 是单位元,−E 是二阶元, I,−I,J,−J,K,−K 是四阶元。 若将 Q8 看成一个有序集,显然, Q8 依次左乘其每一个元素会产生一个置换。比方说, Q8 左乘 I : (E,−E,I,−I,J,−J,K,−K)→(I,−I,−E,E,K,−K,−J,J), 这对应于一个置换(1324)(5768)。这是很符合凯莱定理的:任何一个群都同一个变换群同构,这意味着我们能通过研究常见的置换群去“解剖”抽象的 Q8 的结构。 此外, Q8 还具有一个很棒的性质: Q8 的每一个子群都是正规子群。我们可以简单证明一下。 证明:对于 Q8 的任意子群 H ,由拉格朗日定理,|H|||Q8|=8,故 |H|=1,2,4,8 。 当 |H|=1,8 时, H 是Q8 的平凡子群,显然是正规子群; 当 |H|=2 时, H={E,−E} ,易验证,此时 H 是一个正规子群; 当|H|=4 时,任取 q∈Q8 ,若 q∈H ,此时 qH=Hq ;若 q∉H ,则 Q8=H∪Hq=qH∪H ,从而 qH=Hq 。 综上所述, Q8 的每一个子群都是正规子群,命题得证。 因为 Q8 这个很棒的性质,要从 Q8 中构造一个商群就容易得多了,而这个操作在数学上是很常用的。可以说,很多走代数方向的研究生,掌握 Q8 的各种性质几乎已经成为一个“能力标配”!!!很多时候,我们不仅要仰望星空,还需要脚踏实地。四元数在 Unity 这个游戏引擎当中是很常用的,主要也是用于刚体的三维旋转。 Unity 提供了 Quaternion.AngleAxis 这一个方法来创建旋转,该方法的声明如下所示:
static function AngleAxis (angle : float, axis : Vector3) : Quaternion //绕axis轴旋转angle,创建一个旋转,即返回一个四元数变换。这是继承自 Quaternion 类的一个方法。当我们想对某一个三维向量进行旋转时,我们仅需要采用 Quaternion.AngleAxis 返回的旋转变换左乘该三维向量即可,不需要再进行右乘的操作(方便多了有木有⁄(⁄ ⁄•⁄ω⁄•⁄ ⁄)⁄,就相当于前面所说的 p′=qpq∗ 的形式)。代码如下所示:
//将待旋转向量绕y轴旋转30度 Vector3 newVector = Quaternion.AngleAxis(30, Vector3.up) * oldVector;在 Unity 中四元数的插值也是很方便的,它提供了 Quaternion.Lerp 这个方法进行插值,该方法的声明如下所示:
static function Lerp (from : Quaternion, to : Quaternion, t : float) : Quaternion //通过t值from向to之间插值,并且规范化结果。 //这个比Slerp更快但是如果旋转较远看起来就比较差。这同样是继承自 Quaternion 类的一个方法。在实际使用时,若你想实现一个物体在某两个朝向之间转动的效果,我们可以将它放到 Update() 方法里来实现,该方法会被每帧调用一次,代码如下所示:
using UnityEngine; using System.Collections; public class example : MonoBehaviour { public Transform from; public Transform to; public float speed = 0.1F; void Update() { transform.rotation = Quaternion.Lerp(from.rotation, to.rotation, Time.time * speed); } }Quaternion 类还提供了许多有用的方法,限于篇幅,大家可以阅读 Unity 的 API 文档深入了解。此外,网上也有很多实现了四元数的开源库,大家可以去了解一下。就我个人而言,我认为 GLM 这个开源库做得还是不错的(没试过其它的……囧)。 呼呼呼……终于完成这篇“吐血之作”了,希望能对大家有帮助(也不知道有木有人看2333)。这篇写得有点长,开学以后就没那么多时间写了,以后的篇幅当然会短得多,而且更新的频率可能不高……但能肯定的是以后的每一篇博文都会用心写。下一篇博文的主题预计会是今年 GDC 上提到的随机数字生成( RNG ),可以小小期待一波(完结撒花(^▽^))!