骨骼蒙皮動畫(SkinnedMesh)的原理解析

CopperDong發表於2018-04-20

一)3D模型動畫基本原理和分類

3D模型動畫的基本原理是讓模型中各頂點的位置隨時間變化。主要種類有Morph(變形)動畫,關節動畫和骨骼蒙皮動畫(SkinnedMesh)從動畫資料的角度來說,三者一般都採用關鍵幀技術,即只給出關鍵幀的資料,其他幀的資料使用插值得到。但由於這三種技術的不同,關鍵幀的資料是不一樣的。

Morph(漸變,變形)動畫是直接指定動畫每一幀的頂點位置,其動畫關鍵中儲存的是Mesh所有頂點在關鍵幀對應時刻的位置。

關節動畫的模型不是一個整體的Mesh,而是分成很多部分(Mesh),通過一個父子層次結構將這些分散的Mesh組織在一起,父Mesh帶動其下子Mesh的運動,各Mesh中的頂點座標定義在自己的座標系中,這樣各個Mesh是作為一個整體參與運動的。動畫幀中設定各子Mesh相對於其父Mesh的變換(主要是旋轉,當然也可包括移動和縮放),通過子到父,一級級的變換累加(當然從技術上,如果是矩陣操作是累乘)得到該Mesh在整個動畫模型所在的座標空間中的變換(從本文的視角來說就是世界座標系了,下同),從而確定每個Mesh在世界座標系中的位置和方向,然後以Mesh為單位渲染即可。關節動畫的問題是,各部分Mesh中的頂點是固定在其Mesh座標系中的,這樣在兩個Mesh結合處就可能產生裂縫

第三類就是骨骼蒙皮動畫即SkinnedMesh了,骨骼蒙皮動畫的出現解決了關節動畫的裂縫問題,而且效果非常酷,發明這個演算法的人一定是個天才,因為SkinnedMesh的原理簡單的難以置信,而效果卻那麼好。骨骼動畫的基本原理可概括為:在骨骼控制下,通過頂點混合動態計算蒙皮網格的頂點,而骨骼的運動相對於其父骨骼,並由動畫關鍵幀資料驅動。一個骨骼動畫通常包括骨骼層次結構資料,網格(Mesh)資料,網格蒙皮資料(skin info)和骨骼的動畫(關鍵幀)資料。下面將具體分析。

二)SkinnedMesh原理和結構分析

SkinnedMesh中文一般稱作骨骼蒙皮動畫,正如其名,這種動畫中包含骨骼(Bone)和蒙皮(Skinned Mesh)兩個部分,Bone的層次結構和關節動畫類似,Mesh則和關節動畫不同:關節動畫中是使用多個分散的Mesh,而Skinned MeshMesh是一個整體,也就是說只有一個Mesh,實際上如果沒有骨骼讓Mesh運動變形,Mesh就和靜態模型一樣了。Skinned Mesh技術的精華在於蒙皮,所謂的皮並不是模型的貼圖(也許會有人這麼想過吧),而是Mesh本身,蒙皮是指將Mesh中的頂點附著(繫結)在骨骼之上,而且每個頂點可以被多個骨骼所控制,這樣在關節處的頂點由於同時受到父子骨骼的拉扯而改變位置就消除了裂縫。Skinned Mesh這個詞從字面上理解似乎是有皮的模型,哦,如果貼圖是皮,那麼普通靜態模型不也都有嗎?所以我覺得應該理解為具有蒙皮資訊的Mesh或可當做皮膚用的Mesh,這個皮膚就是Mesh。而為了有皮膚功能,Mesh還需要蒙皮資訊,即Skin資料,沒有Skin資料就是一個普通的靜態Mesh了。Skin資料決定頂點如何繫結到骨骼上頂點的Skin資料包括頂點受哪些骨骼影響以及這些骨骼影響該頂點時的權重(weight)另外對於每塊骨骼還需要骨骼偏移矩陣(BoneOffsetMatrix)用來將頂點從Mesh空間變換到骨骼空間。在本文中,提到骨骼動畫中的Mesh特指這個皮膚Mesh,提到模型是指骨骼動畫模型整體。骨骼控制蒙皮運動,而骨骼本身的運動呢?當然是動畫資料了。每個關鍵幀中包含時間和骨骼運動資訊,運動資訊可以用一個矩陣直接表示骨骼新的變換,也可用四元數表示骨骼的旋轉,也可以隨便自己定義什麼只要能讓骨骼動就行。除了使用編輯設定好的動畫幀資料,也可以使用物理計算對骨骼進行實時控制。

下面分別具體分析骨骼蒙皮動畫中的結構部件。

1)理解骨骼和骨骼層次結構(Bone Hierarchy)

首先要明確一個觀念:骨骼決定了模型整體在世界座標系中的位置和朝向

先看看靜態模型吧,靜態模型沒有骨骼,我們在世界座標系中放置靜態模型時,只要指定模型自身座標系在世界座標系中的位置和朝向。在骨骼動畫中,不是把Mesh直接放到世界座標系中,Mesh只是作為Skin使用的,是依附於骨骼的,真正決定模型在世界座標系中的位置和朝向的是骨骼。在渲染靜態模型時,由於模型的頂點都是定義在模型座標系中的,所以各頂點只要經過模型座標系到世界座標系的變換後就可進行渲染。而對於骨骼動畫,我們設定模型的位置和朝向,實際是在設定根骨骼的位置和朝向,然後根據骨骼層次結構中父子骨骼之間的變換關係計算出各個骨骼的位置和朝向,然後根據骨骼對Mesh中頂點的繫結計算出頂點在世界座標系中的座標,從而對頂點進行渲染。要記住,在骨骼動畫中,骨骼才是模型主體,Mesh不過是一層皮,一件衣服

如何理解骨骼?請看第二個觀念:骨骼可理解為一個座標空間

在一些文章中往往會提到關節和骨骼,那麼關節是什麼?骨骼又是什麼?下圖是一個手臂的骨骼層次的示例。

骨骼只是一個形象的說法,實際上骨骼可理解為一個座標空間,關節可理解為骨骼座標空間的原點關節的位置由它在父骨骼座標空間中的位置描述。上圖中有三塊骨骼,分別是上臂,前臂和兩個手指。Clavicle(鎖骨)是一個關節,它是上臂的原點,同樣肘關節(elbow joint)是前臂的原點,腕關節(wrist)是手指骨骼的原點。關節既決定了骨骼空間的位置,又是骨骼空間的旋轉和縮放中心。為什麼用一個4X4矩陣就可以表達一個骨骼,因為4X4矩陣中含有的平移分量決定了關節的位置,旋轉和縮放分量決定了骨骼空間的旋轉和縮放。我們來看前臂這個骨骼,其原點位置是位於上臂上某處的,對於上臂來說,它知道自己的座標空間某處(即肘關節所在的位置)有一個子空間,那就是前臂,至於前臂裡面是啥就不考慮了。當前臂繞肘關節旋轉時,實際是前臂座標空間在旋轉,從而其中包含的子空間也在繞肘關節旋轉,在這個例子中是finger骨骼。和實際生物骨骼不同的是,我們這裡的骨骼並沒有實質的骨頭,所以前臂旋轉時,他自己沒啥可轉的,改變的只是座標空間的朝向。你可以說上圖的藍線在轉,但實際藍線並不存在,藍線只是畫上去表示骨骼之間關係的,真正轉的是骨骼空間,我們能看到在轉的是wrist joint,也就是兩個finger骨骼的座標空間,因為他們是子空間,會跟隨父空間運動,就好比人跟著地球轉一樣。

骨骼就是座標空間,骨骼層次就是巢狀的座標空間關節只是描述骨骼的位置即骨骼自己的座標空間原點在其父空間中的位置,繞關節旋轉是指骨骼座標空間(包括所有子空間)自身的旋轉,如此理解足矣。但還有兩個可能的疑問,一是骨骼的長度問題,由於骨骼是座標空間,沒有所謂的長度和寬度的限制,我們看到的長度一方面是蒙皮後的結果,另一方面子骨骼的原點(也就是關節)的位置往往決定了視覺上父骨骼的長度,比如這裡upper arm線段的長度實際是由elbow joint的位置決定的。第二個問題,手指的那個端點是啥啊?實際上在我們的例子中手指沒有子骨骼,所以那個端點並不存在:)那是為了方便演示畫上去的。實際問題中總有最下層的骨骼,他們不能決定其他骨骼了,他們的作用只剩下控制Mesh頂點。對了,那麼手指的長度如何確定?我們看到的長度應該是由蒙皮決定的,也就是由Mesh中屬於手指的那些點離腕關節的距離決定。

經過一段長篇大論,我們終於清楚骨骼和骨骼層次是啥了,但是為什麼要將骨骼組織成層次結構呢?答案是為了做動畫方便,設想如果只有一塊骨骼,那麼讓他動起來就太簡單了,動畫每一幀直接指定他的位置即可。如果是n塊呢?通過組成一個層次結構,就可以通過父骨骼控制子骨骼的運動,牽一髮而動全身,改變某骨骼時並不需要設定其下子骨骼的位置,子骨骼的位置會通過計算自動得到。上文已經說過,父子骨骼之間的關係可以理解為,子骨骼位於父骨骼的座標系中。我們知道物體在座標系中可以做平移變換,以及自身的旋轉和縮放變換。子骨骼在父骨骼的座標系中也可以做這些變換來改變自己在其父骨骼座標系中的位置和朝向等。那麼如何表示呢?由於4X4矩陣可以同時表示上述三種變換,所以一般描述骨骼在其父骨骼座標系中的變換時使用一個矩陣,也就是DirectXSkinned Mesh中的Frame TransformMatrix。實際上這不是唯一的方法,但應該是公認的方法,因為矩陣不光可以同時表示多種變換還可以方便的通過連乘進行變換的組合,這在層次結構中非常方便。在本文的例子-最簡單的skinned mesh例項中,我只演示了平移變換,所以只用一個3d座標就可以表示子骨骼在父骨骼中的位置。下面是Bone Class最初的定義:

classBone

{

public:

floatm_x, m_y, m_z;//這個座標是定義在父骨骼座標系中的

};

OK,除了使用矩陣,座標或某東西描述子骨骼的位置,我們的Bone Class定義中還需要一些指標來建立層次結構,也就是說我們要能通過父骨骼找到子骨骼或反之。問題是我們需要什麼指標呢?從父指向子還是反之?結論是看你需要怎麼用了。如果使用矩陣,需要將父子骨骼矩陣級聯相乘,無論你的矩陣是左乘列向量還是右乘行向量,從哪邊開始乘不重要,只要乘法中父子矩陣的左右位置正確,所以可以在骨骼中只存放指向父的指標,從子到父每次得到父矩陣迴圈相乘。也可以像DX中那樣從根開始相乘並遞迴。在文字的DEMO中由於沒用矩陣,直接使用座標相加計算座標,所以要指定父的位置,然後計算出子的位置,那麼需要在Bone Class中加入子骨骼的指標,因為子骨骼有n個,所以需要n個指標嗎?不一定,看看DirectX的做法,只需要兩個就搞定了,指向第一子的和指向兄弟骨骼的。這樣事先就不需要知道有多少子了。下面是修改後的Bone Class:

classBone

{

Bone*m_pSibling;

Bone*m_pFirstChild;

floatm_x, m_y, m_z;//pos in its parent's space

floatm_wx, m_wy, m_wz; //pos in world space

};

同時增加了一組座標,存放計算好的世界座標系座標。

將各個骨骼相對於其父骨骼擺放好,就行成了一個骨骼層次結構的初始位置,所謂初始是指定義骨骼層次時,那後來呢?後來動畫改變了骨骼的相對位置,準確的說一般是改變了骨骼自身的旋轉而位置保持不變(特殊情況總是存在,比如雷曼,可以把拳頭扔出去的那個傢伙),總之骨骼動了,位置變化了。初始位置很重要,因為通過初始位置骨骼層次間的變換,我們確定了骨骼之間的關係,然後在動畫中你可以只用旋轉

假設我們通過某種方法建立了骨骼層次結構,那麼每一塊骨骼的位置都依賴於其父骨骼的位置,而根骨骼沒有父,他的位置就是整個骨骼體系在世界座標系中的位置。可以認為root的父就是世界座標系。但是初始位置時,根骨骼一般不是在世界原點的,比如使用3d maxcharacter studio建立的biped骨架時,一般兩腳之間是世界原點,而根骨骼-骨盆位於原點上方(+z軸上)。這有什麼關係呢?其實也沒什麼大不了的,只是我們在指定骨骼動畫模型整體座標時,比如設定座標為(0,0,0),則根骨骼-骨盆被置於世界原點,假如xy平面是地面,那麼人下半個身子到地面下了。我們想讓兩腳之間算作人的原點,這樣設定(0,0,0)的座標時人就站在地面上了,所以可以在兩腳之間設定一個額外的根骨骼放在世界原點上,或者這個骨骼並不需要真實存在,只是在你的骨骼模型結構中儲存骨盆骨骼到世界原點的變換矩陣。在微軟X檔案中,一般有一個Scene_Root節點,這算一個額外的骨骼吧,他的變換矩陣為單位陣,表示他初始位於世界原點,而真正骨骼的根Bip01,作為Scene_root的子骨骼,其變換矩陣表示相對於root的位置。說這麼多其實我只是想解釋下,為什麼要存在Scene_Root這種額外的骨骼,以及加深理解骨骼定位骨骼動畫模型整體的世界座標的作用。

有了骨骼類,現在讓我們看一下建立骨骼層次的程式碼,在bone class中增加一個建構函式和兩個成員函式:

classBone

{

public:

Bone(floatx, float y, float z)

:m_pSibling(NULL),m_pFirstChild(NULL),m_pFather(NULL),

m_x(x),m_y(y),m_z(z){}

voidSetFirstChild(Bone* pChild)

{

m_pFirstChild= pChild; m_pFirstChild->m_pFather = this;

}

voidSetSibling(Bone* pSibling)

{

m_pSibling= pSibling; m_pSibling->m_pFather = m_pFather;

}

};

注意我增加了一個成員變數,Bone*m_pFather,這是指向父骨骼的指標,在這個例子中計算骨骼動畫時本不需要這個指標,但我為了畫一條從父骨骼關節到子骨骼關節的連線,增加了它,因為每個骨骼只有第一子骨骼的指標,繪製父骨骼時從父到子畫線就只能畫一條,所以記錄每個骨骼的父,在繪製子骨骼時畫這根線。

有了這個函式,就可以建立骨骼層次了,例如:

Bone*g_boneRoot;

Bone*g_bone1, *g_bone21, *g_bone22;

voidbuildBones()

{

g_boneRoot= new Bone(0, 0, 0);

g_bone1= new Bone(0.1, 0, 0);

g_bone21= new Bone(0.0, 0.1, 0);

g_bone22= new Bone(0.1, 0.0, 0);

g_boneRoot->SetFirstChild(g_bone1);

g_bone1->SetFirstChild(g_bone21);

g_bone21->SetSibling(g_bone22);

}

接下來是骨骼層次中最核心的部分,更新骨骼由於動畫的作用,某個骨骼的變換(TransformMatrix)變了,這時就要根據新的變換來計算,所以這個過程一般稱作UpdateBoneMatrix因為骨骼的變換都是相對父的,要變換頂點必須使用世界變換矩陣,所以這個過程是根據更新了的某些骨骼的骨骼變換矩陣(TransformMatrix)計算出所有骨骼的世界變換矩陣(也即CombinedMatrix。在本文的例子中,骨骼只能平移,甚至我們沒有用矩陣,所以當有骨骼變動時要做的只是直接計算骨骼的世界座標,因此函式命名為ComputeWorldPos,相當於UpdateBoneMatrix後再用頂點乘以CombinedMatrix。

classBone

{

//givefather's world pos, compute the bone's world pos

voidComputeWorldPos(float fatherWX, float fatherWY, float fatherWZ)

{

m_wx= fatherWX+m_x;

m_wy= fatherWY+m_y;

m_wz= fatherWZ+m_z;

if(m_pSibling!=NULL)

m_pSibling->ComputeWorldPos(fatherWX,fatherWY, fatherWZ);

if(m_pFirstChild!=NULL)

m_pFirstChild->ComputeWorldPos(m_wx,m_wy, m_wz);

}

};

其中的遞迴呼叫使用了微軟例子的思想。

有了上述函式,當某骨骼運動時就可以讓其子骨骼跟隨運動了,但是怎麼讓骨骼運動呢?這就是動畫問題了。我不打算在這個簡單的例子中使用關鍵幀動畫,而只是通過程式每幀改變某些骨骼的位置,DEMO中animateBones就是做這個的,你可以在裡面改變不同的骨骼看看效果。在本文下面會對骨骼的關鍵幀動畫做簡單的討論。

至此,我們定義了骨骼類的結構,手工建立了骨骼層次(實際引擎應該從檔案讀入),並且可以根據新位置更新骨骼了(實際引擎應該從動畫資料讀入新的變換或使用物理計算),這樣假如我們用連線將骨骼畫出來,並且讓某個骨骼動起來,我們就會看見他下面的子骨骼跟著動了。當然只有骨骼是不夠的,我們要讓Mesh跟隨骨骼運動,下面就是蒙皮了。

2)蒙皮資訊和蒙皮過程

2-1)Skin info的定義

上文曾討論過,SkinnedMesh中Mesh是作為皮膚使用,蒙在骨骼之上的。為了讓普通的Mesh具有蒙皮的功能,必須新增蒙皮資訊,即Skininfo。我們知道Mesh是由頂點構成的,建模時頂點是定義在模型自身座標系的,即相對於Mesh原點的,而骨骼動畫中決定模型頂點最終世界座標的是骨骼,所以要讓骨骼決定頂點的世界座標,這就要將頂點和骨骼聯絡起來,Skininfo正是起了這個作用。下面是DEMO中頂點類的定義的程式碼片段:

#defineMAX_BONE_PER_VERTEX 4       //用來設定可同時影響該頂點的最大骨骼數

classVertex

{

floatm_x, m_y, m_z; //local pos in mesh space

floatm_wX, m_wY, m_wZ;//blended vertex pos, in world space

//skininfo

intm_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中由於沒有使用矩陣,所以變換的順序和理論順序是一致的)

2-2)BoneOffset Matrix的含義和計算方法

上文已經說過:“骨骼動畫中決定模型頂點最終世界座標的是骨骼,所以要讓骨骼決定頂點的世界座標”,現在讓我們看下頂點受一塊骨骼的作用時的座標變換過程:

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中包括旋轉和縮放,這樣可以提高建立動畫時的容錯性。

在本文DEMO中,我們也沒有使用矩陣儲存Bone Offset,而只用了一個座標儲存偏移位置。

classBoneOffset

{

public:

floatm_offx, m_offy, m_offz;

};

在Bone class中,有一個方法用來計算Bone Offset

classBone

{

public:

BoneOffsetm_boneOffset;

//called after ComputeWorldPos() when boneloaded but not animated

voidComputeBoneOffset()

{

m_boneOffset.m_offx= -m_wx;

m_boneOffset.m_offy= -m_wy;

m_boneOffset.m_offz= -m_wz;

if(m_pSibling!=NULL)

m_pSibling->ComputeBoneOffset();

if(m_pFirstChild!=NULL)

m_pFirstChild->ComputeBoneOffset();

}

};

在ComputeBoneOffset()中,使用計算好的骨骼的世界座標來計算bone offset,這兒的計算只是取一個負數,在實際引擎中,如果bone offset是一個矩陣,這兒就應該是求逆矩陣,本文不做討論了。注意由於我們計算Bone offset時是使用計算好的世界座標,所以在這之前必須在初始位置時對根骨骼呼叫ComputeWorldPos()以計算出各個骨骼在初始位置時的世界座標。

2-3)最終:頂點混合(vertexblending)

現在我們有了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進行硬體加速計算。

3)動畫資料和播放動畫

正如前面所說,本例子中並沒有使用動畫資料,但動畫資料在骨骼動畫中確實最重要的,因為我們的最終目的就是播放動畫。所以作為DEMO的補充,這兒簡要討論一下動畫資料相關問題。其實我覺得動畫的處理在骨骼動畫中是很靈活的,需要專門的一篇文章討論。

本文的最開始說,3D模型動畫的基本原理是讓模型中各頂點的位置隨時間變化。骨骼動畫的情況是,骨骼的位置隨時間變化,頂點位置隨骨骼變化。所以動畫資料中必然包含的是骨骼的運動資訊。可以在動畫幀中包含某時刻骨骼的TransformMatrix,但骨骼一般只是做旋轉,所以也可以用一個四元數表示。但有時候骨骼層次整體會在動畫中進行平移,所以可能需要在動畫幀中包含根骨骼的位置資訊。播放動畫時,給出當前播放的時間值,對於每塊需要動畫的骨骼,根據這個值找出該骨骼前後兩個關鍵幀,根據時間差進行插值,對於四元數要使用四元數球面插值。然後將插值得到的四元數轉換成TransformMatrix,再呼叫UpdateBoneMatrix(其含義上文已介紹)更新計算整個骨骼層次的CombinedMatrix。

4)總結

從結構上看,SkinnedMesh包括:動畫資料,骨骼資料,包含SkininfoMesh資料,以及Bone OffsetMatrix

從過程上看,載入階段:載入並建立骨骼層次結構,計算或載入Bone Offset Matrix,載入Mesh資料和Skininfo(如果是DX的SkinnedMesh這個過程更復雜,因為還涉及到Matrix Palette等)。執行階段:根據時間從動畫資料中獲取骨骼當前時刻的TransformMatrix,呼叫UpdateBoneMatrix計算出各骨骼的CombinedMatrix,對於每個頂點根據Skin info進行VertexBlending計算出頂點的世界座標,最終進行模型的渲染。


相關文章