一步一步的使用C++和OPENGL實現COLLADA骨骼動畫 第一部分

一步一步的使用C++和OPENGL實現COLLADA骨骼動畫

第一部分

 

英文原作者:waZim

原文標題:Step by Step Skeletal Animation in C++ and OpenGL, Using COLLADA

原文地址:http://www.wazim.com/Collada_Tutorial_1.htm

 

Sleepy譯

Sleepysoft#163.com

 

 

譯註:

這是一篇詳細介紹COLLADA文件(也就是DAE文件,3D模型文件的一種)格式的文章。之所以翻譯這篇文章的原因,一是這篇文章的確寫得很好很詳細,另一方面關於DAE文件格式的中文資料非常的少,每次看E文的也累,所以正好翻譯出來一了百了。

我是從看dancingwind(周煒)與AKER翻譯的NEHE Opengl教程開始學習Opengl的,對這些將外國的優秀文章和教程漢化的人,我向他們致以由衷的感謝,同時也以此譯文向他們致敬。

另外,本人E文水平有限,有些詞翻譯得不是很準(但我相信應該不會對閱讀的人造成誤導),如果發現錯誤和不完善的地方(估計會有很多),大家可以通過郵件與我交流,我會在第一時間更正錯誤。

謝謝。 

                                                                                              Sleepy

本次修改:2011-09-08

 

 

 

 

面向對象:初級到高級

 

介紹:HI,我是waZim,歡迎來到我的第一篇骨骼動畫的教程。這一系列教程由兩部分組成:

1.       瞭解如何讀取COLLADA文件(概括的介紹COLLADA文件)。

2.       用C++和OPENGL去真正實現第一部分所講的內容。

 

這兩篇文章還可以進一步的分成很多小的子章節,在我們講解的過程中會一一進行詳細解釋。

前言背景什麼的就不譯了,請參看原文
第一部份:

 

閱讀與理解COLLADA文件

       正如在前面的介紹部分裏所說,這篇教程分爲兩個部份。第一部分的一般性的講解並不考慮和涉及任何編程語言。但是如果你想直接跳到第二部分去看程序實現的話,你非常可能會感到完全無法理解從而無法繼續下去。所以強烈建議對於COLLADA文件一無所知的初學者來說,還是耐心看完第一部分的介紹再去看第二部分的實現。

       廢話不多說,讓我們開始吧。

 

COLLADA文件

       在我們準備開始深入挖掘COLLADA文件的意義之前,我希望你們先下載一個實例文件,這個文件我們將做爲此教程從頭到尾討論的對象(所以大家還是下載回來對照參看吧)。大家可以在COLLADA模型中心中找到它。它的名字叫“astroBoy_walk.dae”,如果你到處都找不到這個文件,那麼好吧,你可以到這篇教程所在網頁的“下載”部分找到它。(我怎麼找不到)

       就像我們之前所說的,COLLADA文件以XML的形式存儲。現在大家可以打開前面所說的示例文件看看,你可以用你最喜歡的文本編輯器打開這個文件(IE就不錯)。你會看到一個根結點名爲“COLLADA”,如果你所用的文本編輯器支持展開與摺疊XML結點的話(IE就可以),你可以通過點擊+-號把各個結點展開收起來成這個樣子:

圖1:COLLADA文件的概覽

 

在.dae文件或.xml文件的根結點<COLLADA>下你會找到很多library這樣的東西,它們就是用來存儲模型中各種不同各類的信息的。比如<library_geometries>就是用來存儲幾何數據的(就是三角形啊,還有所謂的mesh啊 – 另外mesh這個詞好像大家叫成英文的比較多,下面遇到這個詞就不譯成中文了);<library_lights>則是用來存儲光照和場景數據的。大家看看圖1,並不是什麼製造火箭般的高科技是不是,通過這些叫library_xx的東西我們能找到模型實際的各種數據。而像如幾何數據的存儲區會有<geometry>名字的結點,而光照數據的存儲區會有叫<light>的結點,這表明這些數據存儲區裏存儲的模型或光照數據常常不止一組。現在,讓我們來一個一個地分析每個數據區,按照每個數據區的重要性不同,我會將它們合理的排列在這篇教程的不同位置。

首先,爲了讓問題變得簡單,正如我說的這是篇入門教程,所以我們不會討論COLLADA文件的每一個方面,爲了在教程中除去其中的複雜的部分,我們來設定幾個前提條件。

 

前提條件:

1.              雖然無論COLLADA文件從Max中導出還是從Maya中導出照理說應該是一樣的,但實際上在某些情況下總會有那麼一點不同。我們只討論從Max中導出的COLLADA文件,當然這並不是說用Maya的人就杯具了。因爲我仍可以肯定的是,如果COLLADA從Maya中導出時,在彈出的COLLADA導出選項對話框中將“triangulate”這個選項鉤上,並且以“背向矩陣”(backed matrices,我沒用過Maya,也不知道是什麼)方式導出的話,則與Max導出的是一樣的。但是因爲我有用過Maya,所以不知道我的導出器載入Maya導出的文件時會失敗在什麼地方。

2.              COLLADA文件中必須僅僅只有一個mesh,這意味着任何在max文件中有用的數據都已經記錄下來了(原文:which means, anything in the asset's Max file, should be attached.不知道該怎麼譯,不過好像對文章的內容並沒影響)。所以我們在COLLADA文件中的<library_geometries>結點裏不會看到多於1個的<mesh>結點。但如果我們能讀取1個<mesh>,那我們同樣的也能讀取成千上萬個<mesh>不是嗎。

3.              COLLADA裏的幾何圖形是以三角形的方式記錄的,因爲這即使不是最好的,也是比較好的記錄方式,我們可以直接提供三角形數據給OPENGL,所以我們讓Max幫我們將圖形導出爲三角形記錄的方式。

4.              在稍後的實現部分,我們還假定我們所分析的COLLADA中只包含一個貼圖文件。

5.              COLLADA中的動畫至少含有一個骨骼—--至少一個根骨骼(這是很典型的)。嗯,我想,我們能實現骨骼動畫,我們簡直是英雄般的人物。(原文:And I think that’s why we are here, to implement skeletal animation.)

6.              導出到COLLADA中的硬動畫必須以矩陣的形式保存,從本質上來說,在某些情況下這個形成一個動畫的通道而其它情況下則會生成16個動畫通道(什麼是通道,我們稍後解釋)。(原文:Animation exported to COLLADA must be baked in matrices, which essentially in some cases makes 1 channel of animation and in others 16 channels of animation (Now what is channel? It should be explained later).)

7.              動畫只在通道向對象實體施加變化影響時纔有效,請把它們相像得儘量簡單和清晰。如果你固化了矩陣,那麼前面所說的事就理所當然的被完成了,所以不用擔心這些。(原文:Animations can only be valid if the channel targets the "Transform" of the targeted entity, just to keep things clear and easy. When you will bake matrices, then you will have this automatically, so don't need to worry about that.)

8.              動畫不能包含嵌套的動畫。

9.              只支持骨骼動畫(沒有硬動畫)(譯註:那你前面說一大堆硬動畫的事幹毛啊。)

10.          層次中的每個骨頭都必須對某些皮膚產生影響,換句話說,它們都必須關聯到皮膚上。

 

請大家在腦中從頭到尾一直保持上面所列的這些假設,讓我們開始一個一個部分爲你講解。你會覺得一切都很容易,如果你立即跳到實現部分去看你也會發現這些原來並不難。下面的每一節中都會給出相應實現代碼的鏈接。

 

 

從COLLADA文件中讀取幾何數據

<library_geometries>

這是COLLADA文件中最重要的一個library了,如果你需要一個繪製一個角色動畫,在這裏你能找到它的幾何數據。

這個library中包含許多<geometry>類型的結點,它們分別存儲了場景中的各種幾何數據,別忘了COLLADA只有一個文件(也就是沒有其它的附屬數據文件,貼圖的圖片除外),所以只能把所有的大量的數據全放在一個文件裏面。但正如我們假設的,我們只考慮這個文件中只有一個<geometry> node結點,這個結點下也只有一個<mesh>結點的情況。好了,我們找到它了,現在讓我們開始分析它。

 

<mesh>

       我們會在這個被稱爲“網格”的結點中找到我們想要的幾何數據。如果你試着分析這一結點,你會看到至少1到2個<source>結點,它的意義決定於它的類型,它可以存儲頂點、法線、紋理座標等信息。在示例文件中(如果你是從COLLADA.org這個網站下載的話,這個文件裏面不會包含反向動畫,所以你必須將它重新導入Max並且並且以“背向矩陣”重新導出,導出時還要將導出對話框中的“triangulate”這個選項鉤上,如圖2所示),你會找到3個<source>結點,你會發現非常幸運的是在COLLADA中每個source結點都是以同樣的形式來定義的。

圖2:Max導出COLLADA時的設置

 

<souce>

       請記住,我們所討論的所有的XML結點都有一個相應的ID號,這個ID用來定位這個結點在COLLADA文件中的位置,當其它地方需要引用這個結點時,就需要使用這個ID。Source這個結點也並不例外。現在<source>這個結點擁有許多的子結點,但最重要的幾個就是<float_array>或<NAME_array>還有 <technique_common>。

       正如它們的名字所描述的,<float_array>包含了許多的浮點數數據,它們可以用於各種不同用途,在同一個source結點下的<technique_common>指出了它們的具體用途。而<float_array>與<NAME_array>的不同之處僅僅在於前者存儲的是一系列浮點數而後者存儲的是一系列字符串。

       現在我們來看看<technique_common>下的<accessor>子結點是怎麼指明各種array如<float_array>, <NAME_array>或其它各種名字的array結點它們的用途的。<accessor>結點有一個叫做“source”的屬性,它說明的是“這個array到底是什麼意思,它到底是用來幹什麼的”;另一個叫做“count”的屬性說明了這個array有幾組數據;還有一個叫"stride"的屬性則是說明間隔多少數據開始下一個數據(說的複雜了,也就是一組有多少個數據,比如是2個數字一組還是3個數字一組,你懂的)。

好了我希望我不是在講天書,我們來直接看圖表吧,這個圖表解釋了COLLADA的source指示的意義。(在此吐糟一下,原文:hope I am not talking Chinese but let's explain it with a figure and example COLLADA source. 嗯,沒錯,現在你們現在看到的就是Chinese)

 

<source id="vertices_source" name="Vertices">

<float_array id="values" count="6"> 0.3 0.5 0.7 0.2 0.4 0.6 </float_array>

<technique_common>

<accessor source="#values" count="2" stride="3">

<param name="X" type="float"/>

<param name="Y" type="float"/>

<param name="Z" type="float"/>

</accessor>

</technique_common>

</source>

 

圖3:source的結構

 

正    如你們在圖3中所看到的,這是浮點型的數組(注:數據組,不是C語言的數組哈),其中數量是6個 (count="6")(注:仔細看float_array),其中每三個一組(stride="3")共有2組(count="2")(注:仔細看accessor),分別表示XYZ,它們的類型都是浮點型(注:仔細看param)。現在我們看到在<accessor>下有3個<param>子結點,所以我們每個頂點數據是3個一組(x, y, z)(同樣的法線和紋理座標也可能是3個一組)。理解這些信息非常的重要,因爲在我沒有COLLADA的文件說明的時候,只是理解這些就花費了我大量的時間(也許我比較笨吧),所以如果你仍沒理解的話,請再仔細讀一遍。

       簡單來說,這個source說明了以下的意思:“我有2組頂點數據,其中每3個一組,它們都存在<floats_array>裏,總共有6個數字。每一組頂點數據由名爲 “X”,”Y”,”Z”的成份組成,它們都是浮點形的數字”。如果我們讀到一個<source>結點,裏面存放的是紋理貼圖座標的話,那麼每一組數據則是由名爲“S”,”T”,”P”的成份組成。(注:作爲天朝人,理解這些應該無壓力吧)

       好了,這就是source裏面的所有東西了。這個示例文件共有3個<source>結點,在我們分析另外2個之前,或許我們已經猜到了,它們應該存的是法線座標和紋理映射座標。如果你導出模型時還有其它的屬性,那麼你會得到一個有更多<source>結點的文件,比如雙切線(bitangents)和切線(注:tangents,原文是tangets,我猜是筆誤)等等。

       現在我們可以對<source>進行解碼了,但是我們還是不知道哪些source是頂點哪些source是法線等等。我們還要讀取<mesh>下的<vertices>結點來找到存儲頂點數據的source,儘管我實在是想不通他們這麼做的原因,但爲了完整起見,你必須讀這個結點(注:指<vertices>結點),它至少有一個子結點名爲<input>,並且其屬性”semantic”的值是“POSITIONS”,它以另一個名字/ID引用了了頂點的source(注:類似於定義別名,下面有詳細解釋)。然後當你需要頂點的source時,你就會引用到這個新的ID。如果你不明白這一節的內容,那麼請直接跳到下一節,然後你很有可能就明白了。

 

<triangles>

       現在正如我們所假設的一樣,我們只考慮由三角形幾何元素組成的COLLADA文件,所以你在<mesh>下只會看到<triangles>類型的子結點,否則你可能還會看到比如<polylist>這樣的結點,這我們儘量不去考慮它。

       這個<triangles>結點能夠告訴我們所需要的所有構造模型的三角形數據,這些數據在我們之前讀出的3個source裏面(只針對這個文件來說)。在<triangles>裏,"count"屬性告訴我們在這個結點下到底有多少個三角形,而"material"屬性則告訴我們如何從<library_material>下找到相應的材質數據,我們使用這相應的材質數據來渲染對應的三角形。所以你會看到很多的<triangles>節點,它們是根據材質來劃分的(注:也就是說每一類材質的表面與它對應的材質信息一起記錄爲一個<triangles>節點)。所以我們必須讀取所有的<triangles>節點。

       要解碼<triangles>節點我們必須讀取它們的子結點,其中的<input>和<p>結點是最重要的。<input>結點的數量表明每個頂點所具有的屬性的個數。而<p>結點則是頂點相應屬性在相應的<source>結點中的索引(注,不是值,是索引,真正的值請根據索引到相應的source裏面查)。讓我們來看看這個例子。

 

<mesh>

<source id="position"/>

<source id="normal"/>

<source id="textureCoords"/>

 

<vertices id="verts">

<input semantic="POSITION" source="#position"/>

</vertices>

<triangles count="2" material="Bricks">

<input semantic="VERTEX" source="#verts" offset="0"/>

<input semantic="NORMAL" source="#normal" offset="1"/>

<input semantic="TEXCOORD" source="#textureCoords" offset="2" set="1" />

<p>

0 0 1 3 2 1

0 0 2 1 3 2

</p>

</triangles>

</mesh>

 

 圖4:Triangle的結構

 

       正如你從上面的例子中看到的,<vertices>結點將名爲"position"的<source>結點重命名爲"verts",然後以"verts”的名字來定義頂點的source(原文:As you can see from the above example <vertices> node is renaming the "position" source with "verts" and then defining the triangles vertices source with "verts" name.)。這就是我們需要讀取<vertices>結點的原因,只有這樣,我們才能從一堆<source>中找到我們需要的<source>的實際位置。

       如果你讀取<triangles>的子結點,你會讀到3個<input>結點,它們的”semantic”屬性的值分別是"VERTEX" "NORMAL" 和 "TEXCOORD"。這實際上是說,我們三角形數據每個頂點有一個值,第一個是頂點的位置(注:座標),第二個是頂點的法線,第三個是頂點的紋理映射座標。我們怎麼知道在<p>裏面哪個是哪個呢,我們來看看:

 

<input> 結點有semantic屬性= "VERTEX" 它的偏移是 offset = "0",

<input> 結點有 semantic屬性 = "NORMAL" 它的偏移是offset = "1",

<input> 結點有 semantic屬性 = "TEXCOORD" 它的偏移是offset = "2".

 

所以我們從<p>中爲每個三角形每個頂點讀值的時候:

 

第一個值是"VERTEX"也就是三角形頂點位置在名爲"positions"的<source>結點中的索引,

第二個值是"NORMAL"也就是三角形頂點法線的值在名爲"normal"的<source>結點中的索引,

第三個值是"TEXCOORD"也就是紋理映射座標的值在名爲"textureCoords"的<source>結點中的索引。

 

       好,現在有一件事我必須講清楚,所有從<p>這個結點下讀出來的值都是“索引”而不是實際的數據值,所有三角形的所有數據的值都以索引的形式保存是爲了在有重複屬性的情況下節省存儲空間。爲了找到真實的數據值我們必須引用相關的<source>結點,將它們的數據按索引指示的一個一個對應取出來使用。

       構造三角形現在變得非常容易了。你要做的事情就是從<p>這個結點下一次讀取3 * (<triangles>結點下<input>結點的數量)個值,然後以這些值爲索引從相應的<source>結點中讀出真正的數據。如果對於每個頂點只有一個屬性的三角形來說,我們會看到像下面這樣的<triangles>結點,它只有一個<input>子節點。這種情況下你就必須一次從<p>中讀取三個數字作爲索引,然後在相應的名爲"verts"的<source>中根據索引讀取其真正的值。

 

<triangles count="2" material="Bricks">

<input semantic="VERTEX" source="#verts" offset="0"/>

<p>

0 3 2

0 2 1

</p>

</triangles>

 

還有一件我們需要知道的事情就是<triangles>結點的"material"屬性,這個屬性引用了<library_materials>裏面的材質數據,這一個library我們將在稍後的教程中討論。

 

這就是所有的幾何數據了。如果你能正確理解這一部分,那麼接下來的部分對你來說也沒有任何問題了;如果你還沒理解這一部分,那麼請從頭再看幾遍直到你完全明白了爲止。現在如果你立即想跳到本教程的實現部分(第二部分),你應該能讀取並在你的引擎中顯示靜態的3D物體了。如果你還想用材質和貼圖來渲染你的模型甚至還想讓它能夠動起來,你還需要繼續閱讀完這篇教程第一部分的剩下的內容。

 

從COLLADA文件中讀取貼圖文件名

正如大家所知道了,我們在開始做了一些假設,其中之一就是一個COLLADA只對應着一張紋理貼圖,這讓尋找貼圖的文件名變成非常容易。

我們所需要做的一切就是讀取<library_images>下“唯一”的<image>結點中的"id"屬性。一般來說它會是COLLADA中使用的紋理貼圖的文件名。不過它可能並不是正確的文件名,因爲COLLADA可能會創建一個與它文件名不同的ID。所以爲了能夠正確的讀取文件名,我們必須讀取<image>結點下的<init_from>子結點,它給出了完整的路徑,其中也包括文件名。對於我們的目的而言,我們只關心它的文件名,而不是完整的絕對路徑,所以我們讀取完整路徑後僅保存文件名而已。

 

從COLLADA文件中讀取材質數據

我們在“從COLLADA文件中讀取幾何數據”中說過,三角形數據以它們的材質不同來分組,而材質則是在<triangles>結點下的"material"屬性中以ID的形式引用。爲了找到ID所對應的真正材質數據我們必須得讀取<library_materials>。在<library_materials>下,你會找到很多名爲<material>的結點,它們的id屬性就是<triangles>結點中所引用的值。但不幸的是這些<material>結點下只有一個名爲<instance_effect>的子結點,這個子結點下只有一個叫作"url"的屬性。這是因爲<material>也只是引用了一個<library_effects>下的一個“效果”(effect),材質的完整定義其實是在<library_effects>下面。

所以我們保存下<material>的”url”屬性,然後去<library_effects>尋找,但杯具的地方又出現了,<library_effects>可以說是COLLADA中迄今爲止我所知道的最複雜的一個library。特別是當陰影效果和一些根本在COLLADA文檔中找不到說明的內容被添加進去後,這個<library_effects>會變得異常的複雜和難解。但是我答應過我會讓我們所講的東西簡單明瞭,所以我們不會隨便讀取這裏面的數據,除非是它對於定義材質來說非常重要。

如果我們找到任意材質的<material>裏”url”所對應的<effect>結點,我們需要尋找一個名爲<phong>或<blin>的結點,這個結點在<profile_COMMON>結點中,<profile_COMMON>又是<effects>的子結點。<phong>或<blin>結點一般位於<profile_COMMON>的子結點<technique>的裏面。一旦我們找到了<phong>或<blin>,我們繼續看它們下面的關於材質的各種參數,比如"ambient" "diffuse" "specular" "emission" "shininess" 和 "transparency"等等(注:這些都是<phong>或<blin>的子結點)。如果你希望你的模型看起來感覺非常的好,一般來說, "diffuse" "shininess" 和 "transparency"這三組參數足夠你創造出一個有良好觀感的材質了。

我們怎樣才能用簡單的方法從這些結點中讀取數據呢?一般來說,ambient(環境光), emission(輻射光), diffuse(漫反射光) 和 specular(鏡面光) 結點包含4個浮點數,這4個浮點數在它們的<color>子結點裏,這4個浮點數分別表示材質相應顏色屬性的RGBA組分;而reflectivity(反射)和 transparency(透明度)等只包含一個浮點數。

 

<ambient>

<color>1.0 1.0 1.0 1.0</color>

</ambient>

 

<transparency>

<float>0.5</float>

</transparency>

 

如果我們把一張紋理貼在物體的表面,那麼物體的漫反射光將不是簡單的顏色,而是一張貼圖,那麼<diffuse>將不會有名爲<color>的子結點。但爲了簡單起見,我們不必擔心這一點,而是認爲貼圖是貼在物體的漫反射光上,也就是說我們不用從COLLADA中讀取漫反射光的值。但是我們需要使用一個OPENGL中定義的默認的漫反射光的值

這就是我們要實現任何靜態模型所需要的一切了。所以如果你只關心如何從COLLADA中讀取一個靜態的模型,那麼你可以不用讀下面的部分而直接跳到實現部分。如果你的目的不僅僅是這樣,那麼請繼續看我們是怎樣從COLLADA文件中提取動畫數據的。

 

讀取COLLADA文件中的骨骼數據

我們假定我們讀取的是COLLADA文件中的骨骼動畫而不是硬動畫,所以我們需要讀取COLLADA中的骨骼數據。所謂骨骼,我的意思是讀取關節(骨頭)數據。我們同樣必須讀取關節的層次關係數據。這會告訴我們,誰是誰的子關節或誰是某個關節的父關節等等。下面這張圖解釋了關於骨骼的一些術語。記住骨頭和關節實際上是同一個東西,它們只是爲了方便闡述而起的名字,我們從COLLADA文件中讀取的數據實際上是關節數據,骨頭只不過是我們假想的連接兩個關節的線。

 

圖5:骨骼術語

 

在下面這張圖裏你會看到我們的示例文件中的骨骼,還有附在骨骼上的皮膚。

 

圖6:完整的人物造型在動畫中的一個姿勢

 

圖6左邊部分的紅點就是我們從COLLADA中讀出來的關節,連接這些點的線是假想的骨頭,它們可以使皮膚運動起來。在圖的右邊你可以看到另一幀皮膚附着在骨骼上的圖像。

你可能還記得我們的一些假設,其中之一就是,所有的關節都關聯到皮膚上,這樣會使得<library_visual_scenes>變得非常簡單易讀。你所需要做的一切就是在<visual_scenes>找到骨骼根關節(骨頭)的<node>結點,然後讀出整個關節樹。這麼做的缺點之一是,你將會考慮到有很多的影響皮膚的關節,而事實上它們不會對皮膚造成任何影響。但如果你不將所有的骨頭都附加到皮膚上的話,你會看到類型爲"JOINT" 和"NODE"的<node>結點在骨骼層次中混合出現。但如果你將所有的骨頭附加到皮膚上你就會擁有隻有"JOINT"類型的骨骼樹。這也是很多引擎模型導入的默認處理方法。如果在骨骼層次中類型爲"JOINT" 和"NODE"的<node>結點混合出現,你就必須得讀取<library_visual_scenes>下的<instance_controller>結點,然後每一次讀取<skeleton>的時候你都必須的再讀取一個關節數據。那些類型不是"JOINT"的<node>結點實際上仍然是關節,只不過它們沒有任何效果而已,也就是說它們不會對皮膚造成影響。這就是爲什麼我們假設所有骨頭都必須附着在皮膚上,從而使事情容易和簡單。(注:簡單來說,就是將所有類型的接點,無論是在邊界的還是在中間的,都統一考慮,從而使問題處理起來簡單化,如果對其作區分,則會增加很多諸如邊界等的判定條件)

爲了讀取骨骼的層次,你需要一個數據結構,它可以保存同種類型數據結構的大量的子數據和它的父數據的引用(這在實現部分有很清楚的描述)。你還需要保存<node>結點的”SID”屬性。一旦我們建立了這樣的數據結構,我們就要找到根骨骼的結點並且遞歸讀取它們的子骨骼與子骨骼的子骨骼……等等,然後將它們保存在上面所說的數據結構中。當你完成了這些工作,你的數據結構可以清楚的指示出比如:哪個關節是哪個關節的子關節而哪個關節又是哪個關節的父關節。

       那麼我們怎麼能找到根骨骼呢?因爲我們假定一個COLLADA文件中只有一個模型,所以我們不用去讀<scene>結點來查找哪個場景是被實例化的。我們可以立即跳到<library_visual_scenes>裏面唯一的子結點<visual_scene>中去看下面的<node>結點,其<node>結點下有子結點有叫作<instance_controller>的就是我們想要的,我們讀取<instance_controller>下的<skeleton>子結點,它會告訴你根結點的ID。因爲我們將所有的骨骼都關聯到了皮膚上,所以在<instance_controller>下只會有一個<skeleton>子結點,這個結點中記錄的就是我們要找的根骨骼,連接在它上面的所有東西都是骨骼的一部分。

如果你看了COLLADA文件中的<node>結點,你會看到所有的<node>結點的第一個子結點都是一個叫作<matrix>的結點。<matrix>包含了16個浮點數,這夠構成了關節矩陣。這也被稱爲局部骨骼轉換矩陣(the local bone transformation matrix)。當我們將所有關節連接起來,我們需要將它父關節的世界矩陣與子關節的局部矩陣相乘作爲子關節的世界矩陣。對於根關節來說,它沒有父關節,所以它的關節矩陣也就是世界變換矩陣。(注:如果因爲我譯得太差大家不理解的話,我簡要說明如下。所謂的骨骼動畫,骨骼控制皮膚,說白了就是所謂骨骼的變換矩陣影響所謂皮膚的那一部分幾何圖形的繪製,也就是繪製代表“皮膚”的網格之前先用它相關的“骨骼”的變換矩陣來變換一下,從而得到網格繪製的正確位置,這就是所謂骨骼動畫的控制原理。而骨骼是有層次結構的,越是上層的受到別的骨骼影響越少,越是下層的受到別的骨骼的影響就越多,比如你活動一下肘部,雖然你的腕部關節沒動,但你的手掌位置也改變了有木有,而這所謂的影響反應到3D世界也就是它們的變換矩陣的疊加)

到現在爲止,你應該能夠讀取骨骼和通過從每個<node>結點讀出的關節矩陣計算出整個設計好的造型了。在下一節中,我們將讀取與骨架相關聯的蒙皮信息。

 

從COLLADA文件中讀取蒙皮信息

迄今爲止我們已經完成了讀取了幾何數據(頂點信息、材質、紋理貼圖文件名)甚至是模型的骨骼數據。我們還需要知道的就是骨骼是怎麼關聯皮膚(幾何數據)的。我們已經讀取了骨骼中的許多關節。但我們仍然不知道哪個關節關聯哪個頂點。一些關節可能根本不關聯任何的頂點。但如果你們還記得我曾經作過的假設,那就是所有的關節必須附加到皮膚上的話,那麼我們討論的情況的前提是所有的關節都必須關聯到皮膚上。

爲了正確的關聯所有的皮膚(幾何數據),我們需要皮膚的數據,這一節中我會試着讓你瞭解我們從COLLADA文件中的什麼地方能獲取皮膚數據。

再我們進一步的說明之前,有件事情我必須解釋一下。如果我們的人物模型每個頂點只關聯到一個關節上的話,當這個關節移動那麼這部分皮膚當然也相應的會移動,只不過這樣的動畫效果看起來非常的僵硬。這並不是我們實際中所採用的方法,幾乎所有的頂點都會關聯到不止一個關節上。我們通過所謂的“權值”來表達每個關聯的關節對相應皮膚的影響。每個關節對一個頂點有一定百分比的影響,總量是100%。所以權值在皮膚的信息來說是非常重要的的一個。

 

<library_controllers>

<library_controllers>包含了整個模型中所有的關節各自所關聯的頂點和關聯的頂點的權值信息。依照我們的假設,我們只有一組網格和一組骨骼(注:也就是隻1個<mesh>結點和一個<skeleton>結點),所以<library_controllers>下.只有一個<controller>結點。一旦我們找到了這個僅有的<controller>結點,我們繼續找到它的<skin>子結點。在<skin>結點中,找到一個<source>結點,這個<source>結點它的子結點<technique_common>下的子結點<accessor>下的子結點<param>中名爲”name”屬性值是"JOINT"(我不做過多的解釋了,因爲我們在前面讀取幾何數據的時候已經分析過<source>結點了),這個結點的<NAME_array>會給你骨骼中所有關節的名字。現在你懂的,你可以從這個<source>下的<NAME_array>裏的"count"屬性中獲知所有的關節數量。一個<source>的例子如下所示:

 

<source id="boyShape-skin-skin-joints">

<NAME_array id="skin-joints-array" count="5">Bone1 Bone2 Bone3 Bone4 Bone5<NAME_array />

<technique_common>

<acessor source="#skin-joints-array" count="5" stride="1">

                        <param name="JOINT" type="Name" />

</acessor>

</technique_common>

</source>

 

如果你回頭看看<library_visual_scene>中<skeleton>下的<node>結點,你就會看到你從<NAME_array>中讀到的所有關節的名字實際上是前面<node>結點的SID。

要完全讀取皮膚數據,首先我們得先讀取<bind_shape_matrix>,這往往是<skin>的第一個子結點,如果不是的話,那麼遍歷它的所有子結點找到它,然後讀取並保存下來。然後我們開始讀名爲<vertex_weights>的結點了,它的"count"屬性給出了權值的數量,至今爲止我所知道的是,這個值應該等於模型頂點的數量,這個數量我們之前讀取幾何數據時已經讀出來了,因爲我們必須爲每個頂點定義一份權重數據。(注:是一份,不是一個,千萬不要看錯,高潮在後面)

如果你看看<vertex_weights>結點的結構,你會看到至少2個<input>結點,一個的<semantic>屬性爲"JOINT"而另一個的<semantic>屬性爲"WEIGHT";除此之外還有一個<vcount>結點和一個<v>結點。

當我們需要讀取每一個頂點的權值的時候,我們循環N次(N = <vertex_weight>的"count"屬性)讀取<vcount>中的每一個值。每一個值都是影響我們當前正在讀取的頂點的關節數量。所以我們必須嵌套的以一對爲一組(在這裏我們假定在<vertex_weight>中只有兩個<input>結點)讀取M(M = 當前<vcount>的值)組<v>中的索引值。

讀出的每組索引值中,

第一個是之前讀出的名爲"JOINT"的<source>裏<NAME_array>裏面的值的索引(在此假設屬性semantic="JOINT"的<input>它的"offset"屬性值是0)。我們之前提過怎麼樣尋找對應的source了,不過這個的<input>裏面的"source"屬性也給出了對應的source的ID了(所以無論怎麼說都能找到吧)。

第二個是”semantic”="WEIGHT"的那隻<input>中"source"屬性指出的<source>結點裏的索引了(好繞口)(假設這隻<input>的"offset"屬性值是1)。

 

(注:如果我翻譯得你實在看不懂的話,我用純正的中文來解釋一下:我們把<vcount>裏面的值一個一個依次取出來,假設當前取出的值是M,而<input>的數量是C(上面假設的是隻有2個),然後我們得從<v>中一次讀取M*C個值,其中,以C個值爲一組,共有M組數據。爲什麼有M組數據呢,因爲對這個頂點來說,有M個關節能影響它;爲什麼是以C個值爲一組呢,多的我不知道,但就你所看到的當前例子而言,C=2,第一個是影響它的關節的名字的索引值,第二個是這個關節對它影響的權值。關節+權值,二者組合起來就是一個完整的數據了。這麼說應該能明白了吧。)

 

<vertex_weights count="4">

<input semantic="JOINT" source="#joints" offset="0"/>

<input semantic="WEIGHT" source="#weights" offset="1"/>

<vcount>3 2 2 3</vcount>

<v>

1 0 0 1 1 2

1 3 1 4

1 3 2 4

1 0 3 1 2 2

</v>

</vertex_weights>

 

 

在這個例子裏你可以看到<vertex_weight>結點爲4個頂點定義了它們的權值(關聯),第一個頂點有3個關聯的關節,第一個頂點的第一個關聯的關節的序號1,這個序號是用在Ssemantic="JOINT"的那個input指明的source中的<NAME_array>裏的。同樣的它的權值在semantic="WEIGHT"的那個input指明的source中的<float_array>裏,序號是0。

 

<skin>下還有另一個非常重要的子結點,它的名字是<joints>。它一般有兩個<input>子結點:其中一個的屬性semantic="JOINT",它通過"source"屬性引用了一個含“joint”這樣名字的<source>結點;另一個的屬性semantic="INV_BIND_MATRIX",它也通過"source"屬性引用了一個<source>結點,這個引用的結點爲每個關節都定義一個反向綁定矩陣(注:原文with inverse bind matrices for each Joint,全文是And the second <input> with semantic="INV_BIND_MATRIX" references the source with inverse bind matrices for each Joint through the attribute "source")。這個包含了反向綁定矩陣的<source>含有 關節數量*16 個值用以記錄與關節數量一樣的那麼多個反向綁定矩陣。這個矩陣是蒙皮所需要的,大家讀了實現部分後就知道了。

一旦我們讀完<controller>結點,我們會有一個動作綁定矩陣(Bind shape matrix)及很多的關節及它們的反向綁定矩陣(Inverse bind matrices),還有就是我們早先從<visual_scene>中讀取的關節矩陣。每個頂點都受到一個或多個骨骼的影響(記住這個條件的反面就是:每個關節必須至少對一個或多個頂點造成影響,實際上這是不對的,因爲他們可能是端點(注:原文since their might be Joints我想應該正好相反),不影響任何頂點)。因此我們必須擁有它們的權值信息。

到了現在這一步爲止,你應該能夠讀取COLLADA文件中的幾何數據、骨骼數據和蒙皮數據。並且你能夠以原始三角形繪製模型甚至能夠繪製出它的骨骼。儘管我還沒有討論你怎樣可以爲每個關節疊加它們的世界矩陣然後將其以世界座標的形式來繪製從而方便調試使用。但我想我可以給你一個提示,我們必須將父關節的世界矩陣乘以當前關節的矩陣然後將它作爲當然關節的世界矩陣保存起來。我們必須從根關節開始做這件事。從而我們不會從父節點中獲取污染了的矩陣,而且根關節的世界矩陣同時也是根關節本身的變換矩陣,因爲根關節沒有任何的父關節(注:也就是說把開始繪製當然模型時的世界矩陣當作根關節的矩陣,而不要重新的維護一個自己的,整個骨骼每次都從根關節的矩陣也就是當前模型的世界矩陣開始重新計算一遍,這樣也不會造成矩陣重複疊加的錯誤。儘管這裏做了一個很複雜的解釋,但我想實際上他不說大家也都是這麼做的不是嗎)。如果你同時還在讀COLLADA的1.5版規範說明,你可以找到蒙皮的公式,所以你也可以自己將模型擺成文件中定義好的各個形狀(注:動畫數據其實就是一個一個的POSE和擺出這個POSE的時間,只不過按時間的流逝不停的擺出POSE並且還計算出兩個時間點之間的中間POSE從而讓動畫看起來更平滑而已,這是後文)。到現在我們還沒討論到怎麼讓這個模型動起來,我們會在下一節討論這點。

 

 

讀取COLLADA文件中的動畫數據

       迄今爲止我們已經可以讀取靜態模型的所有數據了,還剩下的唯一的事情就是理解和讀取動畫部分的數據。COLLADA的動畫並不是非常成熟,可以說它還處在幼年時期,過一段時間後說不定它的動畫會變得更成熟更好。但就從實現我們的目的這點來看,我們還有許多值得擔心的地方。

      

<library_animations>

       在這個library裏保存了所有的動畫數據。對於每個關節的動畫,你會看到一個<animation>結點,它包含了相關關節的詳細動畫數據。請記住,一個<animation>通道(注:也就是它下面所關聯的一系列數據)會改變它所作用的目標原來的形狀,它的作用目標一般而言是關節(注:而不是所謂的骨骼,骨骼是假想的東西)。

       在<animation>下有三種類型的子結點,第一種通常是一系列的記錄數據的<source>,第二種是<sampler>,第三種是<channel>。你需要<sampler>和<channel>結點來獲得動畫數據關聯的目標。

       在<channel>結點裏你會獲得這個動畫數據作用的對象的ID。(注:這極難翻譯的原文是From <channel> node you pick the target which gives you the ID of the Object on which the Animation data will be applied. And you also get the Sampler ID from where you will pick the sources from which you will pick the animation Data.)

       下面的例子是不不會出現在我們的示例COLLADA文件中的,因爲我們假定文件記錄用的是背向矩陣(backing matrices)。但這樣的例子比較容易理解。

       例子:

<source id="astroBoy-input">

<float_array id="astroBoy-input-array" count="2">0 1.16667</float_array>

< technique_common>

<accessor source="#astroBoy-input-array" count="2" stride="1">

<param name="TIME" type="float"/>

</accessor>

</technique_common >

</source>

<source id="astroBoy-output">

<float_array id="astroBoy-output-array" count="2">2.2 3.5</float_array>

<technique_common>

<accessor source="#astroBoy-output-array" count="2" stride="1">

<param name="TRANSFORM" type="float"/>

</accessor>

</technique_common>

</source>

<source id="astroBoy-interpolations">

<NAME_array id="astroBoy-interpolations-array" count="2">LINEAR LINEAR</NAME_array>

<technique_common>

< accessor source="#astroBoy-interpolations-array" count="2" stride="1">

<param name="INTERPOLATION" type="float"/>

</accessor >

</technique_common>

</source>

 

<sampler id="astroBoy_Skeleton-sampler">

<input semantic="INPUT" source="#astroBoy-input"/>

<input semantic="OUTPUT" source="#astroBoy-output"/>

<input semantic="INTERPOLATION" source="#astroBoy-interpolations"/>

</sampler>

 

<channel source="#astroSkeleton-sampler" target="astroBoy_Skeleton/trans.X"/>

 

現在我們從底部的<channel>結點開始分析。

這表示在場景中有一個叫做"astroBoy_Skeleton"的實體(對我們來說這實體就是關節),它的動畫其中的“X方向變換”(trans.X)是由叫做"astroSkeleton-sampler"的採樣器控制的。

       所以我們需要知道"astroSkeleton-sampler"採樣器是怎樣對實體座標的進行X變換的,我們需要讀取<sampler>結點,它會告訴我們這一點。

       爲了獲得動畫數據,你需要讀取3種輸入信息(也就是<input>)結點。

 

       第一種<intput>結點是:INPUT

       第二種<intput>結點是:OUTPUT

       第三種<intput>結點是:INTERPOLATION

 

當我們開始讀取<sampler>下的<input>結點。

屬性”semantic” = "INPUT"的的告訴我們動畫的輸入 <source>

屬性”semantic” = "OUTPUT"的告訴我們動畫的輸出 <source>

屬性”semantic” = "INTERPOLATON"的告訴我們動畫的插值<source>

 

       當我們讀取這一堆<source>時,我們看到<sampler>下屬性”semantic”爲"INPUT"的<input>子結點所引用的<source>結點,其子結點<technique_common>下的子結點<accessor>下的子節點<param>的名字爲”TIME”,簡單來說這個source包含了動畫的一系列類型爲浮點型的時間信息。

       而<sampler>下屬性”semantic”爲"OUTPUT"的<input>子結點所引用的<source>結點,其子結點<technique_common>下的子結點<accessor>下的子節點<param>的名字爲” TRANSFORM”。這說明了這個source所包含的一系列浮點型的值爲X座標變換,這些變換的值與上面讀取的時間相對應。(爲什麼是X座標的變換呢,因爲<channel>中指明瞭是關於X軸的變換,它所屬的一系列數據自然也是同樣的意義了)

      

       <sampler>下屬性”semantic”爲"INTERPOLATION"的<input>子結點所引用的<source>結點,其子結點<technique_common>下的子結點<accessor>下的子節點<param>的名字爲”INTERPOLATION”。這個source以字符串的方式說明了前面我們讀取的OUPUT中的值所應採取的插值方式(在Max中它的插值方式通常都是”LINERA”(線性插值),所以我們可以不讀這個source而直接默認全部採用線性插值)。

       最後一個source(注:就是插值的那個)是什麼意思呢,比如對應兩個時間點,我們可以相應的從OUTPUT中取出兩個值。那麼如果這個時間正好落在這兩個時間點之間呢,我們怎麼做它的動畫?於是我們通過插值來得到那個中間時間的OUTPUT值。如之前所說的,我們可以用簡單的線性插值來實現。

       你所看到的名爲TIME的source,實際上是動畫的關鍵幀。OUTPUT中所對應的數據,就是關鍵幀的數據。具體來說在這裏就是控制實體的X座標變換的關鍵幀數據了。

       所以在你的代碼中不斷的獲取時間相應的OUTPUT值,並將其作爲X變換因子作用於實體上,那麼你的模型的動畫就實現了。用線性插值計算關鍵幀之間的插值數據,會讓你的動畫看起來更加的平滑。

      

插值是什麼意思?

插值就是計算一個值或多個值間的任意中間值。

 

       比如我們有值X和Y,我們要計算它們兩個的“中間”的值(注:也就是1/2處的值),我們使用0.5作爲插值因數,這個插值因數我們稱之爲“T”。如果我們要找到X和Y間3/4處的值,我們使用的插值因數T=0.75,以此類推(注:原文3-Quater應爲3-Quarter即四分之三)。

       你可以讓T以不同增量比如0.001、0.01、0.05等等做一個從0.0到1.0的循環,然後你就可以得到它們之間的很多很多插值。

       線性插值是一種很簡單的插值方法,它的公式如下所示:

float Interpolate( float a_Value1, float a_Value2, float a_T)

{

return ((a_Value2 * a_T) + ((1 - a_T) * a_Value1));

}          

 

這個公式表明,如果"a_T"爲0,那麼它會返回給你的值;如果它是1,那麼會返回給你的值;如果它是0到1之間的值,那麼它會返回一個a_Value1到a_Value2之間的值。

實際上還有其它更好的插值方法。比如貝塞爾插值,三次方插值等。它們有更爲複雜的公式,而且它們的插值是基於多於兩個值的情況。但我們只使用線性插值,這也是爲了簡單考慮。

現在正如我們之前所說的,這個例子並不是我們的示例文件中所出現的實際內容,所以讓我們來看看實際內容是怎麼樣的。

謹記我們的假設,我們只有兩種類型的<animation>結點,同時我們有16*3=48個<source>和16個<sampler>與16個<channel>結點,或者我們有3個<source>、1個<sampler>和1個<channel>。在第一種情況下"target"屬性在最後的“/”之前含有"transform (X) (Y)"這樣的記錄;而在第二種情況下,"target"屬性在最後的“/”之前則只含有"transform"這樣的記錄。

 

這種情況:

<channel source="#astroSkeleton-sampler" target="astroBoy_Skeleton/transform (0) (0)"/>

或是這種情況:

<channel source="#astroSkeleton-sampler" target="astroBoy_Skeleton/transform"/>

 

第二種情況,我們獲得的矩陣的值,是不屬於那三種source之中任意一種的,這和我們在控制器的反向矩陣中遇到的情況是一樣的。而第一種情況下組成4X4矩陣的每一個值來自不同的source,因此當我們讀數據的時候,必須把它們組合起來。

如果你記得我們從<visual_scene>中讀取每一個關節的矩陣時,這些我們從<animation>結點中讀出的值(它們應該是矩陣,因爲我們將它們背向了 原文:(which will be matrices, since we backed matrices) 什麼玩意),它們通過子結點<channel>的"target"屬性來指明作用的對象(target,實際上是關節joint),它會替換它所作用的關節的矩陣,我們早前從<visual_scene>讀出來的每個關鍵幀在這裏的animation中被定義。我們爲每個關節計算它們的世界矩陣,我們用新的關節矩陣乘以它的父關節世界矩陣

好了,這就是所有的東西了(原文:And that’s all pretty much it.這是什麼鳥語)。如果你從頭到尾讀完了這篇教程,我猜你已經可以寫出你自己的COLLADA文檔導出工具了。而且現在你可以準備去讀這篇教程的下一部分了,如果你之前還沒有看過的話。

 

 

 

 

全文DOC下載:http://download.csdn.net/detail/qyfcool/4732892

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章