結論
我直接拋出結論:
Gimbal Lock 產生的原因不是歐拉角也不是旋轉順序,而是我們的思維方式和程序的執行邏輯沒有對應,也就是說是我們的觀念導致這個情況的發生。
他人解釋
首先我們看一下歐拉角的定義:
用一句話說,歐拉角就是物體繞坐標系三個坐標軸(x,y,z軸)的旋轉角度。
在這里,坐標系可以是世界坐標系,也可以是物體坐標系,旋轉順序也是任意的,可以是xyz,xzy,yxz,zxy,yzx,zyx中的任何一種,甚至可以是xyx,xyy,xzz,zxz等等等等。。。。。。所以說歐拉角多種多樣。歐拉角可分為兩種情況:
1,靜態:即繞世界坐標系三個軸的旋轉,由於物體旋轉過程中坐標軸保持靜止,所以稱為靜態。
2,動態:即繞物體坐標系三個軸的旋轉,由於物體旋轉過程中坐標軸隨着物體做相同的轉動,所以稱為動態。
網上的文章,一般都是這樣解釋的:
是指物體的兩個旋轉軸指向同一個方向。實際上,當兩個旋轉軸平行時,我們就說萬向節鎖現象發生了,換句話說,繞一個軸旋轉可能會覆蓋住另一個軸的旋轉,從而失去一維自由度
通常說來,萬向節鎖發生在使用Eular Angles(歐拉角)的旋轉操作中,原因是Eular Angles按照一定的順序依次獨立地繞軸旋轉。讓我們想象一個具體的旋轉場景,首先物體先繞轉X軸旋轉,然后再繞Y軸,最后繞Z軸選擇,從而完成一個旋轉操作(飄飄白雲譯注:實際是想繞某一個軸旋轉,然而Eular Angle將這個旋轉分成三個獨立的步驟進行),當你繞Y軸旋轉90度之后萬向節鎖的問題就出現了,因為X軸已經被求值了,它不再隨同其他兩個軸旋轉,這樣X軸與Z軸就指向同一個方向(它們相當於同一個軸了)。
看得懂嗎?我是看不太懂~
我的理解
我們先來考慮一下,旋轉到底是怎么個旋轉法。
靜態的情況很好理解,怎么旋轉都不會有問題,萬向節的問題是不會出現在靜態的旋轉過程中的。但是你想像一下動態的旋轉,動態的旋轉,這里會有兩個坐標系,看清楚了,兩個坐標系!
- 世界坐標系
- 物體坐標系
那么這兩者是什么關系呢?
一開始,這兩個坐標系是重合的,但是旋轉開始以后,世界坐標系不會變化,物體坐標系隨着旋轉就發生變化了。
親愛的讀者,你們先想想,這兩個坐標系的關系,你們覺得物體旋轉是繞着那個坐標系旋轉的?
你會說:
你剛剛不是說了嘛!是繞着物體的坐標系旋轉的!
對,沒有錯,那么在物體旋轉的時候,物體的坐標系是不是一直在變化呢?是的!那么我們在給他旋轉的參數的時候考慮到這個問題了嗎?沒有!
就是說我給他的旋轉的參數是基於一種假設:每一次旋轉都是以物體的坐標系為參考來進行的。就是說我是希望它每一次旋轉前,都能夠將旋轉參數在物體坐標系上進行計算。很簡單,一架飛機,作為機長,每次旋轉以后他跟着飛機旋轉了,后面的旋轉操作自然是基於新的物體坐標系來的。
但是實際上,程序解析我給的數據的時候,只是簡單地將三個軸的旋轉一個個的相乘,也就是說,總的來說還是在最開始的那個坐標系(也就是一直不動的世界坐標系)下面計算的。而且需要注意的是: 每一次進行計算的順序是確定不變的! 這也是為什么有人會說萬向節問題是因為旋轉順序導致的樂。
看一下在OpenGL 實現旋轉的代碼:
void configRotateTrans(GLfloat radX, GLfloat radY, GLfloat radZ) {
GLfloat xTrans[4][4] = {0};
GLfloat yTrans[4][4] = {0};
GLfloat zTrans[4][4] = {0};
GLfloat tempMatrix[4][4] = {0};
xTrans[3][3] = 1;
xTrans[0][0] = 1;
xTrans[1][1] = cosf(radX);
xTrans[1][2] = -sinf(radX);
xTrans[2][2] = cosf(radX);
xTrans[2][1] = sinf(radX);
yTrans[3][3] = 1;
yTrans[0][0] = cosf(radY);
yTrans[0][2] = sinf(radY);
yTrans[2][2] = cosf(radY);
yTrans[2][0] = -sinf(radY);
yTrans[1][1] = 1;
zTrans[3][3] = 1;
zTrans[2][2] = 1;
zTrans[0][0] = cosf(radZ);
zTrans[0][1] = -sinf(radZ);
zTrans[1][0] = sinf(radZ);
zTrans[1][1] = cosf(radZ);
// Multiply the 3 matrix
// rotateTrans = xTrans * yTrans * zTrans
multiMatrix(xTrans, yTrans, tempMatrix);
multiMatrix(tempMatrix, zTrans, rotateTrans);
看懂了嗎,物體最終在哪個位置是簡單粗暴地將繞xyz三個旋轉的矩陣連續相乘得到的,計算的順序是x->y->z,那么比如用戶先輸入繞y軸轉90度,再輸入繞x軸轉90度。其實程序執行的時候,還是會先將x軸的數據進行計算,再計算y軸的數據。但是如果用戶先輸入繞y軸轉90度,再輸入繞z軸轉90度,程序還是按照x-y-z的順序來,只是正好用戶也是這樣輸入。
現在我們明確了兩點:
- 物體的旋轉是以世界坐標系為參考的。
- 物體旋轉的順序是確定的,和用戶輸入的旋轉的順序無關。
那么還是剛剛那兩種情況:
操作A:
用戶第一次輸入: 繞Y軸轉90度,第二次輸入:繞X軸轉90度。
實際程序運行:先繞X軸轉90度,再繞Y軸轉90度。
操作B:
用戶第一次輸入: 繞Y軸轉90度,第二次輸入:繞Z軸轉90度。
實際程序運行:先繞Y軸轉90度,再繞Z軸轉90度。
現在發揮一下想象力,當物體繞Y軸轉動90度以后,物體坐標系的X軸和世界坐標系的Z軸是不是變成了同一個軸?好的,那么這個時候,用戶無論輸入的是繞X軸轉還是繞Z軸轉,最終物體轉動是不是都是繞着這個軸(世界Z軸/物體X軸)。上面的操作A和操作B的結果是一樣的!
這就是Gimbal Lock,這並不是什么缺陷,陷阱,而是我們的思維方式是錯誤的,所以導致這個問題的出現。
參考資料:GimbalLock萬向節鎖與四元數旋轉