Working with FBX SDK (1)
仅供个人学习使用,请勿转载,勿用于任何商业用途
作者:clayman
更新2012.5: *****fbx sdk 2013以后的版本做了大幅更新,大量API都进行了修改和更名,请参考新版SDK文档******
模型导入是所有3D程序最基本的功能,但常常也是让很多新手最头疼的问题之一。DirectX虽然提供了直接加载.x文件的功能,不幸的是多年以来,很少有主流建模软件提供了对它的直接支持,各种各样的格式转换程序之间又多多少有些小bug存在,加上近年来ms也逐渐不再使用.x文件,因此,为了将来程序开发更加灵活方便,任何稍有规模的程序都必须重新发明轮子,自己实现模型导入。
当选择支持什么类型的模型文件时,最重要的因素就是交换性----即这种格式是否能被大多数三维软件支持,是否能方便的和其他格式转换;有良好定义以及可扩展性。在各种模型文件中,目前最能满足这三个条件的就是fbx和Collada,本文主要讨论前者。需要说明的是,无论选择什么格式,这些格式都不应该是图形引擎直接读取的格式。虽然我们也可以这么做,但无论fbx,collada或者其他很多格式都是以数据交换为目的而设计的,比如collada本质就是xml文件,因此不适合游戏引擎这类对性能有较高要求的程序。理想的解决方案是把这些格式作为数据来源,通过预处理转换为为特定引擎设计的格式,最后引擎直接读取特有的自定义格式。如果熟悉XNA的话,XNA中的content pipeline就是完成了这样的工作,把模型,纹理转换为特殊设计的xnb格式,加速运行时的读取速度。引擎只需要有读取一种文件的能力即可,而另外有一些列的importer/converter可以把其他格式的文件在预处理阶段,转换为引擎可识别的格式。
如何设计适合自己引擎的文件格式超了本文讨论范围,不过这里举一个小小的例子,说明自定义格式的必要性。以fbx文件为例,假设用记事本打开一个只包含一个mesh模型文件,可以看到数据大概是按以下方式组织的:
vertex { position data…..}
normal {normal data……}
UV {UV data……}
引擎如果直接读取这样的文件,需要在运行时把数据重新解析组织为硬件可以直接使用的格式:从不同位置抽出position,normal,uv数据合成顶点,再把顶点组织为数组,最后放入vertex buffer中。而如果自定义文件话,可以直接就把数据以vertex array的格式保存,比如:
vertexData{ (pos,nol,uv),(pos,nol,uv),……}
这样在读取文件之后,可以直接把数据放入vertex buffer,效率自然是前者不可比的。当然,实际的文件不仅包含顶点数据,还会有很多其他内容。
说了那么多,现在回到正题。本文不会,也不可能详细讨论解析fbx的所有数据,只重点讨论如果解析出游戏引擎最常用到的信息:如何访问mesh,读取相应的顶点,材质以及模型结构(Hierarchy)信息。
先介绍一点关于fbx的基本知识,fbx是Autodesk开发的文件格式,其开发目的就是为了实现Autodesk旗下软件之间的数据交换。鉴于Autodesk已经把主流建模软件公司买的差不多了(maya,3ds max,softimage,motionbuilder…..),几乎所有主流三维建模软件都能导出\导入fbx文件,Autodesk也提供了的专门的软件fbx convert可以把其他流行格式(包括collada)转换为fbx文件。Fbx文件格式本身是不公开的,而是通过FBX SDK实现对fbx文件的读取以及写入,这也是我选择fbx的一个重要原因,作为开发者可以不必关心实际的数据储存细节(用记事本打开ascii码的fbx文件,还是能大概了解实际的数据格式),把文件看做一个数据源对象,通过特定函数就能访问数据源中的特定数据。而稍后我们就会看到,fbx sdk设计的也非常易用。
我们要做的第一步就是从autodesk网站下载FBX SDK(需要先填写一个简单的表格才能下载,嗯嗯,可以乱填),最新版本是2011.3,windows下的安装包大约有450m。安装之后,需要在工程里进行一些简单的设置才能使用。对于visual Studio来说,请**仔细**按照文档Downloading and installing部分的介绍进行配置,除17以外,其他都是必须的,特别注意在16步时,选择正确的lib文件。特别提醒,虽然2011.3包含了的vs2010下的lib,但是有重大bug,会在导入某些fbx文件时,出现” debug assertion failed”错误(坑爹啊,浪费了我两天),推荐在vs2005/2008下开发。
接下来,就可以动手写代码了。使用fbx sdk时,最先遇到的两个对象就是KFbxSdkManage和KFbxScene。Fbx sdk中大部分类的命名都以KFbx开头(为什么是k呢….?)。KFbxSdkManage是sdk中的中心类,负责了整个sdk内部状态的管理,很多其他对象创建也依赖于KFbxSdkManage,程序中只需要有一个KFbxSdkManage类的实例即可。KFbxScene如其名所示,代表了一个场景,而这里的场景就是fbx文件中包含的所有信息,fbx文件导入以后,在程序中就是一个KFbxScene对象。可以用以下代码完成这两个对象的创建。
KFbxScene *scene;
KFbxSdkManager *sdkManager;
void FbxImporter::Init()
{
sdkManager = KFbxSdkManager::Create();
KFbxIOSettings* ios = KFbxIOSettings::Create(sdkManager,IOSROOT);
sdkManager->SetIOSettings(ios);
scene = KFbxScene::Create(sdkManager,"");
}
注意,示例代码省略了必要的错误检查。上面代码中出现了KFbxIOSettings类,这是一个用来配置KFbxSdkManage的对象,可以通过这个对象设置一些导入导出时的行为,比如可以选择不导入材质,动画等等。有了这两个对象之后,下一步就可以导入fbx文件了,这需要用到KFbxImporter对象,他会自动解析fbx文件中的数据,并保存到KFbxScene对象中。实际上除fbx以外KFbxImporter还能导入一些其他格式的文件。实例代码如下:
{
KFbxImporter* sceneImporter = KFbxImporter::Create(this->sdkManager,"");
sceneImporter->Initialize(fileName,-1,this->sdkManager->GetIOSettings());
sceneImporter->Import(scene);
sceneImporter->Destroy();
}
文件加载之后,接下来就是用相应的方法,找出我们需要的数据。这里要稍微补充一点fbx组织数据的方式。前面说过,当用sdk来处理fbx文件时,它更像是一个数据源或者说一个对象,所以你应该以对象的方式来看待fbx,而不是文件的角度。如果你对scene graph/tree有所了解的话,fbx其实就是一个scene graph/tree!KFbxScene是根节点,包含了一系列子节点KFbxNode,每个KFbxNode又有其自己的子节点。KFbxNode包含了坐标变换信息,可以通过一系列get函数取得,其他数据作为KFbxNodeAttribute对象,包含在KFbxNode内部,这里的其他数据是指mesh,Nurbs,skeletion,camara,light等定义在KFbxNodeAttribute::EAttributeType中的类型。一个KFbxNode可以有多个子KFbxNode,但只能有一个KFbxNodeAttribute对象,可以通过KFbxNodeAttribute的GetAttributeType()方法,确定当前node的所包含的实际数据类型:
更正:又仔细看了文档,KFbxNode可以有多个KFbxNodeAttribute对象,GetNodeAttribute()返回默认的attribute对象。
{
KFbxNode* root = scene->GetRootNode();
for (int i=0;i<root->GetChildCount();i++)
{
WalkHierarchy(root->GetChild(i),0,&(this->root));
}
}
void FbxImporter::WalkHierarchy(KFbxNode *fbxNode, int depth)
{
KFbxNodeAttribute* nodeAtt = fbxNode->GetNodeAttribute();
if(nodeAtt == NULL)
{
ss<<"Name:"<<fbxNode->GetName()<<" NodeType:"<<"None";
}
else
{
switch (nodeAtt->GetAttributeType())
{
case KFbxNodeAttribute::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:
}
}
//process children
for (int i=0;i<fbxNode->GetChildCount();i++)
{
WalkHierarchy(fbxNode->GetChild(i),depth+1);
}
}
说到这里,我们已经解决了第一个问题:获得场景结构信息。所有KFbxNode构成的树就是场景结构。而其中KFbxNodeAttribute为skeletion的节点组成的树,可能就是某个模型的骨骼。下图是解析两个不同文件得到的节点关系:
根据模型师建模习惯的不同,导出节点顺序是不一样的,比如上面的文件把骨骼单独作为一个树,下面的文件则用了一种混排的方式,一个node下同时有子骨骼节点和mesh节点。 接下来,看如何读出顶点信息,注意下面仅以mesh为例,介绍一些常见操作。首先,用以下代码获得一个node中所包含的mesh数据:
{
if(nodeAtt->GetAttributeType() == KFbxNodeAttribute::eMESH)
{
KFbxMesh *mesh = dynamic_cast<KFbxMesh*>(nodeAtt);
if(!mesh->IsTriangleMesh())
{
KFbxGeometryConverter converter(sdkManager);
// #1
converter.TriangulateInPlace(fbxNode);
mesh = dynamic_cast<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,可以用以上两种方法实现。TriangulateMesh和TriangulateInPlace区别在于前者返回一个三角化之后的新mesh,后者则是对当前数据进行三角化。注意TriangulateInPlace之后需要重新获取mesh指针,否则代码会出错。Mesh类的大部分成员函数用途都一目了然,只是有一些概念需要注意:
1. GetPolygonCount() 返回三角形数量;
2. GetControlPointsCount() 返回控点数量,这里控点的概念和DirectX中常说的顶点非常类似,但不完全一样,更像是只包含了position的顶点。也就是说如果这个顶点被n个多边形共享(比如立方体八个角的点),而在每个多边形上又有不同的纹理坐标或者法线,那么稍后将分裂或者说生成n个包含position,normal,uvs等信息的顶点;
3. GetControlPoints () 返回控点数组指针;
4. GetPolygonVertexCount() 这是个迷惑人的名字,这个函数返回的其实是大家熟悉的vertex index count,对triange list来说,其实就是GetPolygonCount() * 3;
5. GetPolygonVertices() 返回索引数组指针;
下面的代码演示了如何把从fbx文件中读取的顶点,索引数据保存到一个非常简单的文件中:
save model
vertex = mesh->GetControlPoints();
vertexCount = mesh-> GetControlPointsCount();
..........
void SaveData(const char *fileName,KFbxVector4* vertex,int vertexCount,int *indices,int indicesCount)
{
//convert kfbxvector4[] to float[],notice we only use the first 3 element(x,y,z) of a kfbxvector4
float *verts = new float[vertexCount*3];
float *pV = verts;
for (int i=0;i<vertexCount;i++)
{
*pV = static_cast<float>(vertex[i][0]);
pV++;
*pV = static_cast<float>(vertex[i][1]);
pV++;
*pV = static_cast<float>(vertex[i][2]);
pV++;
}
//create file
std::ofstream fs(fileName,std::ios_base::out|std::ios_base::binary);
//write geometryInfo: vertex and index count;
int geometryInfo[2] = {vertexCount,indicesCount};
fs.write(reinterpret_cast<const char*>(geometryInfo),sizeof(int)*2);
//write vertex data
fs.write(reinterpret_cast<const char*>(verts),sizeof(float)*vertexCount*3);
short *sIndices = NULL;
//convert to 16 bit index if possible to save memory
if(vertexCount < 65535)
{
sIndices = new short[indicesCount];
short *currentIndex = sIndices;
for (int i=0;i<indicesCount;i++,currentIndex++)
{
*currentIndex = indices[i];
}
//write index data to file
fs.write(reinterpret_cast<const char*>(sIndices),sizeof(short)*indicesCount);
}
else
{
fs.write(reinterpret_cast<const char*>(indices),sizeof(int)*indicesCount);
}
fs.close();
delete[] verts;
if(*sIndices != NULL)
{
delete[] sIndices;
}
}
下面的XNA代码演示了从刚才保存的文件中读出数据并渲染:
class ModelReader
{
int vertexCount;
int indexCount;
VertexBuffer mVertexBuffer;
IndexBuffer mIndexBuffer;
public void LoadFile(GraphicsDevice graphics,string fileName)
{
//open file
FileStream fs = new FileStream(fileName, FileMode.Open);
BinaryReader br = new BinaryReader(fs);
vertexCount = br.ReadInt32();
indexCount = br.ReadInt32();
//read vertex data
VertexPositionOnly[] verts = new VertexPositionOnly[vertexCount];
for (int i = 0; i < 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);
//read index data
short[] indices = new short[indexCount];
for (int i = 0; i < indexCount; i++)
{
indices[i] = br.ReadInt16();
}
//create index buffer
mIndexBuffer = new IndexBuffer(graphics, IndexElementSize.SixteenBits, indexCount, BufferUsage.None);
mIndexBuffer.SetData(indices);
HashSet<short> hash = new HashSet<short>();
br.Close();
fs.Close();
}
public void Draw(GraphicsDevice graphics)
{
graphics.SetVertexBuffer(mVertexBuffer);
graphics.Indices = mIndexBuffer;
graphics.DrawIndexedPrimitives(PrimitiveType.TriangleList, 0, 0, vertexCount, 0, indexCount / 3);
}
}
目前我们已经从fbx文件中导出了最基本的信息,下次继续讨论如何获取noraml,uv,material等信息.........