1. 程式人生 > >解析 FBX 模型檔案作為 Direct3D 的渲染模型

解析 FBX 模型檔案作為 Direct3D 的渲染模型

一般自己寫一些D3D的程式時比較頭疼的就是缺少資源,畢竟不是學習如何使用3Dmax不是一天兩天的事,而且使用Max製作的模型還不能直接使用,除非你能解析.max檔案,或者給Max寫匯出外掛,這又是另一碼事了。然而網際網路上免費的FBX模型很多而且有些都是真實遊戲匯出的模型,這給我們在資源的提供上幫了大忙。由於資源的缺少這些日子寫了一個FBX的解析器,用來豐富自己程式的場景。再開發過程中我也參閱了很多網上寫過FBX解析器的文章,這些文章讓我再開發的過程中少走了不少彎路,但是這些文章基本上都是講述FBX的檔案結構和如何讀取,然而從FBX讀取出來的資料和真正要使用D3D渲染的資料還是有差距的,今天我就結合遊戲開發(D3D)分享一下開發FBX經歷。

在開始之前首先要說明一點,這次做的FBX解析器並不通用,它非常針對地面向遊戲開發,原因會再後面一點點說明。下面開始正文。

一. FBX SDK:

1. 使用SDK:

由於FBX的檔案格式是非公開的,所以要讀取FBX檔案必須使用FBX SDK才可以。說到這裡插一句,網上可以搜到很多關於FBX檔案格式的文章,在知道格式的情況下可以不使用SDK自己去解析,但是這有一個問題,由於FBX的格式是非公開的,所以Autodesk不會保證FBX的檔案格式是固定的,它很可能會跟隨3DMax匯出器的版本變化而變化(據我所知確實有很多版本的FBX檔案)。這就好像Windows API一樣,雖然介面沒有變化但是裡面的實現是完全不一樣,要想自己去相容各個版本實在太困難了,還不如使用FBX SDK。

2. SDK版本:

我使用的FBX SDK的版本是2016.1.2,在Autodesk的官網上免費下載。這個版本支援VS2015。

3. SDK的使用:

SDK提供了dll和lib兩種載入模式,支援x86/x64/Windows Store。SDK有文件,但是這文件寫的跟沒有也差不多,基本就是程式裡面的註釋。幸虧它還有比較豐富的例子,否則使用基本靠猜了。

二. 基本概念:

1. 頂點:

(1)首先從遊戲開發方面來說,用過D3D/OGL渲染的都知道Draw需要的最基本資源就是頂點,最終使用它的是Vertex Shader,不管你在建立頂點緩衝的時候採取單流多屬性還是多流單屬性,所有屬性的個數必須一致,也就是有多少個Position就得有多少個UV(Normal/Tangent/Binormal…),頂點屬性的個數不一致是無法渲染的。

(2)然後在看FBX。FBX的頂點概念和遊戲開發中的概念有區別,它的頂點各屬性的數量是不一致的(這點繼承自Max),頂點的每個屬性有可能都有自己的索引,這樣它就能保證資料不會有重複。這在編輯器裡面是很重要的,屬於核心資料,但是這就給我們在使用FBX的時候添加了不少麻煩事(事實上麻煩事基本都出自這裡)。由於頂點各屬性的數量不一致就意味著要從FBX的頂點轉換到D3D可以使用的頂點必定要對頂點資料進行拆分。

(3)FBX特殊的頂點屬性——ControlPoint。FBX的頂點幾乎所有屬性都可以有多套(UV/Normal/Tangent/Binormal等等,多套UV在渲染中經常用到,比如BaseTexture/ShadowMap/LightMap什麼的,但是多套Normal我還真沒見用過,這就是遊戲的特殊之處),唯獨ControlPoint只有一套,這個ControlPoint其實就是咱們用頂點座標Position。

2. Submesh

什麼是Submesh?舉例:

(a)坦克的履帶需要一個UV迴圈動畫,動畫對於履帶的貼圖就有要求,UV方向必須固定而且圖必須迴圈,這就跟坦克的車身的圖有衝突,造成履帶的貼圖和車身的貼圖必須分開,圖分開了但是模型是不分開的,履帶和車身還在一個模型中。

(b) 遊戲中的人物的某個部位能換圖,比如徽章,當人物由一顆星升級到三顆星如果把人物的整張貼圖都換了就造成了資源重複的浪費,為了避免浪費就要把徽章的地方分開單獨使用一張圖,其他不動的地方使用一張圖。

我們管一個DrawCall畫出來的部分叫Submesh。
D3D中沒有Submesh的概念,它只認DrawCall。DrawCall的資源需求就是VB/IB/IA/Shaders/RenderStates以及畫多少個頂點(VB Only)或者畫多少個索引(VB+IB)。它的工作模式可以看出VB或者IB在一個DrawCall中是連續的,翻譯過來就是一個Submesh中的頂點資料是連續的,一個Submesh的頂點資料不能插入其他Submesh的資料,否則渲染出來的畫面就不對了。

三. 思路:

在開始寫程式碼之前需要理清我們的思路,就是我們(D3D)需要什麼。
我們需要的是頂點各個屬性的流,並且流中同一個Submesh的資料是連續的。需求再具體一點:

  • Submesh的數量
  • 每個Submesh中頂點/索引的數量
  • 頂點屬性的數量(多少個流)
  • 模型頂點索引數量
  • 模型頂點數量、

四. 行動:

1. 模型頂點索引數量:

這個資料是最好獲得的,通過fbxsdk::FbxMesh::GetPolygonCount可以知道模型有多少個Polygon,PolygonCount*3就是模型頂點索引數量由於D3D是按照三角形渲染的所以強制一個Polygon中頂點數量必須是3,這裡是一個針對遊戲開發而沒有做通用處理的地方(我在開發的時候判斷如果Polygon的頂點數量不是3直接放棄解析返回載入失敗)。通過fbxsdk::FbxMesh::GetPolygonSize可以獲取Polygon中頂點個數量。

2. 獲取Submesh數量和每個Submesh中索引的數量:

第一項資料就讓我折騰了很久。FBX中也沒有Submesh的概念,無法直接獲取Submesh的數量。經過SDK例子的研究是網上前輩們探索留下的文章發現它有一個材質(Material)的概念,通過分析發現,這個材質和我們需要的Submesh等價。FBX提供了模型當前模型有多少個材質的方法 fbxsdk::FbxNode::GetMaterialCount,通過這個方法就可以知道模型有幾個Submesh了。然而麻煩的是FBX無法直接提供從哪個頂點到哪個頂點屬於一個Submesh(Material),它只提供了每個Polygon(在D3D中就是三角形,但是FBX還支援多邊形)使用了哪個材質,而且這些Polygon的排序是雜亂無章的,這就需要你在整理頂點資料的之前對頂點重新排序,把相同Submesh的Polygon歸到一起。當把Polygon按照Submesh重新排序完畢之後,每個Submesh有多少Polygon就知道了,從而每個Submesh的索引數量也就知道了(依舊是PolygonCount*3)。然而重新排序勢必會導致頂點的順序和索引的順序和FBX的原始資料不一致,這也是沒辦法的事情。在整個FBX靜態模型的解析過程中有兩個核心問題,這就是其中一個。

3. 頂點屬性的數量:

FBX SDK並沒有提供一個返回多少頂點屬性的方法,由於除Position外每種屬性都能有多套資料,所以要想模型中到底有多少個屬性就需要自己去遍歷。由於針對遊戲開發,所以我們不需要去遍歷所有屬性,我們只需要獲取我們必要的屬性資料就行。
包括次世代引擎對模型資料的需求無非就是Position/UV/Normal/Tangent/Binormal/Color,除了UV會用到多套,其他屬性一套就夠用了,如果涉及到動畫再加上BlendWeight/BlendIndex。Position屬性是必有的。fbxsdk::FbxMesh::GetElementNormal/ GetElementTangent/ GetElementBinormal/ GetElementUV可以獲取對應屬性物件以及資料及其索引,這些方法都有一個相同的int引數,這個引數的意思就是你需要第幾套屬性的資料,由於我們只需要一套,所以傳0就可以。如果返回物件指標是空則表示頂點沒有此項屬性。這樣頂點屬性的數量順利獲得。

4. 頂點數量和每個Submesh中的頂點數量:

這是最麻煩的一個地方,前面說過在整個FBX靜態模型的解析過程中有兩個核心問題,這就是第二個。
在介紹頂點基本概念的時候我們說過FBX模型的頂點屬性的個數不一致,之所以數量不一致主要是因為FBX的每個屬性都可能使用了索引(如何判定後面介紹)。但是我們在渲染的時候是不允許出現這種現象的,D3D要求每個所有屬性的數量必須一致。如何解決?
最簡單的辦法就是直接展開。按照我們剛才已經按Submesh排好序的Polygon的順序遍歷一遍。使用fbxsdk::FbxMesh::GetPolygonVertex可以獲取指定Polygon上面頂點的索引,它的第一個引數是哪個Polygon第二個引數是Polygon上的哪個頂點,返回值就是頂點索引。
擁有頂點索引之後獲取個屬性的值:

  • Position:
    通過fbxsdk::FbxMesh::GetControlPoints獲取一個fbxsdk::FbxVector4的陣列指標,用索引就可以獲取Position的值了。

  • N/T/B:
    這老三位稍微麻煩點,因為他們使用有可能使用了索引。上面已經說過說過如何獲取這三位屬性物件的方法。通過呼叫GetDirectArray可以獲取屬性的資料,但是如何使用還需要看屬性是否使用了索引。判斷是否使用了索引需要兩個屬性物件的兩個引數配合說明,一個是GetMappingMode另一個是GetReferenceMode。
    以Normal為例,當獲取了fbxsdk::FbxGeometryElementNormal屬性物件之後,呼叫GetMappingMode方法獲取的值必然是eByPolygonVertex,在這個前提下再呼叫GetReferenceMode,如果是eDirect則沒使用索引,如果是eIndexToDirect則使用了索引。如果沒使用索引就可以像Position一樣通過索引去直接索引DirectArray獲取資料,如果使用了索引則需要呼叫屬性物件的GetIndexArray,它返回的是一個fbxsdk::FbxLayerElementArrayTemplate&,使用頂點索引從它裡面獲取二次索引,再從DirectArray中索引資料。

  • UV:
    這個更麻煩點。UV在GetMappingMode的時候有可能是eByControlPoint,在這個條件下如果GetMappingMode是eDirect則沒使用了索引,如果是eIndexToDirect則使用了索引。如果GetMappingMode返回的是eByPolygonVertex的畫不管GetMappingMode返回的是什麼值都使用了索引。獲取資料的方法上面的Normal一樣

到這裡頂點屬性資料全部展開,事實上這個時候的資料已經可以拿去D3D渲染了,但是事情還沒完。把資料全部展開之後會有大量的重複資料,舉例:兩個三角形共面且共邊,這時你會發現這6個頂點中共邊的4個頂點有2個頂點的資料完全一樣,假如說頂點有Position(float3)和UV(float2)兩個屬性,那麼資料冗餘(sizeof(float)3+sizeof(float)2)2=40位元組,2/6=33.33%!這只是一個小例子,我用了一個全部展開需要40000多個頂點模型做了個測試,如果進行頂點合併,最後只剩下了8000多個頂點。要知道視訊記憶體頻寬是一個多麼寶貴的資源,這種浪費不能容忍啊。
回到本小節的開頭,我們最後一步需要知道模型中頂點的數量。在模型屬性資料全展開的情況下Polygon
3就是模型頂點的數量,進行頂點合併之後有多少呢?這個靠公式計算是算不出來的,只有合併之後才能知道。這就好像影象壓縮一樣,一個沒壓縮過的點陣圖資料量是BytePerPixel
Width
Height位元組,一旦壓縮過了到底有多大和影象的內容相關,影象變化越少壓縮比例越大壓縮後資料量也越小。
合併屬性需要注意,只有當一個頂點的所有屬性全部相同才能合併,合併屬性的時候原則上是要比較資料是否相等,比如Position需要比較xyz的數值,但是這個計算量是非常大的,而且還有浮點數閾值的問題。其實仔細想想可以利用屬性的索引來比較,這樣不僅計算量小而且還準確,因為比較索引是整數比較,沒有誤差。
到此,我們(D3D)所需要的所有資料都已經準備完畢,用資料建立VB/IB就可以進行渲染了。

5. 如何載入不同來源的FBX:

引擎一般都是有自己的檔案系統,資源打包到一個整合檔案中,如果讓FBX解析一個不是來自檔案的模型呢?
FBX其實已經為這種情況留下了介面。fbxsdk::FbxImporter的Initialize介面有一個接受fbxsdk::FbxStream的版本,自己可以過載FbxStream來自己實現源資料的提供方法。注意,實現fbxsdk::FbxStream::Open/Close這兩個介面的時候必須把流的位置置為0,也就是流起始位置,我就被這個坑坑了一天的時間。

五. 總結:

1. 優化:

優化步驟進行了頂點合併,這能大量節省空間,其實還有優化空間。現在解析出來的模型都是按照TrangleList排列的,頂點索引數量是Polygon*3,如果能把TrangleList再轉為TrangleStript則索引數量也會有一定節省。但是這個過程是非常考演算法的,而且從什麼地方切也很講究,技術含量很大,我放棄了。

2. 完成度:

這次完成的只是一個讀取靜態模型的解析器,動畫讀取沒有做。

3. 關於FBX模型:

FBX其實是一個大而全的檔案格式。由於針對遊戲開發,所以解析器並不能完全解析所有FBX檔案,也沒有必要完全支援。比如說FBX還支援材質(真正的材質),場景,燈光,攝像機,但是這些對遊戲來說沒用,因為所有引擎都有自己的材質編輯器,而且編輯出來的材質也是要複用從而形成材質庫的,不可能繫結到特定模型上至於場景燈光攝像機更是引擎必須提供的工具,沒有人會用Max去做這些東西。
說實在的FBX真不適合做遊戲的模型,因為解析起來耗時太多,尤其是像遊戲Loading這種時間長了遭不住。遊戲還是需要自己特定的模型檔案格式,最好就是直接讀到記憶體不用任何轉換。

六. 後記:

這次我在使用方面寫的不多,網上很多文章都有如何初始化FBX遍歷結點判定結點作用等文章,再加上SDK的例子也有解釋,所以也不必熬述。希望能對大家有所幫助。