[Unity] Unity 3D 中的旋轉
Unity 3D 中的旋轉
一、Unity 3D 中 Rotation
在Unity中,旋轉通常可以用一個三維向量(x,y,z)表示。實際上這是尤拉角。三個分量分別是繞x軸、y軸和z軸的旋轉角度。
要對一個GameObject進行旋轉,可以直接通過如下程式碼:
transform.Rotate(xAngle, yAngle, zAngle);
那麼有如下疑問:
- 上述的x軸、y軸、z軸指的是哪組基?是世界座標系下的xyz軸,還是區域性座標系下的xyz軸?還是其他?
- 旋轉的正方向如何?
- 旋轉的順序如何?
下面一一解答。
二、旋轉軸
首先,回答第一個問題,到底旋轉軸是哪個座標系的基?分為如下三種情況。
1. 旋轉軸:Inspector 中 Transform 的旋轉數值
對於這一個情況,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元件的旋轉軸是父節點的模型空間座標軸,如果沒有父節點,則旋轉軸是世界空間座標軸。
上圖顯示瞭如果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中進行旋轉,旋轉軸就是區域性座標系的座標軸。
3. 旋轉軸:在Script中使用Rotate函式,在Space.World中旋轉
在Inspector中將Rotate Space設定為World後,執行結果見下圖。這裡我們知道,長方體的父節點的Y軸不是World的Y軸,而這裡的長方體是繞著世界座標系下的Y軸旋轉的。
所以得出結論:在Space.World中進行旋轉,旋轉軸是世界座標系的座標軸。
4. 靜態尤拉角和動態尤拉角
前面說到的旋轉軸的問題,在數學上有對應的概念。這就是所謂的靜態尤拉角和動態尤拉角。
所謂靜態尤拉角,就是其旋轉軸使用的是靜止不動的參考系。動態尤拉角,使用的是剛體本身作為參考系,因而參考系會隨著剛體的旋轉而旋轉。
因此,使用Space.World旋轉,以及Inspector中的旋轉是靜態尤拉角;使用Space.Self旋轉,是動態尤拉角。
三、旋轉的正方向
來到第二個問題,由於Unity中區域性座標系和世界座標系都是左手座標系,所以這裡旋轉的正方向可由右手法則判定。
四、旋轉的順序
下面來看第三個問題,旋轉的順序,即我們的尤拉角(xAngle, yAngle, zAngle)由三個分量組成,分別對應著繞x軸旋轉,繞y軸旋轉和繞z軸旋轉,那麼是如何繞著這三個軸進行旋轉的呢?
這裡也分為靜態尤拉角和動態尤拉角的情況進行討論。
1. 靜態尤拉角
這種情況對應著上面所述的使用Space.World進行旋轉,以及Inspector中的旋轉。即使旋轉軸在旋轉的過程中保持不變,旋轉的順序會決定最後的旋轉結果。我們看下面的例子會很清晰的理解:
- 情形一:首先繞世界座標系的x軸旋轉90度,再繞世界座標系的y軸旋轉90度
- 情形二:首先繞世界座標系的y軸旋轉90度,再繞世界座標系的x軸旋轉90度
可以看到,由於旋轉順序的不同,最終導致了旋轉結果的不同!(究其本質,是因為矩陣乘法不滿足交換律)
對於旋轉的順序,一般沒有定式,因此,需要在使用時明確的指定出其順序。對此有一個專門的術語,稱為順規。如果在座標系中的旋轉,先繞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軸旋轉
- 情形二:以狀態B時的y軸旋轉
Unity中的情況究竟如何呢?直接執行下面的程式碼會看到結果:
void Start () {
transform.Rotate(90, 90, 0, Space.Self);
}
可以發現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);
}