四元數(shù)介紹旋轉(zhuǎn),應(yīng)該是三種坐標變換——縮放、旋轉(zhuǎn)和平移,中最復(fù)雜的一種了。大家應(yīng)該都聽過,有一種旋轉(zhuǎn)的表示方法叫四元數(shù)。按照我們的習(xí)慣,我們更加熟悉的是另外兩種旋轉(zhuǎn)的表示方法——矩陣旋轉(zhuǎn)和歐拉旋轉(zhuǎn)。矩陣旋轉(zhuǎn)使用了一個4*4大小的矩陣來表示繞任意軸旋轉(zhuǎn)的變換矩陣,而歐拉選擇則是按照一定的坐標軸順序(例如先x、再y、最后z)、每個軸旋轉(zhuǎn)一定角度來變換坐標或向量,它實際上是一系列坐標軸旋轉(zhuǎn)的組合。 那么,四元數(shù)又是什么呢?簡單來說,四元數(shù)本質(zhì)上是一種高階復(fù)數(shù)(聽不懂了吧。。。),是一個四維空間,相對于復(fù)數(shù)的二維空間。我們高中的時候應(yīng)該都學(xué)過復(fù)數(shù),一個復(fù)數(shù)由實部和虛部組成,即x = a bi,i是虛數(shù)單位,如果你還記得的話應(yīng)該知道i^2 = -1。而四元數(shù)其實和我們學(xué)到的這種是類似的,不同的是,它的虛部包含了三個虛數(shù)單位,i、j、k,即一個四元數(shù)可以表示為x = a bi cj dk。那么,它和旋轉(zhuǎn)為什么會有關(guān)系呢? 在Unity里,tranform組件有一個變量名為rotation,它的類型就是四元數(shù)。很多初學(xué)者會直接取rotation的x、y、z,認為它們分別對應(yīng)了Transform面板里R的各個分量。當(dāng)然很快我們就會發(fā)現(xiàn)這是完全不對的。實際上,四元數(shù)的x、y、z和R的那三個值從直觀上來講沒什么關(guān)系,當(dāng)然會存在一個表達式可以轉(zhuǎn)換,在后面會講。 大家應(yīng)該和我一樣都有很多疑問,既然已經(jīng)存在了這兩種旋轉(zhuǎn)表示方式,為什么還要使用四元數(shù)這種聽起來很難懂的東西呢?我們先要了解這三種旋轉(zhuǎn)方式的優(yōu)缺點:
四元數(shù)和歐拉角基礎(chǔ)知識前面說過,一個四元數(shù)可以表示為q = w xi yj zk,現(xiàn)在就來回答這樣一個簡單的式子是怎么和三維旋轉(zhuǎn)結(jié)合在一起的。為了方便,我們下面使用q = ((x, y, z),w) = (v, w),其中v是向量,w是實數(shù),這樣的式子來表示一個四元數(shù)。 我們先來看問題的答案。我們可以使用一個四元數(shù)q=((x,y,z)sinθ2, cosθ2) 來執(zhí)行一個旋轉(zhuǎn)。具體來說,如果我們想要把空間的一個點P繞著單位向量軸u = (x, y, z)表示的旋轉(zhuǎn)軸旋轉(zhuǎn)θ角度,我們首先把點P擴展到四元數(shù)空間,即四元數(shù)p = (P, 0)。那么,旋轉(zhuǎn)后新的點對應(yīng)的四元數(shù)(當(dāng)然這個計算而得的四元數(shù)的實部為0,虛部系數(shù)就是新的坐標)為: p′=qpq?1 其中,q=(cosθ2, (x,y,z)sinθ2) ,q?1=q?N(q),由于u是單位向量,因此 N(q)=1,即q?1=q?。右邊表達式包含了四元數(shù)乘法。相關(guān)的定義如下:
它的證明這里不再贅述,有興趣的可以參見這篇文章。主要思想是構(gòu)建了一個輔助向量k,它是將p繞旋轉(zhuǎn)軸旋轉(zhuǎn)θ/2得到的。證明過程嘗試證明wk?=kv?,以此證明w與v、k在同一平面內(nèi),且與v夾角為θ。 我們舉個最簡單的例子:把點P(1, 0, 1)繞旋轉(zhuǎn)軸u = (0, 1, 0)旋轉(zhuǎn)90°,求旋轉(zhuǎn)后的頂點坐標。首先將P擴充到四元數(shù),即p = (P, 0)。而q = (u*sin45°, cos45°)。求p′=qpq?1的值。建議大家一定要在紙上計算一邊,這樣才能加深印象,連筆都懶得動的人還是不要往下看了。最后的結(jié)果p` = ((1, 0, -1), 0),即旋轉(zhuǎn)后的頂點位置是(1, 0, -1)。 如果想要得到復(fù)合旋轉(zhuǎn),只需類似復(fù)合矩陣那樣左乘新的四元數(shù),再進行運算即可。 我們來總結(jié)下四元數(shù)旋轉(zhuǎn)的幾個需要注意的地方:
下面是幾點建議:
和其他類型的轉(zhuǎn)換首先是軸角到四元數(shù): 給定一個單位長度的旋轉(zhuǎn)軸(x, y, z)和一個角度θ。對應(yīng)的四元數(shù)為: q=((x,y,z)sinθ2, cosθ2) 這個公式的推導(dǎo)過程上面已經(jīng)給出。 歐拉角到四元數(shù): 給定一個歐拉旋轉(zhuǎn)(X, Y, Z)(即分別繞x軸、y軸和z軸旋轉(zhuǎn)X、Y、Z度),則對應(yīng)的四元數(shù)為: 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) 它的證明過程可以依靠軸角到四元數(shù)的公式進行推導(dǎo)。 其他參考鏈接: 四元數(shù)的插值這里的插值指的是球面線性插值。 設(shè)t是一個在0到1之間的變量。我們想要基于t求Q1到Q2之間插值后四元數(shù)Q。它的公式是: Q3 = (sin((1-t)A)/sin(A))*Q1 (sin((tA)/sin(A))*Q2) Q = Q3/|Q3|,即單位化 四元數(shù)的創(chuàng)建在了解了上述知識后,我們就不需要那么懼怕四元數(shù)了,實際上它和矩陣類似,不同的只是它的表示方式以及運算方式。那么在Unity里如何利用四元數(shù)進行旋轉(zhuǎn)呢?Unity里提供了非常多的方式來創(chuàng)建一個四元數(shù)。例如Quaternion.AngleAxis(float angle, Vector3 axis),它可以返回一個繞軸線axis旋轉(zhuǎn)angle角度的四元數(shù)變換。我們可以一個Vector3和它進行左乘,就將得到旋轉(zhuǎn)后的Vector3。在Unity里只需要用一個“ * ”操作符就可以進行四元數(shù)對向量的變換操作,相當(dāng)于我們上述講到的p′=qpq?1操作。如果我們想要進行多個旋轉(zhuǎn)變換,只需要左乘其他四元數(shù)變換即可。例如下面這樣: Vector3 newVector = Quaternion.AngleAxis(90, Vector3.up) * Quaternion.LookRotation(someDirection) * someVector; 盡管歐拉角更容易我們理解,但四元數(shù)比歐拉角要強大很多。Unity提供了這兩種方式供我們選擇,我們可以選擇最合適的變換。 例如,如果我們需要對旋轉(zhuǎn)進行插值,我們可以首先使用Quaternion.eulerAngles來得到歐拉角度,然后使用Mathf.Clamp對其進行插值運算。 最后更新Quaternion.eulerAngles或者使用Quaternion.Euler(yourAngles)來創(chuàng)建一個新的四元數(shù)。 transform.rotation = Quaternion.AngleAxis(degrees, transform.right) * transform.rotation; 關(guān)于Quaternion的其他函數(shù),后面再補充吧,原理類似~ 補充:歐拉旋轉(zhuǎn)在文章開頭關(guān)于歐拉旋轉(zhuǎn)的細節(jié)沒有解釋的太清楚,而又有不少人詢問相關(guān)問題,我盡量把自己的理解寫到這里,如有不對還望指出。 歐拉旋轉(zhuǎn)是怎么運作的歐拉旋轉(zhuǎn)是我們最容易理解的一種旋轉(zhuǎn)方式。以我們生活中為例,一個舞蹈老師告訴我們,完成某個舞蹈動作需要先向你的左邊轉(zhuǎn)30°,再向左側(cè)彎腰60°,再起身向后彎腰90°(如果你能辦到的話)。上面這樣一個旋轉(zhuǎn)的過程其實和我們在三維中進行歐拉旋轉(zhuǎn)很類似,即我們是通過指明繞三個軸旋轉(zhuǎn)的角度來進行旋轉(zhuǎn)的,不同的是,日常生活中我們更愿意叫這些軸為前后左右上下。而這也意味著我們需要指明一個旋轉(zhuǎn)順序。這是因為,先繞X軸旋轉(zhuǎn)90°、再繞Y軸30°和先繞Y軸旋轉(zhuǎn)90°、再繞X軸30°得到的是不同的結(jié)果。 在Unity里,歐拉旋轉(zhuǎn)的旋轉(zhuǎn)順序是Z、X、Y,這在相關(guān)的API文檔中都有說明,例如Transform.Rotate。其實文檔中說得不是非常詳細,還有一個細節(jié)我們需要明白。如果你仔細想想,就會發(fā)現(xiàn)有一個非常重要的東西我們沒有說明白,那就是旋轉(zhuǎn)時使用的坐標系。給定一個旋轉(zhuǎn)順序(例如這里的Z、X、Y),以及它們對應(yīng)的旋轉(zhuǎn)角度(α,β,r),有兩種坐標系可以選擇:
很容易知道,這兩種選擇的結(jié)果是不一樣的。但如果把它們的旋轉(zhuǎn)順序顛倒一下,其實結(jié)果就會一樣。說得明白點,在第一種情況下、按ZXY順序旋轉(zhuǎn)和在第二種情況下、按YXZ順序旋轉(zhuǎn)是一樣的。證明方法可以看下這篇文章。而Unity文檔中說明的旋轉(zhuǎn)順序指的是在第一種情況下的順序。 如果你還是不懂這意味著什么,可以試著調(diào)用下這個函數(shù)。例如,你認為下面代碼的結(jié)果是什么: transform.Rotate(new Vector3(0, 30, 90)); 原模型的方向和執(zhí)行結(jié)果如下: 而我們可以再分別執(zhí)行下面的代碼: // First case
transform.Rotate(new Vector3(0, 30, 0));
transform.Rotate(new Vector3(0, 0, 90));
// Second case
// transform.Rotate(new Vector3(0, 0, 90));
// transform.Rotate(new Vector3(0, 30, 0)); 兩種情況的結(jié)果分別是: 可以發(fā)現(xiàn),調(diào)用transform.Rotate(new Vector3(0, 30, 90));是和第一種情況中的代碼是一樣的結(jié)果,即先旋轉(zhuǎn)Y、再旋轉(zhuǎn)Z。進一步實驗,我們會發(fā)現(xiàn)transform.Rotate(new Vector3(30, 90, -40));的結(jié)果是和transform.Rotate(new Vector3(0, 90, 0));transform.Rotate(new Vector3(30, 0, 0));transform.Rotate(new Vector3(0, 0, -40));的結(jié)果一樣的。你會問了,文檔中不是明明說了旋轉(zhuǎn)順序是Z、X、Y嗎?怎么現(xiàn)在完全反過來了呢?原因就是我們之前說的兩種坐標系的選擇。在一次調(diào)用transform.Rotate的過程中,坐標軸是不隨每次單個坐標軸的旋轉(zhuǎn)而旋轉(zhuǎn)的。而在調(diào)用transform.Rotate后,這個旋轉(zhuǎn)坐標系才會變化。也就是說,transform.Rotate(new Vector3(30, 90, -40));執(zhí)行時使用的是第一種情況,而transform.Rotate(new Vector3(0, 90, 0));transform.Rotate(new Vector3(30, 0, 0));transform.Rotate(new Vector3(0, 0, -40));每一句則是分別使用了上一句執(zhí)行后的坐標系,即第二種坐標系情況。因此,我們看起來順序好像是完全是反了,但結(jié)果是一樣的。 上面只是說了一些容易混淆的地方,更多的內(nèi)容大家可以搜搜wiki之類的。 數(shù)學(xué)模型歐拉旋轉(zhuǎn)的數(shù)學(xué)實現(xiàn)就是使用矩陣。而最常見的表示方法就是3*3的矩陣。在Wiki里我們可以找到這種矩陣的表示形式,以下以按XYZ的旋轉(zhuǎn)順序為例,三個矩陣分別表示了: 在計算時,我們將原來的旋轉(zhuǎn)矩陣右乘(這里使用的是列向量)上面的矩陣。從這里我們也可以證明上面所說的兩種坐標系選擇是一樣的結(jié)果,它們之間的不同從這里來看其實就是矩陣相乘時的順序不同。第一種坐標系情況,指的是在計算時,先從左到右直接計算R中3個矩陣的結(jié)果矩陣,最后再和原旋轉(zhuǎn)矩陣相乘,因此順序是XYZ;而第二種坐標系情況,指的是在計算時,從右往左依次相乘,因此順序是反過來的,ZYX。你可以驗證R左乘和右乘的結(jié)果表達式,就可以相信這個結(jié)論了! 萬向節(jié)鎖雖然歐拉旋轉(zhuǎn)非常容易理解,但它會造成臭名昭著的萬向節(jié)鎖問題。我之前給出了鏈接大家可能都看了,但還是不明白這是怎么回事。這里有一篇文章是我目前找到說得最容易懂的中文文章,大家可以看看。 如果你還是不明白,我們來做個試驗。還是使用之前的模型,這次我們直接在面板中把它的歐拉角中的X值設(shè)為90°,其他先保持不變: 此時模型是臉朝下(下圖你看到的只是一個頭頂): 現(xiàn)在,如果我讓你不動X軸,只設(shè)置Y和Z的值,把這個模型的臉轉(zhuǎn)上來,讓它向側(cè)面看,你可以辦到嗎?你可以發(fā)現(xiàn),這時候無論你怎么設(shè)置Y和Z的值,模型始終是臉朝下、在同一平面旋轉(zhuǎn),看起來就是Y和Z控制的是同一個軸的旋轉(zhuǎn),下面是我截取的任意兩種情況: 這就是一種萬向節(jié)鎖的情況。這里我們先設(shè)置X軸為90°也是有原因的,這是因為Unity中歐拉角的旋轉(zhuǎn)順序是ZXY,即X軸是第二個旋轉(zhuǎn)軸。當(dāng)我們在面板中設(shè)置任意旋轉(zhuǎn)值時,Unity實際是按照固定的ZXY順序依次旋轉(zhuǎn)特定角度的。 在代碼里,我們同樣可以重現(xiàn)萬向節(jié)鎖現(xiàn)象。 transform.Rotate(new Vector3(0, 0, 40));
transform.Rotate(new Vector3(0, 90, 0));
transform.Rotate(new Vector3(80, 0, 0)); 我們只需要固定中間一句代碼,即使Y軸的旋轉(zhuǎn)角度始終為90°,那么你會發(fā)現(xiàn)無論你怎么調(diào)整第一句和最后一句中的X或Z值,它會像一個鐘表的表針一樣總是在同一個平面上運動。 萬向節(jié)鎖中的“鎖”,其實是給人一種誤導(dǎo),這可能也是讓很多人覺得難以理解的一個原因。實際上,實際上它并沒有鎖住任何一個旋轉(zhuǎn)軸,只是說我們會在這種旋轉(zhuǎn)情況下會感覺喪失了一個維度。以上面的例子來說,盡管固定了第二個旋轉(zhuǎn)軸的角度為90°,但我們原以為依靠改變其他兩個軸的旋轉(zhuǎn)角度是可以得到任意旋轉(zhuǎn)位置的(因為按我們理解,兩個軸應(yīng)該控制的是兩個空間維度),而事實是它被“鎖”在了一個平面,即只有一個維度了,缺失了一個維度。而只要第二個旋轉(zhuǎn)軸不是±90°,我們就可以依靠改變其他兩個軸的旋轉(zhuǎn)角度來得到任意旋轉(zhuǎn)位置。 數(shù)學(xué)解釋我們從最簡單的矩陣來理解。還是使用XYZ的旋轉(zhuǎn)順序。當(dāng)Y軸的旋轉(zhuǎn)角度為90°時,我們會得到下面的旋轉(zhuǎn)矩陣: 我們對上述矩陣進行左乘可以得到下面的結(jié)果: 可以發(fā)現(xiàn),此時當(dāng)我們改變第一次和第三次的旋轉(zhuǎn)角度時,是同樣的效果,而不會改變第一行和第三列的任何數(shù)值,從而缺失了一個維度。 我們再嘗試著理解下它的本質(zhì)。Wiki上寫,萬向節(jié)鎖出現(xiàn)的本質(zhì)原因,是因為從歐拉角到旋轉(zhuǎn)的映射并不是一個覆蓋映射,即它并不是在每個點處都是局部同胚的。不懂吧。。。恩,我們再來通俗一下解釋,這意味著,從歐拉角到旋轉(zhuǎn)是一個多對一的映射(即不同的歐拉角可以表示同一個旋轉(zhuǎn)方向),而且并不是每一個旋轉(zhuǎn)變化都可以用歐拉角來表示。其他更多的大家去參考wiki吧。 建議還是多看看視頻,尤其是后面的部分。當(dāng)然,如果還是覺得懵懵懂懂的話,在《3D數(shù)學(xué)基礎(chǔ):圖形與游戲開發(fā)》一書中有一話說的很有道理,“如果您從來沒有遇到過萬向鎖情況,你可能會對此感到困惑,而且不幸的是,很難在本書中講清楚這個問題,你需要親身經(jīng)歷才能明白?!币虼耍蠹乙膊灰m結(jié)啦,等到遇到的時候可以想到是因為萬向節(jié)鎖的原因就好。
|
|
來自: 昵稱31750011 > 《web編程》