分享

Unity 中的旋转

 3dC 2016-08-29

参考

1、关于万向节死锁

2、【Unity技巧】四元数(Quaternion)和旋转

一、Unity中的Rotation

在unity中,旋转的表示的常用方法之一,是一个三维向量(x、y、z):


图1、Unity中的旋转

实际上这是欧拉角。这三个分量分别是绕x轴、y轴、z轴旋转的角度。

要对一个object进行旋转,还可以通过代码:

[csharp]view plain copy
在CODE上查看代码片
  1. 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 函数有两个入参:

[csharp]view plain copy
在CODE上查看代码片
  1. public void Rotate(Vector3 eulerAngles, Space relativeTo = Space.Self);  

第二个入参的取值有两种:Space.Self 或者 Space.World。我们先看默认的 Self 的情况。使用下面的一段简单的代码来进行测试:

[csharp]view plain copy
在CODE上查看代码片
  1. public class TestRotate : MonoBehaviour {  


  2. public Space rotateSpace;  


  3. // Update is called once per frame  

  4. void Update () {  

  5. if (Input.GetKeyDown(KeyCode.R))  

  6. transform.Rotate(new Vector3(0, 10, 0), rotateSpace);  

  7. }  

  8. }  

场景中进行测试的是一个圆柱体,其父节点的旋转为(30,30,0),圆柱体初始的旋转为(30,0,0),每次按下R键,就会在Space.Self 下绕 Y轴旋转10度,则结果为:
图2、在Space.Self中旋转

可以看到,圆柱体是绕着本地坐标系的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中的情况:

那么实际中使用的是什么方式呢?运行以下代码,会看到结果如下:

[csharp]view plain copy
在CODE上查看代码片
  1. transform.Rotate(new Vector3(90, 90, 0), Space.Self);  


图6、Unity中动态欧拉角的旋转顺序

可以看到和情况1)相同,所以确认结果为1)。

如果分两次旋转,运行以下代码:

[csharp]view plain copy
在CODE上查看代码片
  1. transform.Rotate(new Vector3(90, 0, 0), Space.Self);  

  2. transform.Rotate(new Vector3(0, 90, 0), Space.Self);  

则效果就和效果2)相同了。
图7、在Unity中,使用动态欧拉角两次旋转

最终结论是:每次使用Space.self进行rotate时,都是绕着调用时刻的坐标轴进行旋转的

3、静态欧拉角和动态欧拉角的等价形式

这里展开讨论一下,静态欧拉角和动态欧拉角是可以相互转换的。具体的数学公式可以参考这篇博客。

其结论就是:在Space.World中旋转以 Z-X-Y 归顺旋转角度(x、y、z),等价于在Space.Self中分别顺次旋转(0,y,0)、(x,0,0)、(0,0,z)

从代码上来说,就是以下两段代码等价:

[csharp]view plain copy
在CODE上查看代码片
  1. private void Rotate_World(float x, float y, float z) {  

  2. transform.Rotate(x, y, z, Space.World);  

  3. }  


  4. private void Rotate_Self(float x, float y, float z) {  

  5. transform.Rotate(0, y, 0, Space.Self);  

  6. transform.Rotate(x, 0, 0, Space.Self);  

  7. transform.Rotate(0, 0, z, Space.Self);  

  8. }  


五、万向节锁(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度是非常少见的。这样就可以尽量的避免万向节锁。

但这终归是指标不治本的方法,另一种完全解决这个问题的方法,就是不再采用欧拉角表示旋转,这就是我们下面要讨论的四元数。

六、Quaternion(四元数)旋转

1、什么是四元数

要表示旋转,其实一个更直接的方式是使用他的变换矩阵,但是矩阵需要的保存空间太大,而这个矩阵中有大量的冗余数据。这就产生了四元数。

对于四元数的解释,可以参考CandyCat的文章,写的非常清楚。这里摘录一部分:

给定一个单位长度的旋转轴(x, y, z)和一个角度θ。对应的四元数为:


给定一个Y-Z-X规顺的欧拉旋转(X, Y, Z),则对应的四元数为:


2、用四元数进行旋转

Unity中的四元数支持和一个Vector3相乘,如果把这个Vector3看作一个向量,那么左乘一个四元数,就相当于对这个向量进行对应的旋转

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多