参考

1、关于万向节死锁

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


一、Unity中的Rotation

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

unity 旋转菜单 unity中的旋转_坐标轴

图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 函数有两个入参:

public void Rotate(Vector3 eulerAngles, Space relativeTo = Space.Self);

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

public class 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);
    }
}

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




unity 旋转菜单 unity中的旋转_unity 旋转菜单_02


图2、在Space.Self中旋转


使用Space.Self进行旋转,旋转轴就是本地坐标系的坐标轴


3、在script中使用 rotate 函数,在 Space.World 中旋转

        下面测试 Space.World 

unity 旋转菜单 unity中的旋转_坐标轴_03

图3、在Space.World中旋转

        注意到这里 Parent 的 Y轴并不是 world 的 Y 轴,而这里的圆柱体明显是绕着世界坐标系下的 Y 轴旋转的,所以如代码所述,

使用Space.World旋转,旋转是绕着世界坐标系的坐标轴旋转的



4、静态欧拉角和动态欧拉角

静态欧拉角和动态欧拉角。

        所谓静态欧拉角,就是其旋转轴使用的是静止不动的参考系。动态欧拉角,使用的是刚体本身作为参考系,因而会随着刚体的旋转而旋转。

        因此,再看看前面的三种情况,使用Space.World旋转,以及 Editor 中的旋转,是静态欧拉角;使用Space.self,是动态欧拉角。


三、旋转的正方向

        由于上面的代码是每次使得圆柱绕Y轴旋转10度,因此从上面的动图就可以看到,是符合左手规则的,即以左手大拇指指向旋转轴,则四指指向为正方向。

        这其实是当然的:unity的本地坐标系和世界坐标系都是左手坐标系,当然应该使用左手法则。

unity 旋转菜单 unity中的旋转_Self_04

图4、旋转的正方向


四、旋转的顺序

        我们之前使用的旋转是一个vector3,包含x、y、z三个向量,分别对应着对 X旋转轴、Y旋转轴、Z旋转轴进行旋转。这里就又产生了一个问题:他是如何绕着这三个轴旋转的呢?

        我们也分为静态欧拉角和动态欧拉角的情况讨论。

1、静态欧拉角

        这种情况对应着上面的editor中显示的旋转,以及使用Space.World进行的旋转。即使旋转轴保持不变,旋转的顺序也决定了最后的旋转效果,我们来看下面的例子:

        现在有一个物体摆放在世界中,现在我们要让他旋转角度(90,90,0)。现在有两种方法。

1)首先绕世界坐标系的x轴旋转90度,再绕世界坐标系的y轴旋转90度

unity 旋转菜单 unity中的旋转_unity 旋转菜单_05

unity 旋转菜单 unity中的旋转_世界坐标系_06

unity 旋转菜单 unity中的旋转_Self_07

初始状态

绕x旋转90度

再绕y旋转90度


2)首先绕世界坐标系的y轴旋转90度,再绕世界坐标系的x轴旋转90度

unity 旋转菜单 unity中的旋转_Self_08

unity 旋转菜单 unity中的旋转_Self_09

unity 旋转菜单 unity中的旋转_世界坐标系_10

初始状态

绕y轴旋转90度

再绕x轴旋转90度


3)旋转顺序的影响

        可以看到,结果完全不同!

        其实从数学上也是可以理解的。在坐标系一文中我们说到,坐标系变换就相当于乘以变换矩阵,现在的旋转,实际上就是坐标系变换。而矩阵乘法是不满足交换律的,因此旋转的顺序不能交换,否则会得到不同的结果。

        对于旋转的顺序,一般没有定式,需要明确指出其顺序。对此有一个专门的术语,称为顺规。如果在这个坐标系中的旋转,先绕x轴旋转,再绕y轴,最后再绕z轴,则称之为X-Y-Z顺规。以此类推。

        对于Unity,从文档中可以看到,使用的是Z-X-Y顺规,这是一种常用的顺规,可以一定程度上避免万向节锁(这一概念我们会在下面讨论)。因此在unity中,使用静态欧拉角旋转(90,90,0),会得到第1小节中的情况:

unity 旋转菜单 unity中的旋转_坐标轴_11

图5、Unity中静态欧拉角的旋转顺序


2、动态欧拉角

        动态欧拉角除了上面说到的顺规问题,还有一个额外的疑问:比如一个物体,初始状态记为A,以zxy顺规旋转(90,90,0),由于没有z轴旋转,第一步当然是绕着当前的x轴旋转90度,此时状态记为B,那么第二步要绕着y轴旋转90的时候,是绕着初始状态A时的y轴旋转,还是绕着此时的B状态下的y轴旋转呢?

        首先来看两者的区别:

1)以初始状态A时的y轴旋转:

unity 旋转菜单 unity中的旋转_世界坐标系_12

unity 旋转菜单 unity中的旋转_坐标轴_13

unity 旋转菜单 unity中的旋转_unity 旋转菜单_14

初始状态

绕x轴旋转90度

绕A状态的y轴旋转90度



2)以状态B下的y轴旋转:

unity 旋转菜单 unity中的旋转_坐标轴_15

unity 旋转菜单 unity中的旋转_unity 旋转菜单_16

unity 旋转菜单 unity中的旋转_Self_17

初始状态

绕x轴旋转90度

绕B状态的y轴旋转90度


3)unity中的情况:

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

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

unity 旋转菜单 unity中的旋转_Self_18

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

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

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

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

        则效果就和效果2)相同了。



unity 旋转菜单 unity中的旋转_unity 旋转菜单_19


图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)

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

private void Rotate_World(float x, float y, float z) {
        transform.Rotate(x, y, z, Space.World);
    }

    private void 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、什么是万向节锁

        在讨论欧拉角旋转的时候,一个绕不开的话题,就是万向节锁。

        对万向节锁的定义可以参考这个视频。简单来说,就是两个旋转轴发生了重合。如下图:

unity 旋转菜单 unity中的旋转_世界坐标系_20


旋转(90,0,z)

unity 旋转菜单 unity中的旋转_unity 旋转菜单_21


旋转(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的文章:

unity 旋转菜单 unity中的旋转_世界坐标系_22

unity 旋转菜单 unity中的旋转_Self_23

        可以看到第三维不会产生变化,这就是旋转分量的缺失。


2、如何产生万向节锁

        从上面的过程就可以看到,要产生万向节锁,只需针对规顺的中间的那个坐标轴,进行90度的旋转,就会使得规顺前后两头的坐标轴产生共线。对于Unity中使用的Z-X-Y规顺,这个中间的坐标轴就是X轴。


3、万向节锁的问题

        很多文章中都会提到,产生了万向节锁之后,就会导致丢失一个旋转分量。但实际上,就算产生了如上的万向节锁,看似不能在绕着原来的世界坐标系的Z轴旋转,但只要调用 Rotate(0,0,z,Space.World),就仍然可以让该物体旋转,并不会让物体无法旋转。

        实际上,万向节锁真正的问题出在做插值动画的时候

        比如,起始状态如下,产生了万向节锁:

unity 旋转菜单 unity中的旋转_Self_24

图8、初始状态

        现在想要让他旋转到以下状态:

unity 旋转菜单 unity中的旋转_Self_25

图9、结束状态

        那么理想的情况如下:

unity 旋转菜单 unity中的旋转_世界坐标系_26

图10、期望的旋转

        但是,注意到由于万向节锁的存在,中间旋转角度(x、y、z)需要产生跳变,那么如果使用普通的插值,就会要从(90,0,0)到(150,90,90)进行插值,那么效果如下:

unity 旋转菜单 unity中的旋转_坐标轴_27

图11、用欧拉角进行插值

        可以看到,物体会沿着一条弧线进行旋转。这就是万向节锁的问题


4、在欧拉旋转中尽力规避万向节锁

        前面说到,产生万向节锁的关键是规顺的中间的那条坐标轴。只要不绕着这个坐标轴旋转90度,就不会发生旋转分量丢失的问题。这就是为什么大多数软件都将 X 轴作为中间的那条坐标轴的原因:常见的旋转插值是对Camera进行的,而如果绕着X轴旋转90度,则意味着正向上,或是正向下,这两种情况都是非常少见的。也就是说,相比于Y轴和Z轴,绕X轴旋转90度是非常少见的。这样就可以尽量的避免万向节锁。

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


六、Quaternion(四元数)旋转

1、什么是四元数

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

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

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

unity 旋转菜单 unity中的旋转_世界坐标系_28

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

unity 旋转菜单 unity中的旋转_坐标轴_29


2、用四元数进行旋转

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