原文地址:Unity 中的旋转
参考
1、关于万向节死锁
在unity中,旋转的表示的常用方法之一,是一个三维向量(x、y、z):
图1、Unity中的旋转
实际上这是欧拉角。这三个分量分别是绕x轴、y轴、z轴旋转的角度。
要对一个object进行旋转,还可以通过代码:
- transform.Rotate(x, y, z);
这里,如果看过《坐标系》一文,就会产生以下两个疑问:
1)x轴、y轴、z轴指的是那组基?是世界坐标系下的xyz轴,还是本地坐标系下的xyz轴?
2)旋转的正方向是如何确定的?
下面分别讨论。
首先回答第一个问题:到底哪个是旋转轴。这又要分为 3种情况。
1、旋转轴:Editor 中 Transform的旋转数值
对这个情况来说,其显示的旋转轴既不是世界坐标系的坐标轴,也不失本地坐标系的坐标轴。Editor中transform的旋转轴是父节点的坐标轴。这点在editor中看的非常明显,因此不再赘述。
2、旋转轴:在script中使用 rotate 函数,在 Space.Self 中旋转
Rotate 函数有两个入参:
- publicvoid Rotate(Vector3 eulerAngles, Space relativeTo = Space.Self);
第二个入参的取值有两种:Space.Self 或者 Space.World。我们先看默认的 Self 的情况。使用下面的一段简单的代码来进行测试:
- publicclass TestRotate : MonoBehaviour {
- public Space rotateSpace;
- // Update is called once per frame
- void Update () {
- if (Input.GetKeyDown(KeyCode.R))
- transform.Rotate(new Vector3(0, 10, 0), rotateSpace);
- }
- }
可以看到,圆柱体是绕着本地坐标系的Y轴旋转的。使用Space.Self进行旋转,旋转轴就是本地坐标系的坐标轴。
3、在script中使用 rotate 函数,在 Space.World 中旋转
下面测试 Space.World
图3、在Space.World中旋转
注意到这里 Parent 的 Y轴并不是 world 的 Y 轴,而这里的圆柱体明显是绕着世界坐标系下的 Y 轴旋转的,所以如代码所述, 使用Space.World旋转,旋转是绕着世界坐标系的坐标轴旋转的 。
4、静态欧拉角和动态欧拉角
前面说到的旋转轴的问题,在数学上有对应的概念。这就是所谓的静态欧拉角和动态欧拉角。
所谓静态欧拉角,就是其旋转轴使用的是静止不动的参考系。动态欧拉角,使用的是刚体本身作为参考系,因而会随着刚体的旋转而旋转。
因此,再看看前面的三种情况,使用Space.World旋转,以及 Editor 中的旋转,是静态欧拉角;使用Space.self,是动态欧拉角。
由于上面的代码是每次使得圆柱绕Y轴旋转10度,因此从上面的动图就可以看到,是符合左手规则的,即以左手大拇指指向旋转轴,则四指指向为正方向。
这其实是当然的:unity的本地坐标系和世界坐标系都是左手坐标系,当然应该使用左手法则。
图4、旋转的正方向
我们之前使用的旋转是一个vector3,包含x、y、z三个向量,分别对应着对 X旋转轴、Y旋转轴、Z旋转轴进行旋转。这里就又产生了一个问题:他是如何绕着这三个轴旋转的呢?
我们也分为静态欧拉角和动态欧拉角的情况讨论。
1、静态欧拉角
这种情况对应着上面的editor中显示的旋转,以及使用Space.World进行的旋转。即使旋转轴保持不变,旋转的顺序也决定了最后的旋转效果,我们来看下面的例子:
现在有一个物体摆放在世界中,现在我们要让他旋转角度(90,90,0)。现在有两种方法。
1)首先绕世界坐标系的x轴旋转90度,再绕世界坐标系的y轴旋转90度
| | |
初始状态 | 绕x旋转90度 | 再绕y旋转90度 |
2)首先绕世界坐标系的y轴旋转90度,再绕世界坐标系的x轴旋转90度
| | |
初始状态 | 绕y轴旋转90度 | 再绕x轴旋转90度 |
3)旋转顺序的影响
可以看到,结果完全不同!
其实从数学上也是可以理解的。在坐标系一文中我们说到,坐标系变换就相当于乘以变换矩阵,现在的旋转,实际上就是坐标系变换。而矩阵乘法是不满足交换律的,因此旋转的顺序不能交换,否则会得到不同的结果。
对于旋转的顺序,一般没有定式,需要明确指出其顺序。对此有一个专门的术语,称为顺规。如果在这个坐标系中的旋转,先绕x轴旋转,再绕y轴,最后再绕z轴,则称之为X-Y-Z顺规。以此类推。
对于Unity,从文档中可以看到,使用的是Z-X-Y顺规,这是一种常用的顺规,可以一定程度上避免万向节锁(这一概念我们会在下面讨论)。因此在unity中,使用静态欧拉角旋转(90,90,0),会得到第1小节中的情况:
图5、Unity中静态欧拉角的旋转顺序
2、动态欧拉角
动态欧拉角除了上面说到的顺规问题,还有一个额外的疑问:比如一个物体,初始状态记为A,以zxy顺规旋转(90,90,0),由于没有z轴旋转,第一步当然是绕着当前的x轴旋转90度,此时状态记为B,那么第二步要绕着y轴旋转90的时候,是绕着初始状态A时的y轴旋转,还是绕着此时的B状态下的y轴旋转呢?
首先来看两者的区别:
1)以初始状态A时的y轴旋转:
| | |
初始状态 | 绕x轴旋转90度 | 绕A状态的y轴旋转90度 |
2)以状态B下的y轴旋转:
| | |
初始状态 | 绕x轴旋转90度 | 绕B状态的y轴旋转90度 |
3)unity中的情况:
那么实际中使用的是什么方式呢?运行以下代码,会看到结果如下:
- transform.Rotate(new Vector3(90, 90, 0), Space.Self);
图6、Unity中动态欧拉角的旋转顺序
可以看到和情况1)相同,所以确认结果为1)。
如果分两次旋转,运行以下代码:
- transform.Rotate(new Vector3(90, 0, 0), Space.Self);
- transform.Rotate(new Vector3(0, 90, 0), Space.Self);
最终结论是:每次使用Space.self进行rotate时,都是绕着调用时刻的坐标轴进行旋转的。
3、静态欧拉角和动态欧拉角的等价形式
这里展开讨论一下,静态欧拉角和动态欧拉角是可以相互转换的。具体的数学公式可以参考这篇博客。
其结论就是:在Space.World中旋转以 Z-X-Y 归顺旋转角度(x、y、z),等价于在Space.Self中分别顺次旋转(0,y,0)、(x,0,0)、(0,0,z)。
从代码上来说,就是以下两段代码等价:
- privatevoid Rotate_World(float x, float y, float z) {
- transform.Rotate(x, y, z, Space.World);
- }
- privatevoid Rotate_Self(float x, float y, float z) {
- transform.Rotate(0, y, 0, Space.Self);
- transform.Rotate(x, 0, 0, Space.Self);
- transform.Rotate(0, 0, z, Space.Self);
- }
五、万向节锁(Gimbal Lock)
1、什么是万向节锁
在讨论欧拉角旋转的时候,一个绕不开的话题,就是万向节锁。
对万向节锁的定义可以参考这个视频。简单来说,就是两个旋转轴发生了重合。如下图:
旋转(90,0,z)
|
旋转(90,y,0)
|
可以看到绕Y轴和绕Z轴旋转产生的效果是相同的,都是在同一个平面旋转。即 Y 轴和 Z 轴产生了共线。
乍一看这很难以理解:在Editor中旋转是使用的是静态欧拉角,旋转轴是固定的,他们两两正交,怎么可能会共线?
首先我们从直观上来解释。这就要说到上面最后一小节的静态欧拉角和动态欧拉角的互相转换。注意到Unity中的旋转是 Z-X-Y 规顺,其和使用动态欧拉角 Y-X-Z 规顺进行旋转等价。而经过动态欧拉角(0,y,0)、(90,0,0)的旋转之后,Z 轴就和初始状态的 Y轴共线。因此这个时候,绕着 Z 轴旋转,就和在世界坐标系下绕着 Y 轴旋转产生的效果类似了。
从数学上也可以解释这个问题。这里可以参考CandyCat的文章:
可以看到第三维不会产生变化,这就是旋转分量的缺失。
2、如何产生万向节锁
从上面的过程就可以看到,要产生万向节锁,只需针对规顺的中间的那个坐标轴,进行90度的旋转,就会使得规顺前后两头的坐标轴产生共线。对于Unity中使用的Z-X-Y规顺,这个中间的坐标轴就是X轴。
3、万向节锁的问题
很多文章中都会提到,产生了万向节锁之后,就会导致丢失一个旋转分量。但实际上,就算产生了如上的万向节锁,看似不能在绕着原来的世界坐标系的Z轴旋转,但只要调用 Rotate(0,0,z,Space.World),就仍然可以让该物体旋转,并不会让物体无法旋转。
实际上,万向节锁真正的问题出在做插值动画的时候。
比如,起始状态如下,产生了万向节锁:
图8、初始状态
现在想要让他旋转到以下状态:
图9、结束状态
那么理想的情况如下:
图10、期望的旋转
但是,注意到由于万向节锁的存在,中间旋转角度(x、y、z)需要产生跳变,那么如果使用普通的插值,就会要从(90,0,0)到(150,90,90)进行插值,那么效果如下:
图11、用欧拉角进行插值
可以看到,物体会沿着一条弧线进行旋转。这就是万向节锁的问题。
4、在欧拉旋转中尽力规避万向节锁
前面说到,产生万向节锁的关键是规顺的中间的那条坐标轴。只要不绕着这个坐标轴旋转90度,就不会发生旋转分量丢失的问题。这就是为什么大多数软件都将 X 轴作为中间的那条坐标轴的原因:常见的旋转插值是对Camera进行的,而如果绕着X轴旋转90度,则意味着正向上,或是正向下,这两种情况都是非常少见的。也就是说,相比于Y轴和Z轴,绕X轴旋转90度是非常少见的。这样就可以尽量的避免万向节锁。
但这终归是指标不治本的方法,另一种完全解决这个问题的方法,就是不再采用欧拉角表示旋转,这就是我们下面要讨论的四元数。
1、什么是四元数
要表示旋转,其实一个更直接的方式是使用他的变换矩阵,但是矩阵需要的保存空间太大,而这个矩阵中有大量的冗余数据。这就产生了四元数。
对于四元数的解释,可以参考CandyCat的文章,写的非常清楚。这里摘录一部分:
给定一个单位长度的旋转轴(x, y, z)和一个角度θ。对应的四元数为:
给定一个Y-Z-X规顺的欧拉旋转(X, Y, Z),则对应的四元数为:
2、用四元数进行旋转
Unity中的四元数支持和一个Vector3相乘,如果把这个Vector3看作一个向量,那么左乘一个四元数,就相当于对这个向量进行对应的旋转。