【RTR4笔记】 第四章(二) 旋转变换

旋转、欧拉角、四元数

Posted Kongouuu's Blog on January 17, 2022

4.2 特殊的矩阵变换操作

4.2.1 欧拉变换

欧拉变换是用来形容一个朝向,例如摄像机所看着的方向的表达方式。为了形容这个朝向,我们最主要必须定三个旋转以及他们围绕着的轴(方向);

  1. Roll:围绕着原始前向量进行的逆时针旋转。一般我们在定义朝向时会先计算出一个 前方/ViewDir 的方向
  2. Yaw:围绕着原始上向量进行的逆时针旋转。在书里把这个称作Head。一般的摄像机会采取[(0.0,1.0,0.0)]这样Y为一的向量作为上方的轴。并且围绕着这个的旋转就是Yaw/HeadYaw的变动在第一人称视角就是摇头往左/右看。
  3. Pitch: 这个旋转是围绕原始右向量的逆时针旋转。在第一人称的视角就是往上/下看。

简单来说就是我们首先需要定义一个整个欧拉系统会去使用的坐标系,其中包含着XYZ三个坐标的方向。那么之后所有的物体或是摄像机的旋转都会基于这三个轴去进行。

这里有一个概念可能混淆的地方,就是我们为了写摄像机会定义的LookAt矩阵中会用到的ViewDir/Up/Right三个向量。这三个向量并不是欧拉用的轴,而是处于这个坐标系统上面的方向向量,也就是说我们为了控制摄像机镜头的移动,我们是使用Pitch/Yaw/Roll去控制摄像机的这三个方向向量。

就像前面说的,为了形容好一个旋转变换需要先考虑一个原始的方向,在右手坐标系一般采取的是沿着-z方向,并且同时Head围绕的上向量为Y轴的方向,最后的Pitch则是围绕着前和上两个向量所叉乘出来的轴(也就是X轴)旋转就可以了。欧拉变换的表达方式是这样的:

\[E(h,p,r) = R_z(r)R_x(p)R_y(h)\]

虽然欧拉的表达方式很简单,我们使用三个方向向量就能定义一个空间,并且基于空间就可以用三个旋转的角度/弧度去控制物体的朝向。不过这又会衍生出很多的小问题,比如说进行两个欧拉变换的插值。对于旋转的变换我们很显然是不能对矩阵插值的,因为我们角度的线性变换在矩阵上是非线性的(sin/cos),所以我们对旋转变换的插值应该使用对角度的插值。

这时候我们考虑两个朝向一样的变换,他们的欧拉角可以是不一样的,比如说:

\[E^1(\pi,0,0)\\ E^2(0,\pi,\pi)\]

这两个欧拉变换都可以让物体朝向后方,但很显然插值后的结果并非朝向同个方向,而是往上看:

\[E' = interp(E^1,E^2,0.5)\\ E' = R_z(\frac{\pi}{2})R_x(\frac{\pi}{2})R_y(\frac{\pi}{2})\]

4.2.2 欧拉角的提取

假设我们依旧是按照上面定义的顺序,并且展开三个旋转,会得到下面的结果:

\[E(h,p,r) = R_z(r)R_x(p)R_y(h)\\\\ = \left[ \matrix{ cosr & -sinr & 0\\ sinr & cosr & 0\\ 0 & 0 & 1 \\ } \right] \left[ \matrix{ 1 & 0 & 0 \\ 0&cosp& -sinp\\ 0&sinp& cosp \\ } \right] \left[ \matrix{ cosh & 0 & sinh \\ 0 & 1 & 0 \\ -sinh & 0 & cosh \\ } \right] \\\\ = \left[ \matrix{ cosr & -sinr\ cosp& sinr\ sinp\\ sinr & cosr\ cosp& -cosr\ sinp \\ 0 & sinp & cosp \\ } \right] \left[ \matrix{ cosh & 0 & sinh \\ 0 & 1 & 0 \\ -sinh & 0 & cosh \\ } \right]\\\\ \left[ \matrix{ e_{00} & e_{01} & e_{02} \\ e_{10} & e_{11} & e_{12} \\ e_{20} & e_{21} & e_{22} \\ } \right] \\\\ = \left[ \matrix{ cosr\ cosh-sinr\ sinp\ sinh & -sinr\ cosp & cosr\ sinh+sinr\ sinp\ cosh\\ sinr\ cosh+cosr\ sinp\ sinh & cosr\ cosp& sinr\ sinh-cosr\ sinp\ cosh \\ -cosp\ sinh & sinp & cosp\ cosh \\ } \right]\]

展开之后很明显有几个部分很方便的可以提取出我们要的三个角度:

\[h:\\ \frac{e_{20}}{e_{22}} = -\frac{sinh}{cosh} = -tanh\\ h = arctan(\frac{sin(-h)}{cosh})=arctan(\frac{e_{20}}{e_{22}})\\\\ r:\\ \frac{e_{01}}{e_{11}} = -\frac{sinr}{cosr} = -tanr\\ r = arctan(\frac{sin(-r)}{cosr})=arctan(\frac{e_{01}}{e_{11}})\\\\ p:\\ p =arcsin(e_{21})\]

通过这样的计算就可以得到我们需要的三个角度。要注意的细节是在进行arctan操作的时候,如果分母是0那么我们将无法计算这个结果。书中使用的是另一个函数来代替arctan,也就是atan2atan2(y,x)可以得到更大范围的角度信息[0,2π],并且不会有分母为0无法计算的情况。

4.2.2.1 万向锁

在讲旋转变换的时候很经常会提到的一个概念就是万向锁,这也是欧拉角的表现方式的一个重大的缺点。我们重新看一下三个变换组合在一起的欧拉变换的表达方式。

\[\left[ \matrix{ cosr\ cosh-sinr\ sinp\ sinh & -sinr\ cosp & cosr\ sinh+sinr\ sinp\ cosh\\ sinr\ cosh+cosr\ sinp\ sinh & cosr\ cosp& sinr\ sinh-cosr\ sinp\ cosh \\ -cosp\ sinh & sinp & cosp\ cosh \\ } \right]\]

这个变换矩阵的问题并不难理解。假设我们把pitch的角度设成90,也就是说cosp = 0,那么矩阵会变成下面这个样子:

\[\left[ \matrix{ cosr\ cosh-sinr\ sinh & 0 & cosr\ sinh+sinr\ cosh\\ sinr\ cosh+cosr\ sinh &0& sinr\ sinh-cosr\ cosh \\ 0 & 1 & 0 \\ } \right]\]

pitch为90的时候,变换矩阵中的一大部分都被锁死了,可以操作的只剩下e00,e02,e10,e12这四个数。而我们无论怎么调整head/roll,这四个值都不可能超过[-1,1]的范围。

在上面的矩阵中,所有对roll的操作,都可以用对head的操作替代,也就是说我们的roll/head都是可以相互替代的,就是说他们的旋转是在同一个轴上进行的,这就是万向锁了。

简单的来说,因为欧拉变换是有顺序的三个旋转,而不是一次直接定义好朝向的旋转。无论我们使用什么顺序,只要中间的那个旋转变换使用了90度,都会把另外两个变换锁在同一个轴上旋转。

4.2.3 围绕特定轴旋转

我们在旋转变幻的时候一般已经设定到了就是围绕着x/y/z去转,但有些时候可能会想围绕着特定的方向旋转,这里我们管这个轴叫r

围绕一个新轴旋转的步骤是非常暴力的,简单来说就是把当前物体旋转到我们指定的轴的坐标系,然后围绕x旋转,之后再旋转回原本处于的坐标系:

\[定义一个轴 r \\ r= (r_x,r_y,r_z)\\ 我们需要为r准备两个归一化正交的向量s,t\\ \\ s = \left\{\begin{aligned} (0,-r_z,r_y)\ if\ |r_x| \le|r_y|\ and\ |r_x|\le|r_z|\\ (-r_z,0,r_x)\ if\ |r_y| \le|r_x|\ and\ |r_y|\le|r_z|\\ (-r_y,0,r_x)\ if\ |r_z| \le|r_x|\ and\ |r_z|\le|r_y|\\ \end{aligned}\right. \\\\ t = r\cross s \\\\ 这里我们就有了一个新的坐标系的三个向量:r,s,t\\ \\ 转换到新的坐标系的转换矩阵M为:\\ M = (r^T,s^T,t^T)\\\\ 围绕新的轴r旋转\phi的变换矩阵为:\\ X = M^{-1}R_x(\phi)M =M^TR_x({\phi})M\]

4.3 四元数

四元数的发明解决了欧拉角有的很多问题,例如万向锁、无法插值等。四元数本质上对整个旋转变换的形容就是把一系列的旋转变换当作单个基于一个特定的轴的旋转。

4.3.1 数学背景

在讨论四元数之前,简单提一下复数。复数有着一个实部和一个虚部,虚部会乘以一个虚数单位,也就是-1的根

\[z = a +bi\\ i^2 = -1\]

我们的四元数也是类似的结果,作为一个四维的向量,他有一个实部和四个虚部。

定义:

\[一个四元数\hat{q}的定义如下:\\ \hat{q}=(q_v,q_w)=iq_x+jq_y+kq_z+q_w\\\\ i^2=j^2=k^2=-1\\\\ jk=-kj=i\\ ki=-ik=j\\ ij=-ji=k\]

在想像四元数的时候可以把他当成有三个虚部一个实部的复数,也可以把ijk当作我们三维空间的三个轴。通过上面的定义我们可以在四元数之间做出很多的计算。整个四元数的表达和原理其实是比较复杂的,rtr4内本身更倾向于讲解四元数之间的计算,对于实际数学上的解读还是应该参考的:

3B1B四元数可视化

4.3.2 四元数旋转计算

四元数这部分书里讲的计算方面的太多了,而且并不太亲民,所以还是网上查了资料按自己的需要编了。

在并不能完全理解四元数的原理的情况下,还是要尽可能地理解他的表达式的大体含义以及如何计算旋转。在简单的了解四元数定义了之后,我们可以把四元数写成下面这样的定义:

\[\hat{q}=(\sin\phi\ u_q,\cos\phi)= \sin\phi\ u_x\ i + \sin\phi\ u_y\ j + \sin\phi\ u_z\ k + \cos\phi\\ u_q 为归一化的向量,代表我们围绕着旋转的轴\\ \phi 为旋转的角度\]

准确的说就是一个轴+角度的表达方式,用来表示我们在特定轴上旋转了多少。

比较简单的使用方法是比如我们要把一个点转到另一个点,我们就可以通过原点连向两个位置取得两个向量。这两个向量的叉乘就会是uq 这个轴。

所以四元数用作旋转计算的流程实际上就是:

  1. 定义一个点
  2. 用欧拉角定义我们的旋转
  3. 把欧拉角的旋转转换成四元数
  4. 把四元数写成旋转矩阵

为什么要这么大费周章呢?我们完全可以理解在静态的情况下这样子跟从欧拉角变成旋转矩阵这个行为并没有任何差别。是的,静态的情况下四元数并没有太大的优势。要说唯一的优势就是,欧拉角->四元数->旋转矩阵 这个流程比 欧拉角->旋转矩阵 的开销要低。

4.3.2.1动态旋转

四元数最大的优势就是可以插值。我们在写游戏的过程中会有很多的情况是希望物体从方向A旋转到方向B,并且是通过插值慢慢进行。这时候常规的欧拉角->旋转矩阵计算是没办法插值的。动态旋转上采取欧拉角也衍生一些小问题。我们先看一下欧拉角的问题:

朝向的唯一性

上面提到过一个欧拉角的小问题:

\[E^1(\pi,0,0)\\ E^2(0,\pi,\pi)\\\]

上面通过两个欧拉的变换可以得出相同的旋转结果。这样会衍生出什么样的问题呢?

首先我们很多情况下是会要用到反推欧拉角的功能的,也就是从一个旋转矩阵反推出三个欧拉角度来进行操作。假设我们同一个旋转矩阵能变出多个不同的欧拉角度,并且还不是因为(角度+360度)这样的基础问题造成的结果,那么我们要怎么去写反推欧拉角的操作?不过当然我们是可以为所有的物体去记录他们的三个欧拉角,这个开销也不算特别大,不过如果可以让三个角度和旋转矩阵之间有双向的操作可能会更灵活?

更重要的是,假设一个系统让多个不同的角度组合能够变成相同的变换,那么这样的系统是没有办法进行插值计算的,从而没办法进行动画/动态的旋转。

插值

我们是很需要旋转的插值的,四元数的最大的作用就是它能够找到两个朝向中间的插值,这样在我们动画的时候可以处理物体旋转时中间的角度。使用欧拉角到转换矩阵的这个流程在处理动画方面比较困难,因为我们没办法很简单的得到每一帧应该有的插值。

当然我们很多时候使用中四元数它并不会替代掉我们的欧拉角,只是我们不会使用欧拉角的三个轴的转换矩阵。我们还是需要通过三个欧拉角去定义我们需要插值的起点和终点,才能使用四元数。

4.3.2.2万向锁 Revisit

四元数能解决万向锁吗?

大部分时候我们都是欧拉角和四元数并用的,这种情况下还是会有万向锁。

首先 为什么有万向锁?万向锁是因为你的程序设计上,旋转的角度是 根据原点和原始坐标去按顺序旋转 这样去设计。如果你程序里每次塞给四元数的数字都是基于原始坐标,那想当然会出现类似的问题。

要使用四元数解决万向锁的问题,就是我们应该使用轴角的表达方式来形容方向。或者说我们的欧拉角都用来做基于目前所在的点和坐标系的操作,来使用四元数,就不会有万向锁。