最近工作內容主要是四元數方面,所以在此對unity的四元數做一個總結,也防止以後自己遺忘。
在計算機圖形學中,旋轉的表示主要包括矩陣、四元數、歐拉角,當然還有軸角對的方式。這幾種方式各有優劣,並且相互之間可以互相轉化。
矩陣形式主要是計算比較方便,直接使用矩陣的乘法就可以完成旋轉操作,通常情況下使用其他形式描述旋轉也是轉換爲矩陣的形式進行計算,矩陣的缺點是需要採用九個數字記錄旋轉狀態,佔用內存過多。同時矩陣形式也不夠直觀。需要注意的是矩陣運算即矩陣的乘法是不可逆的,因爲旋轉也是不可逆的,unity世界座標下的旋轉次序是Z-X-Y;
歐拉角形式的優缺點正好和矩陣形式互補,歐拉角的優點是非常直觀,直接採用三個角度的旋轉角度記錄旋轉數據,並且佔用內存空間較少,缺點是如果需要做旋轉,是不可以直接使用歐拉角的運算的,一般需要轉換爲矩陣形式進行運算。同時由於歐拉角的旋轉次序的問題,歐拉角還有一個萬向節鎖的問題。
四元數相對來說實現了歐拉角和矩陣的折中,四元數需要用四個元素表示旋轉,同時四元數可以直接進行乘法操作,不過四元數相對來說也是不夠直觀,四元數可以理解爲軸角對,繞某個軸旋轉一定的角度,但是四個元素和旋轉軸和旋轉角度的關係相對複雜一些,假設旋轉軸爲(X,Y,Z),旋轉角度爲A,則對應四元數爲(cos(A/2),X*SIN(A/2),Y*SIN(A/2),Z*SIN(A/2)) =(W,X,Y,Z) 。四元數還有一個好處就是可以實現旋轉插值從而實現平滑旋轉。
主要實現了兩個函數:
(1)LookRotation 根據朝向獲得四元數,使得物體面向特定的方向
(2)FromToRotation 將物體從一個特定的朝向變爲另一個朝向的中間四元數
同時實現了配套的四元數與歐拉角之間的相互轉換。
(1)LookRotation(dir)
此函數實現了根據朝向dir= (X,Y,Z)獲取四元數,即從(0,0,1)轉向(X,Y,Z)需要的四元數,注意旋轉次序爲ZXY,並且還有一個需要注意的點爲此函數獲取的四元數轉化爲歐拉角對應的Z角爲0,所以在最終轉化得來的歐拉角中Z爲0
經過計算,假設歐拉角爲(X,Y,Z),朝向爲(x,y,z),朝向和歐拉角之間的換算公式爲
X = acos(sqrt((x^2+z^2)/(x^2+y^2+z^2)))
Y= atan2(x/z)
Z= 0
需要注意角度和弧度之間的換算以及一些邊界條件,同時朝向爲0,0,0時unity返回的四元數爲1,0,0,0,歐拉角爲0,0,0。
上述公式的得來可以簡單的思考爲,Unity的旋轉次序爲Z-X-Y,由於朝向的初始位置爲(0,0,1),因此對於方向向量來說Z的旋轉是無所謂的,因此歐拉角Z爲零,即歐拉角的Z分量不影響最後的朝向;
a、Y分量公式詳解
對於Y分量,可以理解爲LookRotation的逆過程即從(X,Y,Z)轉至(0,0,1)時,首先繞Y軸旋轉,之後繞X軸旋轉,因此繞Y軸旋轉需要使方向向量轉至YOZ平面,即旋轉之後X分量需要爲0,因此tan(y) = (x/z),同時由於arcTan返回的角度範圍爲(-90,,90),還需要做一些邊界換算,換算用程序描述如下
// x/z
if (x == 0)
{
euler.y = 0;
}
else if (z == 0)
{
euler.y = x > 0 ? pi/2 : -pi/2;
}
else
{
euler.y = Atan(x / z);//返回值的範圍爲-pi/2~pi/2
}
//角度邊界換算
if (z < 0) euler.y += pi;
else if (euler.y < 0) euler.y += pi + pi;
euler.y = RadToDeg(euler.y);
由於公式中涉及到除以零的問題,因此最先的判斷就是判斷除0問題,由於朝向本身還有幾何意義,因此對於0/0這種問題,即x和z均爲0的時候,歐拉角中y爲0或者180,因爲只要X=0,方向向量位於YOZ平面,只有在旋轉0或者180的時候方向向量才能位於YOZ平面,之後通過繞X軸的旋轉才能使方向向量轉至(0,0,1)。當X不爲0Z爲0時,方向向量位於XOZ平面,此時需要旋轉的角度爲-90或者90。
至於方向向量繞Y軸的旋轉轉至YOZ平面,是轉至YOZ的上半平面(Z>0),還是下半平面(Z<0),經過試驗,方向向量繞Y軸旋轉至上半平面,因此所得X的範圍是(0,90)或者(270,360)。對於角度換算,只是簡單的atan,不做贅述。
b、X分量詳解
對於X分量,根據上述對Y分量的介紹,可以理解,方向向量繞Y軸旋轉之後Y分量不變,同時該方向向量的模值不變,因此該方向向量與Z軸的角度x滿足sin(x) = (Y/SQRT(X*X+Y*Y+Z*Z))。用程序描述爲:
//x^2 + z^2
float x2Pz2 = x * x + z * z;
//x^2 + y^2+ z^2
float x2Py2Pz2 = x2Pz2 + y * y;
euler.x = Acos((x2Pz2 / x2Py2Pz2).Sqrt());//範圍0~1,對應反三角範圍爲0~90
euler.x = RadToDeg(euler.x);
//邊界換算
if (y > 0) euler.x = 360 - euler.x;
主要還是注意一些邊界問題,同時由於旋轉方向的問題,當Y>0時,旋轉所得角度大於270,需要用360減去所得角度。
(2)LookRotation四元數版本
四元數版本其實就是將上述過程中的歐拉角轉換爲四元數而已,由於朝向轉歐拉角再轉四元數中間會有一些損耗,所以做了公式化簡使得能夠直接從朝向即方向向量獲得四元數。所得公式較爲簡單,主要是要求得X、Y的半角的三角函數,再利用歐拉角轉四元數的公式進行轉換即可
所用 公式如圖所示,因此用代碼描述爲:
//x
float x2pz2 = dir.x * dir.x + dir.z * dir.z;
float cosX = (x2pz2 / (x2pz2 + dir.y * dir.y)).Sqrt();
//if (dir.y > 0) cosX = -cosX;
float cosXDiv2 = ((cosX + 1) / 2).Sqrt();
if (dir.y > 0) cosXDiv2 = -cosXDiv2;
float sinXDiv2 = (1 - cosXDiv2 * cosXDiv2).Sqrt();
//Y
float cosY;
if (dir.x == 0)
{
cosY = dir.z <0 ? -1 : 1;
}
else if (dir.z == 0)
{
cosY = 0;
}
else
{
float xDz = dir.x / dir.z;
cosY = dir.z > 1 ? 1 / (1 + xDz * xDz).Sqrt() : -1 / (1 + xDz * xDz).Sqrt();//可能會有溢出問題
}
float cosYDiv2 = ((cosY + 1) / (1 + 1)).Sqrt();
if (dir.x < 0) cosYDiv2 = -cosYDiv2;
float sinYDiv2 = ((1 - cosY) / (1 + 1)).Sqrt();
q1.w = cosXDiv2 * cosYDiv2;
q1.x = sinXDiv2 * cosYDiv2;
q1.y = cosXDiv2 * sinYDiv2;
q1.z = -sinXDiv2 * sinYDiv2;
主要還是注意角度求半角之後所得三角函數的正負以及一些邊界問題,如果有溢出的話還需要檢測下溢出問題。
(3)四元數與歐拉角之間的相互換算
通過LookRotation所得的歐拉角由於特殊,即Z分量爲0,因此在做四元數與歐拉角之間的轉換的時候相對較爲簡單。
歐拉角轉四元數的公式如下所示
q1.w = cosXDiv2 * cosYDiv2;
q1.x = sinXDiv2 * cosYDiv2;
q1.y = cosXDiv2 * sinYDiv2;
q1.z = -sinXDiv2 * sinYDiv2;
四元數轉歐拉角的公式爲:
//euler.x/2 = atan(x/w) or atan(-z/y)
if (q1.w != 0)
{
//euler.x/2 = atan(x/w)
euler.x = Atan2(q1.x, q1.w);
}
else if (q1.y != 0)
{
//euler.x/2 = atan(-z/y)
euler.x = Atan2(-q1.z, q1.y);
}
else
{
//TO DO w和y同時爲0
euler.x = Atan2(q1.x, q1.w);
}
euler.x += euler.x;
euler.x *= radToDeg;
//euler.y = atan(y/w) or atan(-z/x)
if (q1.w != 0)
{
euler.y = Atan2(q1.y, q1.w);
}
else if (q1.x != 0)
{
euler.y = Atan2(-q1.z, q1.x);
}
else
{
//TO DO w和x同時爲0
euler.y = Atan2(q1.y, q1.w);
}
euler.y += euler.y;
euler.y *= radToDeg;
//範圍從-180 180 映射到 0 360
euler.x = From180To360(euler.x);
euler.y = From180To360(euler.y);
主要還是對於歐拉角來說,
euler.x/2 = atan(x/w) or atan(-z/y)
euler.y = atan(y/w) or atan(-z/x)
主要需要處理下除以0和邊界問題,即反三角函數所得角度的範圍可能和需要的角度範圍是不同的