解析 FBX 模型文件作为 Direct3D 的渲染模型

来源:互联网 发布:mac光盘怎么弹出 编辑:程序博客网 时间:2024/04/30 12:25

一般自己写一些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的例子也有解释,所以也不必熬述。希望能对大家有所帮助。

1 0