unity旋转与四元数
概述:表示旋转
四元数通常用来表示旋转,但表示旋转的方法很多,为什么要用更为复杂的四元数呢?
首先来看以下三种常用的旋转表示方法:
[*]矩阵
[*]欧拉角
[*]轴角对
矩阵
即旋转矩阵。一个向量乘以旋转矩阵可以得到旋转后的向量。严格的说,这里单独列为矩阵是不妥当的,因为欧拉角和轴对角、四元数都可以以矩阵的形式表示,并且在unity内部都是用矩阵来计算,区别在于使用的矩阵。
比如在二维空间中,旋转可以用一个单一的角定义。作为约定,正角表示逆时针旋转。把笛卡尔坐标的列向量关于原点逆时针旋转的矩阵是:
https://www.zhihu.com/equation?tex=M%28%5Ctheta%29+%3D++%5Cbegin%7Bbmatrix%7D++%5Ccos+%5Ctheta+%26+-%5Csin+%5Ctheta+%5C%5C++%5Csin+%5Ctheta+%26+%5Ccos+%5Ctheta+%5Cend%7Bbmatrix%7D
在三维空间就要复杂一些,因为可以绕三个轴旋转。比如绕x轴旋转度,旋转矩阵为:
https://www.zhihu.com/equation?tex=R_x%28%5Ctheta%29+%3D++%5Cbegin%7Bbmatrix%7D++1+%26+0+%26+0+%26+0+%5C%5C++0+%26+%5Ccos%5Ctheta+%26+-%5Csin%5Ctheta+%26+0+%5C%5C++0+%26+%5Csin%5Ctheta+%26+%5Ccos%5Ctheta+%26+0+%5C%5C++1+%26+0+%26+0+%26+1+%5Cend%7Bbmatrix%7D
类似的还有绕y轴旋转 https://www.zhihu.com/equation?tex=R_y%28%5Ctheta%29 、绕z轴旋转 https://www.zhihu.com/equation?tex=R_z%28%5Ctheta%29 ,此处不再写出矩阵形式。
注意矩阵乘法不满足交换律,如果要同时绕三个轴旋转,需要定义一个旋转顺序。在unity里,这个旋转顺序是 https://www.zhihu.com/equation?tex=zxy ,这意味着给定 https://www.zhihu.com/equation?tex=%28%5Ctheta_x%2C+%5Ctheta_y%2C+%5Ctheta_z%29 这样的旋转角度时,得到的组合旋转变换矩阵为https://www.zhihu.com/equation?tex=M_%7Brotate_z%7DM_%7Brotate_x%7DM_%7Brotate_y%7D 。即向量先绕 https://www.zhihu.com/equation?tex=z 轴旋转 https://www.zhihu.com/equation?tex=%5Ctheta_z ,再绕 https://www.zhihu.com/equation?tex=y 轴旋转 https://www.zhihu.com/equation?tex=%5Ctheta_y ,最后绕 https://www.zhihu.com/equation?tex=x 轴旋转 https://www.zhihu.com/equation?tex=%5Ctheta_x 。
在unity的顶点着色器里顶点旋转就是乘以旋转矩阵。实际上顶点变换都是乘以矩阵,并且是以缩放-旋转-平移的形式进行变换。当移动mesh组件物体时,该游戏物体的顶点属性并没有发生变化,是cpu每帧发送变换矩阵给gpu,在顶点shader里对每个顶点进行运算。有一个例外就是对于skinnedMesh的物体,这个下个专题再说。
这种矩阵的缺点在于每一个旋转都需要用3X3的矩阵,即9个float数据表示(这里示范的是齐次坐标多出一个维度),表示方式不够直观,但运算效率高。
欧拉角
欧拉角使用三个旋转弧度表示在 https://www.zhihu.com/equation?tex=x%2Fy%2Fz 轴上的旋转,常用于飞行模拟,比如:
[*]绕右向量的旋转称为:pitch(点头),也叫俯仰角
[*]绕上向量的旋转称为:yaw(摇头),也叫偏航角
[*]绕视方向向量的旋转称为:roll(转头),也叫翻滚角
如下图飞机所示:
欧拉角优点很明显,就是十分直观,而且所需的参数少,只需要三个float型变量。
其实在unity的Transform组件里就是用欧拉角来表示旋转,如下图:
pitch、yaw和roll都可以只用一个float数值来定义,通过不同的值使得飞机做出yaw(偏航),pitch(俯仰)和roll(翻滚)。但是如果你把pitch即x值设置为 https://www.zhihu.com/equation?tex=%5Cpm90%5E%5Ccirc 时,你会发现调整y值跟z值不会有任何区别,即失去了一个维度,这就是万向锁。
当然你可以在unity这样设置旋转:
这里每一个维度的旋转都是旋转其父物体的某一角度,在各自的角度上互不干扰。比如pitch的x值不会影响到yaw的z值,但pitch的x值会影响到所有子物体的x值(相对于世界坐标),不过x、y、z值方向上各自独立,依然可以解决万向锁的问题。
当然在unity里我们不可能给每个物体都设置3个父物体。多说一句,如果在unity里想要实现可绕主角旋转、自身可俯仰、可设置离主角距离的camera,即大多数mmorpg里的camera,可使用这种方法,只不过是将旋转与距离设置分隔开来,写代码就会非常好写。
轴角对
一个以单位矢量定义的旋转轴,再加上一个标量定义的旋转角来表示旋转。通常的表示 https://www.zhihu.com/equation?tex=%5Bx%2Cy%2Cz%2C%5Ctheta%5D ,前面三个表示轴,最后一个表示角度。表示非常直观,也很紧凑,并且不存在万向锁的问题。
轴角最大的一个局限就是不能进行简单的插值,此外,轴角形式的旋转不能直接施于点或矢量,必转换为矩阵或者四元数。
接下来有请四元数登场。
四元数
来看上面所说的轴角对表示方法: https://www.zhihu.com/equation?tex=q+%3D+%5Bx%2Cy%2Cz%2C%5Ctheta%5D ,我们把放在前面,写成 https://www.zhihu.com/equation?tex=%5B%5Ctheta%2C+x%2Cy%2Cz%5D%5CRightarrow%5B%5Ctheta%2C+%28x%2Cy%2Cz%29%5D ,现在把 https://www.zhihu.com/equation?tex=%5Ctheta+ 和轴 https://www.zhihu.com/equation?tex=%28x%2Cy%2Cz%29 做一个变换:
https://www.zhihu.com/equation?tex=q+%3D+%5B%5Ctheta%2C+x%2Cy%2Cz%5D+%3D+%5B%5Ccos+%28%5Ctheta%2F2%29+%5Cspace+%5Cspace+%28sin%28%5Ctheta%2F2%29n_x+%5Cspace%5Cspace+sin%28%5Ctheta%2F2%29n_y+%5Cspace%5Cspace+sin%28%5Ctheta%2F2%29n_z+%5Cspace%29%5D ,
把后面的向量直接用轴来表示,也可以写成
https://www.zhihu.com/equation?tex=q+%3D+%5B%5Ctheta%2C+x%2Cy%2Cz%5D+%3D+%5B%5Ccos+%28%5Ctheta%2F2%29+%5Cspace+%5Cspace+nsin%28%5Ctheta%2F2%29%5D 。
这就是一个四元数。这样的四元数有什么作用呢?
如果有一个向量 https://www.zhihu.com/equation?tex=p 绕轴旋转弧度,那么可以写成如下形式:
https://www.zhihu.com/equation?tex=p%5E%7B%27%7D%3Dqpq%5E%7B-1%7D ,其中 https://www.zhihu.com/equation?tex=q+%3D+%5B%5Ccos+%28%5Ctheta%2F2%29+%5Cspace+%5Cspace+nsin%28%5Ctheta%2F2%29%5D 。
至于这个公式是怎么推导出来的,这
四元数除了可以避免万向锁,还可以表示很好的插值来进行平滑的旋转。(矩阵插值很困难,欧拉角插值可能会遇到万向锁)
unity里提供了Quaternion.Slerp方法来进行两个四元数之间的插值,实现任意两个角度平滑过渡。
四元数的应用
四元数结构紧凑,不受万向锁影响,可以轻松插值。 Unity 内部使用四元数来表示所有旋转,可以使用 Quaternion.operator * 对旋转进行旋转,或对向量进行旋转。比如Transform的Rotate方法,內部实现为:
Quaternion quaternion = Quaternion.Euler(eulers.x, eulers.y, eulers.z);
if (relativeTo == Space.Self)
this.localRotation *= quaternion;
else
this.rotation *= Quaternion.Inverse(this.rotation) * quaternion * this.rotation;
由于四元数不直观,通常做简单旋转操作还是以欧拉角为参数,或者通过欧拉角转换得到一个四元数。在Rotate方法里你传入的是欧拉角,但实际进行运算的是四元数。
页:
[1]