骨骼蒙皮動畫也就是SkinnedMesh,應該是目前用的最多的3D模型動畫了。因為他可以解決關節動畫的裂縫問題,而且原理簡單,效果出色,所以今天詳細的談一下骨骼蒙皮動畫的相關知識。
關節動畫中使用的是多個分散的Mesh,而骨骼蒙皮動畫使用的skinned Mesh是一個整體,也就是只有一個Mesh,實際上如果沒有骨骼讓mesh變形,那就和靜態模型沒有區別了。
首先骨骼動畫的原理為:在骨骼控制下,通過頂點混合動態計算蒙皮網格的頂點,而骨骼的運動相對於其父骨骼,並由動畫關鍵幀數據驅動。這樣就要涉及到4個數據相互協作:
1.骨骼層次結構
2.網格Mesh
3.網格蒙皮數據(skin info)
4.骨骼的關鍵幀動畫
Skinned Mesh技術的精華在於蒙皮,其實名皮並不是模型的貼圖,而是Mesh自身,蒙皮是至Mesh中的定點綁定在骨骼之上,而且每個頂點可以被多個骨骼控制,這樣再關鍵出的定點由於同時收到父子骨骼的拉扯而改變位置就消除了關節動畫產生的裂縫。所以蒙皮其實就是具有蒙皮信息的Mesh。而為了模仿皮膚,Mesh還需要蒙皮信息,也就是skin info,沒有skin數據其實就是靜態的Mesh,skin數據決定了定點如何綁定到骨骼上。頂點的Skin數據還包括頂點應該收哪些骨骼影響,以及骨骼影響的權重,另外對於每塊骨骼還需要骨骼便宜矩陣用來將定點從Mesh空間變換到骨骼空間。骨骼本身的運動依靠的是動畫的關鍵幀數據了,每個關鍵幀中包含時間和骨骼運動信息,運動信息可以用一個矩陣直接表示骨骼的新的變換。
總結起來也就是說,骨骼關鍵幀動畫數據控制骨骼如何進行移動,骨骼移動計算出結果交給帶有骨骼蒙皮數據的Mesh上進行模擬皮膚的移動。
那么具體實現原理如下:
A.骨骼和骨骼層次
首先骨骼決定了模型整體在世界坐標系中的位置和朝向。
為什么這么說?首先靜態模型並沒有骨骼,那我們把一個靜態模型放置到世界坐標系中,只需要指定模型自身坐標系在世界坐標中的位置和方向。在骨骼動畫中,Mesh是依附於骨骼的,真正決定模型在世界坐標系中的位置所以是骨骼。在渲染靜態模型時,由於模型的頂點都是定義在模型坐標系中的,所以各頂點只要經過模型坐標系到世界坐標系的變換后就可進行渲染。而對於骨骼動畫,我們設置模型的位置和朝向,實際是在設置根骨骼的位置和朝向,然后根據骨骼層次結構中父子骨骼之間的變換關系計算出各個骨骼的位置和朝向,然后根據骨骼對Mesh中頂點的綁定計算出頂點在世界坐標系中的坐標,從而對頂點進行渲染。所以在骨骼動畫中,骨骼才是主體,Mesh不過是一層“皮“。
那在虛擬的世界坐標系中到底什么是骨骼?什么是關節?
其實骨骼其實可以理解為一個坐標空間,關節就是坐標空間的原點。
骨骼只是提供了一種人們比較接受的形象的說法,實際上在虛擬世界中,骨骼可以理解為一個坐標空間,關節則可以理解為骨骼坐標空間的原點,關節的位置由它在父骨骼坐標空間中進行位置描述。假設說在虛擬世界中模擬一個人,人有鎖骨,有上臂,有小臂,有手指,那鎖骨就是上臂的原點,同樣肘關節也是小臂骨骼的原點,腕關節就是手指骨骼的原點。關節既決定了骨骼空間的位置,又決定了骨骼空間的旋轉與縮放。我們通常用4*4的矩陣來表達骨骼,是因為4*4矩陣中含有平移分量能決定關節的位置,旋轉縮放分量可以決定骨骼空間的旋轉與縮放。小臂的骨骼原點位置位於上臂某處,對於上臂來說,它知道自己的坐標空間某處有一個子空間,就是小臂。當小臂繞肘關節旋轉的時候,實際上是小臂的坐標空間再旋轉,從而其中包含的子空間也在肘關節旋轉。
那我們確定了骨骼其實就是坐標空間,那么骨骼的層次就是嵌套的坐標空間了。關節只是描述骨骼的位置即骨骼自己的坐標空間原點在其父空間中的位置,繞關節旋轉是指骨骼坐標空間(包括所有子空間)自身的旋轉。但是可能還有兩個疑問,一個是骨骼的長度,因為骨骼是坐標空間,沒有所謂的長度和寬度的限制,所以我們看到的長度一方面是蒙皮后的結果,另一方面子骨骼的原點(也就是關節)的位置往往決定了視覺上父骨骼的長度。那類似天涯明月刀可以調整角色身高的原理其實就是改變了各子骨骼原點的相對位置。第二個可能有的問題是手指的端點是什么?實際問題中,總有最下層的骨骼,他們不能決定其他骨骼了,他們的作用只剩下控制Mesh頂點。比如說手指的長度,其實就是由蒙皮決定的,也就是由Mesh中屬於手指的那些點離腕關節的距離決定。
假設我們通過某種方法建立了骨骼層次結構,那么每一塊骨骼的位置都依賴於其父骨骼的位置,而根骨骼沒有父,他的位置就是整個骨骼體系在世界坐標系中的位置,可以認為root的父就是世界坐標系。但是初始位置時,根骨骼一般不是在世界原點的,比如使用3d maxcharacter studio創建的biped骨架時,一般兩腳之間是世界原點,而根骨骼-骨盆位於原點上方(+z軸上)。這有什么關系呢?其實也沒什么大不了的,只是我們在指定骨骼動畫模型整體坐標時,比如設定坐標為(0,0,0),則根骨骼-骨盆被置於世界原點,假如xy平面是地面,那么人下半個身子到地面下了。我們想讓兩腳之間算作人的原點,這樣設定(0,0,0)的坐標時人就站在地面上了,所以可以在兩腳之間設定一個額外的根骨骼放在世界原點上,或者這個骨骼並不需要真實存在,只是在你的骨骼模型結構中保存骨盆骨骼到世界原點的變換矩陣。一般有一個Scene_Root節點,這算一個額外的骨骼吧,他的變換矩陣為單位陣,表示他初始位於世界原點,而真正骨骼的根Bip01,作為Scene_root的子骨骼,其變換矩陣表示相對於root的位置。說這么多其實我只是想解釋下,為什么要存在Scene_Root這種額外的骨骼,以及加深理解骨骼定位骨骼動畫模型整體的世界坐標的作用。
B.蒙皮信息和蒙皮的過程
Skin info 定義:
SkinnedMesh中Mesh是作為皮膚使用,蒙在骨骼之上的。為了讓普通的Mesh具有蒙皮的功能,必須添加蒙皮信息,即Skininfo。我們知道Mesh是由頂點構成的,建模時頂點是定義在模型自身坐標系的,即相對於Mesh原點的,而骨骼動畫中決定模型頂點最終世界坐標的是骨骼,所以要讓骨骼決定頂點的世界坐標,就要將頂點和骨骼聯系起來,Skininfo正是起了這個作用,下面是DEMO中頂點類的定義的代碼片段:
#defineMAX_BONE_PER_VERTEX 4 //用來設置可同時影響該頂點的最大骨骼數
classVertex
{
//local pos in mesh space
float m_x, m_y, m_z;
//blended vertex pos, in world space
float m_wX, m_wY, m_wZ;
//skininfo
int m_boneNum; //影響該頂點的骨骼數目
Bone*m_bones[MAX_BONE_PER_VERTEX]; //指向這些骨骼的指針
floatm_boneWeights[MAX_BONE_PER_VERTEX]; //這些骨骼作用於該點的權重
};
頂點的Skininfo包含影響該頂點的骨骼數目,指向這些骨骼的指針,這些骨骼作用於該頂點的權重(Skinweight)。由於只是一個簡單的例子,這兒沒有考慮優化,所以用靜態數組存放骨骼指針和權重,且實際引擎中Skin info的定義方式不一定是這樣的,但基本原理一致。
MAX_BONE_PER_VERTEX在這兒用來設置可同時影響頂點的最大骨骼數,實際上由於這個DEMO是手工進行VertexBlending並且也沒用硬件加速,可影響頂點的骨骼數量並沒有限制,只是恰好需要一個常量來定義數組,所以定義了一下。在實際引擎中由於要使用硬件加速,以及為了確保速度,一般會定義最大骨骼數。另外在本DEMO中,Skin info是手工設定的,而在實際項目中,一般是在建模軟件中生成這些信息並導出。
Skin info的作用是使用各個骨骼的變換矩陣對頂點進行變換並乘以權重,這樣某塊骨骼只能對該頂點產生部分影響。各骨骼權重之和應該為1。
Skin info是針對頂點的,然而在使用Skininfo前我們必須要使用Bone OffsetMatrix對頂點進行變換,下面具體討論Bone offset Matrix。(寫下這句話的時候我感覺有些不妥,因為實際是先將所有的矩陣相乘最后再作用於頂點,這兒是按照理論上的順序進行講述吧,請不要與實際情況混淆,其實他們也並不矛盾。而且在我們的DEMO中由於沒有使用矩陣,所以變換的順序和理論順序是一致的)
上文已經說過:“骨骼動畫中決定模型頂點最終世界坐標的是骨骼,所以要讓骨骼決定頂點的世界坐標”,現在讓我們看下頂點受一塊骨骼的作用時的坐標變換過程:
meshvertex (defined in mesh space)---<BoneOffsetMatrix>--->Bone space
---<BoneCombinedTransformMatrix>--->World
從這個過程中可看出,需要首先將模型頂點從模型空間變換到某塊骨骼自身的骨骼空間,然后才能利用骨骼的世界變換計算頂點的世界坐標。BoneOffset Matrix的作用正是將模型從頂點空間變換到骨骼空間。那么Bone Offset Matrix如何得到呢?下面具體分析:
Mesh space是建模時使用的空間,mesh中頂點的位置相對於這個空間的原點定義。比如在3d max中建模時(視xy平面為地面,+z朝上),可將模型兩腳之間的中點作為Mesh空間的原點,並將其放置在世界原點,這樣左腳上某一頂點坐標是(10,10,2),右腳上對稱的一點坐標是(-10,10,2),頭頂上某一頂點的坐標是(0,0,170)。由於此時Mesh空間和世界空間重合,上述坐標既在Mesh空間也在世界空間,換句話說,此時實際是以世界空間作為Mesh空間了。在骨骼動畫中,在世界中放置的是骨骼而不是Mesh,所以這個區別並不重要。在3d max中添加骨骼的時候,也是將骨骼放入世界空間中,並調整骨骼的相對位置使得和mesh相吻合(即設置骨骼的TransformMatrix),得到骨架的初始姿勢以及相應的Transform Matrix(按慣例模型做成兩臂側平舉直立,骨骼也要適合這個姿態)。由於骨骼的TransformMatrix(作用是將頂點從骨骼空間變換到上層空間)是基於其父骨骼空間的,只有根骨骼的Transform是基於世界空間的,所以要通過自下而上一層層Transform變換(如果使用行向量右乘矩陣,這個Transform的累積過程就是C=Mbone*Mfather*Mgrandpar*...*Mroot),得到該骨骼在世界空間上的變換矩陣 - Combined TransformMatrix,即通過這個矩陣可將頂點從骨骼空間變換到世界空間。那么這個矩陣的逆矩陣就可以將世界空間中的頂點變換到某塊骨骼的骨骼空間。由於Mesh實際上就是定義在世界空間了,所以這個逆矩陣就是OffsetMatrix。即Offset Matrix就是骨骼在初始位置(沒有經過任何動畫改變)時將bone變換到世界空間的矩陣(CombinedTransformMatrix)的逆矩陣,有一些資料稱之為Inverse Matrix。在幾何流水線中,是通過變換矩陣將頂點變換到上層空間,最終得到世界坐標,逆矩陣則做相反的事,所以Inverse這種提法也符合慣例。那么Offset這種提法從字面上怎么理解呢?Offset即骨骼相對於世界原點的偏移,世界原點加上這個偏移就變成骨骼空間的原點,同樣定義在世界空間中的點經過這個偏移矩陣的作用也被變換到骨骼空間了。從另一角度理解,在動畫中模型中頂點的位置是根據骨骼位置動態計算的,也就是說頂點跟着骨骼動,但首先必須確定頂點和骨骼之間的相對位置(即頂點在該骨骼坐標系中的位置),一個骨骼可能對應很多頂點,如果要保存這個相對位置每個頂點對於每塊受控制的骨骼都要保存,這樣就要保存太多的矩陣了。。。所以只保存mesh空間到骨骼空間的變換(即OffsetMatrix),然后通過這個變換計算每個頂點在該骨骼空間中的坐標,所以OffsetMatrix也反應了mesh和每塊骨骼的相對位置,只是這個位置是間接的通過和世界坐標空間的關系表達的,在初始位置將骨骼按照模型的形狀擺好是關鍵之處。
以上的分析是通過將mesh space和world space重合得到OffsetMatrix的計算方法。那么如果他們不重合呢?那就要先計算頂點從mesh space變換到world space的變換矩陣,並乘上(還是右乘為例)Combined Matrix的InverseMatrix從而得到OffsetMatrix。但是這不是找麻煩嗎?因為Mesh的原點在哪兒並不重要,為啥不讓他們重合呢?
還有一個問題是,既然OffsetMatrix可以計算出來,為啥還要在骨骼動畫文件中同時提供TransformMatrix和OffsetMatrix呢?實際上文件中確實可以不提供OffsetMatrix,而只在載入時計算。但TransformMatrix不可缺少,動畫關鍵幀數據一般只存儲骨骼的旋轉和根骨骼的位置,骨骼間的相對位置還是要靠TransformMatrix提供。在微軟的X文件結構中提供了OffsetMatrix,原因是什么呢?我不知道。我猜想一個可能的原因是為了兼容性和靈活性,比如mesh並沒有定義在世界坐標系,而是作為一個object放置在3d max中,在導出骨骼動畫時不能簡單的認為mesh的頂點坐標是相對於世界原點的,還要把這個object的位置考慮進去,於是導出插件要計算出OffsetMatrix並保存在x文件中以避免兼容性問題。
關於OffsetMatrix和TransformMatrix含有平移,旋轉和縮放的討論:
首先,OffsetMatrix取決於骨骼的初始位置(即TransformMatrix),由於骨骼動畫中我們使用的是動畫中的位置,初始位置是什么樣並不重要,所以可以在初始位置中只包含平移,而旋轉和縮放在動畫中設置(一般也僅僅使用旋轉,這也是為啥動畫通常中可以用一個四元數表示骨骼的關鍵幀)。在這種情況下,OffsetMatrix只包含平移即可。因此一些引擎的Bone中不存放Transform矩陣,而只存放骨骼在父骨骼空間中的坐標,然后旋轉只在動畫幀中設置,最基本的骨骼動畫即可實現。但也可在Transform和Offset Matrix中包括旋轉和縮放,這樣可以提高創建動畫時的容錯性。
最終:頂點混合!
現在我們有了Skin info,有了Bone offset,可謂萬事具備,只欠東風了。現在就可以做頂點混合了,這是骨骼動畫的精髓所在,正是這個技術消除了關節處的裂縫。頂點混合后得到了頂點新的世界坐標,對所有的頂點執行vertexblending后,從Mesh的角度看,Mesh deform(變形)了,變成動畫需要的形狀了。
首先,讓我們看看使用單塊骨骼對頂點進行作用的過程,以下是DEMO中的相關代碼:
classVertex
{
public:
voidComputeWorldPosByBone(Bone* pBone, float& outX, float& outY, float&outZ)
{
//step1:transform vertex from mesh space to bone space
outX= m_x+pBone->m_boneOffset.m_offx;
outY= m_y+pBone->m_boneOffset.m_offy;
outZ= m_z+pBone->m_boneOffset.m_offz;
//step2:transform vertex from bone space to world sapce
outX+= pBone->m_wx;
outY+= pBone->m_wy;
outZ+= pBone->m_wz;
}
};
這個函數使用一塊骨骼對頂點進行變換,將頂點從Mesh坐標系變換到世界坐標系,這兒使用了骨骼的Bone Offset Matrix和 Combined Transform Matrix (嗯,我知道這兒沒用矩陣,但意思是一樣的對嗎)
對於多塊骨骼,對每塊骨骼執行這個過程並將結果根據權重混合(即vertex blending)就得到頂點最終的世界坐標。進行vertex blending的代碼如下:
classVertex
{
voidBlendVertex()
{//dothe vertex blending, get the vertex's pos in world space
m_wX= 0;
m_wY= 0;
m_wZ= 0;
for(inti=0; i<m_boneNum; ++i)
{
floattx, ty, tz;
ComputeWorldPosByBone(m_bones[i],tx, ty, tz);
tx*=m_boneWeights[i];
ty*=m_boneWeights[i];
tz*=m_boneWeights[i];
m_wX+= tx;
m_wY+= ty;
m_wZ+= tz;
}
}
};
這些函數我都放在Vertex類中了,因為只是一個簡單DEMO所以沒有特別考慮引擎結構問題,在BlendVertex()中,遍歷影響該頂點的所有骨骼,用每塊骨骼計算出頂點的世界坐標,然后使用Skin Weight對這些坐標進行加權平均。tx,ty,tz是某塊骨骼作用后頂點的世界坐標乘以權重后的值,這些值相加后就是最終的世界坐標了。
現在讓我們用一個公式回顧一下Vertexblending的整個過程(使用矩陣變換)
Vworld = Vmesh * BoneOffsetMatrix1 *CombindMatrix1 * Weight1
+Vmesh* BoneOffsetMatrix2 * CombinedMatrix2 * Weight2
+…
+Vmesh * BoneOffsetMatrixN * CombindMatrixN * WeightN
(這個公式使用的是行向量左乘矩陣)
由於BoneOffsetMatrix和Combined Matrix都是矩陣,可以先相乘這樣就減少很多計算了,在實際PC游戲中可以使用VS進行硬件加速計算。
C.動畫事菊和播放動畫
最開始說的,3D模型動畫的基本原理是讓模型中各頂點的位置隨時間變化。骨骼動畫的情況是,骨骼的位置隨時間變化,頂點的位置隨骨骼變化。所以動畫數據中必然包含的是骨骼的運動信息。可以在動畫幀中包含某時刻骨骼的TrabsformMatrix,但骨骼一般只是做旋轉,所以也可以用一個四元數標識。但是有時候骨骼層次整體會在動畫中進行平移,所i可能需要在動畫幀中包含根骨骼的位置信息。播放動畫時,給出當前播放的時間值,對於每塊需要動畫的骨骼,根據這個值找出該骨骼前后兩個關鍵幀,根據時間差進行插值,對於四元數要使用四元數球面插值。然后將插值得到的四元數轉換成TransformMatrix,再調用UpdateBoneMatrix更新計算整個骨骼層次的CombinedMatrix.
最后總結的來說,SkinnedMesh骨骼蒙皮動畫從結構上包括:動畫數據,骨骼數據,包含SkinInfo的Mesh數據,以及Bone OffsetMatrix.
從過程上看分為載入階段和運行階段:載入階段負責載入並簡歷骨骼層次,計算或載入Bone Offset Matrix骨骼偏移矩陣,載入Mesh數據和Skins Info。運行階段:根據時間從動畫數據中獲取骨骼當前時刻的TransformMatrix(世界坐標矩陣),調用UpdateBoneMatrix計算出各個骨骼的CombineMatrix(骨骼空間矩陣),對於每個頂點根據Skin Info進行VertexBlending(頂點混合)計算出頂點的世界坐標,最終進行模型的渲染。
本文為CSDN博主「無敵的成長日記」所寫原理解析的總結,遵循 CC 4.0 BY-SA 版權協議,非常感謝無敵的成長日記的深刻理解(原文地址): https://blog.csdn.net/jimoshuicao/article/details/9283071