旋转, 处处都能看到的一种现象. 大到宇宙中星体的自转、公转, 小到车辆轮子的运动, 甚至我们的手一伸一握, 都包含着指节围绕关节的旋转. 你可能会想, 这么常见的东西一定很简单吧? 其实并没有那么简单. 作为一名游戏开发工程师, 我们能在网上找到关于欧拉角, 四元数, 万向锁的资料数不胜数, 但大都不成体系. 而且研究比较深入的反而是在运动控制, 飞行控制领域. 可能飞控不好就会炸机…

下面让我们来看下Unity中旋转相关的知识点.

Unity中的旋转

手头有一个简易的飞机模型, 放在Unity里正好合适, 你也可以选择一款自己喜欢的机型. (一方面, 旋转中的几个术语来源于飞行. 另一方面, 飞机模型比起球体, 立方体来讲更加直观.)

先转一下

我们在Inspector中, 可以让飞机分别绕X, Y, Z轴旋转一下. 可以看到这三种旋转分别对应了飞机可以做的三种基本旋转:

英文 中文
X pitch 俯仰角
Y yaw 偏航角
Z roll 滚转角

X,Y,Z轴的颜色正好和三原色Red,Green,Blue对应. 在游戏开发中我们还能在其他地方也看到这种对应关系.

当X轴旋转±90°时

我们先在Inspector中将飞机绕X轴旋转90°, 这时再操作Y轴与Z轴, 我们可以惊奇地发现, 旋转效果 似乎 是完全一致的. 我们再也不能通过旋转Y轴, 让飞机偏航(yaw)了.

此时(保持绕X轴旋转90°), 我们先后操作Y轴和Z轴相同的度数(例如30°), 我们会发现两次操作抵消了, 飞机的姿态回到了原位, 即Y,Z轴都是0°的状态.

我们再让飞机的X轴旋转-90°, 再操作Y轴和Z轴一定的度数(例如各30°), 可以看到其效果等同于单独操作Y轴或Z轴其度数之和(30°+30°=60°).

不可跨越的90°

Unity提供了多种旋转的Api, 其中由对象的transform提供的eulerAngles可以获取或设置欧拉角. 我们尝试用其让飞机做持续旋转的动画.

我们使用如下代码:

1
2
3
4
void Update()
{
    transform.eulerAngles += new Vector3(30 * Time.deltaTime, 0, 0);
}

效果如下:

本来期望飞机可以绕X轴持续进行360°旋转, 但可以看到从0°旋转到90°附近无法跨越过去, 在90°附近反复横跳. 同样如果我们从0°开始, 以上述反方向旋转, 则无法跨过-90°.

这些看起来还真是奇怪的问题呢!

旋转基础

三角函数

想要深入研究旋转, 基础的三角函数是必须的. 但我们在这里就不展开了, 仅给出需要了解的部分定理或公式.

  • 和差化积公式
  • 半角/倍角公式

二维空间旋转

二维空间中, 物体只能围绕某一个点旋转, 我们一般选择绕原点旋转.

在上图中, 我们通过将原基向量 p , q 旋转θ°得到新的基向量 p , q , 所以可以得到二维空间的旋转矩阵:

R(θ)=[p q ]=[cos(θ)sin(θ) sin(θ)cos(θ) ] 

之所以选择原点是因为我们总是可以对物体做平移操作, 先平移至原点, 旋转之后再平移回去. 从数学上讲, 平移操作是非线形操作, 我们需要将二维空间升至三维空间做平移, 即引入齐次坐标. 平移和旋转恰好可以互不影响地存在于升维后的矩阵中, 所以我们可以简便地采用以原点为中心点做旋转.

坐标系与旋转正方向

在三维空间中, 左手坐标系中采用左手法则确定旋转正方向, 右手坐标系采用右手法则确定旋转正方向. Unity中采用了左手坐标系, 因此我们下面提到的旋转都遵循左手法则.

绕坐标轴旋转

我们先看一下三维空间中绕x轴旋转. 因为绕x轴旋转, 则其x坐标不变, 也意味着其x基向量不变, 与二维空间的旋转类似.

由上图我们可以得到原基向量 p , q , r 旋转θ°后得到新的基向量 p , q , r , 也得到三维空间中绕x轴的旋转矩阵:

Rx(θ)=[p q r ]=[100 0cos(θ)sin(θ) 0sin(θ)cos(θ) ] 

同理, 我们可以很快得出绕y轴的旋转矩阵.

Ry(θ)=[p q r ]=[cos(θ)0sin(θ) 010 sin(θ)0cos(θ) ] 

以及绕z轴的旋转矩阵.

Rz(θ)=[p q r ]=[cos(θ)sin(θ)0 sin(0)cos(0)0 001 ] 

绕任意轴的旋转

旋转也可以围绕任意的轴, 我们仍然不考虑平移的问题, 假设旋转轴通过原点.

如上图所示, 向量 v (米黄色)围绕单位向量 n 旋转 θ°, 得到新的向量 v (土黄色). v 可以分解为和 n 平行的和垂直的两个变量 v (红色), v (绿色), 既 v=v+v . 旋转后 v 因为和 n 平行不受影响, v 变为 v . 我们可以看到 vv 之间的夹角为 θ (注意: 不是向量 vv 之间的夹角为 θ). 还添加了一个与 nv 都垂直的向量 w , w 的长度和 vv 相等.

根据这些已知的关系, 我们有以下等式:

v 可以看做 vn 上的投影, 我们有:

(1)v=(vn)n 

v 是其平行于和垂直于 n 的向量和:

(2)v=vv=v(vn)n 

nv 平行:

(3)n×v=0 

wnv 叉乘所得:

(4)w=n×v =n×(vv) =n×vn×v =n×v0 =n×v 

我们首先可以看到, v 可以看做其在 vw 上的投影向量之和, 所以我们有:

(5)v=vcos(θ)+wsin(θ) =(v(vn)n)cos(θ)+(n×v)sin(θ) 

旋转后的向量 v :

(6)v=v+v =(vn)n+(v(vn)n)cos(θ)+(n×v)sin(θ) 

由此我们得出了任意向量 v 绕单位向量 n 旋转 θ 后的向量 v 公式.

我们知道旋转矩阵由其旋转后的坐标系的基向量组成, 我们使用上面公式(6), 就可以求解新的基向量以及旋转矩阵.

R(n,θ)=[p q r] 

矩阵形式太复杂了, 暂时先不详细列出了.

旋转的几种表示方法

旋转有多种表示方法, 例如轴角法, 旋转矩阵, 欧拉角, 四元数, 游戏引擎中用到了多种旋转表示方法. 我们来看看这些不同表示方法之间的联系.

旋转矩阵

我们可以用基向量 p,q,r 旋转后的基向量 p,q,r 组成 3×3 旋转矩阵. 旋转矩阵为正交矩阵.

正交矩阵就意味着, 其转置矩阵等于其逆矩阵, 并且转置矩阵和原矩阵乘积为单位矩阵I.

MT=M1  MMT=MM1=I 

  • 旋转矩阵的优点

    • 矩阵形式可以方便地在物体坐标系和惯性坐标系之间旋转向量.
    • 常见的图形学API都是使用矩阵. 无论如何存储旋转数据, 在最终渲染前都需要在某处将其转换为矩阵形式.
    • 方便坐标系嵌套类型的计算. 只需要将矩阵依次相乘即可得到最终的旋转矩阵.
    • 求逆向旋转方便. 逆向旋转需要逆矩阵, 而逆矩阵与转置矩阵相同, 通过简单的转置即可求得.
    • 矩阵不存在万向锁
    • 可以表达绕任意轴旋转, 四元数只能绕物体中心的轴
  • 旋转矩阵的缺点

    • 需要更多内存. 矩阵需要存储9个数字, 相比欧拉角只需要3个数字, 四元数仅需要4个数字.
    • 矩阵非常不直观, 也几乎不可能不借助计算机计算出任意方位的旋转矩阵.
    • 矩阵可能不是合法的旋转矩阵. 矩阵蠕变可能会导致旋转矩阵不合法, 需要通过矩阵正交化解决.

矩阵有9个数字组成, 这相当于给旋转矩阵带来了9个自由度. 但显然不是随意填入9个数字都能是旋转矩阵. 旋转矩阵要求其3个行向量是单位矩阵, 并且3个行向量需要互相垂直.我们也可以理解为, 我们把三维空间的问题转化为九维空间带有六个约束的问题.

轴角法

使用绕任意轴旋转的方法, 也可以表达旋转.

  • 优点

    没有万向锁的问题

  • 缺点

    • 插值不够平滑, 甚至会产生跳跃问题

    • 不能保证插值得到的矢量长度相等. (TODO 为什么????)

  • 和四元数比较 轴角法在三维空间中, 四元数是四维空间.

欧拉角

数学家欧拉证明了单个角位移可以由多个角位移组合而成.

旋转顺序

必须明确旋转顺序, 指定其旋转轴???? Unity的旋转顺序??? Unity使用 ZXY 顺序.

固定轴和动态轴

角度范围

欧拉角优点:

  • 直观
  • 可以表达大于180° 的旋转.
  • 存储占用数据少

欧拉角缺点

  • 万向锁问题

四元数

复数和旋转

可以从复数的角度理解复数 i . 将数字 1 旋转90°, 可以得到 1i=i , 继续旋转90°, 可以得到 ii=1 .

  • 复数可以看作两元数???
  • 三元数???

复数与欧拉旋转定理???

四元数的由来

i2=j2=k2=ijk=1  ij=k=ji  jk=i=kj  ki=j=ik 

四元数的基础

  • 四元数的记法

    四元数包含一个标量分量和一个三维向量分量. 标量分量常记作 w, 向量分量可以记作单一向量 v 或分开的 x, y, z.

    两种记法如下: [w,v]  [w,(x,y,z)] 

  • 四元数与轴角法

  • 负四元数

    将四元数的各分量加上负号, 得到负四元数. q=[w,(x,y,z)]=[w,(x,y,z)]=[w,v]=[w,v] 

    qq 代表的角位移是相同的. θ° 加上 360° 的奇数倍, 不会改变四元数代表的角位移, 四元数变为负四元数. 三维旋转的角位移有两种不同的四元数表示法, 它们互为负四元数.

  • 单位四元数

    单位四元数表示没有旋转. 由负四元数可知, 几何上有两种四元数可以表示单位四元数: [1,0][1,0] .

    但在数学上只认为 [1,0] 为单位四元数. qq尽管代表了相同的角位移, 我们依然认为其不等. [1,0] 也不是单位四元数.

    任意四元数 q 乘以单位四元数, 依然得到其自身.

    单位四元数的模为1.

    要区分单位旋转和单位四元数.

    ??? 单位四元数可能定义为 q^=[0,v^] , 其中 v^ 为单位向量. q^2=[0,v^][0,v^]=[v^v^,v^×v^]=[1,0] 

    只看实数部分, 单位四元数的平方为 -1.

    所有的旋转都是单位四元数.

  • 纯四元数

    标量分量为0的四元数称为纯四元数.

四元数的运算

  • 加减法

  • 模也被称作范数.

    q=[w,(x,y,z)] =w2+x2+y2+z2 =[w,v] =w2+v2 

  • 共轭和逆 四元数的共轭记作 q , 将四元数的向量分量变负得到其共轭. q=[w,v] =[w,v] =[w,(x,y,z)] =[w,(x,y,z)] 

    四元数的逆记作 q1 , 四元数的共轭除以其模得到其逆. q1=qq 

    四元数乘以其逆得到单位四元数. qq1=[1,0] 

    单位四元数的共轭和逆相等.

    四元数 q 和其共轭 q 代表了相反的角位移. 因为是将其向量分量变负, 相当于颠倒了旋转方向.

  • 乘法-叉乘

    四元数有点乘和叉乘, 如果不特别指出, 乘法指叉乘.

    • 定义

    四元数的叉乘算法如下: (w1+x1i+y1j+z1k)(w2+x2i+y2j+z2k) =w1w2+w1x2i+w1y2j+w1z2k+ x1w2i+x1x2i2+x1y2ij+x1z2ik+ y1w2j+y1x2ji+y1y2j2+y1z2jk+ z1w2k+z1x2ki+z1y2kj+z1z2k2 =w1w2+w1x2i+w1y2j+w1z2k+ x1w2i+x1x2(1)+x1y2(k)+x1z2(j)+ y1w2j+y1x2(k)+y1y2(1)+y1z2(i)+ z1w2k+z1x2(j)+z1y2(i)+z1z2(1) =w1w2x1x2y1y2z1z2+ (w1x2+x1w2+y1z2z1y2)i+ (w1y2+y1w2+z1x2x1z2)j+ (w1z2+z1w2+x1y2y1x2)k 

    所以四元数乘法可以定义如下: [w1,(x1,y1,z1)][w2,(x2,y2,z2)]= [w1w2x1x2y1y2z1z2 w1x2+x1w2+y1z2z1y2 w1y2+y1w2+z1x2x1z2 w1z2+z1w2+x1y2y1x2] 

    [w1,v1][w2,v2]=[w1w2v1v2,w1v2+w2v1+v2×v1] 

    • 叉乘的符号

      不需要为四元数叉乘使用符号

    • 叉乘的结合律和 交换律

      四元数满足结合律, 但不满足交换律. (ab)c=a(bc)  abba 

    • 叉乘的模

      四元数叉乘的模等于模的乘积. q1q2=q1q2 

    • 叉乘的逆

      四元数叉乘的逆等于各个四元数的逆以相反的顺序相乘. (ab)1=b1a1 

    • 叉乘和旋转

      假设有三维点 (x,y,z) , 可以扩展为四元数 p=[0,(x,y,z)] . 设 q=[cosθ2,nsinθ2] , 其中 n 是单位向量作为旋转轴, θ 为旋转角. 下面公式可以表示 pn 旋转 θ : p=qpq1 

      上面公式的本质是, 将在四维空间上的旋转限制在一个三维空间中.

      四元数叉乘在处理多次旋转上很有优势, 我们看下将点 p 先旋转 a , 再旋转 b : p=b(apa1)b1 =(ba)p(a1b1) =(ba)p(ba)1  我们可以看到先执行 a 旋转, 再执行 b 旋转, 等价于执行乘积 ba 的单次旋转.

      可以通过改变四元数叉乘的定义, 来反转乘法顺序. 此时并没有改变四元数的基本性质和几何解释. [w1,v1][w2,v2]=[w1w2v1v2,w1v2+w2v1+v1v2] 

      调整叉乘定义后, 旋转定义如下: p=q1pq 

      连续旋转乘法和旋转发生顺序一致: p=b1(a1pa)b =(b1a1)p(ab) =(ab)1p(ab) 

    • 四元数的差

      利用给定方位 ab , 求出 ab 的旋转角位移 d . d 被定义为 ab 的差. ad=b a1(ad)=a1b (a1a)d=a1b [1,0]d=a1b d=a1b 

      在数学上, 两个四元数之间的差更类似于除法, 而不是普通代数意义上的减法.

    • 四元数的点乘 q1q2=[w1,v1][w2,v2]=w1w2+v1v2 =[w1,(x1,y1,z1)][w2,(x2,y2,z2)]=w1w2+x1x2+y1y2+z1z2 

      点乘的结果为标量. 对于单位四元数 q1 q2 , 1q1q21.

      通常只关心点乘的绝对值, 因为 q1q2=(q1q2) . q1q2 的绝对值越大说明其代表的角位移越相似.

    • 四元数的对数, 指数

      四元数的对数和指数都是定义如此, 不需要证明.

      设有 α=θ2 , 单位向量 n . 有四元数: q=[cosαnsinα]  q 的对数定义为: logq=log([cosαnsin(α)])=[0αn] 

      四元数的指数以相反方式定义. 有四元数 q=[0,αn] . 则有: exp(p)=exp([0,αn])=[cosα,nsinα] 

      我们可以看到对于四元数而言, 对数和指数依然是互为逆运算. exp(log(q))=q 

    • 标量乘运算

      标量乘运算定义为每个分量乘以标量. kq=k[w,v]=[kw,kv] =k[w,(x,y,z)]=[kw,(kx,ky,kz)] 

    • 四元数的幂

      四元数作为底数, 记作 qt . 当 t 从0到1变化时, qt[1,0]q . 此时得到 qt 相当于 q 的一部分.

      因为四元数总是使用最短圆弧, 当 t 的范围超出 [0, 1] 时, 代表的可能是短圆弧. 例如, q 代表绕坐标轴顺时针旋转30°. 那么 q2 代表顺时针旋转60°, q13 代表逆时针旋转10°. q8 则不是代表顺时针旋转240°, 而是逆时针旋转80°, 走了短圆弧.

      代数中的 (as)t=ast 对四元数不适用.

      qt=exp(tlog(q)) 

四元数插值(slerp)

球面线性插值(Spherical Linear Interpolation)缩写为 slerp, 可以在两个四元数之间做平滑插值. 公式为: q=slerp(q0,q1,t)  t在0到1之间变换.

计算插值一般需要先计算出两个值的差, 并乘以系数t, 加上初始值就是我们需要的插值. 四元数也是如此.

四元数插值步骤如下:

  • 计算 q0q1 的插值. Δq=q01q1 

  • 计算 Δqt 相关的部分.

    t 在 [0, 1]之间变换, 获取 Δq 的一部分, 恰好是计算四元数幂的操作.

    (Δq)t 

  • 计算初始值加上差的一部分.

    对旋转四元数来说, 需要用乘法把两次旋转组合成新的旋转四元数. q0(Δq)t 

最终得到的slerp插值公式如下: slerp(q0,q1,t)=q0(Δq)t=q0(q01q1)t 

上面的推导过程是按照代数的方法推导的.

我们可以在四维空间中解释四元数. 旋转四元数都是单位四元数, 它们都在四维的一个超球面上.

slerp的基本思想就是在四维超球面上沿连接两个四元数的四维超弧上做插值, 这也是球面线性插值这个名称的来历.

可以从二维关系类比四维的情形.

图中 v0,v1,vt 都是单位向量, 其模都为1: v0=v1=vt=1 

图中 k1v1v1 平行, 所以有: k0v0+k1v1=vt 

在三角形 AOB 中有: sin(tθ)=ABvt=AB 

在三角形 ACB 中有: sin(θ)=ABk1v1=ABk1 

由上面两个式子可以得出: k1=sin(tθ)sin(θ) 

继续求解 k0 : k0v0=k0=OC=OBBC=vtcos(tθ)k1cos(θ)=cos(tθ)k1cos(θ) 

代入 k1 并化简, 这里会使用一次三角函数和公式 sin(αβ)=sin(α)cos(β)cos(α)sin(β) : k0=cos(tθ)k1cos(θ) =cos(tθ)cos(θ)sin(tθ)sin(θ) =sin(θ)cos(tθ)cos(θ)sin(tθ)sin(θ) =sin(θtθ)sin(θ) =sin((1t)θ)sin(θ) 

最终我们得到: vt=k0v0+k1v1=sin((1t)θ)sin(θ)v0+sin(tθ)sin(θ)v1 

推广到四元数有: slerp(q0,q1,t)=sin((1t)θ)sin(θ)q0+sin(tθ)sin(θ)q1 

  • 小角度插值 如果只对四元数做简单线性插值(Lerp), 得到的可能就不是一个单位四元数. lerp(q0,q1,t)=(1t)q0+tq1 

    线性插值后除以其模, 得到单位四元数, 这种方法称为正规化线性插值(NLerp).

    Lerp的问题是结果可能不是一个单位四元数, 无法用于旋转. NLerp的问题是当有较大角度插值时, 其角速度变换过大.

    在小角度插值时, Slerp可能会因sin带来除0的问题, 所以可以在小角度时替换为NLerp插值. NLerp计算更为简单, 在小角度插值时, 相较Slerp误差也并不大, 可以作为一种Slerp的优化手段.

四元数样条-squad

slerp可以提供两个方位之间的插值, 并且可以保持插值的角速度固定. 当我们使用slerp给多个方位之间插值时, 插值的角速度会在中间方位处发生突变.

squad在slerp基础上, 实现了多点一阶连续可导

四元数映射-实数??

四元数映射-矢量映射???

四元数与点

四元数的优点

  • 四元数仅需要4个数字就可以表达旋转. 存储空间占用较少, 计算效率高.
  • 四元数不受万向锁的困扰.
  • 四元数容易插值.
  • 相比矩阵可以更容易给出旋转轴与旋转角.

四元数的缺点

  • 四元数由复数(???四维复数)的概念构造, 不易理解, 不符合人的直觉.
  • 单个四元数不能表达任意方向超过180° 的旋转.

四元数和轴角

四元数和轴角法有一定关系, 绕 n 轴旋转 θ 对应的四元数为: q=[cos(θ2)sin(θ2)n]=[cos(θ2)sin(θ2)nxsin(θ2)nysin(θ2)nz] 

Unity与四元数

Unity在引擎内部使用四元数存储旋转和方位, 在其文档中1有明确指出.

Unity提供了 Quaternion 类, 用于相关操作. 下面是其常用的几个方法/属性:

  • Quaternion.identity

    值为[1, \vec{0}] 单位旋转(只读). 表示不旋转, 物体与其父对象或世界轴完全对齐.

    单位旋转和单位四元数不同.

  • Quaternion.LookRotation 根据旋转后物体的 forward (和物体Z轴对齐), upwards (X轴与 forwardupwards 的叉积对齐, upwards 不一定与Y轴对齐) 创建Quaternion.

  • Quaternion.FromToRotation 从 fromDirectiontoDirection 的旋转.

  • Quaternion.Slerp 插值

  • Quaternion.Euler 返回欧拉角表示的四元数旋转.

  • Quaternion.Angle 返回两个四元数之间的夹角(应该绕同一点旋转, 否则无意义)

旋转与群论

有没有三元数

  • 复数相当于二元数 a+bi
  • 三元数如果定义为 a+bi+cj , 会遇到 ij 的乘积无定义的情况

参考文档

文档

为什么你天天只看见四元数和八元数? - 知乎 Understanding Quaternions 中文翻译《理解四元数》 Understanding Quaternions | 3D Game Engine Programming 对游戏开发中的四元数的一些理解 - 知乎