模型導(dǎo)入是所有3D程序最基本的功能,但常常也是讓很多新手最頭疼的問題之一。DirectX雖然提供了直接加載.x文件的功能,不幸的是多年以來,很少有主流建模軟件提供了對它的直接支持,各種各樣的格式轉(zhuǎn)換程序之間又多多少有些小bug存在,加上近年來ms也逐漸不再使用.x文件,因此,為了將來程序開發(fā)更加靈活方便,任何稍有規(guī)模的程序都必須重新發(fā)明輪子,自己實現(xiàn)模型導(dǎo)入。
當(dāng)選擇支持什么類型的模型文件時,最重要的因素就是交換性----即這種格式是否能被大多數(shù)三維軟件支持,是否能方便的和其他格式轉(zhuǎn)換;有良好定義以及可擴展性。在各種模型文件中,目前最能滿足這三個條件的就是fbx和Collada,本文主要討論前者。需要說明的是,無論選擇什么格式,這些格式都不應(yīng)該是圖形引擎直接讀取的格式。雖然我們也可以這么做,但無論fbx,collada或者其他很多格式都是以數(shù)據(jù)交換為目的而設(shè)計的,比如collada本質(zhì)就是xml文件,因此不適合游戲引擎這類對性能有較高要求的程序。理想的解決方案是把這些格式作為數(shù)據(jù)來源,通過預(yù)處理轉(zhuǎn)換為為特定引擎設(shè)計的格式,最后引擎直接讀取特有的自定義格式。如果熟悉XNA的話,XNA中的contentpipeline就是完成了這樣的工作,把模型,紋理轉(zhuǎn)換為特殊設(shè)計的xnb格式,加速運行時的讀取速度。引擎只需要有讀取一種文件的能力即可,而另外有一些列的importer/converter可以把其他格式的文件在預(yù)處理階段,轉(zhuǎn)換為引擎可識別的格式。
如何設(shè)計適合自己引擎的文件格式超了本文討論范圍,不過這里舉一個小小的例子,說明自定義格式的必要性。以fbx文件為例,假設(shè)用記事本打開一個只包含一個mesh模型文件,可以看到數(shù)據(jù)大概是按以下方式組織的:
vertex { positiondata…..}
normal {normaldata……}
UV {UV data……}
引擎如果直接讀取這樣的文件,需要在運行時把數(shù)據(jù)重新解析組織為硬件可以直接使用的格式:從不同位置抽出position,normal,uv數(shù)據(jù)合成頂點,再把頂點組織為數(shù)組,最后放入vertexbuffer中。而如果自定義文件話,可以直接就把數(shù)據(jù)以vertex array的格式保存,比如:
vertexData{(pos,nol,uv),(pos,nol,uv),……}
這樣在讀取文件之后,可以直接把數(shù)據(jù)放入vertexbuffer,效率自然是前者不可比的。當(dāng)然,實際的文件不僅包含頂點數(shù)據(jù),還會有很多其他內(nèi)容。
說了那么多,現(xiàn)在回到正題。本文不會,也不可能詳細(xì)討論解析fbx的所有數(shù)據(jù),只重點討論如果解析出游戲引擎最常用到的信息:如何訪問mesh,讀取相應(yīng)的頂點,材質(zhì)以及模型結(jié)構(gòu)(Hierarchy)信息。
先介紹一點關(guān)于fbx的基本知識,fbx是Autodesk開發(fā)的文件格式,其開發(fā)目的就是為了實現(xiàn)Autodesk旗下軟件之間的數(shù)據(jù)交換。鑒于Autodesk已經(jīng)把主流建模軟件公司買的差不多了(maya,3dsmax,softimage,motionbuilder…..),幾乎所有主流三維建模軟件都能導(dǎo)出導(dǎo)入fbx文件,Autodesk也提供了的專門的軟件fbxconvert可以把其他流行格式(包括collada)轉(zhuǎn)換為fbx文件。Fbx文件格式本身是不公開的,而是通過FBXSDK實現(xiàn)對fbx文件的讀取以及寫入,這也是我選擇fbx的一個重要原因,作為開發(fā)者可以不必關(guān)心實際的數(shù)據(jù)儲存細(xì)節(jié)(用記事本打開ascii碼的fbx文件,還是能大概了解實際的數(shù)據(jù)格式),把文件看做一個數(shù)據(jù)源對象,通過特定函數(shù)就能訪問數(shù)據(jù)源中的特定數(shù)據(jù)。而稍后我們就會看到,fbxsdk設(shè)計的也非常易用。
我們要做的第一步就是從autodesk網(wǎng)站下載FBXSDK(需要先填寫一個簡單的表格才能下載,嗯嗯,可以亂填),最新版本是2011.3,windows下的安裝包大約有450m。安裝之后,需要在工程里進(jìn)行一些簡單的設(shè)置才能使用。對于visualStudio來說,請**仔細(xì)**按照文檔Downloading andinstalling部分的介紹進(jìn)行配置,除17以外,其他都是必須的,特別注意在16步時,選擇正確的lib文件。特別提醒,雖然2011.3包含了的vs2010下的lib,但是有重大bug,會在導(dǎo)入某些fbx文件時,出現(xiàn)”debug assertion failed”錯誤(坑爹啊,浪費了我兩天),推薦在vs2005/2008下開發(fā)。
接下來,就可以動手寫代碼了。使用fbxsdk時,最先遇到的兩個對象就是KFbxSdkManage和KFbxScene。Fbxsdk中大部分類的命名都以KFbx開頭(為什么是k呢….?)。KFbxSdkManage是sdk中的中心類,負(fù)責(zé)了整個sdk內(nèi)部狀態(tài)的管理,很多其他對象創(chuàng)建也依賴于KFbxSdkManage,程序中只需要有一個KFbxSdkManage類的實例即可。KFbxScene如其名所示,代表了一個場景,而這里的場景就是fbx文件中包含的所有信息,fbx文件導(dǎo)入以后,在程序中就是一個KFbxScene對象??梢杂靡韵麓a完成這兩個對象的創(chuàng)建。
InitinitsdkKFbxScene *scene;
KFbxSdkManager *sdkManager;
voidFbxImporter::Init()
{
sdkManager =KFbxSdkManager::Create();
KFbxIOSettings*ios = KFbxIOSettings::Create(sdkManager,IOSROOT);
sdkManager->SetIOSettings(ios);
scene = KFbxScene::Create(sdkManager,"");
}
注意,示例代碼省略了必要的錯誤檢查。上面代碼中出現(xiàn)了KFbxIOSettings類,這是一個用來配置KFbxSdkManage的對象,可以通過這個對象設(shè)置一些導(dǎo)入導(dǎo)出時的行為,比如可以選擇不導(dǎo)入材質(zhì),動畫等等。有了這兩個對象之后,下一步就可以導(dǎo)入fbx文件了,這需要用到KFbxImporter對象,他會自動解析fbx文件中的數(shù)據(jù),并保存到KFbxScene對象中。實際上除fbx以外KFbxImporter還能導(dǎo)入一些其他格式的文件。實例代碼如下:
LoadfilevoidFbxImporter::LoadScene(const char* fileName){
KFbxImporter*sceneImporter = KFbxImporter::Create(this->sdkManager,"");
sceneImporter->Initialize(fileName,-1,this->sdkManager->GetIOSettings());
sceneImporter->Import(scene);
sceneImporter->Destroy();
}
文件加載之后,接下來就是用相應(yīng)的方法,找出我們需要的數(shù)據(jù)。這里要稍微補充一點fbx組織數(shù)據(jù)的方式。前面說過,當(dāng)用sdk來處理fbx文件時,它更像是一個數(shù)據(jù)源或者說一個對象,所以你應(yīng)該以對象的方式來看待fbx,而不是文件的角度。如果你對scenegraph/tree有所了解的話,fbx其實就是一個scenegraph/tree!KFbxScene是根節(jié)點,包含了一系列子節(jié)點KFbxNode,每個KFbxNode又有其自己的子節(jié)點。KFbxNode包含了坐標(biāo)變換信息,可以通過一系列g(shù)et函數(shù)取得,其他數(shù)據(jù)作為KFbxNodeAttribute對象,包含在KFbxNode內(nèi)部,這里的其他數(shù)據(jù)是指mesh,Nurbs,skeletion,camara,light等定義在KFbxNodeAttribute::EAttributeType中的類型。一個KFbxNode可以有多個子KFbxNode,但只能有一個KFbxNodeAttribute對象,可以通過KFbxNodeAttribute的GetAttributeType()方法,確定當(dāng)前node的所包含的實際數(shù)據(jù)類型:
更正:又仔細(xì)看了文檔,KFbxNode可以有多個KFbxNodeAttribute對象,GetNodeAttribute()返回默認(rèn)的attribute對象。
visitnodevoidFbxImporter::WalkHierarchy(){
KFbxNode*root = scene->GetRootNode();
for (int i=0;i<</SPAN>root->GetChildCount();i++)
{
WalkHierarchy(root->GetChild(i),0,&(this->root));
}
}
voidFbxImporter::WalkHierarchy(KFbxNode*fbxNode, int depth)
{
KFbxNodeAttribute*nodeAtt = fbxNode->GetNodeAttribute();
if(nodeAtt == NULL)
{
ss<<"Name:"<<fbxNode->GetName()<<"NodeType:"<<"None";
}
else
{
switch(nodeAtt->GetAttributeType())
{
caseKFbxNodeAttribute::eMARKER:break;
case KFbxNodeAttribute::eSKELETON:break;
case KFbxNodeAttribute::eMESH:ProcessMesh(nodeAtt) break;
case KFbxNodeAttribute::eCAMERA: break;
case KFbxNodeAttribute::eLIGHT: break;
case KFbxNodeAttribute::eBOUNDARY:break;
case KFbxNodeAttribute::eOPTICAL_MARKER:break;
case KFbxNodeAttribute::eOPTICAL_REFERENCE:break;
case KFbxNodeAttribute::eCAMERA_SWITCHER:break;
case KFbxNodeAttribute::eNULL: break;
case KFbxNodeAttribute::ePATCH: break;

case KFbxNodeAttribute::eNURB: break;
case KFbxNodeAttribute::eNURBS_SURFACE:break;
case KFbxNodeAttribute::eNURBS_CURVE:break;
case KFbxNodeAttribute::eTRIM_NURBS_SURFACE:break;
case KFbxNodeAttribute::eUNIDENTIFIED:
}
}
//processchildren
for (int i=0;i<</SPAN>fbxNode->GetChildCount();i++)
{
WalkHierarchy(fbxNode->GetChild(i),depth+1);
}
}
說到這里,我們已經(jīng)解決了第一個問題:獲得場景結(jié)構(gòu)信息。所有KFbxNode構(gòu)成的樹就是場景結(jié)構(gòu)。而其中KFbxNodeAttribute為skeletion的節(jié)點組成的樹,可能就是某個模型的骨骼。下圖是解析兩個不同文件得到的節(jié)點關(guān)系:
根據(jù)模型師建模習(xí)慣的不同,導(dǎo)出節(jié)點順序是不一樣的,比如上面的文件把骨骼單獨作為一個樹,下面的文件則用了一種混排的方式,一個node下同時有子骨骼節(jié)點和mesh節(jié)點。接下來,看如何讀出頂點信息,注意下面僅以mesh為例,介紹一些常見操作。首先,用以下代碼獲得一個node中所包含的mesh數(shù)據(jù):
meshinfovoidProcessMesh(KFbxNodeAttribute*nodeAtt){
if(nodeAtt->GetAttributeType() == KFbxNodeAttribute::eMESH)
{
KFbxMesh *mesh= dynamic_cast<</SPAN>KFbxMesh*>(nodeAtt);
if(!mesh->IsTriangleMesh())
{
KFbxGeometryConverterconverter(sdkManager);
// #1
converter.TriangulateInPlace(fbxNode);
mesh = dynamic_cast<</SPAN>KFbxMesh*>(fbxNode->GetNodeAttribute());
// #2
//mesh =converter.TriangulateMesh(mesh);
}
std::cout<<“TriangleCount:" <<mesh->GetPolygonCount()
<<" VertexCount:"<<mesh->GetControlPointsCount()
<<"IndexCount:"<<mesh->GetPolygonVertexCount()
<<"Layer:"<<mesh->GetLayerCount()
<<" DeformerCount:"<<mesh->GetDeformerCount(KFbxDeformer::eSKIN)
<<"MaterialCount:"<< fbxNode->GetMaterialCount();
}
}
Fbx文件中包含的mesh不一定是由三角形組成,還可能是四邊形,五邊形等等,因此,要做的第一步,就是三角化mesh,可以用以上兩種方法實現(xiàn)。TriangulateMesh和TriangulateInPlace區(qū)別在于前者返回一個三角化之后的新mesh,后者則是對當(dāng)前數(shù)據(jù)進(jìn)行三角化。注意TriangulateInPlace之后需要重新獲取mesh指針,否則代碼會出錯。Mesh類的大部分成員函數(shù)用途都一目了然,只是有一些概念需要注意:
1. GetPolygonCount()返回三角形數(shù)量;
2. GetControlPointsCount()返回控點數(shù)量,這里控點的概念和DirectX中常說的頂點非常類似,但不完全一樣,更像是只包含了position的頂點。也就是說如果這個頂點被n個多邊形共享(比如立方體八個角的點),而在每個多邊形上又有不同的紋理坐標(biāo)或者法線,那么稍后將分裂或者說生成n個包含position,normal,uvs等信息的頂點;
3. GetControlPoints ()返回控點數(shù)組指針;
4. GetPolygonVertexCount()這是個迷惑人的名字,這個函數(shù)返回的其實是大家熟悉的vertex index count,對triangelist來說,其實就是GetPolygonCount() * 3;
5. GetPolygonVertices()返回索引數(shù)組指針;
下面的代碼演示了如何把從fbx文件中讀取的頂點,索引數(shù)據(jù)保存到一個非常簡單的文件中:
savedatasavedatasave model
vertex = mesh->GetControlPoints();
vertexCount =mesh-> GetControlPointsCount();
..........
voidSaveData(const char *fileName,KFbxVector4* vertex,int vertexCount,int *indices,int indicesCount)
{
//convert kfbxvector4[] to float[],notice weonly use the first 3 element(x,y,z) of akfbxvector4
float *verts = new float[vertexCount*3];
float *pV = verts;
for (int i=0;i<</SPAN>vertexCount;i++)
{
*pV = static_cast<</SPAN>float>(vertex[i][0]);
pV++;
*pV = static_cast<</SPAN>float>(vertex[i][1]);
pV++;
*pV = static_cast<</SPAN>float>(vertex[i][2]);
pV++;
}
//createfile
std::ofstreamfs(fileName,std::ios_base::out|std::ios_base::binary);
//writegeometryInfo: vertex and index count;
int geometryInfo[2] = {vertexCount,indicesCount};
fs.write(reinterpret_cast<</SPAN>const char*>(geometryInfo),sizeof(int)*2);
//writevertex data
fs.write(reinterpret_cast<</SPAN>const char*>(verts),sizeof(float)*vertexCount*3);
short*sIndices = NULL;
//convert to 16 bit index if possible to savememory
if(vertexCount <</SPAN> 65535)
{
sIndices =new short[indicesCount];
short *currentIndex = sIndices;
for (int i=0;i<</SPAN>indicesCount;i++,currentIndex++)
{
*currentIndex = indices[i];
}
//write index data to file
fs.write(reinterpret_cast<</SPAN>const char*>(sIndices),sizeof(short)*indicesCount);
}
else
{
fs.write(reinterpret_cast<</SPAN>const char*>(indices),sizeof(int)*indicesCount);
}
fs.close();
delete[] verts;
if(*sIndices != NULL)
{
delete[] sIndices;
}
}
下面的XNA代碼演示了從剛才保存的文件中讀出數(shù)據(jù)并渲染:
rendermodelreadmodelclassModelReader
{
intvertexCount;
int indexCount;
VertexBuffer mVertexBuffer;
IndexBuffer mIndexBuffer;
publicvoid LoadFile(GraphicsDevicegraphics,stringfileName)
{
//open file
FileStream fs = new FileStream(fileName, FileMode.Open);
BinaryReader br =new BinaryReader(fs);
vertexCount =br.ReadInt32();
indexCount =br.ReadInt32();
//readvertex data
VertexPositionOnly[] verts = new VertexPositionOnly[vertexCount];
for(int i = 0;i <</SPAN>vertexCount; i++)
{
verts[i] =new VertexPositionOnly(new Vector3(
br.ReadSingle(), br.ReadSingle(), br.ReadSingle()));
}
//create vertex buffer
VertexDeclaration vd = new VertexDeclaration(new VertexElement(0,VertexElementFormat.Vector3,VertexElementUsage.Position,0));
mVertexBuffer =new VertexBuffer(graphics, vd, vertexCount,BufferUsage.None);
mVertexBuffer.SetData(verts);
//readindex data
short[] indices = new short[indexCount];
for (int i = 0;i <</SPAN>indexCount; i++)
{
indices[i] =br.ReadInt16();
}
//create index buffer
mIndexBuffer = new IndexBuffer(graphics,IndexElementSize.SixteenBits, indexCount, BufferUsage.None);
mIndexBuffer.SetData(indices);
HashSet<</SPAN>short> hash = new HashSet<</SPAN>short>();
br.Close();
fs.Close();
}
publicvoid Draw(GraphicsDevice graphics)
{
graphics.SetVertexBuffer(mVertexBuffer);
graphics.Indices =mIndexBuffer;
graphics.DrawIndexedPrimitives(PrimitiveType.TriangleList,0, 0,vertexCount, 0,indexCount /3);
}
}
接下來我們繼續(xù)討論如何取得normal,tangent,binormal和uv信息。先介紹一些關(guān)于KFbxLayer對象的概念。KFbxLayer對象是一個容器,對mesh來說,它包含了除控點,多邊形信息以外大部分?jǐn)?shù)據(jù),比如normal,tangent,vertexcolor,uv等等。一個mesh可以包含多個KFbxLayer對象,不同layer之間的元素類型,個數(shù)通常都不相同。下面是一個簡單的mesh結(jié)構(gòu)關(guān)系:
mesh ---- layer 0 { KFbxLayerElementNormal,KFbxLayerElementTangent, KFbxLayerElementUV…..}
|
|------layer 1 {KFbxLayerElementUV………}
|
|-- ………………..
|
|-- layer n
雖然FBX允許有多層layer,但很多軟件包括maya和max都只處理包含在第一個layer中的normal等數(shù)據(jù)!每種保存在KFbxLayer的元素都繼承于KFbxLayerElement,比如KFbxLayerElementNormal對應(yīng)normal數(shù)據(jù),KFbxLayerElementTangent對應(yīng)tangent的數(shù)據(jù)??梢酝ㄟ^KFbxLayer中定義的各種Get函數(shù),返回需要的KFbxLayerElement,如果為空,則說明當(dāng)前l(fā)ayer中沒有這種元素。KFbxLayerElement還中包含了兩個非常重要的屬性KFbxLayerElement::EMappingMode和KFbxLayerElement::EReferenceMode。
MappingMode定義了當(dāng)前類型的元素如何映射到mesh上。舉例來說,對于KFbxLayerElementNormal,eBY_POLYGON_VERTEX表示如果一個頂點被n個多邊形共享,那么這個頂點就有n條法線與之相對應(yīng);eBY_CONTROL_POINT則表示每個頂點無論被幾個多邊形共享,都只有一條normal;eBY_POLYGON則表示構(gòu)成多邊形的n個頂點只對應(yīng)著一條normal。某些MappingMode只對特定的KFbxLayerElement有效,請詳細(xì)參考文檔。通常對于有hardedge的模型來說,MappingMode只能是eBY_POLYGON_VERTEX,而平滑模型則可以是eBY_CONTROL_POINT。
ReferenceMode定義了如何訪問相關(guān)的數(shù)據(jù)。同樣舉例來說,每個KFbxLayerElement內(nèi)部通??赡馨瑑蓚€數(shù)組,分別稱為DirectArray和IndexArray。如果referencemode為eDIRECT,則第i個控點相對的element元素就在DirectArray的第i位置(第i個控點的normal在KFbxLayerElementNormal.DirectArray[i]中),此時IndexArray為空。eINDEX_TO_DIRECT通常和eBY_POLYGON_VERTEX一起使用,因為一個控點可能對應(yīng)多個值,所以這時必須用多邊形頂點索引(也就是GetPolygonVertexCount()返回的值)來獲得某個多邊形頂點所對應(yīng)的值:KfbxLayerElement.DirectArray[IndexArray[vertexIndex]]。下面代碼演示了如何遍歷所有l(wèi)ayer,獲得每個頂點/控點對應(yīng)的法線:
Get normalKFbxLayerElementNormal*leNormal=pMesh->GetLayer(0)->GetNormals();if(leNormal)
{
switch(leNormal->GetMappingMode())
{
caseKFbxLayerElement::eBY_CONTROL_POINT:
switch(leNormal->GetReferenceMode())
{
caseKFbxLayerElement::eDIRECT:
KFbxVector4normal=leNormal->GetDirectArray().GetAt(lControlPointIndex));
break;
caseKFbxLayerElement::eINDEX_TO_DIRECT:
{
intid=leNormal->GetIndexArray().GetAt(lControlPointIndex);
KFbxVector4normal=leNormal->GetDirectArray().GetAt(id));
}
break;
default:
break;//otherreferencemodesnotshownhere!
}
break;
caseKFbxLayerElement::eBY_POLYGON_VERTEX:
{
//polygonID=triange1,2,3.....n
//positionId=1,2,3fortriange//vertex!!!!
intvertexIndex=pMesh->GetPolygonVertex(polygonID,positionId);
switch(leNormal->GetReferenceMode())
{
caseKFbxLayerElement::eDIRECT:
caseKFbxLayerElement::eINDEX_TO_DIRECT:
{
Display2DVector(header,leUV->GetDirectArray().GetAt(vertexIndex))
}
break;
default:
break;//otherreferencemodesnotshownhere!
}
}
break;
}
}
為了避免混淆,再強調(diào)一下控點和頂點的區(qū)別。首先,控點只包含位置信息,頂點則包含了位置,法線,紋理坐標(biāo)等信息。如果mesh中所有l(wèi)ayer中的所有元素MappingMode都是eBY_CONTROL_POINT,則控點數(shù)量和頂點一一對應(yīng)。如果是eBY_POLYGON_VERTEX,則有可能需要分裂控點。比如一個控點被n個多邊形共享,則對應(yīng)著n條法線,需要分裂成n個頂點,但是,控點所對應(yīng)的n條法線中有些可能是相同的(nonehard edge)------所以eBY_POLYGON_VERTEX通常和eINDEX_TO_DIRECT配合使用-----因此最終分裂出來的頂點數(shù)有可能小于n。Tangent,bionormal,vertexcolor的訪問與此類似,而且一般來說,只需要讀出第一個layer中的數(shù)據(jù)即可。如何根據(jù)不同的normal等信息分裂控點,組合頂點,需要我們自己來實現(xiàn),這里不詳細(xì)討論。
UV的訪問方式和上面提到的方法類似,但稍稍有些區(qū)別。前面說過,雖然每個mesh都允許多個layer,但通常只會有一組normal,tangent等數(shù)據(jù),uv則可能有多組(比如一組uv用于普通貼圖,另外一組用于lightmap),并且有可能保存在同一layer中,也有可能分別保存在多個layer中。但是fbx文件中有一個奇怪的問題,很多模型雖然只有一組UV,但會被識別出多組UV出現(xiàn)在不同layer中,并且不是每個layer中存在的數(shù)據(jù)都相同或者有效??!
上圖中,第一個文件是正確的,2組UV分別在兩個layer中;下面的文件則多出了2層只含UV的layer,注意多余的uv名稱都是map1.FBX論壇上好像有人也遇到了同樣的問題,不過都沒有官方的解釋,文檔中也沒有討論。解決方法是我們可以通過檢查每組UV的名稱來確定某組UV是否是重復(fù):
代碼foreachlayer{
intuvSetCount=layer->GetUVSetCount();
if(uvSetCount>0)
{
//iteratealluvchannelindexedbyelement_texture_type
for(inttextureIndex=KFbxLayerElement::eDIFFUSE_TEXTURES;textureIndex<</SPAN>KFbxLayerElement::eLAST_ELEMENT_TYPE;textureIndex++)
{
KFbxLayerElement*uvElement=layer->GetUVs(KFbxLayerElement::ELayerElementType(textureIndex));
if(!uvElement)
continue;
uvSetsName=uvElement->GetName();
if(!CheckUVSetsNameExists(uvSetsName))
{
//processuvdata
}}
}}
GetUVs (KFbxLayerElement::ELayerElementType type)返回對應(yīng)type類型的UV,不存在則返回NULL。這里的type是KFbxLayerElement::ELayerElementType枚舉中eDIFFUSE_TEXTURES到eDISPLACEMENT_TEXTURES之間的值。可以把這個枚舉理解為UV的通道標(biāo)識符,比如GetUVs(eDIFFUSE_TEXTURES)返回diffusetexture通道的紋理,注意,這里eDIFFUSE_TEXTURES并不指這組UV只能用于diffusemap,而只是一個標(biāo)識符!對于只有一組uv的模型來說,紋理數(shù)據(jù)通常都在這個通道中。
我們已經(jīng)基本解析出模型中的幾何信息。接下來看如何獲得材質(zhì),特別是紋理信息。與前面的元素不同,material不保存在layer中,而是保存在node里,一個node可以包含多個材質(zhì)。SDK文檔中關(guān)于材質(zhì),紋理之間關(guān)系的介紹非常讓人迷惑,有些接口也很常奇怪。雖然Layer中有一個名為GetMaterials()的方法,但其返回的KFbxLayerElementMaterial對象中GetDirectArray()只會返回空值,也就是說無法通過它獲得真正表示材質(zhì)的KFbxSurfaceMaterial對象。下面的代碼展示了如何取得材質(zhì),以及相應(yīng)的數(shù)值類參數(shù)。
代碼for(intlIndex=0;lIndex<</SPAN>lNode->GetMaterialCount()lIndex++){
KFbxSurfaceMaterial*lMaterial=lNode->GetMaterial(lIndex)
//converttopropersubtype
if(lMaterial->GetClassId().Is(KFbxSurfaceLambert::ClassId))
{
((KFbxSurfaceLambert*)lMaterial)->GetAmbientColor()
((KFbxSurfaceLambert*)lMaterial)->GetDiffuseColor()
...
}
elseif(lMaterial->GetClassId().Is(KFbxSurfacePhong::ClassId))
{
((KFbxSurfacePhong*)lMaterial)->GetAmbientColor()
((KFbxSurfacePhong*)lMaterial)->GetDiffuseColor()
...
}
}
邏輯上來說,KFbxSurfaceMaterial其實是個抽象類,需要把它轉(zhuǎn)換為合適的兩個子類,才能得到實際材質(zhì)參數(shù)。紋理則要更特別一些(注意,雖然layer中也有GetTextures(),但我測試的時候總返回空值)。一個材質(zhì)會包含多個紋理通道,每個通道同樣以KFbxLayerElement::ELayerElementType中關(guān)于紋理的枚舉作為標(biāo)識符,每個通道可以包含多個KFbxTexture或者KFbxLayeredTexture,其中,KFbxTexture就對應(yīng)著一張紋理,而KFbxLayeredTexture則又包含了多個KFbxTexture對象,類似如下結(jié)構(gòu):
KFbxSurfaceMaterial : contains one or moretextureProperty, identified byKFbxLayerElement::ELayerElementType
textureProperty : contains one or moretexture/layerTexture;
layerTexture: contains more than onetexture
下面的代碼展示了如何獲得紋理信息。
Get TexturevoidFbxImporter::ParseMaterial(KFbxNode*fbxNode,HamsterEngine::Node*node){
//iterateallmaterial
for(inti=0;i<</SPAN>fbxNode->GetMaterialCount();i++)
{
KFbxSurfaceMaterial*mat=fbxNode->GetMaterial(i);
if(mat)
{
//iteratealltexturechannel
for(inttextureIndex=0;textureIndex<</SPAN>KFbxLayerElement::LAYERELEMENT_TYPE_TEXTURE_COUNT;textureIndex++)
{
//getcurrenttexturechannel
KFbxPropertyproperty=mat->FindProperty(KFbxLayerElement::TEXTURE_CHANNEL_NAMES[textureIndex]);
//haschannel?
if(property.IsValid())
{
//layeredtexture?
if(layerCount>property.GetSrcObjectCount(KFbxLayeredTexture::ClassId);)
{
//iteratealllayeredtexture
for(intlayerId=0;layerId<</SPAN>layerCount;layerId++)
{
KFbxLayeredTexture*layeredTex=KFbxCast<</SPAN>KFbxLayeredTexture>(property.GetSrcObject(KFbxLayeredTexture::ClassId,layerId));
intnumTex=layeredTex->GetSrcObjectCount(KFbxTexture::ClassId);
//iteratealltextureinthislayer
for(inttexId=0;texId<</SPAN>numTex;texId++)
{
KFbxTexture*tex=KFbxCast<</SPAN>KFbxTexture>(layeredTex->GetSrcObject(KFbxTexture::ClassId,texId));
if(tex)
{
std::cout<<"Texturename:"<<tex->GetName()
<<"fileName:"<<tex->GetFileName()
<<"uvSet: "<<tex->UVSet.Get();
}
}
}
}
else
{
intnumTextures=property.GetSrcObjectCount(KFbxTexture::ClassId);
////iterateallsimpletexture
for(inttexId=0;texId<</SPAN>numTextures;texId++)
{
KFbxTexture*tex=KFbxCast<</SPAN>KFbxTexture>(property.GetSrcObject(KFbxTexture::ClassId,texId));
if(tex)
{
std::cout<<"Texturename:"<<tex->GetName()
<<"fileName:"<<tex->GetFileName()
<<"uvSet:"<<tex->UVSet.Get();
}
}
}
}
}
}
}
}
KFbxTexture.UVSet.Get()返回當(dāng)前紋理所綁定的UVSet名稱,可以由此獲得紋理和UV的綁定關(guān)系。
之前所說的KFbxLayerElementMaterial并不是完全沒用,還必須用它獲得mappingmode,對材質(zhì)來說,最常見的兩個值是:eALL_SAME和eBY_POLYGON,前者表示整個mesh的材質(zhì)都相同,沒太多可說的;后者表示材質(zhì)只應(yīng)用到mesh中的部分多邊形,這就比較麻煩了,上圖中第二個文件就是這種情況。不同材質(zhì)意味著紋理或者shader改變,我們必須把eBY_POLYGON的mesh根據(jù)材質(zhì)劃分為不同子mesh才能導(dǎo)入到DirectX程序中。幸運的是sdk提供了這樣的函數(shù),讓我們不用自己計算:
KFbxGeometryConverterlConverter(pSdkManager)lConverter.SplitMeshPerMaterial(lMesh)
注意:
before and after the call to SplitMeshPerMaterial, you should see adifference in the number: there will be the old mesh, plus one newmesh (node attribute) for each material.
It will work only on mesh that have material mapped “per-face”(Mapping Mode is KFbxLayerElement::eBY_POLYGON). It does NOT workon meshes with material mapped per-vertex/per-edge/etc.It willcreate as many meshes on output that there are materials applied toit.If one mesh have some polygons with material A, some polygonswith material B,and some polygons with NO material, it shouldcreate 3 meshes after calling this function.The newly createdmeshes should be attached to the same KFbxNode that hold theoriginal KFbxMesh.The original KFbxMesh STAY UNCHANGED.Now, the newmesh will have Normals, UVs, vertex color, material andtextures.
以上介紹了模型導(dǎo)入時從fbx文件中提取,常見數(shù)據(jù)的方法,也還有很多方面沒有討論,比如skininfo和animation。對skin來說,相應(yīng)的權(quán)重等信息保存在KfbxDeformer對象中,可以通過KFbxNode獲得。至于動畫目前我暫時還沒有時間研究,如果有好心人實現(xiàn)了,不妨也寫篇教程順便告訴我一聲:)。另外最先說過,fbx是一種可擴展的格式,可以通過UserProperties屬性添加很多自定義屬性,這里介紹了如何在maya和max中添加自定義屬性,SDK中的UserPropertiessample則介紹了如何取得這些屬性。文章中所涉及的函數(shù)只介紹了基本用法,詳細(xì)信息請參考文檔。另外文檔中雖然沒有太多示例代碼,但sdk中附帶的ImportSceneSample是一個非常好的例子,展示了解析fbx文件的方方面面,值得仔細(xì)研究。
轉(zhuǎn)載于:http://www.cnblogs.com/clayman/archive/2010/12/11/1902782.html
愛華網(wǎng)



