Skeletal Model and Skinning Animation

來源:互聯網
上載者:User

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只是出於示範目的,並未經過任何最佳化,請根據實際情況參考使用。

 

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.