欧拉角、顺规、旋转矩阵、四元数
最早接触四元数是unity3d里面的旋转,当时不太理解使用四元数的意义(在此时的我看来欧拉角已经可以完全表示一个朝向了),后来在旋转时出了很多莫名其妙的问题,发现有必要remake一下。
参考资料,感谢大佬们
https://krasjet.github.io/quaternion/quaternion.pdf
https://www.bilibili.com/video/BV1SW411y7W1?spm_id_from=333.999.0.0
为什么直接改欧拉角,效果经常与预期不符
现在考虑一个问题:在你修改欧拉角时,意味的到底是“绕自身坐标轴旋转”,还是“绕世界坐标轴旋转”?
在unity里面测试,多测测就会发现:既不是按自身也不是按世界,而是有时按自身有时按世界??
如果你随便拿一支笔(不要用unity),用手先绕X轴旋转90度,再绕Y轴旋转90度;再反过来试一下,会发现得到的位置不一样了。
这是因为旋转不具有交换律,相应的矩阵乘法也不具有交换律。
欧拉角
欧拉角需要区分为两种:动态欧拉角和静态欧拉角,他们都需要定义顺规。
静态欧拉角即绕世界静止坐标系旋转,动态欧拉角即绕自身坐标系旋转,前一个轴的旋转会影响下一个轴。
这里只分析动态欧拉角(我认为更好用)
顺规
考虑这么一个问题:如何实现一个可以自由旋转的飞机(假设面向+Z轴,要求Yaw-Pitch-Roll)?
可以用三个嵌套(做父物体)的箱子,最里面的箱子可以直接用飞机本身,所有物体的坐标都是相对父物体的;
第一个箱子仅绕自身y轴旋转(实现Yaw),第二个仅绕自身x轴旋转(Pitch),第三个仅绕自身z轴旋转(Roll)
通过控制这三个箱子,我们可以用比较好理解的方式合理、正确地旋转飞机。
这样的箱子便是动态欧拉角中的万向节,上述例子其实就是一个顺规为YXZ的动态欧拉角。
注意:这个顺规如果改变的话,飞机的旋转就很奇怪了,这并不是因为其他顺规错了(在数学上都没错)
而是因为我们对飞机的自然直觉就是符合动态欧拉角下的Yaw-Pitch-Roll顺规的。
在遇到其他复杂旋转问题时也可以使用箱子嵌套的方式思考,然后得出一个理想的顺规。
至于万向节锁,个人认为并不是什么大问题(飞机头垂直向上时,Roll和Yaw结果相同),依旧符合我们的自然直觉,且可以通过Pitch化解。
旋转矩阵表示
这里使用列向量。
我们通常用一个transform矩阵来表达一个物体的位置,大小,朝向信息。
- 它表示的是物体从世界坐标的原点(没有缩放、平移、旋转)到当前状态经历的变换。
- 它能表示物体的坐标系。
- 更一般地,它是物体顶点从局部坐标变换到世界坐标时需要乘上的矩阵。
直接维护Transform
单独的旋转、平移、缩放矩阵形式在此不再多提及,它们直接应用在一个向量(点)上的意义非常明确。
假设有了一个物体,它的Transform为 \(T\)(已经经过了一些复杂变换),现要让这个物体继续进行某种位移,位移矩阵为 \(V\)
假如我们直接令 \(T =V\times T\),即在左侧乘,效果是怎么样的?
可以想象 \(T\) 的右边还有个向量 \(v\),考虑这单个 \(v\) 的变换,它先变换 \(T\),然后再位移 \(V\),即最终效果为在世界坐标系下位移了 \(V\),很好理解。
如果 \(T=T\times V\) 呢?
这个 \(v\) 在所有原先的变换前,先进行了位移,然后再变换 \(T\),相应的,这个位移本身也会被变换 \(T\)。
如果把变换前的 \(T\) 看做一个坐标系,就可以说,这是在物体自身坐标系下位移了 \(V\)。
另一个角度,如果要在自身坐标系下位移,可以利用相似矩阵 \(TVT^{-1}\),乘上原有的矩阵就变成 \(TV\) 了。
总结:
- 左边乘变换矩阵,相当于在世界(父)坐标系下变换
- 右边乘变换矩阵,相当于在自身坐标系下变换
用旋转矩阵表示旋转不会遇到歧义、死锁的问题,缺点是占用空间大,运算量大(欧拉角使用3个数,四元数使用4个数,而旋转矩阵使用了16个数),另外不太直观。
从Transform中看
如果把Transform矩阵看成一个仿射变换和一个位移的组合(世界坐标下位移),通常我们说它是先仿射再位移得到的。
为什么?不是因为先位移不对,而是先位移的话,位移的量就不再是矩阵第四列的三个值了。我们说先仿射再位移,是基于位移量直接等于第四列的值来说的。
而从仿射矩阵中分解出缩放和旋转,需要另做一些计算。
我更喜欢用基向量、坐标系的方式来考虑矩阵,它的前三列分别表示了这个坐标系的三个基向量,最后一列表示这个坐标系原点的位置。
四元数表示
\[ \newcommand{\vec}{\bold} \]
https://krasjet.github.io/quaternion/quaternion.pdf 推导的非常好
四元数表示为 \(q=[s, \vec v], \vec v=[x,y,z]\),\(q = a+bi+cj+dk\)
有三个虚部,满足关系: \[ i^2 = j^2 = k^2 = ijk = -1 \] 可以推得 \(jk=i, ij=k\) 等等关系
四元数乘法也按照多项式相乘规则,展开化简后可得: \[ \begin{align} q_1 &= [s, \vec v], q_2 = [t, \vec u]\\ q_1 q_2 &= [st-\vec v\cdot \vec u, s\vec u+t\vec v + \vec v \times \vec u] \end{align} \] 如果两个四元数的实部都为0(纯四元数),则会得到: \[ q_1q_2 = [-\vec v\cdot \vec u, \vec v \times \vec u] \] 四元数的逆为: \[ q^{-1} = \frac{q^*}{|q|^2} \] 其中 \(q^*\) 为共轭四元数,即将虚部取反,实部不变。
下面直接给出结论:
对任意向量 \(\vec v\),沿着轴 \(\vec u\) 旋转 \(\theta\) 度得到 \(\vec v'\)
可以构造四元数:\(v = [0, \vec v], q = [cos(\frac{1}{2}\theta), sin(\frac{1}{2}\theta) \vec u]\),\(q\) 为单位四元数。
旋转可以用四元数乘法表示: \[ [0, \vec v'] = qvq^{*} \] 同样的,给定一个四元数,也可以轻松得到对应的旋转轴和角度。
连续进行两次旋转,等价于四元数累乘。 \[ pqvq^*p^* = (pq)v(pq)^* \]