演讲标题:
Practical Quaternions: An Easy Guide to 3D Rotations for Non-Mathematicians
演讲者信息:
Patrick Martin是来自Google的Developer Relations Engineer,在游戏行业拥有超过十年的经验,从事移动游戏、PC 游戏和智能玩具方面的工作。他参与开发了Firebase游戏教程、Star Wars BB-8玩具、Sphero 智能球、Space Miner: Space Ore Bust 、iOS版《大富翁》等。
摘要
Patrick在开发工作中曾遇到多次只能用四元数解决的旋转问题。这篇演讲的主要目的是帮助游戏开发者们从实用而非数学的角度理解四元数(Quaternions),从而更好地实现游戏中的三维旋转。Patrick以他参与开发的iOS版《大富翁》中的旋转功能为例,从实际角度展现了四元数的实用性。演讲中涉及到一些三角函数和简单线性代数,但别害怕,一旦理解四元数,你会发现它也是很直观的。
为什么要用四元数
要实现三维空间中的旋转,一种比较直观的方式是动态欧拉角,也就是用(α, β, γ)三个值来表示某一朝向与物体坐标系xyz三条坐标轴的夹角。
用一架飞机来理解动态欧拉角,通常称沿三轴的旋转为“Yaw, Pitch & Roll”。Roll轴是贯穿机身的前后方向,Pitch轴是贯穿机翼的左右方向,Roll轴是垂直于机身所在平面的竖直方向。对应地,Yaw作为动词指是飞机左右旋转飞行朝向,Pitch指机头机尾上下俯仰,Roll指绕机翼围绕机身左右倾斜。
图1. Yaw, Pitch & Roll 示意图
动态欧拉角很好理解,那为什么我们还要用四元数呢?Patrick 在演讲中展示了一段动画,两架飞机,分别用四元数和欧拉角实现旋转,在两个相同的朝向之间来回摆动。用四元数的飞机摆动自然平稳,而用欧拉角的飞机却不停抽风。
图2. 分别用四元数和欧拉角实现旋转的效果
这种现象的诱因被称为万向锁(gimbal lock),是使用动态欧拉角表示三维物体的旋转时会出现的问题。正常状态下Yaw, Pitch & Roll 三个旋转轴相互独立,但一旦选择±90°作为俯仰角,就会导致三个轴中的两个所在平面重合,丢失了一个自由度。如图,飞机绕绿色Pitch轴旋转了90°,与粉色Yaw轴重合了,这时绕蓝色Roll轴的旋转就与绕粉色Yaw轴的旋转就没有任何区别。
万向锁
万向锁的现象导致我们不能只依赖动态欧拉角,而需要新工具——四元数。
理解四元数的七条规则
Patrick向我们介绍了7条理解四元数的简单规则:
四元数的虚数部分就是旋转轴。旋转一半发生在实部,一半发生在虚部。做乘法其实就是在应用旋转。逆操作(inverse/conjugate)就是虚数部分取反。与对向量做LERP(线性插值)相似,对四元数做SLERP。从右手系转换到左手系,对虚部中的一个取反。相信你用的数学库。
规则1&2:理解四元数的构造
要表示一个旋转,要定义一个旋转轴n,这是一个单位向量;以及绕旋转轴的旋转量θ。
图4. 旋转轴-单位向量n & 旋转量θ
一个四元数(s, i, j, k)由实数部分s和虚数部分i,j,k组成。
规则1:四元数的虚数部分就是旋转轴。
向量n在x,y,z三个方向上的分量nx, ny, nz.
图5. 虚数部分与旋转轴
那么如何表示旋转量θ?
规则2:旋转一半发生在实部,一半发生在虚部。
实部 = cos(θ/2), 虚部 = sin(θ/2)·n(注意n是向量,虚部三个维度nx, ny, nz分别乘以 sin(θ/2))。记住这两个表达式,下面将经常用到。
图6. 表示“绕向量n轴旋转θ”的四元数
尝试用代码创建几个四元数:
Tips: 在草稿纸上画一张图复习一下三角函数:始终记得cos是在x轴上的投影,sin是在y轴上的投影。
图7. 单位元上的三角函数
1. 表示无旋转的四元数identity = (1, 0, 0, 0).
图8. 创建无旋转四元数的代码
2. 表示上仰的四元数pitch = (0, 1, 0, 0).
图9. 创建pitch四元数的代码图片
3.表示yaw的四元数(0, 0, 1, 0)和表示roll的四元数(0, 0, 0, 1)。
图10. 创建yaw和roll四元数的代码
笔者按照上述代码在Unity中进行实验,效果如下:
注意Unity中的四元数表示为(x, y, z, w),演讲中的四元数表示为(s, i, j, k),其中w与s对应。x, y, z分别与i,j,k对应。
图11. Unity中的四元数数值
图12. Unity中的四元数旋转图例
在debug的过程中我们可能需要监测游戏对象的rotation数值来了解它的旋转状态是否符合预期,但很多人表示,看到一个四元数无法像看到一个欧拉角一样直观地理解。对此Patrick总结了一些直观理解四元数的tips:
1. 记得实数部分=cos(θ/2),可以画出单元位半圆,据此去估计实数部分所在的角度。
以四元数(0.9, 0, 0, 0.4)为例,从实数部分0.9可以估算出θ约为45°。
图13. 用实数部分cos(θ/2)估算θ的大小
2. 相似地,虚数部分= 三个维度*sin(θ/2)。(0.9, 0, 0, 0.4)这个四元数中x, y轴对应的虚数部分均为0,易知这个旋转是绕z轴一定程度的roll,结合刚才估算θ约为45°,估算出这个四元数表示一个约45°的roll。
在Unity中进行实验Quaternion.AngleAxis(45, Vector3.forward)得到的四元数数值刚好完全对应。其实Unity中的这一数值经过近似,并不准确。但证明了这种方法足够用于直观估算这一旋转的角度。
3. 再看一个x, y轴对应的虚数部分非零的例子(0.9, 0.3, 0.3, 0)。实数部分仍是0.9,即约45°,可以想象为绕一个xy平面上的对角线进行45°的旋转。在Unity中这刚好与Quaternion.AngleAxis(45, Vector3.right + Vector3.up)对应。
规则3&4:四元数乘法&取逆
下面来看Patrick用四元数解决的实际问题。他曾参与《大富翁》(Monopoly)游戏iPad版本开发,重写了相机系统使得相机可以从四个玩家的角度进行拍摄。
在桌游中,相机视角通常都是从上方俯视全局(视角1),但我们希望可以让相机可以沿水平方向绕着移动中的角色拍摄(视角2)。
图14. 左:视角1俯视全局视角 右:视角2角色视角
从《大富翁》游戏的宣传视频中我们可以看到这样视角变换的动画。
图15. 《大富翁》游戏中的相机视角移动效果
Patrick在开发中用以下代码表示相机在各个位置时的rotation:
图16. 《大富翁》游戏代码中用到的四元数
如何让相机降到另一条侧边上?把这个旋转拆成两步,第一步向右旋转90°,第二步从空中降下来。
规则3:做乘法其实就是在应用旋转。
叠加两个步骤就是从右向左将旋转四元数相乘(注意顺序不可交换,向量/矩阵乘法没有交换性)。通过简单的实验我们也可以验证顺序的重要性:先pitch再roll和先roll再pitch的结果并不相同。
图17. 旋转顺序不同造成的差异
在Unity中要注意避免*=的写法,这样乘上的操作会被依次放在右边,先写的反而后执行,与思路不匹配。(关于Unity中四元数旋转执行顺序,详见官方文档中Quaternion的operator*的部分)。
规则4:逆操作(inverse/conjugate)就是虚数部分取反。
图18. 四元数乘法计算
由乘法自然拓展到取逆,在数学上对四元数取逆比较复杂,但是在游戏中可以偷懒,回忆一下规则1,四元数的虚数部分就是旋转轴。直接对虚数部分全部取反,即可得到围绕相反旋转轴进行的等量旋转,即得到逆操作四元数。
图19. 四元数取逆
规则5:四元数插值
要制作旋转的动画,我们往往会使用到插值。回忆规则5:与对向量做LERP(线性插值)相似,对四元数做SLERP。你也可以对四元数做线性插值,类似于(1 – t)·q0 + t·q1,但是这样会存在问题:大部分情况下,生成的四元数未经归一化(normalize),在旋转时就会同时存在对物体的缩放,而这是我们不想看到的。
图20. 左侧飞机使用线性插值,导致旋转的同时存在缩放
为了解决这一问题,我们对四元数做SLERP。这一公式比较复杂,不过重点就是可以使四元数的长度始终保持为1。具体的实现通常会由数学库帮忙完成。
图21. SLERP公式
规则6:坐标系转换
三维笛卡尔坐标系分为左手坐标系和右手坐标系,许多进行物理计算的设备和软件倾向于使用右手系,而Unity等游戏开发软件使用左手系。当我们需要在两者之间转换一个坐标(x, y,z)时,对xyz其中一者(通常是z)取反即可。在两坐标系之间转换四元数也是这样。
规则6:从右手系转换到左手系,对虚部i,j,k中的一个取反。通常取(s, i, j, -k)即可。
规则7:相信计算机
虽然四元数背后的数学十分复杂,但大部分情况下数学库已经帮你完成了这些计算。规则7:相信你用的数学库,理解抽象概念,然后直接使用四元数即可。
总结
四元数是游戏开发非常有用的三维旋转工具,尽管存在看起来不够直观的缺点,但可以避免万向锁问题,从而实现理想的旋转效果。希望通过分享这7条规则,能帮助你对四元数有一个直观的理解,在开发中自由地应用四元数吧! |