Blog: http://blog.csdn.net/n5 & http://www.cnblogs.com/winfree (3D筆記only)
Histroy:
Version:1.01 Date:2008-11-01
修改了一些不精確的用語(yǔ)
Version:1.00 Date:2008-10-19
(此文發(fā)布于csdn blog, 整理收集于cnblogs)
講述骨骼動(dòng)畫的資料很多,但大部分都是針對(duì)DX8或DX9的SkinnedMesh進(jìn)行講解。我覺(jué)得對(duì)于骨骼動(dòng)畫初學(xué)者增加了不必要的負(fù)擔(dān),還沒(méi)有理解骨骼動(dòng)畫的實(shí)質(zhì)就已被DX復(fù)雜的架構(gòu)搞得暈頭轉(zhuǎn)向了。這篇文章把注意力集中在骨骼動(dòng)畫的基本組成結(jié)構(gòu)和原理上,并實(shí)現(xiàn)了一個(gè)最簡(jiǎn)單純手工的自定義骨骼動(dòng)畫例子幫助理解(使用最簡(jiǎn)單的OpenGL指令,甚至沒(méi)有使用矩陣)。這篇文章在我學(xué)習(xí)理解骨骼動(dòng)畫的過(guò)程中逐步完善,是對(duì)這個(gè)技術(shù)的理解總結(jié),屬于學(xué)習(xí)筆記。學(xué)習(xí)過(guò)程中參考了很多資料,其中給我啟發(fā)最大的是Frank Luna寫的”Skinned Mesh Character Animation with Direct3D 9.0c”。由于本人自身也是初學(xué)者,所以錯(cuò)誤和不精確的地方在所難免,歡迎指正和討論,討論請(qǐng)加入3DGameStudy郵件列表:http://happyfire.googlepages.com/3dgamestudymaillist。另外文本不涉及任何高級(jí)骨骼動(dòng)畫技術(shù),也不涉及DX架構(gòu)的SkinnedMesh技術(shù)和硬件加速,但本文中會(huì)引用SkinnedMesh中的約定俗成的名詞,如Transform Matrix,Bone Offset Matrix等。
一)3D模型動(dòng)畫基本原理和分類
3D模型動(dòng)畫的基本原理是讓模型中各頂點(diǎn)的位置隨時(shí)間變化。主要種類有Morph動(dòng)畫,關(guān)節(jié)動(dòng)畫和骨骼蒙皮動(dòng)畫(Skinned Mesh)。從動(dòng)畫數(shù)據(jù)的角度來(lái)說(shuō),三者一般都采用關(guān)鍵幀技術(shù),即只給出關(guān)鍵幀的數(shù)據(jù),其他幀的數(shù)據(jù)使用插值得到。但由于這三種技術(shù)的不同,關(guān)鍵幀的數(shù)據(jù)是不一樣的。
Morph(漸變,變形)動(dòng)畫是直接指定動(dòng)畫每一幀的頂點(diǎn)位置,其動(dòng)畫關(guān)鍵中存儲(chǔ)的是Mesh所有頂點(diǎn)在關(guān)鍵幀對(duì)應(yīng)時(shí)刻的位置。
關(guān)節(jié)動(dòng)畫的模型不是一個(gè)整體的Mesh,而是分成很多部分(Mesh),通過(guò)一個(gè)父子層次結(jié)構(gòu)將這些分散的Mesh組織在一起,父Mesh帶動(dòng)其下子Mesh的運(yùn)動(dòng),各Mesh中的頂點(diǎn)坐標(biāo)定義在自己的坐標(biāo)系中,這樣各個(gè)Mesh是作為一個(gè)整體參與運(yùn)動(dòng)的。動(dòng)畫幀中設(shè)置各子Mesh相對(duì)于其父Mesh的變換(主要是旋轉(zhuǎn),當(dāng)然也可包括移動(dòng)和縮放),通過(guò)子到父,一級(jí)級(jí)的變換累加(當(dāng)然從技術(shù)上,如果是矩陣操作是累乘)得到該Mesh在整個(gè)動(dòng)畫模型所在的坐標(biāo)空間中的變換(從本文的視角來(lái)說(shuō)就是世界坐標(biāo)系了,下同),從而確定每個(gè)Mesh在世界坐標(biāo)系中的位置和方向,然后以Mesh為單位渲染即可。關(guān)節(jié)動(dòng)畫的問(wèn)題是,各部分Mesh中的頂點(diǎn)是固定在其Mesh坐標(biāo)系中的,這樣在兩個(gè)Mesh結(jié)合處就可能產(chǎn)生裂縫。
第三類就是骨骼蒙皮動(dòng)畫即Skinned Mesh了,骨骼蒙皮動(dòng)畫的出現(xiàn)解決了關(guān)節(jié)動(dòng)畫的裂縫問(wèn)題,而且效果非常酷,發(fā)明這個(gè)算法的人一定是個(gè)天才,因?yàn)?/span>Skinned Mesh的原理簡(jiǎn)單的難以置信,而效果卻那么好。骨骼動(dòng)畫的基本原理可概括為:在骨骼控制下,通過(guò)頂點(diǎn)混合動(dòng)態(tài)計(jì)算蒙皮網(wǎng)格的頂點(diǎn),而骨骼的運(yùn)動(dòng)相對(duì)于其父骨骼,并由動(dòng)畫關(guān)鍵幀數(shù)據(jù)驅(qū)動(dòng)。一個(gè)骨骼動(dòng)畫通常包括骨骼層次結(jié)構(gòu)數(shù)據(jù),網(wǎng)格(Mesh)數(shù)據(jù),網(wǎng)格蒙皮數(shù)據(jù)(skin info)和骨骼的動(dòng)畫(關(guān)鍵幀)數(shù)據(jù)。下面將具體分析。
二)Skinned Mesh原理和結(jié)構(gòu)分析
Skinned Mesh中文一般稱作骨骼蒙皮動(dòng)畫,正如其名,這種動(dòng)畫中包含骨骼(Bone)和蒙皮(Skinned Mesh)兩個(gè)部分,Bone的層次結(jié)構(gòu)和關(guān)節(jié)動(dòng)畫類似,Mesh則和關(guān)節(jié)動(dòng)畫不同:關(guān)節(jié)動(dòng)畫中是使用多個(gè)分散的Mesh,而Skinned Mesh中Mesh是一個(gè)整體,也就是說(shuō)只有一個(gè)Mesh,實(shí)際上如果沒(méi)有骨骼讓Mesh運(yùn)動(dòng)變形,Mesh就和靜態(tài)模型一樣了。Skinned Mesh技術(shù)的精華在于蒙皮,所謂的皮并不是模型的貼圖(也許會(huì)有人這么想過(guò)吧),而是Mesh本身,蒙皮是指將Mesh中的頂點(diǎn)附著(綁定)在骨骼之上,而且每個(gè)頂點(diǎn)可以被多個(gè)骨骼所控制,這樣在關(guān)節(jié)處的頂點(diǎn)由于同時(shí)受到父子骨骼的拉扯而改變位置就消除了裂縫。Skinned Mesh這個(gè)詞從字面上理解似乎是有皮的模型,哦,如果貼圖是皮,那么普通靜態(tài)模型不也都有嗎?所以我覺(jué)得應(yīng)該理解為具有蒙皮信息的Mesh或可當(dāng)做皮膚用的Mesh,這個(gè)皮膚就是Mesh。而為了有皮膚功能,Mesh還需要蒙皮信息,即Skin數(shù)據(jù),沒(méi)有Skin數(shù)據(jù)就是一個(gè)普通的靜態(tài)Mesh了。Skin數(shù)據(jù)決定頂點(diǎn)如何綁定到骨骼上。頂點(diǎn)的Skin數(shù)據(jù)包括頂點(diǎn)受哪些骨骼影響以及這些骨骼影響該頂點(diǎn)時(shí)的權(quán)重(weight),另外對(duì)于每塊骨骼還需要骨骼偏移矩陣(BoneOffsetMatrix)用來(lái)將頂點(diǎn)從Mesh空間變換到骨骼空間。在本文中,提到骨骼動(dòng)畫中的Mesh特指這個(gè)皮膚Mesh,提到模型是指骨骼動(dòng)畫模型整體。骨骼控制蒙皮運(yùn)動(dòng),而骨骼本身的運(yùn)動(dòng)呢?當(dāng)然是動(dòng)畫數(shù)據(jù)了。每個(gè)關(guān)鍵幀中包含時(shí)間和骨骼運(yùn)動(dòng)信息,運(yùn)動(dòng)信息可以用一個(gè)矩陣直接表示骨骼新的變換,也可用四元數(shù)表示骨骼的旋轉(zhuǎn),也可以隨便自己定義什么只要能讓骨骼動(dòng)就行。除了使用編輯設(shè)定好的動(dòng)畫幀數(shù)據(jù),也可以使用物理計(jì)算對(duì)骨骼進(jìn)行實(shí)時(shí)控制。
下面分別具體分析骨骼蒙皮動(dòng)畫中的結(jié)構(gòu)部件。
1)理解骨骼和骨骼層次結(jié)構(gòu)(Bone Hierarchy)
首先要明確一個(gè)觀念:骨骼決定了模型整體在世界坐標(biāo)系中的位置和朝向。
先看看靜態(tài)模型吧,靜態(tài)模型沒(méi)有骨骼,我們?cè)谑澜缱鴺?biāo)系中放置靜態(tài)模型時(shí),只要指定模型自身坐標(biāo)系在世界坐標(biāo)系中的位置和朝向。在骨骼動(dòng)畫中,不是把Mesh直接放到世界坐標(biāo)系中,Mesh只是作為Skin使用的,是依附于骨骼的,真正決定模型在世界坐標(biāo)系中的位置和朝向的是骨骼。在渲染靜態(tài)模型時(shí),由于模型的頂點(diǎn)都是定義在模型坐標(biāo)系中的,所以各頂點(diǎn)只要經(jīng)過(guò)模型坐標(biāo)系到世界坐標(biāo)系的變換后就可進(jìn)行渲染。而對(duì)于骨骼動(dòng)畫,我們?cè)O(shè)置模型的位置和朝向,實(shí)際是在設(shè)置根骨骼的位置和朝向,然后根據(jù)骨骼層次結(jié)構(gòu)中父子骨骼之間的變換關(guān)系計(jì)算出各個(gè)骨骼的位置和朝向,然后根據(jù)骨骼對(duì)Mesh中頂點(diǎn)的綁定計(jì)算出頂點(diǎn)在世界坐標(biāo)系中的坐標(biāo),從而對(duì)頂點(diǎn)進(jìn)行渲染。要記住,在骨骼動(dòng)畫中,骨骼才是模型主體,Mesh不過(guò)是一層皮,一件衣服。
如何理解骨骼?請(qǐng)看第二個(gè)觀念:骨骼可理解為一個(gè)坐標(biāo)空間。
在一些文章中往往會(huì)提到關(guān)節(jié)和骨骼,那么關(guān)節(jié)是什么?骨骼又是什么?下圖是一個(gè)手臂的骨骼層次的示例。

骨骼只是一個(gè)形象的說(shuō)法,實(shí)際上骨骼可理解為一個(gè)坐標(biāo)空間,關(guān)節(jié)可理解為骨骼坐標(biāo)空間的原點(diǎn)。關(guān)節(jié)的位置由它在父骨骼坐標(biāo)空間中的位置描述。上圖中有三塊骨骼,分別是上臂,前臂和兩個(gè)手指。Clavicle(鎖骨)是一個(gè)關(guān)節(jié),它是上臂的原點(diǎn),同樣肘關(guān)節(jié)(elbow joint)是前臂的原點(diǎn),腕關(guān)節(jié)(wrist)是手指骨骼的原點(diǎn)。關(guān)節(jié)既決定了骨骼空間的位置,又是骨骼空間的旋轉(zhuǎn)和縮放中心。為什么用一個(gè)4X4矩陣就可以表達(dá)一個(gè)骨骼,因?yàn)?/span>4X4矩陣中含有的平移分量決定了關(guān)節(jié)的位置,旋轉(zhuǎn)和縮放分量決定了骨骼空間的旋轉(zhuǎn)和縮放。我們來(lái)看前臂這個(gè)骨骼,其原點(diǎn)位置是位于上臂上某處的,對(duì)于上臂來(lái)說(shuō),它知道自己的坐標(biāo)空間某處(即肘關(guān)節(jié)所在的位置)有一個(gè)子空間,那就是前臂,至于前臂里面是啥就不考慮了。當(dāng)前臂繞肘關(guān)節(jié)旋轉(zhuǎn)時(shí),實(shí)際是前臂坐標(biāo)空間在旋轉(zhuǎn),從而其中包含的子空間也在繞肘關(guān)節(jié)旋轉(zhuǎn),在這個(gè)例子中是finger骨骼。和實(shí)際生物骨骼不同的是,我們這里的骨骼并沒(méi)有實(shí)質(zhì)的骨頭,所以前臂旋轉(zhuǎn)時(shí),他自己沒(méi)啥可轉(zhuǎn)的,改變的只是坐標(biāo)空間的朝向。你可以說(shuō)上圖的藍(lán)線在轉(zhuǎn),但實(shí)際藍(lán)線并不存在,藍(lán)線只是畫上去表示骨骼之間關(guān)系的,真正轉(zhuǎn)的是骨骼空間,我們能看到在轉(zhuǎn)的是wrist joint,也就是兩個(gè)finger骨骼的坐標(biāo)空間,因?yàn)樗麄兪亲涌臻g,會(huì)跟隨父空間運(yùn)動(dòng),就好比人跟著地球轉(zhuǎn)一樣。
骨骼就是坐標(biāo)空間,骨骼層次就是嵌套的坐標(biāo)空間。關(guān)節(jié)只是描述骨骼的位置即骨骼自己的坐標(biāo)空間原點(diǎn)在其父空間中的位置,繞關(guān)節(jié)旋轉(zhuǎn)是指骨骼坐標(biāo)空間(包括所有子空間)自身的旋轉(zhuǎn),如此理解足矣。但還有兩個(gè)可能的疑問(wèn),一是骨骼的長(zhǎng)度問(wèn)題,由于骨骼是坐標(biāo)空間,沒(méi)有所謂的長(zhǎng)度和寬度的限制,我們看到的長(zhǎng)度一方面是蒙皮后的結(jié)果,另一方面子骨骼的原點(diǎn)(也就是關(guān)節(jié))的位置往往決定了視覺(jué)上父骨骼的長(zhǎng)度,比如這里upper arm線段的長(zhǎng)度實(shí)際是由elbow joint的位置決定的。第二個(gè)問(wèn)題,手指的那個(gè)端點(diǎn)是啥啊?實(shí)際上在我們的例子中手指沒(méi)有子骨骼,所以那個(gè)端點(diǎn)并不存在:)那是為了方便演示畫上去的。實(shí)際問(wèn)題中總有最下層的骨骼,他們不能決定其他骨骼了,他們的作用只剩下控制Mesh頂點(diǎn)。對(duì)了,那么手指的長(zhǎng)度如何確定?我們看到的長(zhǎng)度應(yīng)該是由蒙皮決定的,也就是由Mesh中屬于手指的那些點(diǎn)離腕關(guān)節(jié)的距離決定。
經(jīng)過(guò)一段長(zhǎng)篇大論,我們終于清楚骨骼和骨骼層次是啥了,但是為什么要將骨骼組織成層次結(jié)構(gòu)呢?答案是為了做動(dòng)畫方便,設(shè)想如果只有一塊骨骼,那么讓他動(dòng)起來(lái)就太簡(jiǎn)單了,動(dòng)畫每一幀直接指定他的位置即可。如果是n塊呢?通過(guò)組成一個(gè)層次結(jié)構(gòu),就可以通過(guò)父骨骼控制子骨骼的運(yùn)動(dòng),牽一發(fā)而動(dòng)全身,改變某骨骼時(shí)并不需要設(shè)置其下子骨骼的位置,子骨骼的位置會(huì)通過(guò)計(jì)算自動(dòng)得到。上文已經(jīng)說(shuō)過(guò),父子骨骼之間的關(guān)系可以理解為,子骨骼位于父骨骼的坐標(biāo)系中。我們知道物體在坐標(biāo)系中可以做平移變換,以及自身的旋轉(zhuǎn)和縮放變換。子骨骼在父骨骼的坐標(biāo)系中也可以做這些變換來(lái)改變自己在其父骨骼坐標(biāo)系中的位置和朝向等。那么如何表示呢?由于4X4矩陣可以同時(shí)表示上述三種變換,所以一般描述骨骼在其父骨骼坐標(biāo)系中的變換時(shí)使用一個(gè)矩陣,也就是DirectX SkinnedMesh中的FrameTransformMatrix。實(shí)際上這不是唯一的方法,但應(yīng)該是公認(rèn)的方法,因?yàn)榫仃嚥还饪梢酝瑫r(shí)表示多種變換還可以方便的通過(guò)連乘進(jìn)行變換的組合,這在層次結(jié)構(gòu)中非常方便。在本文的例子-最簡(jiǎn)單的skinned mesh實(shí)例中,我只演示了平移變換,所以只用一個(gè)3d坐標(biāo)就可以表示子骨骼在父骨骼中的位置。下面是Bone Class最初的定義:
class Bone
{
public:
float m_x, m_y, m_z;//這個(gè)坐標(biāo)是定義在父骨骼坐標(biāo)系中的
};
OK,除了使用矩陣,坐標(biāo)或某東西描述子骨骼的位置,我們的Bone Class定義中還需要一些指針來(lái)建立層次結(jié)構(gòu),也就是說(shuō)我們要能通過(guò)父骨骼找到子骨骼或反之。問(wèn)題是我們需要什么指針呢?從父指向子還是反之?結(jié)論是看你需要怎么用了。如果使用矩陣,需要將父子骨骼矩陣級(jí)聯(lián)相乘,無(wú)論你的矩陣是左乘列向量還是右乘行向量,從哪邊開(kāi)始乘不重要,只要乘法中父子矩陣的左右位置正確,所以可以在骨骼中只存放指向父的指針,從子到父每次得到父矩陣循環(huán)相乘。也可以像DX中那樣從根開(kāi)始相乘并遞歸。在文本的DEMO中由于沒(méi)用矩陣,直接使用坐標(biāo)相加計(jì)算坐標(biāo),所以要指定父的位置,然后計(jì)算出子的位置,那么需要在Bone Class中加入子骨骼的指針,因?yàn)樽庸趋烙?/span>n個(gè),所以需要n個(gè)指針嗎?不一定,看看DirectX的做法,只需要兩個(gè)就搞定了,指向第一子的和指向兄弟骨骼的。這樣事先就不需要知道有多少子了。下面是修改后的Bone Class:
class Bone
{
Bone* m_pSibling;
Bone* m_pFirstChild;
float m_x, m_y, m_z;//pos in its parent's space
float m_wx, m_wy, m_wz; //pos in world space
};
同時(shí)增加了一組坐標(biāo),存放計(jì)算好的世界坐標(biāo)系坐標(biāo)。
將各個(gè)骨骼相對(duì)于其父骨骼擺放好,就行成了一個(gè)骨骼層次結(jié)構(gòu)的初始位置,所謂初始是指定義骨骼層次時(shí),那后來(lái)呢?后來(lái)動(dòng)畫改變了骨骼的相對(duì)位置,準(zhǔn)確的說(shuō)一般是改變了骨骼自身的旋轉(zhuǎn)而位置保持不變(特殊情況總是存在,比如雷曼,可以把拳頭扔出去的那個(gè)家伙),總之骨骼動(dòng)了,位置變化了。初始位置很重要,因?yàn)橥ㄟ^(guò)初始位置骨骼層次間的變換,我們確定了骨骼之間的關(guān)系,然后在動(dòng)畫中你可以只用旋轉(zhuǎn)。
假設(shè)我們通過(guò)某種方法建立了骨骼層次結(jié)構(gòu),那么每一塊骨骼的位置都依賴于其父骨骼的位置,而根骨骼沒(méi)有父,他的位置就是整個(gè)骨骼體系在世界坐標(biāo)系中的位置。可以認(rèn)為root的父就是世界坐標(biāo)系。但是初始位置時(shí),根骨骼一般不是在世界原點(diǎn)的,比如使用3d max character studio創(chuàng)建的biped骨架時(shí),一般兩腳之間是世界原點(diǎn),而根骨骼-骨盆位于原點(diǎn)上方(+z軸上)。這有什么關(guān)系呢?其實(shí)也沒(méi)什么大不了的,只是我們?cè)谥付ü趋绖?dòng)畫模型整體坐標(biāo)時(shí),比如設(shè)定坐標(biāo)為(0,0,0),則根骨骼-骨盆被置于世界原點(diǎn),假如xy平面是地面,那么人下半個(gè)身子到地面下了。我們想讓兩腳之間算作人的原點(diǎn),這樣設(shè)定(0,0,0)的坐標(biāo)時(shí)人就站在地面上了,所以可以在兩腳之間設(shè)定一個(gè)額外的根骨骼放在世界原點(diǎn)上,或者這個(gè)骨骼并不需要真實(shí)存在,只是在你的骨骼模型結(jié)構(gòu)中保存骨盆骨骼到世界原點(diǎn)的變換矩陣。在微軟X文件中,一般有一個(gè)Scene_Root節(jié)點(diǎn),這算一個(gè)額外的骨骼吧,他的變換矩陣為單位陣,表示他初始位于世界原點(diǎn),而真正骨骼的根Bip01,作為Scene_root的子骨骼,其變換矩陣表示相對(duì)于root的位置。說(shuō)這么多其實(shí)我只是想解釋下,為什么要存在Scene_Root這種額外的骨骼,以及加深理解骨骼定位骨骼動(dòng)畫模型整體的世界坐標(biāo)的作用。
有了骨骼類,現(xiàn)在讓我們看一下建立骨骼層次的代碼,在bone class中增加一個(gè)構(gòu)造函數(shù)和兩個(gè)成員函數(shù):
class Bone
{
public:
Bone(float x, float y, float z)
:m_pSibling(NULL),m_pFirstChild(NULL),m_pFather(NULL),
m_x(x),m_y(y),m_z(z){}
void SetFirstChild(Bone* pChild)
{
m_pFirstChild = pChild; m_pFirstChild->m_pFather = this;
}
void SetSibling(Bone* pSibling)
{
m_pSibling = pSibling; m_pSibling->m_pFather = m_pFather;
}
};
注意我增加了一個(gè)成員變量,Bone* m_pFather,這是指向父骨骼的指針,在這個(gè)例子中計(jì)算骨骼動(dòng)畫時(shí)本不需要這個(gè)指針,但我為了畫一條從父骨骼關(guān)節(jié)到子骨骼關(guān)節(jié)的連線,增加了它,因?yàn)槊總€(gè)骨骼只有第一子骨骼的指針,繪制父骨骼時(shí)從父到子畫線就只能畫一條,所以記錄每個(gè)骨骼的父,在繪制子骨骼時(shí)畫這根線。
有了這個(gè)函數(shù),就可以創(chuàng)建骨骼層次了,例如:
Bone* g_boneRoot;
Bone* g_bone1, *g_bone21, *g_bone22;
void buildBones()
{
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);
}
接下來(lái)是骨骼層次中最核心的部分,更新骨骼!由于動(dòng)畫的作用,某個(gè)骨骼的變換(TransformMatrix)變了,這時(shí)就要根據(jù)新的變換來(lái)計(jì)算,所以這個(gè)過(guò)程一般稱作UpdateBoneMatrix。因?yàn)楣趋赖淖儞Q都是相對(duì)父的,要變換頂點(diǎn)必須使用世界變換矩陣,所以這個(gè)過(guò)程是根據(jù)更新了的某些骨骼的骨骼變換矩陣(TransformMatrix)計(jì)算出所有骨骼的世界變換矩陣(也即CombinedMatrix)。在本文的例子中,骨骼只能平移,甚至我們沒(méi)有用矩陣,所以當(dāng)有骨骼變動(dòng)時(shí)要做的只是直接計(jì)算骨骼的世界坐標(biāo),因此函數(shù)命名為ComputeWorldPos,相當(dāng)于UpdateBoneMatrix后再用頂點(diǎn)乘以CombinedMatrix。
class Bone
{
//give father's world pos, compute the bone's world pos
void ComputeWorldPos(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);
}
};
其中的遞歸調(diào)用使用了微軟例子的思想。
有了上述函數(shù),當(dāng)某骨骼運(yùn)動(dòng)時(shí)就可以讓其子骨骼跟隨運(yùn)動(dòng)了,但是怎么讓骨骼運(yùn)動(dòng)呢?這就是動(dòng)畫問(wèn)題了。我不打算在這個(gè)簡(jiǎn)單的例子中使用關(guān)鍵幀動(dòng)畫,而只是通過(guò)程序每幀改變某些骨骼的位置,DEMO中animateBones就是做這個(gè)的,你可以在里面改變不同的骨骼看看效果。在本文下面會(huì)對(duì)骨骼的關(guān)鍵幀動(dòng)畫做簡(jiǎn)單的討論。
至此,我們定義了骨骼類的結(jié)構(gòu),手工創(chuàng)建了骨骼層次(實(shí)際引擎應(yīng)該從文件讀入),并且可以根據(jù)新位置更新骨骼了(實(shí)際引擎應(yīng)該從動(dòng)畫數(shù)據(jù)讀入新的變換或使用物理計(jì)算),這樣假如我們用連線將骨骼畫出來(lái),并且讓某個(gè)骨骼動(dòng)起來(lái),我們就會(huì)看見(jiàn)他下面的子骨骼跟著動(dòng)了。當(dāng)然只有骨骼是不夠的,我們要讓Mesh跟隨骨骼運(yùn)動(dòng),下面就是蒙皮了。
2)蒙皮信息和蒙皮過(guò)程
2-1)Skin info的定義
上文曾討論過(guò),Skinned Mesh中Mesh是作為皮膚使用,蒙在骨骼之上的。為了讓普通的Mesh具有蒙皮的功能,必須添加蒙皮信息,即Skin info。我們知道Mesh是由頂點(diǎn)構(gòu)成的,建模時(shí)頂點(diǎn)是定義在模型自身坐標(biāo)系的,即相對(duì)于Mesh原點(diǎn)的,而骨骼動(dòng)畫中決定模型頂點(diǎn)最終世界坐標(biāo)的是骨骼,所以要讓骨骼決定頂點(diǎn)的世界坐標(biāo),這就要將頂點(diǎn)和骨骼聯(lián)系起來(lái),Skin info正是起了這個(gè)作用。下面是DEMO中頂點(diǎn)類的定義的代碼片段:
#define MAX_BONE_PER_VERTEX 4
class Vertex
{
float m_x, m_y, m_z; //local pos in mesh space
float m_wX, m_wY, m_wZ;//blended vertex pos, in world space
//skin info
int m_boneNum;
Bone* m_bones[MAX_BONE_PER_VERTEX];
float m_boneWeights[MAX_BONE_PER_VERTEX];
};
頂點(diǎn)的Skin info包含影響該頂點(diǎn)的骨骼數(shù)目,指向這些骨骼的指針,這些骨骼作用于該頂點(diǎn)的權(quán)重(Skin weight)。由于只是一個(gè)簡(jiǎn)單的例子,這兒沒(méi)有考慮優(yōu)化,所以用靜態(tài)數(shù)組存放骨骼指針和權(quán)重,且實(shí)際引擎中Skin info的定義方式不一定是這樣的,但基本原理一致。
MAX_BONE_PER_VERTEX在這兒用來(lái)設(shè)置可同時(shí)影響頂點(diǎn)的最大骨骼數(shù),實(shí)際上由于這個(gè)DEMO是手工進(jìn)行Vertex Blending并且也沒(méi)用硬件加速,可影響頂點(diǎn)的骨骼數(shù)量并沒(méi)有限制,只是恰好需要一個(gè)常量來(lái)定義數(shù)組,所以定義了一下。在實(shí)際引擎中由于要使用硬件加速,以及為了確保速度,一般會(huì)定義最大骨骼數(shù)。另外在本DEMO中,Skin info是手工設(shè)定的,而在實(shí)際項(xiàng)目中,一般是在建模軟件中生成這些信息并導(dǎo)出。
Skin info的作用是使用各個(gè)骨骼的變換矩陣對(duì)頂點(diǎn)進(jìn)行變換并乘以權(quán)重,這樣某塊骨骼只能對(duì)該頂點(diǎn)產(chǎn)生部分影響。各骨骼權(quán)重之和應(yīng)該為1。
Skin info是針對(duì)頂點(diǎn)的,然而在使用Skin info前我們必須要使用Bone Offset Matrix對(duì)頂點(diǎn)進(jìn)行變換,下面具體討論Bone offset Matrix。(寫下這句話的時(shí)候我感覺(jué)有些不妥,因?yàn)閷?shí)際是先將所有的矩陣相乘最后再作用于頂點(diǎn),這兒是按照理論上的順序進(jìn)行講述吧,請(qǐng)不要與實(shí)際情況混淆,其實(shí)他們也并不矛盾。而且在我們的DEMO中由于沒(méi)有使用矩陣,所以變換的順序和理論順序是一致的)
2-2)Bone Offset Matrix的含義和計(jì)算方法
上文已經(jīng)說(shuō)過(guò):“骨骼動(dòng)畫中決定模型頂點(diǎn)最終世界坐標(biāo)的是骨骼,所以要讓骨骼決定頂點(diǎn)的世界坐標(biāo)”,現(xiàn)在讓我們看下頂點(diǎn)受一塊骨骼的作用時(shí)的坐標(biāo)變換過(guò)程:
mesh vertex (defined in mesh space)---<BoneOffsetMatrix>--->Bone space
---<BoneCombinedTransformMatrix>--->World
從這個(gè)過(guò)程中可看出,需要首先將模型頂點(diǎn)從模型空間變換到某塊骨骼自身的骨骼空間,然后才能利用骨骼的世界變換計(jì)算頂點(diǎn)的世界坐標(biāo)。Bone Offset Matrix的作用正是將模型從頂點(diǎn)空間變換到骨骼空間。那么Bone Offset Matrix如何得到呢?下面具體分析:
Mesh space是建模時(shí)使用的空間,mesh中頂點(diǎn)的位置相對(duì)于這個(gè)空間的原點(diǎn)定義。比如在3d max中建模時(shí)(視xy平面為地面,+z朝上),可將模型兩腳之間的中點(diǎn)作為Mesh空間的原點(diǎn),并將其放置在世界原點(diǎn),這樣左腳上某一頂點(diǎn)坐標(biāo)是(10,10,2),右腳上對(duì)稱的一點(diǎn)坐標(biāo)是(-10,10,2),頭頂上某一頂點(diǎn)的坐標(biāo)是(0,0,170)。由于此時(shí)Mesh空間和世界空間重合,上述坐標(biāo)既在Mesh空間也在世界空間,換句話說(shuō),此時(shí)實(shí)際是以世界空間作為Mesh空間了。在骨骼動(dòng)畫中,在世界中放置的是骨骼而不是Mesh,所以這個(gè)區(qū)別并不重要。在3d max中添加骨骼的時(shí)候,也是將骨骼放入世界空間中,并調(diào)整骨骼的相對(duì)位置使得和mesh相吻合(即設(shè)置骨骼的TransformMatrix),得到骨架的初始姿勢(shì)以及相應(yīng)的Transform Matrix(按慣例模型做成兩臂側(cè)平舉直立,骨骼也要適合這個(gè)姿態(tài))。由于骨骼的Transform Matrix(作用是將頂點(diǎn)從骨骼空間變換到上層空間)是基于其父骨骼空間的,只有根骨骼的Transform是基于世界空間的,所以要通過(guò)自下而上一層層Transform變換(如果使用行向量右乘矩陣,這個(gè)Transform的累積過(guò)程就是C=Mbone*Mfather*Mgrandpar*...*Mroot),得到該骨骼在世界空間上的變換矩陣 - Combined Transform Matrix,即通過(guò)這個(gè)矩陣可將頂點(diǎn)從骨骼空間變換到世界空間。那么這個(gè)矩陣的逆矩陣就可以將世界空間中的頂點(diǎn)變換到某塊骨骼的骨骼空間。由于Mesh實(shí)際上就是定義在世界空間了,所以這個(gè)逆矩陣就是Offset Matrix。即OffsetMatrix就是骨骼在初始位置(沒(méi)有經(jīng)過(guò)任何動(dòng)畫改變)時(shí)將bone變換到世界空間的矩陣(CombinedTransformMatrix)的逆矩陣,有一些資料稱之為InverseMatrix。在幾何流水線中,是通過(guò)變換矩陣將頂點(diǎn)變換到上層空間,最終得到世界坐標(biāo),逆矩陣則做相反的事,所以Inverse這種提法也符合慣例。那么Offset這種提法從字面上怎么理解呢?Offset即骨骼相對(duì)于世界原點(diǎn)的偏移,世界原點(diǎn)加上這個(gè)偏移就變成骨骼空間的原點(diǎn),同樣定義在世界空間中的點(diǎn)經(jīng)過(guò)這個(gè)偏移矩陣的作用也被變換到骨骼空間了。從另一角度理解,在動(dòng)畫中模型中頂點(diǎn)的位置是根據(jù)骨骼位置動(dòng)態(tài)計(jì)算的,也就是說(shuō)頂點(diǎn)跟著骨骼動(dòng),但首先必須確定頂點(diǎn)和骨骼之間的相對(duì)位置(即頂點(diǎn)在該骨骼坐標(biāo)系中的位置),一個(gè)骨骼可能對(duì)應(yīng)很多頂點(diǎn),如果要保存這個(gè)相對(duì)位置每個(gè)頂點(diǎn)對(duì)于每塊受控制的骨骼都要保存,這樣就要保存太多的矩陣了。。。所以只保存mesh空間到骨骼空間的變換(即OffsetMatrix),然后通過(guò)這個(gè)變換計(jì)算每個(gè)頂點(diǎn)在該骨骼空間中的坐標(biāo),所以OffsetMatrix也反應(yīng)了mesh和每塊骨骼的相對(duì)位置,只是這個(gè)位置是間接的通過(guò)和世界坐標(biāo)空間的關(guān)系表達(dá)的,在初始位置將骨骼按照模型的形狀擺好是關(guān)鍵之處。
以上的分析是通過(guò)將mesh space和world space重合得到Offset Matrix的計(jì)算方法。那么如果他們不重合呢?那就要先計(jì)算頂點(diǎn)從mesh space變換到world space的變換矩陣,并乘上(還是右乘為例)Combined Matrix的Inverse Matrix從而得到Offset Matrix。但是這不是找麻煩嗎?因?yàn)?/span>Mesh的原點(diǎn)在哪兒并不重要,為啥不讓他們重合呢?
還有一個(gè)問(wèn)題是,既然Offset Matrix可以計(jì)算出來(lái),為啥還要在骨骼動(dòng)畫文件中同時(shí)提供TransformMatrix和OffsetMatrix呢?實(shí)際上文件中確實(shí)可以不提供OffsetMatrix,而只在載入時(shí)計(jì)算。但TransformMatrix不可缺少,動(dòng)畫關(guān)鍵幀數(shù)據(jù)一般只存儲(chǔ)骨骼的旋轉(zhuǎn)和根骨骼的位置,骨骼間的相對(duì)位置還是要靠TransformMatrix提供。在微軟的X文件結(jié)構(gòu)中提供了OffsetMatrix,原因是什么呢?我不知道。我猜想一個(gè)可能的原因是為了兼容性和靈活性,比如mesh并沒(méi)有定義在世界坐標(biāo)系,而是作為一個(gè)object放置在3d max中,在導(dǎo)出骨骼動(dòng)畫時(shí)不能簡(jiǎn)單的認(rèn)為mesh的頂點(diǎn)坐標(biāo)是相對(duì)于世界原點(diǎn)的,還要把這個(gè)object的位置考慮進(jìn)去,于是導(dǎo)出插件要計(jì)算出OffsetMatrix并保存在x文件中以避免兼容性問(wèn)題。
關(guān)于OffsetMatrix和TransformMatrix含有平移,旋轉(zhuǎn)和縮放的討論:
首先,OffsetMatrix取決于骨骼的初始位置(即TransformMatrix),由于骨骼動(dòng)畫中我們使用的是動(dòng)畫中的位置,初始位置是什么樣并不重要,所以可以在初始位置中只包含平移,而旋轉(zhuǎn)和縮放在動(dòng)畫中設(shè)置(一般也僅僅使用旋轉(zhuǎn),這也是為啥動(dòng)畫通常中可以用一個(gè)四元數(shù)表示骨骼的關(guān)鍵幀)。在這種情況下,OffsetMatrix只包含平移即可。因此一些引擎的Bone中不存放Transform矩陣,而只存放骨骼在父骨骼空間中的坐標(biāo),然后旋轉(zhuǎn)只在動(dòng)畫幀中設(shè)置,最基本的骨骼動(dòng)畫即可實(shí)現(xiàn)。但也可在Transform和Offset Matrix中包括旋轉(zhuǎn)和縮放,這樣可以提高創(chuàng)建動(dòng)畫時(shí)的容錯(cuò)性。
在本文DEMO中,我們也沒(méi)有使用矩陣保存Bone Offset,而只用了一個(gè)坐標(biāo)保存偏移位置。
class BoneOffset
{
public:
float m_offx, m_offy, m_offz;
};
在Bone class中,有一個(gè)方法用來(lái)計(jì)算Bone Offset
class Bone
{
public:
BoneOffset m_boneOffset;
//called after ComputeWorldPos() when bone loaded but not animated
void ComputeBoneOffset()
{
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()中,使用計(jì)算好的骨骼的世界坐標(biāo)來(lái)計(jì)算bone offset,這兒的計(jì)算只是取一個(gè)負(fù)數(shù),在實(shí)際引擎中,如果bone offset是一個(gè)矩陣,這兒就應(yīng)該是求逆矩陣,其實(shí)由于旋轉(zhuǎn)矩陣是正交的,只要求出旋轉(zhuǎn)矩陣的轉(zhuǎn)置矩陣,并將平移部分取反即可,本文不做討論了。注意由于我們計(jì)算Bone offset時(shí)是使用計(jì)算好的世界坐標(biāo),所以在這之前必須在初始位置時(shí)對(duì)根骨骼調(diào)用ComputeWorldPos()以計(jì)算出各個(gè)骨骼在初始位置時(shí)的世界坐標(biāo)。
2-3)最終:頂點(diǎn)混合(vertex blending)
現(xiàn)在我們有了Skin info,有了Bone offset,可謂萬(wàn)事具備,只欠東風(fēng)了。現(xiàn)在就可以做頂點(diǎn)混合了,這是骨骼動(dòng)畫的精髓所在,正是這個(gè)技術(shù)消除了關(guān)節(jié)處的裂縫。頂點(diǎn)混合后得到了頂點(diǎn)新的世界坐標(biāo),對(duì)所有的頂點(diǎn)執(zhí)行vertex blending后,從Mesh的角度看,Mesh deform(變形)了,變成動(dòng)畫需要的形狀了。
首先,讓我們看看使用單塊骨骼對(duì)頂點(diǎn)進(jìn)行作用的過(guò)程,以下是DEMO中的相關(guān)代碼:
class Vertex
{
public:
void ComputeWorldPosByBone(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;
}
};
這個(gè)函數(shù)使用一塊骨骼對(duì)頂點(diǎn)進(jìn)行變換,將頂點(diǎn)從Mesh坐標(biāo)系變換到世界坐標(biāo)系,這兒使用了骨骼的Bone Offset Matrix和 Combined Transform Matrix (嗯,我知道這兒沒(méi)用矩陣,但意思是一樣的對(duì)嗎)
對(duì)于多塊骨骼,對(duì)每塊骨骼執(zhí)行這個(gè)過(guò)程并將結(jié)果根據(jù)權(quán)重混合(即vertex blending)就得到頂點(diǎn)最終的世界坐標(biāo)。進(jìn)行vertex blending的代碼如下:
class Vertex
{
void BlendVertex()
{//do the vertex blending,get the vertex's pos in world space
m_wX = 0;
m_wY = 0;
m_wZ = 0;
for(int i=0; i<m_boneNum; ++i)
{
float tx, 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;
}
}
};
這些函數(shù)我都放在Vertex類中了,因?yàn)橹皇且粋€(gè)簡(jiǎn)單DEMO所以沒(méi)有特別考慮引擎結(jié)構(gòu)問(wèn)題,在BlendVertex()中,遍歷影響該頂點(diǎn)的所有骨骼,用每塊骨骼計(jì)算出頂點(diǎn)的世界坐標(biāo),然后使用Skin Weight對(duì)這些坐標(biāo)進(jìn)行加權(quán)平均。tx,ty,tz是某塊骨骼作用后頂點(diǎn)的世界坐標(biāo)乘以權(quán)重后的值,這些值相加后就是最終的世界坐標(biāo)了。
現(xiàn)在讓我們用一個(gè)公式回顧一下Vertex blending的整個(gè)過(guò)程(使用矩陣變換)
Vworld = Vmesh * BoneOffsetMatrix1 * CombindMatrix1 * Weight1
+ Vmesh* BoneOffsetMatrix2 * CombinedMatrix2 * Weight2
+ …
+ Vmesh * BoneOffsetMatrixN * CombindMatrixN * WeightN
(這個(gè)公式使用的是行向量左乘矩陣)
由于BoneOffsetMatrix和Combined Matrix都是矩陣,可以先相乘這樣就減少很多計(jì)算了,在實(shí)際PC游戲中可以使用VS進(jìn)行硬件加速計(jì)算。
3)動(dòng)畫數(shù)據(jù)和播放動(dòng)畫
正如前面所說(shuō),本例子中并沒(méi)有使用動(dòng)畫數(shù)據(jù),但動(dòng)畫數(shù)據(jù)在骨骼動(dòng)畫中確實(shí)最重要的,因?yàn)槲覀兊淖罱K目的就是播放動(dòng)畫。所以作為DEMO的補(bǔ)充,這兒簡(jiǎn)要討論一下動(dòng)畫數(shù)據(jù)相關(guān)問(wèn)題。其實(shí)我覺(jué)得動(dòng)畫的處理在骨骼動(dòng)畫中是很靈活的,需要專門的一篇文章討論。
本文的最開(kāi)始說(shuō),3D模型動(dòng)畫的基本原理是讓模型中各頂點(diǎn)的位置隨時(shí)間變化。骨骼動(dòng)畫的情況是,骨骼的位置隨時(shí)間變化,頂點(diǎn)位置隨骨骼變化。所以動(dòng)畫數(shù)據(jù)中必然包含的是骨骼的運(yùn)動(dòng)信息。可以在動(dòng)畫幀中包含某時(shí)刻骨骼的Transform Matrix,但骨骼一般只是做旋轉(zhuǎn),所以也可以用一個(gè)四元數(shù)表示。但有時(shí)候骨骼層次整體會(huì)在動(dòng)畫中進(jìn)行平移,所以可能需要在動(dòng)畫幀中包含根骨骼的位置信息。播放動(dòng)畫時(shí),給出當(dāng)前播放的時(shí)間值,對(duì)于每塊需要?jiǎng)赢嫷墓趋溃鶕?jù)這個(gè)值找出該骨骼前后兩個(gè)關(guān)鍵幀,根據(jù)時(shí)間差進(jìn)行插值,對(duì)于四元數(shù)要使用四元數(shù)球面線性插值。然后將插值得到的四元數(shù)轉(zhuǎn)換成Transform Matrix,再調(diào)用UpdateBoneMatrix(其含義上文已介紹)更新計(jì)算整個(gè)骨骼層次的CombinedMatrix。
4)總結(jié)
從結(jié)構(gòu)上看,SkinnedMesh包括:動(dòng)畫數(shù)據(jù),骨骼數(shù)據(jù),包含Skin info的Mesh數(shù)據(jù),以及Bone Offset Matrix。
從過(guò)程上看,載入階段:載入并建立骨骼層次結(jié)構(gòu),計(jì)算或載入Bone Offset Matrix,載入Mesh數(shù)據(jù)和Skin info(如果是DX的Skinned Mesh這個(gè)過(guò)程更復(fù)雜,因?yàn)檫€涉及到Matrix Palette等)。運(yùn)行階段:根據(jù)時(shí)間從動(dòng)畫數(shù)據(jù)中獲取骨骼當(dāng)前時(shí)刻的Transform Matrix,調(diào)用UpdateBoneMatrix計(jì)算出各骨骼的CombinedMatrix,對(duì)于每個(gè)頂點(diǎn)根據(jù)Skin info進(jìn)行Vertex Blending計(jì)算出頂點(diǎn)的世界坐標(biāo),最終進(jìn)行模型的渲染。
三)關(guān)于本文的例子
這個(gè)例子做了盡可能的簡(jiǎn)化,只包含一個(gè)cpp文件,使用OpenGL和GLUT作為渲染器和框架,僅有400多行代碼。例子中手工創(chuàng)建了一個(gè)骨骼層次和Mesh,手工設(shè)置Skin info并自動(dòng)計(jì)算BoneOffset,使用程序控制骨骼平移演示了骨骼層次的運(yùn)動(dòng)和骨骼影響下Mesh頂點(diǎn)的運(yùn)動(dòng),例子中甚至沒(méi)有使用矩陣。本例子僅作理解骨骼動(dòng)畫之用。

截圖中綠色網(wǎng)格是模型原始形狀,藍(lán)色是骨骼,紅色是動(dòng)畫時(shí)的模型形狀。DEMO中左數(shù)第二個(gè)骨骼做上下運(yùn)動(dòng),最下方的骨骼做x方向平移。DEMO沒(méi)有使用旋轉(zhuǎn),而實(shí)際的骨骼動(dòng)畫中往往是沒(méi)有平移只有旋轉(zhuǎn)的,因?yàn)楦觳仓荒苻D(zhuǎn)不能變長(zhǎng),但原理一致。
代碼的執(zhí)行過(guò)程為,初始化時(shí):
buildBones();//創(chuàng)建骨骼層次
buildMesh(); //創(chuàng)建Mesh,設(shè)置Skin info,計(jì)算Bone offset
每幀運(yùn)行時(shí):
//draw original mesh
g_mesh->DrawStaticMesh(0,0,0);
//move bones
animateBones();
//update all bone's pos in bone tree
g_boneRoot->ComputeWorldPos(0, 0, 0);
//update vertex pos by bones, using vertex blending
g_mesh->UpdateVertices();
//draw deformed mesh
g_mesh->Draw();
//draw bone
g_boneRoot->Draw();
為確保本文的完整性,下面貼出所有代碼。
// A simplest Skinned Mesh demo, written by n5, 2008.10,
// My email:happyfirecn@yahoo.com.cn
// My blog: http://blog.csdn.net/n5
#include <GL/glut.h>
#define NULL 0
//-------------------------------------------------------------
class BoneOffset
{
public:
//BoneOffset transform a vertex from mesh space to bone space.
//In other words, it is the offset from mesh space to a bone's space.
//For each bone, there is a BoneOffest.
//If we add the offset to the vertex's pos (in mesh space), we get the vertex's pos in bone space
//For example: if a vertex's pos in mesh space is (100,0,0), the bone offset is (-20,0,0), so the vertex's pos in bone space is (80,0,0)
//Actually, BoneOffset is the invert transform of that we place a bone in mesh space, that is (-20,0,0) means the bone is at (20,0,0) in mesh space
float m_offx, m_offy, m_offz;
};
//----------------------------------------------------------------
class Bone
{
public:
Bone() {}
Bone(float x, float y, float z):m_pSibling(NULL),m_pFirstChild(NULL),m_pFather(NULL),m_x(x),m_y(y),m_z(z){}
~Bone() {}
Bone* m_pSibling;
Bone* m_pFirstChild;
Bone* m_pFather; //only for draw bone
void SetFirstChild(Bone* pChild) { m_pFirstChild = pChild; m_pFirstChild->m_pFather = this; }
void SetSibling(Bone* pSibling) { m_pSibling = pSibling; m_pSibling->m_pFather = m_pFather; }
float m_x, m_y, m_z;//pos in its parent's space
float m_wx, m_wy, m_wz; //pos in world space
//give father's world pos, compute the bone's world pos
void ComputeWorldPos(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);
}
BoneOffset m_boneOffset;
//called after compute world pos when bone loaded but not animated
void ComputeBoneOffset()
{
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();
}
void Draw()
{
glColor3f(0,0,1.0);
glPointSize(4);
glBegin(GL_POINTS);
glVertex3f(m_wx,m_wy,m_wz);
glEnd();
if(m_pFather!=NULL)
{
glBegin(GL_LINES);
glVertex3f(m_pFather->m_wx,m_pFather->m_wy,m_pFather->m_wz);
glVertex3f(m_wx,m_wy,m_wz);
glEnd();
}
if(m_pSibling!=NULL)
m_pSibling->Draw();
if(m_pFirstChild!=NULL)
m_pFirstChild->Draw();
}
};
//--------------------------------------------------------------
#define MAX_BONE_PER_VERTEX 4
class Vertex
{
public:
Vertex():m_boneNum(0)
{
}
void ComputeWorldPosByBone(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;
}
void BlendVertex()
{//do the vertex blending,get the vertex's pos in world space
m_wX = 0;
m_wY = 0;
m_wZ = 0;
for(int i=0; i<m_boneNum; ++i)
{
float tx, 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;
}
}
float m_x, m_y, m_z; //local pos in mesh space
float m_wX, m_wY, m_wZ;//blended vertex pos, in world space
//skin info
int m_boneNum;
Bone* m_bones[MAX_BONE_PER_VERTEX];
float m_boneWeights[MAX_BONE_PER_VERTEX];
void SetBoneAndWeight(int index, Bone* pBone, float weight)
{
m_bones[index] = pBone;
m_boneWeights[index] = weight;
}
};
//-----------------------------------------------------------
class SkinMesh
{
public:
SkinMesh():m_vertexNum(0){}
SkinMesh(int vertexNum):m_vertexNum(vertexNum)
{
m_vertexs = new Vertex[vertexNum];
}
~SkinMesh()
{
if(m_vertexNum>0)
delete[] m_vertexs;
}
void UpdateVertices()
{
for(int i=0; i<m_vertexNum; ++i)
{
m_vertexs[i].BlendVertex();
}
}
void DrawStaticMesh(float x, float y, float z)
{
glColor3f(0,1.0,0);
glPointSize(4);
glBegin(GL_POINTS);
for(int i=0; i<m_vertexNum; ++i)
glVertex3f(m_vertexs[i].m_x+x,m_vertexs[i].m_y+y,m_vertexs[i].m_z+z);
glEnd();
glBegin(GL_LINE_LOOP);
for(int i=0; i<m_vertexNum; ++i)
glVertex3f(m_vertexs[i].m_x+x,m_vertexs[i].m_y+y,m_vertexs[i].m_z+z);
glEnd();
}
void Draw()
{
glColor3f(1.0,0, 0);
glPointSize(4);
glBegin(GL_POINTS);
for(int i=0; i<m_vertexNum; ++i)
glVertex3f(m_vertexs[i].m_wX,m_vertexs[i].m_wY,m_vertexs[i].m_wZ);
glEnd();
glBegin(GL_LINE_LOOP);
for(int i=0; i<m_vertexNum; ++i)
glVertex3f(m_vertexs[i].m_wX,m_vertexs[i].m_wY,m_vertexs[i].m_wZ);
glEnd();
}
int m_vertexNum;
Vertex* m_vertexs; //array of vertices in mesh
};
//--------------------------------------------------------------
Bone* g_boneRoot;
Bone* g_bone1, *g_bone2, *g_bone31, *g_bone32;
void buildBones()
{
g_boneRoot = new Bone(0, 0, 0);
g_bone1 = new Bone(0.2, 0, 0);
g_bone2 = new Bone(0.2, 0, 0);
g_bone31 = new Bone(0.2, 0.1, 0);
g_bone32 = new Bone(0.2, -0.1, 0);
g_boneRoot->SetFirstChild(g_bone1);
g_bone1->SetFirstChild(g_bone2);
g_bone2->SetFirstChild(g_bone31);
g_bone31->SetSibling(g_bone32);
}
void deleteBones()
{
delete g_boneRoot;
delete g_bone1;
delete g_bone2;
delete g_bone31;
delete g_bone32;
}
void animateBones()
{
static int dir=-1, dir2=-1;
//animate bones manually
g_bone1->m_y +=0.00001f*dir;
if(g_bone1->m_y<-0.2 || g_bone1->m_y>0.2)
dir*=-1;
g_bone32->m_x +=0.00001f*dir2;
if(g_bone32->m_x<0 || g_bone32->m_x>0.2)
dir2*=-1;
}
SkinMesh* g_mesh;
void buildMesh()
{
float _meshData[]=
{//x,y,z
-0.1,0.05,0,
0.1,0.05,0,
0.3,0.05,0,
0.45,0.06,0,
0.6,0.15,0,
0.65,0.1,0,
0.5,0,0,
0.65,-0.1,0,
0.6,-0.15,0,
0.45,-0.06,0,
0.3,-0.05,0,
0.1,-0.05,0,
-0.1,-0.05,0,
};
float _skinInfo[]=
{//bone_num,bone id(0,1,2,31 or 32), bone weight 1~4,
1, 0, -1, -1, -1, 1.0, 0.0, 0.0, 0.0,
2, 0, 1, -1, -1, 0.5, 0.5, 0.0, 0.0,
2, 1, 2, -1, -1, 0.5, 0.5, 0.0, 0.0,
2, 2, 31, -1, -1, 0.3, 0.7, 0.0, 0.0,
2, 2, 31, -1, -1, 0.2, 0.8, 0.0, 0.0,
1, 31, -1, -1, -1, 1.0, 0.0, 0.0, 0.0,
2, 31, 32, -1, -1, 0.5, 0.5, 0.0, 0.0,
1, 32, -1, -1, -1, 1.0, 0.0, 0.0, 0.0,
2, 2, 32, -1, -1, 0.2, 0.8, 0.0, 0.0,
2, 2, 32, -1, -1, 0.3, 0.7, 0.0, 0.0,
2, 1, 2, -1, -1, 0.5, 0.5, 0.0, 0.0,
2, 0, 1, -1, -1, 0.5, 0.5, 0.0, 0.0,
1, 0, -1, -1, -1, 1.0, 0.0, 0.0, 0.0,
};
int vertexNum = sizeof(_meshData)/(sizeof(float)*3);
g_mesh = new SkinMesh(vertexNum);
for(int i=0; i<vertexNum; ++i)
{
g_mesh->m_vertexs[i].m_x = _meshData[i*3];
g_mesh->m_vertexs[i].m_y = _meshData[i*3+1];
g_mesh->m_vertexs[i].m_z = _meshData[i*3+2];
}
//set skin info
for(int i=0; i<vertexNum; ++i)
{
g_mesh->m_vertexs[i].m_boneNum = _skinInfo[i*9];
for(int j=0; j<g_mesh->m_vertexs[i].m_boneNum; ++j)
{
Bone* pBone = g_boneRoot;
if(_skinInfo[i*9+1+j]==1)
pBone = g_bone1;
else if(_skinInfo[i*9+1+j]==2)
pBone = g_bone2;
else if(_skinInfo[i*9+1+j]==31)
pBone = g_bone31;
else if(_skinInfo[i*9+1+j]==32)
pBone = g_bone32;
g_mesh->m_vertexs[i].SetBoneAndWeight(j, pBone, _skinInfo[i*9+5+j]);
}
}
//compute bone offset
g_boneRoot->ComputeWorldPos(0, 0, 0);
g_boneRoot->ComputeBoneOffset();
}
void deleteMesh()
{
delete g_mesh;
}
void myInit()
{
buildBones();
buildMesh();
}
void myQuit()
{
deleteBones();
deleteMesh();
}
void myReshape(int width, int height)
{
GLfloat h = (GLfloat) height / (GLfloat) width;
glViewport(0, 0, (GLint) width, (GLint) height);
glMatrixMode(GL_PROJECTION);
glLoadIdentity();
// glFrustum(-1.0, 1.0, -h, h, 5.0, 60.0);
glFrustum(-1.0, 1.0, -h, h, 1.0, 100.0);
glMatrixMode(GL_MODELVIEW);
glLoadIdentity();
glTranslatef(0.0, 0.0, -1.0);
}
void myDisplay(void)
{
glClear(GL_COLOR_BUFFER_BIT);
//draw original mesh
g_mesh->DrawStaticMesh(0,0,0);
//move bones
animateBones();
//update all bone's pos in bone tree
g_boneRoot->ComputeWorldPos(0, 0, 0);
//update vertex pos by bones, using vertex blending
g_mesh->UpdateVertices();
//draw deformed mesh
g_mesh->Draw();
//draw bone
g_boneRoot->Draw();
glFlush();
glutSwapBuffers();
}
void myIdle(void)
{
myDisplay();
}
int main(int argc, char *argv[])
{
glutInit(&argc, argv);
glutInitDisplayMode(GLUT_RGB | GLUT_DEPTH | GLUT_DOUBLE);
glutInitWindowPosition(100, 100);
glutInitWindowSize(640, 480);
glutCreateWindow("A simplest skinned mesh DEMO, by happyfirecn@yahoo.com.cn");
glutDisplayFunc(myDisplay);
glutReshapeFunc(myReshape);
glutIdleFunc(myIdle);
myInit();
glutMainLoop();
myQuit();
return 0;
}