Skeletal Model and Skinning Animation
僅供個人學習使用,請勿轉載,勿用於任何商業用途。
為了方便討論,先定義幾個術語:Model,一系列MeshPart(MP)(類似d3d裡的subset)的集合;MeshPart,組成Model的單元,包含定義幾何體的實際資料以及材質,也是最小的渲染單元。
第一個問題,為什麼Model需要由多個MeshPart組成,如何劃定一個Model分成幾個MP?理想情況下,Model所包含的MP越少越好,最好是一個Model只包含一個MP,這樣一次DrawPrimitive/DrawIndexedPrimitive,就能完成整個模型的渲染。但通常有兩種情況需要把Model分為不同MP:1,材質(紋理,材質參數)不相同的部分;2,能獨立移動的部分。
通常情況下,作為程式員,你不必關心如何劃分Model,模型師會做好一切,並且儲存為檔案給你使用,你所要關心的是如何找出檔案中所儲存的Model和MP。假設匯出檔案裡儲存的就是圖中這個模型,那麼可能遇到3種情況:1, 檔案把模型記錄為14個不同的model,它們共同組成了另外一個model(人);2,檔案中只包含一個model,但這個model有14個MP;3,這種情況則是前兩種混合的結果。
上面是兩個不同的fbx檔案,左邊那個是以第三種方式組織的,右邊那個則是以第1種方式組織。圖裡Geometry和我們所說的MP概念類似,但不完全一樣,所以數值上看起來有些奇怪。
如何匯入模型超出了本文的討論範圍,現在假設你已經通過某種方式把模型資料載入到程式裡,得到了14個MP,但僅僅有幾何體資料是不夠的。如果查看匯出檔案中的資料,會發現所有MP都以自己的局部座標來儲存頂點。也就是說如果你直接渲染這14個MP,他們會全部重疊在原點。因此,還需要知道每個MP在這個model中的位置。
三個Lcl開頭的資料就是fbx檔案中記錄的MP變換資訊。Lcl表示局部變換,這裡的局部相對於誰呢?相對於他的父節點。這裡就需要引入一個新概念skeletal structure或者bone hierarchy,前者指模型的實際拓撲結構,後者指對這種拓撲結構的描述。具體來說,圖中你看到的人物輪廓形象,就是這個模型的skeletal structure,而 “手連結到手臂,後手臂連結到肩膀”這樣的描述,就是bone hierarchy。描述bone hierarchies最直觀(但並非最佳化)的方式就是用Tree。顯然,對同一skeletal structure的描述可以有無數多種,依據你選擇哪個部分為root,不同的描述方式之間並沒有本質上的差別,通常,模型檔案裡都會包含特定的bone hierarchy資料。為了方便討論,下文不再對structure和bone hierarchy做區分。最後解釋一下另外一個術語bone/join(兩者其實是一樣的),這是一個很容易誤導人的概念,對電腦模型來說,其實並沒有bone這種東西,通常所說的bone應該包含兩個概念:變換以及串連。假設你用Tree來儲存模型的hierarchy資訊,每個node裡儲存了相應的變換資訊(比如matrix),那麼可以認為每個node就是一個bone。但也有可能用一個數組來儲存hierarchy資訊,再用另外一個資料記錄所有Matrix,這時就很難說哪一部分是bone。為了方便,下文的bone只表示變換,或者說代表一個matrix。
假設你已經通過某種方式匯入了hierarchy資訊,以軀幹為根節點,有以下資料結構:
Trunk
|
|-----neck----head
|
|-----left shoulder----left arm---left hand
|
|………………….
那麼現在已經有足夠資料計算每個MP的world matrix,並且渲染它們了,假設在(0,0,0)點渲染這個模型:
Code
worldMatrix = Matrix.CreateTranslate(0,0,0);
trunkMatrix =localTrunkMatrix * worldMatrix; //因為軀幹是根節點,所以局部變換通常為標準矩陣
neckMatrix = neckLocalMatrix * TrunkMatrix;
headMatrix = neckMatrix * headMatrix;
leftShoulder = leftShoudlerlocalMatrix * TrunkMatrix;
……………………………..
drawMP(trunkMatrix, trunkData)
drawMP(neckMatrix, neckData)
……………………………….
你不必總是通過這樣的方式來計算每個MP的實際全局座標。對於靜態模型來說,最好的方式是在載入模型或者預先處理的過程中,就把頂點變換到模型空間中,假設圖中的模型是個木頭人,可以這樣預先處理他頭上的頂點:
Code
rootMatrix = localTrunkMatirx //通常為標準矩陣;
neckMatrix = neckLocalMatrix * TrunkMatrix;
headMatrix = neckMatrix * headMatrix;
transformedVertex[]
for( i < HeadVertexCount)
transformedVertex[i] = headVertex[i] * headMatrix;
CreateNewHeadVertexBuffer(transformedVertex);
現在,如果在x,y,z點渲染這個模型,只需要:
worldMatrix = Matrix.CreateTranslate(x,y,z);
drawMP(worldMatrix, trunkData)
drawMP(worldMatrix, neckData)
drawMP(worldMatrix, headData)
……………………………….
完全省略了一步步通過父節點,計算當前MP實際全局座標的步驟,同時也不再需要儲存模型的hierarchy資訊。
對於部分動態模型來說,可以預先把局部變換轉變為模型空間的變換,比如:
trunkModelMat = localTrunkMat;
neckModelMat = localNeckMat * trunkModelMat;
headModelMat = localHeadMat * neckModelMatrix;
…………………..
在x,y,z點渲染這個模型時:
worldMatrix = Matrix.CreateTranslate(x,y,z);
drawMP(trunkModelMat * worldMatrix, trunkData)
drawMP(neckModelMat * worldMatrix, neckData)
drawMP(headModelMat * worldMatrix, headData)
這種方法同樣不需要儲存模型的hierarchy資訊。
如果這兩種方法那麼好,那為什麼還要講解最初那種複雜的方法呢?應為最初的方法是最通用的,無論什麼樣的模型都可以處理。預變換頂點的方法通常只適用於靜態模型。至於預計算變換,假設你希望通過程式修改了手臂的位置,手掌也自動隨之一起移動的話,顯然就無能為力了,因為我們已經丟失了hierarchy資訊。此時,必須獨立計算手掌應該如何移動,這比簡單的通過父節點計算出變換複雜的多。
目前,我們已經知道了如何渲染skeletal model,進一步渲染skeletal animation也就非常簡單了。
先來看看如何描述動畫。最基本的模型動畫有三種形式:位移變化,縮放變化和旋轉變化。只要記錄下動畫時間內每個時刻的變換資訊,也就是主要畫面格,就能重現這個動畫:
dictionary mpMatrices; //每一幀裡所有mp的local matrix
dictionary keyFrames; //一個動畫序列中的所有幀
float currentTime;
currentMpMatrices = keyFrames[currentTime];
worldMatrix = Matrix.CreateTranslate(x,y,z);
trunkMatrix = currentMpMatrices[trunk] * worldMatrix;
neckMatrix = currentMpMatrices[neck] * TrunkMatrix;
headMatrix = currentMpMatrices[head] * headMatrix;
……………………………..
drawMP(trunkMatrix, trunkData)
drawMP(neckMatrix, neckData)
目前我們知道了如何渲染skeletal model和skeletal animation,但它們通常只適用於剛體,也就是不會發生形變的模型,比如汽車,機器人。對於人或者動物這樣更進階的對像來說,需要使用skinning animation。對skinning mesh來說,一個MP中的頂點不再只受到一個bone的影響,而有可能最多受到n個bone的影響,為了兼顧效率,Realtime Compute中通常1<n<= 4。比如肩膀部位的頂點會受到脖子,軀幹,手臂等部分骨骼的影響。此外,每個MP也不一定再對應著一個bone,有可能包含多個。假設用一個數組來儲存模型中的所有骨骼:matrix[] bones;
每個頂點就必須記錄下它將受到哪幾個骨骼的影響,這稱為boneIndex。此外,每個頂點還需要記錄每個bone對它影響的權重,稱為boneWeight,所有權重的和必須為1。可以用獨立的資料結構來儲存boneIndex和boneWeight,但最常見的還是把它們作為頂點資料的一部分,可能使用這樣的頂點結構:struct Vertex
{
vector3 position;
vector3 normal;
vector2 UV
vector4 boneIndex;
vector4 boneWeight;
……other data…..
}
計算頂點最終位置的公式為:position;
resultPos;
foreach boneIndex in VertexBoneindices
resultPos = position * bones[boneIndex] * boneWeight;
現在問題來了,由於我們不知道一個MP中的某個頂點究竟受到哪幾個bone的影響,所以不能再像之前那樣,只為每個MP提供一個matrix就渲染。而是需要把整個bone數組都提供給MP。當然,要先計算出正確的matrix才行:Matrix[] bones = Matrix[14]; //假設每個mp仍然只包含一個bone
worldMatrix = Matrix.CreateTranslate(x,y,z);
bones[0] = trunkMatrix =localTrunkMatrix * worldMatrix;
bones[1] = neckMatrix = neckLocalMatrix * TrunkMatrix;
bones[2] = headMatrix = neckMatrix * headMatrix;
bones[3] = leftShoulder = leftShoudlerlocalMatrix * TrunkMatrix;
……………………………..
drawMP(bones, trunkData)
drawMP(bones, neckData)
注意,每個bone在數組中的位置必須和頂點中記錄的相同。
把渲染skeletion animation和skinning mesh的技術結合起來,就得到了skinning animation。相關方法就不再詳細討論了。
最後附上GPU渲染skinning mesh的HLSL代碼:
float4x4 View;
float4x4 Projection;
float4x4 Bones[MaxBones];
// Vertex shader input structure.
struct VS_INPUT
{
float4 Position : POSITION0;
float3 Normal : NORMAL0;
float2 TexCoord : TEXCOORD0;
float4 BoneIndices : BLENDINDICES0;
float4 BoneWeights : BLENDWEIGHT0;
};
// Vertex shader program.
VS_OUTPUT VertexShader(VS_INPUT input)
{
VS_OUTPUT output;
// Blend between the weighted bone matrices.
float4x4 skinTransform = 0;
skinTransform += Bones[input.BoneIndices.x] * input.BoneWeights.x;
skinTransform += Bones[input.BoneIndices.y] * input.BoneWeights.y;
skinTransform += Bones[input.BoneIndices.z] * input.BoneWeights.z;
skinTransform += Bones[input.BoneIndices.w] * input.BoneWeights.w;
// Skin the vertex position.
float4 position = mul(input.Position, skinTransform);
output.Position = mul(mul(position, View), Projection);
// Skin the vertex normal, then compute lighting.
float3 normal = normalize(mul(input.Normal, skinTransform));
.
}
ps:注意,文中所有虛擬碼以及所用資料結構,shader只是出於示範目的,並未經過任何最佳化,請根據實際情況參考使用。