• <ins id="pjuwb"></ins>
    <blockquote id="pjuwb"><pre id="pjuwb"></pre></blockquote>
    <noscript id="pjuwb"></noscript>
          <sup id="pjuwb"><pre id="pjuwb"></pre></sup>
            <dd id="pjuwb"></dd>
            <abbr id="pjuwb"></abbr>

            永遠(yuǎn)也不完美的程序

            不斷學(xué)習(xí),不斷實(shí)踐,不斷的重構(gòu)……

            常用鏈接

            統(tǒng)計(jì)

            積分與排名

            好友鏈接

            最新評(píng)論

            Skinned Mesh原理解析和一個(gè)最簡(jiǎn)單的實(shí)現(xiàn)示例(轉(zhuǎn))

             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ì)DX8DX9SkinnedMesh進(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 MatrixBone 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 MeshMesh是一個(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)為(000),則根骨骼-骨盆被置于世界原點(diǎn),假如xy平面是地面,那么人下半個(gè)身子到地面下了。我們想讓兩腳之間算作人的原點(diǎn),這樣設(shè)定(000)的坐標(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ò)程序每幀改變某些骨骼的位置,DEMOanimateBones就是做這個(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-1Skin info的定義

            上文曾討論過(guò),Skinned MeshMesh是作為皮膚使用,蒙在骨骼之上的。為了讓普通的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-2Bone 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)是(10102),右腳上對(duì)稱的一點(diǎn)坐標(biāo)是(-10102),頭頂上某一頂點(diǎn)的坐標(biāo)是(00170)。由于此時(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 spaceworld space重合得到Offset Matrix的計(jì)算方法。那么如果他們不重合呢?那就要先計(jì)算頂點(diǎn)從mesh space變換到world space的變換矩陣,并乘上(還是右乘為例)Combined MatrixInverse Matrix從而得到Offset Matrix。但是這不是找麻煩嗎?因?yàn)?/span>Mesh的原點(diǎn)在哪兒并不重要,為啥不讓他們重合呢?

            還有一個(gè)問(wèn)題是,既然Offset Matrix可以計(jì)算出來(lái),為啥還要在骨骼動(dòng)畫文件中同時(shí)提供TransformMatrixOffsetMatrix呢?實(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)于OffsetMatrixTransformMatrix含有平移,旋轉(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)。但也可在TransformOffset 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è)公式使用的是行向量左乘矩陣)

             

            由于BoneOffsetMatrixCombined 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 infoMesh數(shù)據(jù),以及Bone Offset Matrix

                   從過(guò)程上看,載入階段:載入并建立骨骼層次結(jié)構(gòu),計(jì)算或載入Bone Offset Matrix,載入Mesh數(shù)據(jù)和Skin info(如果是DXSkinned 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文件,使用OpenGLGLUT作為渲染器和框架,僅有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;

            }

            posted on 2009-09-04 18:04 狂爛球 閱讀(1337) 評(píng)論(0)  編輯 收藏 引用 所屬分類: 圖形編程

            99精品久久久久久久婷婷| 亚洲国产成人久久综合一区77| 亚洲AV乱码久久精品蜜桃| 国产精品18久久久久久vr| 国产精品九九久久精品女同亚洲欧美日韩综合区| 99久久精品国产麻豆| 伊人久久大香线蕉综合网站| 久久天天躁狠狠躁夜夜96流白浆| 久久免费小视频| 亚洲中文字幕无码久久综合网 | 久久婷婷激情综合色综合俺也去| 久久免费小视频| 久久久久久久久波多野高潮| 精品免费tv久久久久久久| 久久天天躁夜夜躁狠狠| 久久精品国内一区二区三区| 亚洲AV乱码久久精品蜜桃| 亚洲国产高清精品线久久| 国产精品99久久久久久宅男 | 亚洲国产天堂久久综合| 色综合久久最新中文字幕| 人妻无码中文久久久久专区| 久久国产V一级毛多内射| 精品一区二区久久久久久久网站| 无码人妻久久一区二区三区免费丨 | 91精品国产色综久久 | 99re久久精品国产首页2020| 精品久久久久久中文字幕大豆网| 久久久久成人精品无码| 国产精品99久久久久久猫咪| 99久久亚洲综合精品成人| 久久精品国产秦先生| 99久久www免费人成精品| 日本精品久久久中文字幕| 色综合久久中文综合网| 91久久精品国产91性色也| 国产精品久久久久久久午夜片| 一级做a爰片久久毛片人呢| 精品无码久久久久久国产| 日本精品久久久久影院日本| 亚洲国产成人久久一区久久|