1. 程式人生 > >[原創] 骨骼運動變換的數學計算過程詳解

[原創] 骨骼運動變換的數學計算過程詳解

2. 骨骼運動積累變換的計算

接下來我們進一步來考慮骨骼運動的情況。描述一根骨骼運動的最典型的方法,是將其運動分解成為相對於其自身本地座標系的旋轉和平移(一般是先旋轉、再平移),其旋轉、平移可以簡單地採用一個旋轉四元組Quarternion和一個平移向量Translation來表示。這裡為了簡便,我們將其二者均統一成一個單一的變換矩陣形式。

設子骨骼相對自己的本地座標系的變換為Mtc,而父骨骼相對與自己的本地座標系的變換為Mtp,則有:經變換後,從本地子骨骼座標轉換到父骨骼座標:V’p=Vc*Mtc*MC->P;再將V’p變換至世界座標系,最終得到骨骼運動之後、Vc在世界系下的新位置:V’w
= V’p*Mtp*MP->W


總之,V’w= Vc*[ Mtc*MC->P *(Mtp*MP->W) ]看出規律來了嘛?對於任何一個骨骼,我們都可以根據頂點在該骨骼本地座標系中的位置,求出骨骼運動之後其在世界座標系下的新位置,其過程就是一個(本地座標系內變換)*(變換到父座標系)* (父座標系內變換)*(變換到祖座標系)*…. 直到變換到世界座標系的過程。如果我們將“(父座標系內變換)*(變換到祖座標系)*…. 直到變換到世界座標系”這一個變換鏈看成是父骨骼的積累變換,那麼,其子骨骼的積累變換則為“(本地變換)*(變換到父座標系)*(父骨骼的積累變換)”。

換而言之,只要知道了當前骨骼的積累變換
,那麼,我們便能很快地將當前骨骼頂點的本地座標快速地轉換到變換後的世界座標系下。而對於二足動物而言,根據骨骼父子關係來計算其各自的積累變換隻不過是一個簡單的樹形先序遍歷過程罷了。這便是骨骼變換的更新過程。

3. 與骨骼相關聯的Mesh節點座標計算

OK,現在我們可以來更新Skinned Mesh上面的節點了。這裡我們繼續使用上圖那個例子。現在我們的問題是:已知Mesh上各節點的世界座標、各骨骼的積累變換矩陣和各骨骼的本地座標系原點在世界系下的位置,求各Mesh節點在骨骼運動之後的世界座標

這裡唯一需注意的地方就是,為了應用骨骼積累變換來求取節點的新世界座標,我們必須首先將節點的世界座標轉換成為與之相關聯的骨骼座標系下的本地座標。換而言之,某節點V
,它與子骨骼相連,我們已知的是V的世界座標Vw和子骨骼的積累變換矩陣,求運動後V的新世界座標。這裡的關鍵是怎樣根據Vw來求得其在子骨骼座標系下的本地座標Vc。在我們的例子裡面,這個Vw->Vc的過程簡直太簡單了,直接以Vw減去(dxc, dyc, dzc)即可搞定。

這個過程值得好好理解,因為在網上的資料中,針對這一過程的理解和表述是最混亂的。首先,我們考慮骨骼靜止的情況,即所有骨骼的初始狀態,這個狀態叫做“參考姿勢”(Reference Pose)。對於每一根骨骼,都存在著一個初始的矩陣與之相關聯,該變換被稱為參考姿勢下的骨骼初始逆變換,其作用是將參考姿勢下與該骨骼相關聯節點的世界座標轉換成為骨骼的本地座標,即用來將Vw變換至Vc

舉個例子,在上例中,設子骨骼在參考姿勢下其初始逆變換為MW->C,則有:

bone_4.PNG

當然,這只是個最簡單的平移的示例了,如果MC->W中包含了複雜變換的話,MW->C形式也會更加複雜。一般而言,參考姿勢下各骨骼的初始逆變換都是會在模型檔案中直接給出的,目的就是為了方便模型的使用者。(提示:這個MW->C其實就是Direct3D API中GetBoneOffsetMatrix所返回的資訊)

OK,至此,數學上的任督二脈我們都已經打通了,最後來看看骨骼變換的全計算過程吧:

1. 讀取Mesh節點(世界座標形式)和所有骨骼在參考姿勢下的初始逆變換
2. 遍歷骨骼樹形結構,計算所有骨骼的運動積累變換
3. 遍歷骨骼C,針對所有與C相關聯的Mesh節點V(其世界座標為Vw):
    3.1. 利用C的初始逆變換獲得V在骨骼座標系下的本地座標Vc,這相當於抵消了參考姿勢
    3.2. 利用C的積累變換和Vc來獲得骨骼運動後V的新世界座標
4. 至此,Mesh中所有節點更新完畢,我們最終得到了骨骼運動之後的新模型

4. 骨骼變換過程中Mesh節點平滑加權計算

在上文中,我們只考慮了“一個節點的位置只由一根骨頭所決定”的情況。而在複雜的骨骼變換過程中,一個面板節點的新位置很可能由若干根骨頭所決定,比方說肘部的節點,前臂和上臂的骨骼運動均有可能影響其位置。

在這種情況下,我們可以定義一個平滑權值的概念,考慮節點V,其關聯的n根骨頭為Bi(1<=i<=n),則針對每一根骨頭Bi,都有一個與之相對應的權值ai,且所有ai的和為1。針對每個骨骼Bi的運動,我們都可以求得一個與之相對應的V節點的新座標Vi,像這樣的新座標將一共有n個。然後我們便可以利用權值ai來對這些新座標進行平滑加權了,即V節點最終唯一的新座標為:Vw=Sum(ai*Vi)

這個過程被稱為頂點混合(Vertex Blending),由於計算方式規範、且計算量較大,目前多數是利用硬體來實現的,一般的3D加速硬體均支援n=4的頂點混合計算。很簡單的思路,不是麼?

5. 盟軍2中骨骼變換所特有的問題

盟軍2中的骨骼變換過程大致與我們在上面舉的例子類似。其大致的情況為:

1. 所有骨骼的初始逆變換都是平移陣,類似上文所舉的那個例子
2. 所有骨骼的運動均被描述成為相對於本地座標的旋轉(四元組)和平移(向量)
3. 沒有采用頂點混合,即n=1,模型上某頂點的位置由且僅由一根骨骼所決定
4. 其資料檔案中,所有座標系和相關的資料均採用的是右手系

這前三點倒也就罷了,都好說,唯獨第4點是一件讓人頭痛的事,why?因為盟軍骨骼運動採用的是類似OpenGL的右手系,而我所採用的Direct3D(包括其數學函式在內)則使用的是左手系。究竟是誰野蠻並不重要,重要的是這事實上造成了不少混淆與麻煩。用Direct3D來針對右手係數據進行計算和渲染會導致怎樣的問題?很顯然,就是符號錯亂所導致的計算錯亂,然後最終導致模型錯亂。

仔細分析一下上述過程,我們面臨的問題其實可分為兩個層面,一個是骨骼計算層面,而另一個則是模型繪製層面。真正正確的做法是:不能將兩者混為一談,必須對其過程進行嚴格區分,即,計算時不應該考慮繪製時所面臨左右手系相關的問題,反之亦然。也就是說,最清晰的思路是,計算時統一成同一種座標系(左手或右手)進行計算,而繪製時再統一轉換成另一種座標系來進行操作,兩者互不相干。針對盟2這個情況,最合適的方法是所有模型計算均堅持採用其資料本身的右手系來進行。至於如何將右手係數據模型用左手系的D3D畫出來,則是小兒科了,有個最簡單的辦法:交換頂點的y-z座標資料,即可實現右手系模型到左手系模型的轉換,其對應變換矩陣為:

bone_5.PNG

OK,目標明確了:我們想利用D3D現成的左手係數學函式來進行實質性的右手系模型計算。讓我們來分析一下計算過程,具體思考一下其所涉及的操作哪些是會受到左右手系影響的。其實,矩陣、向量乘法加法等普通運算是固定的,不會受座標系設定影響,而在三種基本變換(旋轉、平移、縮放)中,後兩者也不會受影響,唯獨旋轉比較關鍵,因為其具體描述取決於我們究竟採用的是各種座標系。

既然始作俑者已經鎖定了,再來考慮下怎麼描述這個Angle-Displacement的問題比較簡潔。我們首先可以排除矩陣形式的旋轉描述,原因很簡單,4*4,多麻煩;其次也可以排除問題多多的尤拉角。最合適的分析方式應該是採用Axis-Angle軸角形式的旋轉四元組描述,即Quaternion。根據四元組的幾何含義,可知在不同座標系下,其定義分別為:

A. 在左手系下,旋轉軸向量為n順時鐘旋轉t,則有:
q=[cos(t/2)  sin(t/2)n]
q=
[cos(t/2)  sin(t/2)nx  sin(t/2)ny  sin(t/2)nz]
q=
[w x y z]B. 在右手系下,旋轉軸向量為n,逆時鐘旋轉t,則有:
q=[cos(t/2)  sin(t/2)n]
q=
[cos(t/2)  sin(t/2)nx  sin(t/2)ny  sin(t/2)nz]
q=
[w x y z]可以發現,唯一的區別在於,左右手系下角度t的旋轉定義是相反的。Quarternion的四個分量,後面x/y/z三個分量在不同的座標系下應該符號互反。

OK,現在我們已經看清楚問題的本質了:由於盟軍資料檔案中儲存的Quaternion是右手系的,那麼,為了能夠使用DX中的左手系函式進行實質性的右手系計算,我們必須在第一時間將其右手系Quarternion的x,y,z進行一次符號翻轉,注意,這是個一勞永逸的操作:在進行了一次符號翻轉之後,便再也不需要考慮該Quaternion相關的左右手系計算問題了。這裡的“左手系函式”包括一切旋轉相關的D3D函式,如“將四元組轉換成矩陣”、“四元組球面插值Slerp”等等,在使用這些函式之前,我們必須首先問一下自己:其資料輸入在其初始化時曾經經過了一次符號翻轉嗎?如果是,那麼恭喜你了,計算出來的結果肯定是對的(即正確的右手系計算結果),儘管你使用的是左手系的D3D函式。

以上便是盟軍2骨骼計算時所會碰到的一個特殊問題。說白了,其麻煩的本質在於我們所擁有的資料和所使用的3D API所採用的座標系不一致,從而影響了我們的計算。在上文中,我已給出瞭解決這個問題的關鍵思路和方法,希望對碰到類似問題的朋友有所幫助。