每个模型文件都有自己的格式,有自研引擎的模型格式,有AutoDesk提供的模型文件格式,比如FBX模型文件,因为Unity与UE4引擎的使用而备受关注,FBX文件是AutoDesk提供的SDK,已经封装好了,我们并不能查看到其内部结构。网上也有很多关于这方面的文章,但是都没有真正解释FBX文件的内部结构,以及自己如何封装程序加载FBX模型文件。本篇博客就教给读者这两方面的知识,这样更有助于读者理解FBX文件,从而可以将FBX的加载代码移植到自己的引擎中或者自己的SDK中,当然也有助于理解Unity和UE4引擎中使用的FBX模型文件,下面我们先介绍FBX文件内部结构。
FBX文件内部结构
我们要加载模型文件,必须要清楚模型文件的内部结构,否则我们无法写程序加载模型文件,FBX模型文件是一种二进制文件,我们打开只能看到一些局部信息,无法知道它真正的内部结构,也有的文件提供了模型文件的内部结构比如OBJ文件,OBJ文件是无法导出骨骼动画的,只能是静态的,但是它文件的内部结构是可以参考的,所有模型格式的文件内部结构有自己的共性,比如模型信息:模型面数,三角形数,顶点坐标,骨骼动画信息,模型版本号信息等等,这些信息每个模型文件都会有的。
接下来我们分析FBX文件的内部结构,我们先打开FBX模型的二进制文件:
这个是我们的FBX文件模型,虽然它是二进制的,但是我们从二进制模型文件信息中还是可以看到一点端倪的,比如FBXVersion 版本号,NodeAttributeL结点属性,ObjectTypeS 对象类型,RotationPivotS旋转信息,ScalingOffsetS缩放偏移等等,相信读者看起来还是比较费劲的。
下面先看看我们已经实现的FBX文件内部结构,如下图所示:
上图显示的是我们带有动作的FBX模型文件内部结构,node attribute, animation curve node,body,skin等等,这些我们需要通过代码实现,我们使用的是C++语言,下面我们把FBX模型文件中的重要内容给读者展示如下:
上图是FBX文件的核心内容的标识,在具体实现时,我们可通过枚举表示,代码定义如下所示:
enum class Type
{
ROOT,
GEOMETRY,
MATERIAL,
MESH,
TEXTURE,
LIMB_NODE,
NULL_NODE,
NODE_ATTRIBUTE,
CLUSTER,
SKIN,
ANIMATION_STACK,
ANIMATION_LAYER,
ANIMATION_CURVE,
ANIMATION_CURVE_NODE
};
下面我们告诉读者如何加载FBX模型文件,该模块也可以移植到自己的引擎或者SDK中,作者自己写的引擎使用的也是FBX SDK,用的也是本博客实现的接口模块。
加载FBX模型文件
我们先从加载FBX模型文件开始,加载模型文件的实现内容如下所示:
FILE* fp = fopen("c.fbx", "rb");
if (!fp) return false;
fseek(fp, 0, SEEK_END);
long file_size = ftell(fp);
fseek(fp, 0, SEEK_SET);
auto* content = new ofbx::u8[file_size];
fread(content, 1, file_size, fp);
g_scene = ofbx::load((ofbx::u8*)content, file_size);
看着是不是眼熟,上面的代码我们经常用于读取二进制文件,在这段代码中最为关键的函数是load加载FBX模型文件,下面实现load函数,如下所示:
IScene* load(const u8* data, int size)
{
std::unique_ptr<Scene> scene = std::make_unique<Scene>();
scene->m_data.resize(size);
memcpy(&scene->m_data[0], data, size);
OptionalError<Element*> root = tokenize(&scene->m_data[0], size);
if (root.isError())
{
Error::s_message = "";
root = tokenizeText(&scene->m_data[0], size);
if (root.isError()) return nullptr;
}
scene->m_root_element = root.getValue();
assert(scene->m_root_element);
if(!parseConnections(*root.getValue(), scene.get())) return nullptr;
if(!parseTakes(scene.get())) return nullptr;
if(!parseObjects(*root.getValue(), scene.get())) return nullptr;
parseGlobalSettings(*root.getValue(), scene.get());
return scene.release();
}
在load函数中,首先判断加