为了避免万向节死锁的问题,Unity中一般用四元数来表示物体旋转。Unity为物体旋转提供了各种API,例如RotateAround、Rotate、LookAt等方法,本文主要介绍用四元数乘法表示旋转的方法。
四元数的乘法可以看做对一个物体施加两次旋转,最终的旋转角度由这两次旋转角度决定,旋转的顺序对旋转的结果会产生影响(q1*q2≠q2*q1),因为四元数乘法的本质是矩阵的乘法。
物体的坐标分为世界坐标(global)和局部坐标(local),世界旋转坐标(如transform.rotation)代表物体相对于世界坐标系下的旋转角度,局部旋转坐标(如transform.localRotation)代表物体相对于父物体坐标系下的旋转角度,如果没有父物体,则局部旋转坐标与世界旋转坐标相同。另外,在inspector中的Transform值也是物体的局部坐标。
直接利用四元数乘法的方式对物体进行旋转是一种很好的方式,但是在这个过程中经常涉及到世界坐标、局部坐标之间的转化以及在各自坐标系下进行旋转的操作,再加上四元数是高维空间下的表示方法,使用者不能直观地理解其所代表的旋转过程,因此确定旋转表达式是一件令人头疼的事情,经常会出现错误。在此略去了四元数繁琐的原理,用简单直观的方式介绍Unity中世界坐标以及局部坐标下四元数旋转的差异。
在进一步介绍之前,首先对文章中用到的概念进行定义
在世界坐标下旋转:指对transform.rotation赋值
在局部坐标下旋转:指对transform.localRotation赋值
绕世界轴(world axis)旋转:指旋转轴属于世界坐标系,例如(0,1,0)代表世界的y轴
绕本地轴(local axis)旋转:指旋转轴属于物体坐标系,例如(0,1,0)代表物体的y轴
对于四元数旋转只需要记住一条:
只有乘法顺序决定了旋转轴属于世界坐标系还是物体坐标系。
因此
绕本地轴进行旋转:transform.rotation = transform.rotation * Quaternion;
绕世界轴进行旋转:transform.rotation = Quaternion * transform.rotation;
除此以外不需要知道任何本地旋转坐标或者对旋转轴进行坐标系之间的变换。对于旋转A*B的理解完全取决于你如何去看待它,你可以将它理解为对B进行全局旋转A(绕世界轴旋转),也可以理解为对A进行局部旋转B(绕本地轴旋转)。举个例子,R是当前的旋转角度,Ry是在y轴进行一定角度的旋转,如果想要绕世界坐标系的y轴进行旋转,用Ry*R,如果想要绕物体坐标系的y轴旋转,用R*Ry。
之前讲对于四元数旋转只需记住只有旋转顺序决定了旋转轴属于世界坐标系还是物体坐标系,那么对于局部坐标下的四元数旋转操作也是一样的:
绕本地轴进行旋转:transform.localRotation = transform.localRotation * Quaternion;
绕世界轴进行旋转:transform.localRotation = Quaternion * transform.localRotation;
尽管表达式类似,但是得到的结果却不尽相同,具体来说,对于绕本地轴的旋转,在世界坐标以及局部坐标下进行操作的结果是相同的,而绕世界轴旋转时,二者结果是不一样的。原因就是物体的旋转实际上可以拆分成两个过程:物体本地的旋转(相对于父物体的旋转)+父物体的旋转,即transform.rotation = transform.parent.transform.rotation * transform.localRotation。
这种情况下按本地轴旋转时,局部坐标下的旋转操作可以写成transform.rotation=transform.parent.transform.rotation * transform.localRotation * Quaternion,与全局坐标下的旋转操作相同,不会出现问题
但是按世界轴旋转时,局部坐标下的旋转操作为transform.localRotation = Quaternion * transform.localRotation,可以写成 transform.rotation = transform.parent.transform.rotation * Quaternion * transform.localRotation;而全局坐标系下的操作为transform.rotation = Quaternion * transform.rotation,可以写成 transform.rotation = Quaternion * transform.parent.transform.rotation * transform.localRotation,二者乘法顺序不一致,因此所代表的旋转不相同
以上讲的难免非常抽象,下面举个例子来进行说明。首先设置一个父物体parentCube,其rotation为(-90,0,0),两个子物体childCube1、childCube2,二者localrotation(相对于父物体的坐标)都设置为(-30,-16,-25)。
首先对绕本地轴旋转进行验证:
Quaternion rotU = Quaternion.AngleAxis(50 * Time.deltaTime, Vector3.up);
childCube1.transform.rotation = childCube1.transform.rotation* rotU;
childCube2.transform.localRotation = childCube2.transform.localRotation * rotU;
实验结果:
右乘代表物体绕本地轴旋转,因此childCube旋转是绕自身y轴进行的,parentCube的旋转并不会对childCube旋转产生影响,两种方式旋转相同
然后对绕世界轴旋转进行验证:
Quaternion rotU = Quaternion.AngleAxis(50 * Time.deltaTime, Vector3.up);
childCube1.transform.rotation = rotU * childCube1.transform.rotation;
childCube2.transform.localRotation = rotU * childCube2.transform.localRotation;
实验结果:
左乘代表物体绕世界轴旋转,在世界坐标下进行旋转的时候无需考虑父物体的旋转角度,导致childCube1最终旋转轴是世界坐标系的y轴
而在局部坐标下进行旋转的时候可以理解为先将父物体的旋转角度置零,此时childCube2绕世界坐标系的y轴旋转,之后再进行父物体的旋转,这样就会导致物体旋转轴随着父物体改变而改变,换句话说就是说物体的旋转轴会指向父物体的y轴。下图可以动态的显示这一过程:
为了验证rotation与localrotation之间的关系:物体的旋转=物体相对于父物体的旋转+父物体的旋转,即transform.rotation=transform.parent.transform.rotation * transform.localRotation`,可以运行如下代码:
Quaternion rotU = Quaternion.AngleAxis(50 * Time.deltaTime, Vector3.up);
childCube1.transform.rotation = childCube1.transform.parent.transform.rotation*
rotU * childCube1.transform.localRotation;
childCube2.transform.localRotation = rotU * childCube2.transform.localRotation;
结果如下:
二者旋转轴相同,即都是父物体的y轴
在进行四元数旋转的时候特别需要注意乘法顺序,左乘代表绕世界轴旋转,右乘代表绕本地轴旋转。在对世界坐标和局部坐标进行右乘时二者会得到相同的结果,而进行左乘时会得到不同的结果,本质原因是对局部坐标操作时需要考虑父物体的旋转,而对世界坐标操作时不需要。只需要把localrotation的表达式写成rotation的表达式就能够更加清楚地认识到物体是如何进行旋转的。
https://answers.unity.com/questions/810579/quaternion-multiplication-order.html
https://forum.unity.com/threads/understanding-rotations-in-local-and-world-space-quaternions.153330/