【轉】四元數(Quaternion)和旋轉


四元數介紹

 

旋轉,應該是三種坐標變換——縮放、旋轉和平移,中最復雜的一種了。大家應該都聽過,有一種旋轉的表示方法叫四元數。按照我們的習慣,我們更加熟悉的是另外兩種旋轉的表示方法——矩陣旋轉和歐拉旋轉。矩陣旋轉使用了一個4*4大小的矩陣來表示繞任意軸旋轉的變換矩陣,而歐拉選擇則是按照一定的坐標軸順序(例如先x、再y、最后z)、每個軸旋轉一定角度來變換坐標或向量,它實際上是一系列坐標軸旋轉的組合。

 

那么,四元數又是什么呢?簡單來說,四元數本質上是一種高階復數(聽不懂了吧。。。),是一個四維空間,相對於復數的二維空間。我們高中的時候應該都學過復數,一個復數由實部和虛部組成,即x = a + bi,i是虛數單位,如果你還記得的話應該知道i^2 = -1。而四元數其實和我們學到的這種是類似的,不同的是,它的虛部包含了三個虛數單位,i、j、k,即一個四元數可以表示為x = a + bi + cj + dk。那么,它和旋轉為什么會有關系呢?

 

在Unity里,tranform組件有一個變量名為rotation,它的類型就是四元數。很多初學者會直接取rotation的x、y、z,認為它們分別對應了Transform面板里R的各個分量。當然很快我們就會發現這是完全不對的。實際上,四元數的x、y、z和R的那三個值從直觀上來講沒什么關系,當然會存在一個表達式可以轉換,在后面會講。

 

大家應該和我一樣都有很多疑問,既然已經存在了這兩種旋轉表示方式,為什么還要使用四元數這種聽起來很難懂的東西呢?我們先要了解這三種旋轉方式的優缺點:

 

  • 矩陣旋轉
    • 優點:
      • 旋轉軸可以是任意向量;
    • 缺點:
      • 旋轉其實只需要知道一個向量+一個角度,一共4個值的信息,但矩陣法卻使用了16個元素;
      • 而且在做乘法操作時也會增加計算量,造成了空間和時間上的一些浪費;

  • 歐拉旋轉
    • 優點:
      • 很容易理解,形象直觀;
      • 表示更方便,只需要3個值(分別對應x、y、z軸的旋轉角度);但按我的理解,它還是轉換到了3個3*3的矩陣做變換,效率不如四元數;
    • 缺點:
      • 之前提到過這種方法是要按照一個固定的坐標軸的順序旋轉的,因此不同的順序會造成不同的結果;
      • 會造成萬向節鎖(Gimbal Lock)的現象。這種現象的發生就是由於上述固定坐標軸旋轉順序造成的。理論上,歐拉旋轉可以靠這種順序讓一個物體指到任何一個想要的方向,但如果在旋轉中不幸讓某些坐標軸重合了就會發生萬向節鎖,這時就會丟失一個方向上的旋轉能力,也就是說在這種狀態下我們無論怎么旋轉(當然還是要原先的順序)都不可能得到某些想要的旋轉效果,除非我們打破原先的旋轉順序或者同時旋轉3個坐標軸。這里有個視頻可以直觀的理解下;
      • 由於萬向節鎖的存在,歐拉旋轉無法實現球面平滑插值;

  • 四元數旋轉
    • 優點:
      • 可以避免萬向節鎖現象;
      • 只需要一個4維的四元數就可以執行繞任意過原點的向量的旋轉,方便快捷,在某些實現下比旋轉矩陣效率更高;
      • 可以提供平滑插值;
    • 缺點:
      • 比歐拉旋轉稍微復雜了一點點,因為多了一個維度;
      • 理解更困難,不直觀;
 
 

四元數和歐拉角

 
 

基礎知識

 
 
前面說過,一個四元數可以表示為q = w + xi + yj + zk,現在就來回答這樣一個簡單的式子是怎么和三維旋轉結合在一起的。為了方便,我們下面使用q = ((x, y, z),w) = (v, w),其中v是向量,w是實數,這樣的式子來表示一個四元數。
 
我們先來看問題的答案。我們可以使用一個四元數 q=((x,y,z)sinθ2cosθ2) 來執行一個旋轉。具體來說,如果我們想要把空間的一個點P繞着單位向量軸u = (x, y, z)表示的旋轉軸旋轉θ角度,我們首先把點P擴展到四元數空間,即四元數p = (P, 0)。那么,旋轉后新的點對應的四元數(當然這個計算而得的四元數的實部為0,虛部系數就是新的坐標)為:

p=qpq1
 
其中, q=(cosθ2, (x,y,z)sinθ2) ,q1=qN(q),由於u是單位向量,因此
N(q)=1,即q1=q∗。右邊表達式包含了四元數乘法。相關的定義如下:
  • 四元數乘法:q1q2=(v1×v2+w1v2+w2v1,w1w2v1v2)
     
  • 共軛四元數:q=(v⃗ ,w)

  • 四元數的模:N(q) = √(x^2 + y^2 + z^2 +w^2),即四元數到原點的距離

  • 四元數的逆:q1=qN(q)

 
它的證明這里不再贅述,有興趣的可以參見 這篇文章。主要思想是構建了一個輔助向量k,它是將p繞旋轉軸旋轉θ/2得到的。證明過程嘗試證明 wk=kv∗,以此證明w與v、k在同一平面內,且與v夾角為θ。
 
我們舉個最簡單的例子:把點P(1, 0, 1)繞旋轉軸u = (0, 1, 0)旋轉90°,求旋轉后的頂點坐標。首先將P擴充到四元數,即p = (P, 0)。而q = (u*sin45°, cos45°)。求 p=qpq1的值。建議大家一定要在紙上計算一邊,這樣才能加深印象,連筆都懶得動的人還是不要往下看了。最后的結果p` = ((1, 0, -1), 0),即旋轉后的頂點位置是(1, 0, -1)。
 
如果想要得到復合旋轉,只需類似復合矩陣那樣左乘新的四元數,再進行運算即可。
 
我們來總結下四元數旋轉的 幾個需要注意的地方
 
  • 用於旋轉的四元數,每個分量的范圍都在(-1,1);

  • 每一次旋轉實際上需要兩個四元數的參與,即q和q*;

  • 所有用於旋轉的四元數都是單位四元數,即它們的模是1;
 
 
下面是幾點建議:
 
  • 實際上,在Unity里即便你不知道上述公式和變換也絲毫不妨礙我們使用四元數,但是有一點要提醒你,除非你對四元數非常了解,那么不要直接對它們進行賦值

  • 如果你不想知道原理,只想在Unity里找到對應的函數來進行四元數變換,那么你可以使用這兩個函數:Quaternion.EulerQuaternion.eulerAngles。它們基本可以滿足絕大多數的四元數旋轉變換。
 
 

和其他類型的轉換

 
首先是 軸角到四元數
 
給定一個單位長度的旋轉軸(x, y, z)和一個角度θ。對應的四元數為:
q=((x,y,z)sinθ2cosθ2
 
 
這個公式的推導過程上面已經給出。
 
歐拉角到四元數
 
給定一個歐拉旋轉(X, Y, Z)(即分別繞x軸、y軸和z軸旋轉X、Y、Z度),則對應的四元數為:
 
x = sin(Y/2)sin(Z/2)cos(X/2)+cos(Y/2)cos(Z/2)sin(X/2)
y = sin(Y/2)cos(Z/2)cos(X/2)+cos(Y/2)sin(Z/2)sin(X/2)
z = cos(Y/2)sin(Z/2)cos(X/2)-sin(Y/2)cos(Z/2)sin(X/2)
w = cos(Y/2)cos(Z/2)cos(X/2)-sin(Y/2)sin(Z/2)sin(X/2)
q = ((x, y, z), w)
 
它的證明過程可以依靠軸角到四元數的公式進行推導。
 
其他 參考鏈接
 
 
 

四元數的插值

 
這里的插值指的是球面線性插值。
 
設t是一個在0到1之間的變量。我們想要基於t求Q1到Q2之間插值后四元數Q。它的公式是:

Q3  = (sin((1-t)A)/sin(A))*Q1 + (sin((tA)/sin(A))*Q2)
Q = Q3/|Q3|,即單位化
 
 
 

四元數的創建

 
在了解了上述知識后,我們就不需要那么懼怕四元數了,實際上它和矩陣類似,不同的只是它的表示方式以及運算方式。那么在Unity里如何利用四元數進行旋轉呢?Unity里提供了非常多的方式來創建一個四元數。 例如Quaternion.AngleAxis(float angle, Vector3 axis),它可以返回一個繞軸線axis旋轉angle角度的四元數變換。我們可以一個Vector3和它進行左乘,就將得到旋轉后的Vector3。在Unity里只需要用一個“ * ”操作符就可以進行四元數對向量的變換操作,相當於我們上述講到的 p=qpq1操作。如果我們想要進行多個旋轉變換,只需要左乘其他四元數變換即可。例如下面這樣:
[csharp]  view plain  copy
 
 print?在CODE上查看代碼片派生到我的代碼片
  1. Vector3 newVector = Quaternion.AngleAxis(90, Vector3.up) * Quaternion.LookRotation(someDirection) * someVector;  
盡管歐拉角更容易我們理解,但四元數比歐拉角要強大很多。Unity提供了這兩種方式供我們選擇,我們可以選擇最合適的變換。
例如,如果我們需要對旋轉進行插值,我們可以首先使用Quaternion.eulerAngles來得到歐拉角度,然后使用Mathf.Clamp對其進行插值運算。
最后更新Quaternion.eulerAngles或者使用Quaternion.Euler(yourAngles)來創建一個新的四元數。
 
又例如,如果你想要組合旋轉,比如讓人物的腦袋向下看或者旋轉身體,兩種方法其實都可以,但一旦這些旋轉不是以世界坐標軸為旋轉軸,比如人物扭動脖子向下看等,那么四元數是一個更合適的選擇。Unity還提供了transform.forward, transform.right and transform.up 這些非常有用的軸,這些軸可以和Quaternion.AngleAxis組合起來,來創建非常有用的旋轉組合。例如,下面的代碼讓物體執行低頭的動作:
[csharp]  view plain  copy
 
 print?在CODE上查看代碼片派生到我的代碼片
  1. transform.rotation = Quaternion.AngleAxis(degrees, transform.right) * transform.rotation;  


關於Quaternion的其他函數,后面再補充吧,原理類似~
 
 
 

補充:歐拉旋轉

 
在文章開頭關於歐拉旋轉的細節沒有解釋的太清楚,而又有不少人詢問相關問題,我盡量把自己的理解寫到這里,如有不對還望指出。
 
 

歐拉旋轉是怎么運作的

 
 
歐拉旋轉是我們最容易理解的一種旋轉方式。以我們生活中為例,一個舞蹈老師告訴我們,完成某個舞蹈動作需要先向你的左邊轉30°,再向左側彎腰60°,再起身向后彎腰90°(如果你能辦到的話)。上面這樣一個旋轉的過程其實和我們在三維中進行歐拉旋轉很類似,即我們是通過指明繞三個軸旋轉的角度來進行旋轉的,不同的是,日常生活中我們更願意叫這些軸為前后左右上下。而這也意味着我們需要指明一個旋轉順序。這是因為,先繞X軸旋轉90°、再繞Y軸30°和先繞Y軸旋轉90°、再繞X軸30°得到的是不同的結果。
 
在Unity里,歐拉旋轉的旋轉順序是Z、X、Y,這在相關的API文檔中都有說明,例如 Transform.Rotate。其實文檔中說得不是非常詳細,還有一個細節我們需要明白。如果你仔細想想,就會發現有一個非常重要的東西我們沒有說明白,那就是旋轉時使用的坐標系。給定一個旋轉順序(例如這里的Z、X、Y),以及它們對應的旋轉角度(α,β,r),有兩種坐標系可以選擇:
  1. 繞坐標系E下的Z軸旋轉α,繞坐標系E下的Y軸旋轉β,繞坐標系E下的X軸旋轉r,即進行一次旋轉時不一起旋轉當前坐標系;
  2. 繞坐標系E下的Z軸旋轉α,繞坐標系E在繞Z軸旋轉α后的新坐標系E'下的Y軸旋轉β,繞坐標系E'在繞Y軸旋轉β后的新坐標系E''下的X軸旋轉r, 即在旋轉時,把坐標系一起轉動;
 
很容易知道,這兩種選擇的結果是不一樣的。但如果把它們的旋轉順序顛倒一下,其實結果就會一樣。說得明白點,在第一種情況下、按ZXY順序旋轉和在第二種情況下、按YXZ順序旋轉是一樣的。證明方法可以看下 這篇文章。而Unity文檔中說明的旋轉順序指的是在第一種情況下的順序。
 
如果你還是不懂這意味着什么,可以試着調用下這個函數。例如,你認為下面代碼的結果是什么:
[csharp]  view plain  copy
 
 print?在CODE上查看代碼片派生到我的代碼片
  1. transform.Rotate(new Vector3(0, 30, 90));  

原模型的方向和執行結果如下:
 
 
 
 
而我們可以再分別執行下面的代碼:
[csharp]  view plain  copy
 
 print?在CODE上查看代碼片派生到我的代碼片
  1.         // First case  
  2.         transform.Rotate(new Vector3(0, 30, 0));  
  3.         transform.Rotate(new Vector3(0, 0, 90));  
  4.   
  5.         // Second case  
  6. //      transform.Rotate(new Vector3(0, 0, 90));  
  7. //      transform.Rotate(new Vector3(0, 30, 0));  

兩種情況的結果分別是:
 
 
可以發現,調用transform.Rotate(new Vector3(0, 30, 90));是和第一種情況中的代碼是一樣的結果,即先旋轉Y、再旋轉Z。進一步實驗,我們會發現transform.Rotate(new Vector3(30, 90, -40));的結果是和transform.Rotate(new Vector3(0, 90, 0));transform.Rotate(new Vector3(30, 0, 0));transform.Rotate(new Vector3(0, 0, -40));的結果一樣的。你會問了,文檔中不是明明說了旋轉順序是Z、X、Y嗎?怎么現在完全反過來了呢?原因就是我們之前說的兩種坐標系的選擇。在一次調用transform.Rotate的過程中,坐標軸是不隨每次單個坐標軸的旋轉而旋轉的。而在調用transform.Rotate后,這個旋轉坐標系才會變化。也就是說,transform.Rotate(new Vector3(30, 90, -40));執行時使用的是第一種情況,而transform.Rotate(new Vector3(0, 90, 0));transform.Rotate(new Vector3(30, 0, 0));transform.Rotate(new Vector3(0, 0, -40));每一句則是分別使用了上一句執行后的坐標系,即第二種坐標系情況。因此,我們看起來順序好像是完全是反了,但結果是一樣的。
 
上面只是說了一些容易混淆的地方,更多的內容大家可以搜搜wiki之類的。
 
 

數學模型

歐拉旋轉的數學實現就是使用矩陣。而最常見的表示方法就是3*3的矩陣。在 Wiki里我們可以找到這種矩陣的表示形式,以下以按XYZ的旋轉順序為例,三個矩陣分別表示了:
 
 
 
在計算時,我們將原來的旋轉矩陣右乘(這里使用的是列向量)上面的矩陣。從這里我們也可以證明上面所說的兩種坐標系選擇是一樣的結果,它們之間的不同從這里來看其實就是矩陣相乘時的順序不同。第一種坐標系情況,指的是在計算時,先從左到右直接計算R中3個矩陣的結果矩陣,最后再和原旋轉矩陣相乘,因此順序是XYZ;而第二種坐標系情況,指的是在計算時,從右往左依次相乘,因此順序是反過來的,ZYX。你可以驗證R左乘和右乘的結果表達式,就可以相信這個結論了!
 
 

萬向節鎖

 
 
雖然歐拉旋轉非常容易理解,但它會造成臭名昭著的萬向節鎖問題。我之前給出了鏈接大家可能都看了,但還是不明白這是怎么回事。這里 有一篇文章是我目前找到說得最容易懂的中文文章,大家可以看看。
 
如果你還是不明白,我們來做個試驗。還是使用之前的模型,這次我們直接在面板中把它的歐拉角中的X值設為90°,其他先保持不變:
 
此時模型是臉朝下(下圖你看到的只是一個頭頂):
 
現在,如果我讓你不動X軸,只設置Y和Z的值,把這個模型的臉轉上來,讓它向側面看,你可以辦到嗎?你可以發現,這時候無論你怎么設置Y和Z的值,模型始終是臉朝下、在同一平面旋轉,看起來就是Y和Z控制的是同一個軸的旋轉,下面是我截取的任意兩種情況:
 
 
這就是一種萬向節鎖的情況。這里我們先設置X軸為90°也是有原因的,這是因為Unity中歐拉角的旋轉順序是ZXY,即X軸是第二個旋轉軸。當我們在面板中設置任意旋轉值時,Unity實際是按照固定的ZXY順序依次旋轉特定角度的。
 
在代碼里,我們同樣可以重現萬向節鎖現象。
[csharp]  view plain  copy
 
 print?在CODE上查看代碼片派生到我的代碼片
  1. transform.Rotate(new Vector3(0, 0, 40));  
  2. transform.Rotate(new Vector3(0, 90, 0));  
  3. transform.Rotate(new Vector3(80, 0, 0));  

我們只需要固定中間一句代碼,即使Y軸的旋轉角度始終為90°,那么你會發現無論你怎么調整第一句和最后一句中的X或Z值,它會像一個鍾表的表針一樣總是在同一個平面上運動。
 
萬向節鎖中的“鎖”,其實是給人一種誤導,這可能也是讓很多人覺得難以理解的一個原因。實際上,實際上它並沒有鎖住任何一個旋轉軸,只是說我們會在這種旋轉情況下會感覺喪失了一個維度。以上面的例子來說,盡管固定了第二個旋轉軸的角度為90°,但我們原以為依靠改變其他兩個軸的旋轉角度是可以得到任意旋轉位置的(因為按我們理解,兩個軸應該控制的是兩個空間維度),而事實是它被“鎖”在了一個平面,即只有一個維度了,缺失了一個維度。而只要第二個旋轉軸不是±90°,我們就可以依靠改變其他兩個軸的旋轉角度來得到任意旋轉位置。
 
 

數學解釋

 
我們從最簡單的矩陣來理解。還是使用XYZ的旋轉順序。當Y軸的旋轉角度為90°時,我們會得到下面的旋轉矩陣:
 
 
我們對上述矩陣進行左乘可以得到下面的結果:
 
可以發現,此時當我們改變第一次和第三次的旋轉角度時,是同樣的效果,而不會改變第一行和第三列的任何數值,從而缺失了一個維度。
 
我們再嘗試着理解下它的本質。 Wiki上寫,萬向節鎖出現的本質原因,是因為從歐拉角到旋轉的映射並不是一個覆蓋映射,即它並不是在每個點處都是局部同胚的。不懂吧。。。恩,我們再來通俗一下解釋,這意味着,從歐拉角到旋轉是一個多對一的映射(即不同的歐拉角可以表示同一個旋轉方向),而且並不是每一個旋轉變化都可以用歐拉角來表示。其他更多的大家去參考wiki吧。
 
 
建議還是多看看視頻,尤其是后面的部分。當然,如果還是覺得懵懵懂懂的話,在《3D數學基礎:圖形與游戲開發》一書中有一話說的很有道理,“如果您從來沒有遇到過萬向鎖情況,你可能會對此感到困惑,而且不幸的是,很難在本書中講清楚這個問題,你需要親身經歷才能明白。”因此,大家也不要糾結啦,等到遇到的時候可以想到是因為萬向節鎖的原因就好。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM