Clayman's Graphics Corner

DirectX,Shader & Game Engine Programming

  博客园 :: 首页 :: 博问 :: 闪存 :: 新随笔 :: 联系 :: 订阅 订阅 :: 管理 ::

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对象。可以用以下代码完成这两个对象的创建。

Init
init sdk 
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还能导入一些其他格式的文件。实例代码如下:

Load file
void FbxImporter::LoadScene(const char* fileName)
{
    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对象。

visit node
void FbxImporter::WalkHierarchy()
{
    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数据:

mesh info
void ProcessMesh(KFbxNodeAttribute* nodeAtt)
{
   
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 data
save data 
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代码演示了从刚才保存的文件中读出数据并渲染:

render model
read model 
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, 
00, vertexCount, 0, indexCount / 3);
    }
}

 

     目前我们已经从fbx文件中导出了最基本的信息,下次继续讨论如何获取noraml,uv,material等信息.........

 

 

posted on 2010-12-10 06:40  clayman  阅读(19499)  评论(20编辑  收藏  举报