1. 程式人生 > >[Unity] Unity 3D 中的旋轉

[Unity] Unity 3D 中的旋轉

Unity 3D 中的旋轉

一、Unity 3D 中 Rotation

在Unity中,旋轉通常可以用一個三維向量(x,y,z)表示。實際上這是尤拉角。三個分量分別是繞x軸、y軸和z軸的旋轉角度。

要對一個GameObject進行旋轉,可以直接通過如下程式碼:

transform.Rotate(xAngle, yAngle, zAngle);

那麼有如下疑問:

  1. 上述的x軸、y軸、z軸指的是哪組基?是世界座標系下的xyz軸,還是區域性座標系下的xyz軸?還是其他?
  2. 旋轉的正方向如何?
  3. 旋轉的順序如何?

下面一一解答。

二、旋轉軸

首先,回答第一個問題,到底旋轉軸是哪個座標系的基?分為如下三種情況。

1. 旋轉軸:Inspector 中 Transform 的旋轉數值

TransformExample1

對於這一個情況,Unity Doc 中有明確的說明,

The position, rotation and scale values of a Transform are measured relative to the Transform’s parent. If the Transform has no parent, the properties are measured in world space.

即,Editor中Transform元件的旋轉軸是父節點的模型空間座標軸,如果沒有父節點,則旋轉軸是世界空間座標軸。

unity_transform

上圖顯示瞭如果Transform有父節點,如圖中的”Mesh”,則Position將是在其父節點(這裡是”Cow”)的模型空間中的位置;如果沒有父節點,Position就是在世界空間中的位置。同樣,Transform中的Rotation和Scale也是相同的道理。

2. 旋轉軸:在Script中使用Rotate函式,在Space.Self中旋轉

public void Rotate(Vector3 eulerAngles, Space relativeTo = Space.Self);
public void Rotate(float xAngle, float yAngle, float
zAngle, Space relativeTo = Space.Self); public void Rotate(Vector3 axis, float angle, Space relativeTo = Space.Self);

有上述三種過載函式,這裡主要以第一種為例。其中第二個引數的取值有兩種:Space.Self 或者 Space.World。

使用如下程式碼,測試上述函式的作用。

using UnityEngine;
using System.Collections;

public class Rotate : MonoBehaviour {
    public Space m_RotateSpace;
    public float m_RotateSpeed = 20f;

    // Update is called once per frame
    void Update()
    {
        transform.Rotate(Vector3.up * m_RotateSpeed * Time.deltaTime, m_RotateSpace);
    }
}

場景中進行測試的是一個長方體,其父節點的旋轉為(30,30,0),圓柱體的初始旋轉為(0,0,0)。在Inspector中將Rotate Space設定為Self後,執行結果見下圖。可見,長方體是繞著區域性座標系的Y軸旋轉的。

得出結論:在Space.Self中進行旋轉,旋轉軸就是區域性座標系的座標軸。

RotateSpaceSelf

3. 旋轉軸:在Script中使用Rotate函式,在Space.World中旋轉

在Inspector中將Rotate Space設定為World後,執行結果見下圖。這裡我們知道,長方體的父節點的Y軸不是World的Y軸,而這裡的長方體是繞著世界座標系下的Y軸旋轉的。

所以得出結論:在Space.World中進行旋轉,旋轉軸是世界座標系的座標軸。

RotateSpaceWorld

4. 靜態尤拉角和動態尤拉角

前面說到的旋轉軸的問題,在數學上有對應的概念。這就是所謂的靜態尤拉角和動態尤拉角。

所謂靜態尤拉角,就是其旋轉軸使用的是靜止不動的參考系。動態尤拉角,使用的是剛體本身作為參考系,因而參考系會隨著剛體的旋轉而旋轉。

因此,使用Space.World旋轉,以及Inspector中的旋轉是靜態尤拉角;使用Space.Self旋轉,是動態尤拉角。

三、旋轉的正方向

來到第二個問題,由於Unity中區域性座標系和世界座標系都是左手座標系,所以這裡旋轉的正方向可由右手法則判定。

四、旋轉的順序

下面來看第三個問題,旋轉的順序,即我們的尤拉角(xAngle, yAngle, zAngle)由三個分量組成,分別對應著繞x軸旋轉,繞y軸旋轉和繞z軸旋轉,那麼是如何繞著這三個軸進行旋轉的呢?

這裡也分為靜態尤拉角和動態尤拉角的情況進行討論。

1. 靜態尤拉角

這種情況對應著上面所述的使用Space.World進行旋轉,以及Inspector中的旋轉。即使旋轉軸在旋轉的過程中保持不變,旋轉的順序會決定最後的旋轉結果。我們看下面的例子會很清晰的理解:

  • 情形一:首先繞世界座標系的x軸旋轉90度,再繞世界座標系的y軸旋轉90度

RotateOrder1

  • 情形二:首先繞世界座標系的y軸旋轉90度,再繞世界座標系的x軸旋轉90度

RotateOrder2

可以看到,由於旋轉順序的不同,最終導致了旋轉結果的不同!(究其本質,是因為矩陣乘法不滿足交換律)

對於旋轉的順序,一般沒有定式,因此,需要在使用時明確的指定出其順序。對此有一個專門的術語,稱為順規。如果在座標系中的旋轉,先繞x軸旋轉,再繞y軸,最後再繞z軸,則稱之為X-Y-Z順規。以此類推。

對於Unity,從文件中可以看到,其transform.Rotate()使用的是Z-X-Y順規。因此如果在Unity中,使用靜態尤拉角旋轉(90,90,0)得到情形一的情況。

2. 動態尤拉角

這種情況對應著上面所述的使用Space.Self進行旋轉。動態尤拉角除了上面說到的順規問題(同樣是Z-X-Y順規),還有一個疑問:比如一個物體,初始狀態記為A,以Z-X-Y順規旋轉(90,90,0),由於沒有z軸旋轉,第一步當然是繞著當前的x軸旋轉90度,此時狀態記為B,那麼第二步要繞著y軸旋轉90的時候,是繞著初始狀態A時的y軸旋轉,還是繞著此時的B狀態下的y軸旋轉呢?

首先來看下兩者的區別:

  • 情形一:以狀態A時的y軸旋轉

RotateOrder3

  • 情形二:以狀態B時的y軸旋轉

RotateOrder4

Unity中的情況究竟如何呢?直接執行下面的程式碼會看到結果:

void Start () {
    transform.Rotate(90, 90, 0, Space.Self);
}

RotateOrder5

可以發現Unity中的情況與情形一相同。所以第二步要繞著y軸旋轉90的時候,是繞著初始狀態A時的y軸旋轉。

為了得到情形二中的效果,可以分兩次旋轉,執行如下程式碼:

void Start () {
    transform.Rotate(90, 0, 0, Space.Self);
    transform.Rotate(0, 90, 0, Space.Self);
}

可以發現,此時的效果與情形一中相同了。

最終,我們的結論是:Unity中每次使用Space.Self進行Rotate時,都是繞著呼叫時刻的區域性座標系的座標軸進行旋轉的。

3. 靜態尤拉角和動態尤拉角的等價形式

靜態尤拉角和動態尤拉角是可以相互轉換的。

轉化規則就是:靜態尤拉角中,在某一座標系E下按照某一順規如X-Y-Z旋轉角度(a, b, c),等價於動態尤拉角中,在E下旋轉(0, 0, c),在旋轉後的座標系E’中旋轉(0, b, 0),在旋轉後的新座標系E”中旋轉(a, 0, 0)。

在Space.Self中旋轉以 Z-X-Y 順規旋轉角度(a, b, c),等價於在Space.Self中旋轉(0, b, 0),在新的Space.Self中旋轉(a, 0, 0),在更新的Space.Self中旋轉(0, 0, c)。

下面我們來證明上述兩種旋轉是等價的。通過複合旋轉矩陣的方式。

記:

繞座標系E下的Z軸旋轉c的旋轉矩陣為Rz,
繞座標系E下X軸旋轉a的旋轉矩陣為Rx,
繞座標系E下Y軸旋轉b的旋轉矩陣為Ry;

繞座標系E下的Y軸旋轉b的矩陣為Rb(Rb == Ry),
繞座標系E在繞Y軸旋轉b後的新座標系E’下的X軸旋轉a的旋轉矩陣為Ra,
繞座標系E’在繞X軸旋轉a後的新座標系E”下的Z軸旋轉c的旋轉矩陣為Rc。

另外,這裡將矩陣R的逆記為R~。

求證:Rz * Rx * Ry == Rb * Ra * Rc

證明:
Rb == Ry,由定義相同可知。
Ra = (Rb~) * Rx * Rb,要得到繞座標系E在繞Y軸旋轉b後的新座標系E’下的X軸旋轉a的旋轉矩陣Ra,先應用Rb~旋轉到座標系E下,然後繞座標系E下的X軸旋轉a,最後應用Rb轉回到座標系E’。
Rc = ((Rb * Ra)~) * Rz * (Rb * Ra),理由同上。
所以有,
右邊 = Rb * Ra * Rc
= Rb * Ra * ((Rb * Ra)~) * Rz * (Rb * Ra)
= Rz * Rb * Ra
= Rz * Rb * (Rb~) * Rx * Rb
= Rz * Rx * Rb
= Rz * Rx * Ry = 左邊
證畢!

從程式碼上來說,就是下面兩個函式是等價的。

private void RotateStatic(float a, float b, float c)
{
    // 靜態尤拉角,依次繞著呼叫Rotate時的區域性座標系的z,x,y軸旋轉a,b,c角度
    transform.Rotate(a, b, c, Space.Self);
}

private void RotateDynamic(float a, float b, float c)
{
    // 動態尤拉角,繞著呼叫Rotate時的區域性座標系的y軸旋轉b角度
    transform.Rotate(0, b, 0, Space.Self);
    // 動態尤拉角,繞著呼叫Rotate時的區域性座標系的y軸旋轉a角度
    transform.Rotate(a, 0, 0, Space.Self);
    // 動態尤拉角,繞著呼叫Rotate時的區域性座標系的y軸旋轉c角度
    transform.Rotate(0, 0, c, Space.Self);
}

五、萬向節鎖(Gimbal Lock)

1. 什麼是萬向節鎖

2. 如何產生萬向節鎖

3. 萬向節鎖的問題

3. 在尤拉旋轉中盡力避免萬向節鎖

六、四元數(Quaternion)旋轉

1. 什麼是四元數

2. 用四元數進行旋轉

參考