每个模型文件都有自己的格式,有自研引擎的模型格式,有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函数中,首先判断加载的文件是不是fbx文件,如果是再执行下面的操作,parseConnections和parseTakes也是针对FBX文件的验证信息,它们验证的信息,我们通过代码已经将其实现出来了,截图如下所示:

关键函数是parseObjects,这个是解释FBX文件的全部内容,首先实现的是Mesh,Material,Texture的解释,模型必须有材质贴图的,材质信息在FBX模型文件中也带有此信息的,下面的代码就是解决此问题的,接口如下:

if (iter.second.element->id == "Geometry"){Property* last_prop = iter.second.element->first_property;while (last_prop->next) last_prop = last_prop->next;if (last_prop && last_prop->value == "Mesh"){obj = parseGeometry(*scene, *iter.second.element);}}else if (iter.second.element->id == "Material"){obj = parseMaterial(*scene, *iter.second.element);}

parseGeometry函数解决的是模型的几何信息,具体实现如下所示:

static OptionalError<Object*> parseGeometry(const Scene& scene, const Element& element)
{assert(element.first_property);const Element* vertices_element = findChild(element, "Vertices");if (!vertices_element || !vertices_element->first_property) return Error("Vertices missing");const Element* polys_element = findChild(element, "PolygonVertexIndex");if (!polys_element || !polys_element->first_property) return Error("Indices missing");std::unique_ptr<GeometryImpl> geom = std::make_unique<GeometryImpl>(scene, element);std::vector<Vec3> vertices;if (!parseDoubleVecData(*vertices_element->first_property, &vertices)) return Error("Failed to parse vertices");std::vector<int> original_indices;if (!parseBinaryArray(*polys_element->first_property, &original_indices)) return Error("Failed to parse indices");std::vector<int> to_old_indices;geom->triangulate(original_indices, &geom->to_old_vertices, &to_old_indices);geom->vertices.resize(geom->to_old_vertices.size());for (int i = 0, c = (int)geom->to_old_vertices.size(); i < c; ++i){geom->vertices[i] = vertices[geom->to_old_vertices[i]];}geom->to_new_vertices.resize(vertices.size()); // some vertices can be unused, so this isn't necessarily the same size as to_old_vertices.const int* to_old_vertices = geom->to_old_vertices.empty() ? nullptr : &geom->to_old_vertices[0];for (int i = 0, c = (int)geom->to_old_vertices.size(); i < c; ++i){int old = to_old_vertices[i];add(geom->to_new_vertices[old], i);}const Element* layer_material_element = findChild(element, "LayerElementMaterial");if (layer_material_element){const Element* mapping_element = findChild(*layer_material_element, "MappingInformationType");const Element* reference_element = findChild(*layer_material_element, "ReferenceInformationType");std::vector<int> tmp;if (!mapping_element || !reference_element) return Error("Invalid LayerElementMaterial");if (mapping_element->first_property->value == "ByPolygon" &&reference_element->first_property->value == "IndexToDirect"){geom->materials.reserve(geom->vertices.size() / 3);for (int& i : geom->materials) i = -1;const Element* indices_element = findChild(*layer_material_element, "Materials");if (!indices_element || !indices_element->first_property) return Error("Invalid LayerElementMaterial");if (!parseBinaryArray(*indices_element->first_property, &tmp)) return Error("Failed to parse material indices");int tmp_i = 0;for (int poly = 0, c = (int)tmp.size(); poly < c; ++poly){int tri_count = getTriCountFromPoly(original_indices, &tmp_i);for (int i = 0; i < tri_count; ++i){geom->materials.push_back(tmp[poly]);}}}else{if (mapping_element->first_property->value != "AllSame") return Error("Mapping not supported");}}const Element* layer_uv_element = findChild(element, "LayerElementUV");while (layer_uv_element){const int uv_index = layer_uv_element->first_property ? layer_uv_element->first_property->getValue().toInt() : 0;if (uv_index >= 0 && uv_index < Geometry::s_uvs_max){std::vector<Vec2>& uvs = geom->uvs[uv_index];std::vector<Vec2> tmp;std::vector<int> tmp_indices;GeometryImpl::VertexDataMapping mapping;if (!parseVertexData(*layer_uv_element, "UV", "UVIndex", &tmp, &tmp_indices, &mapping)) return Error("Invalid UVs");if (!tmp.empty()){uvs.resize(tmp_indices.empty() ? tmp.size() : tmp_indices.size());splat(&uvs, mapping, tmp, tmp_indices, original_indices);remap(&uvs, to_old_indices);}}do{layer_uv_element = layer_uv_element->sibling;} while (layer_uv_element && layer_uv_element->id != "LayerElementUV");}const Element* layer_tangent_element = findChild(element, "LayerElementTangents");if (layer_tangent_element){std::vector<Vec3> tmp;std::vector<int> tmp_indices;GeometryImpl::VertexDataMapping mapping;if (findChild(*layer_tangent_element, "Tangents")){if (!parseVertexData(*layer_tangent_element, "Tangents", "TangentsIndex", &tmp, &tmp_indices, &mapping)) return Error("Invalid tangets");}else{if (!parseVertexData(*layer_tangent_element, "Tangent", "TangentIndex", &tmp, &tmp_indices, &mapping))  return Error("Invalid tangets");}if (!tmp.empty()){splat(&geom->tangents, mapping, tmp, tmp_indices, original_indices);remap(&geom->tangents, to_old_indices);}}const Element* layer_color_element = findChild(element, "LayerElementColor");if (layer_color_element){std::vector<Vec4> tmp;std::vector<int> tmp_indices;GeometryImpl::VertexDataMapping mapping;if (!parseVertexData(*layer_color_element, "Colors", "ColorIndex", &tmp, &tmp_indices, &mapping)) return Error("Invalid colors");if (!tmp.empty()){splat(&geom->colors, mapping, tmp, tmp_indices, original_indices);remap(&geom->colors, to_old_indices);}}const Element* layer_normal_element = findChild(element, "LayerElementNormal");if (layer_normal_element){std::vector<Vec3> tmp;std::vector<int> tmp_indices;GeometryImpl::VertexDataMapping mapping;if (!parseVertexData(*layer_normal_element, "Normals", "NormalsIndex", &tmp, &tmp_indices, &mapping)) return Error("Invalid normals");if (!tmp.empty()){splat(&geom->normals, mapping, tmp, tmp_indices, original_indices);remap(&geom->normals, to_old_indices);}}return geom.release();
}

它主要解决的问题是读取模型的顶点,多边形索引,法线,切线,UV等模型信息。再看看parseMaterial函数实现的内容如下所示:

static OptionalError<Object*> parseMaterial(const Scene& scene, const Element& element)
{MaterialImpl* material = new MaterialImpl(scene, element);const Element* prop = findChild(element, "Properties70");material->diffuse_color = { 1, 1, 1 };if (prop) prop = prop->child;while (prop){if (prop->id == "P" && prop->first_property){if (prop->first_property->value == "DiffuseColor"){material->diffuse_color.r = (float)prop->getProperty(4)->getValue().toDouble();material->diffuse_color.g = (float)prop->getProperty(5)->getValue().toDouble();material->diffuse_color.b = (float)prop->getProperty(6)->getValue().toDouble();}}prop = prop->sibling;}return material;
}

以上实现的内容效果如下所示:

这样我们把FBX模型文件的关键信息就一一解释出来了,在这里还有一个很重要的就是骨骼动画信息,并且FBX也有变形的实现,在这里我们都做了一一解释,代码如下所示:

else if (iter.second.element->id == "AnimationStack"){obj = parse<AnimationStackImpl>(*scene, *iter.second.element);if (!obj.isError()){AnimationStackImpl* stack = (AnimationStackImpl*)obj.getValue();scene->m_animation_stacks.push_back(stack);}}else if (iter.second.element->id == "AnimationLayer"){obj = parse<AnimationLayerImpl>(*scene, *iter.second.element);}else if (iter.second.element->id == "AnimationCurve"){obj = parseAnimationCurve(*scene, *iter.second.element);}else if (iter.second.element->id == "AnimationCurveNode"){obj = parse<AnimationCurveNodeImpl>(*scene, *iter.second.element);}else if (iter.second.element->id == "Deformer"){IElementProperty* class_prop = iter.second.element->getProperty(2);if (class_prop){if (class_prop->getValue() == "Cluster")obj = parseCluster(*scene, *iter.second.element);else if (class_prop->getValue() == "Skin")obj = parse<SkinImpl>(*scene, *iter.second.element);}}

对应的实现效果如下所示:

上面显示的是Take001动画文件信息,当然并不只限于以上信息,还有动作文件名字,动作播放动画时间信息,实现的效果如下所示:

以上关于FBX文件关键信息解释,另外还有很多细节函数实现,在这里就不一一给读者展示了,文章最后会把代码给读者,我们封装的模块可以直接用于DX或者OpenGL的加载的,我们上面用于演示的FBX动作文件在Unity中的显示如下所示:

我们的代码是用VS2017实现的,代码下载地址:
链接:https://pan.baidu.com/s/1q5vvdrkr5VBKdtWbw2UUBg 密码:7mvi

深入理解加载FBX模型文件相关推荐

  1. 动态加载Fbx模型文件

    为什么80%的码农都做不了架构师?>>>    动态加载FBX文件 方法1(已测试过) 1 将模型拖动到场景中 ,调整好位置.(制作prefab需要) 2 新建Resources(如 ...

  2. vue使用three.js加载.FBX模型文件

    1.需要安装的依赖 //three依赖yarn add three//tween.js依赖yarn add @tweenjs/tween.js 2. 封装组件 draw.vue 放在component ...

  3. qt使用assimp加载模型_iOS使用assimpKit加载FBX模型步骤详解

    研究背景 体积:一个.dae模型大概有1M那么大 而.fbx模型0.5M 现状 xcode现在不能直接读取.fbx模型 方案 1.使用assimpKit加载 2.使用Wish3D加载 结果 1.使用a ...

  4. xBIM 实战01 在浏览器中加载IFC模型文件

    系列目录    [已更新最新开发文章,点击查看详细]  一.创建Web项目 打开VS,新建Web项目,选择 .NET Framework 4.5  选择一个空的项目 新建完成后,项目结构如下: 二.添 ...

  5. java加载pmml模型文件报错_PMML总结与思考PMML模型生成和加载示例

    在机器学习用于产品的时候,我们经常会遇到跨平台的问题.比如我们用Python基于一系列的机器学习库训练了一个模型,但是有时候其他的产品和项目想把这个模型集成进去,但是这些产品很多只支持某些特定的生产环 ...

  6. OSG 加载 fbx模型 渲染 已解决

    在日常项目功能开发中.需要对 fbx模型进行渲染. 现在做一下代码整理. //offsetosg::MatrixTransform* mt = new osg::MatrixTransform();m ...

  7. Unity动态加载3D模型

    Unity动态加载3D模型 在Unity中创建游戏对象的方法有 3 种: 第一种是将物体模型资源由 Project 视图直接拖曳到 Hierarchy 面板中: 第二种是在 Unity 3D 菜单 G ...

  8. PHP加载3D模型【WebGL】

    这是另一篇关于如何使用 PHP加载 3D 模型的文章. 在这里,我使用 Laravel 作为后端及其存储. 我在前端使用 Three.Js 库来渲染和显示 3D 模型. 我将向您展示如何上传 3D 模 ...

  9. Three.js加载外部模型骨骼动画

    加载外部模型骨骼动画 上节课是通过Threejs程序创建一个骨骼动画然后解析播放,本节课是加载解析一个外部的骨骼动画模型文件. 查看骨骼动画数据 在解析模型骨骼动画之前,先加载外部的三维模型,查看骨骼 ...

  10. Flutter 加载3D模型方案总结

    文章目录 前言 一.flutter_cube 1.依赖 2.使用 二.model_viewer_plus 1.依赖 2.配置 2.使用 三.结合three.js 1. 配置three.js 下载资源 ...

最新文章

  1. 深度学习对抗样本的八个误解与事实
  2. python监控mysql数据改变_python3小脚本-监控服务器性能并插入mysql数据库
  3. RoBERTa中文预训练模型:RoBERTa for Chinese
  4. 什么叫网站灰度发布?
  5. c# Invoke和BeginInvoke 区别
  6. 为什么我们对90后的迎合难以成功?
  7. 学习Spring Boot:(二十六)使用 RabbitMQ 消息队列
  8. 2021中国实体零售数字化专题报告——便利店篇
  9. leetcode - 15. 三数之和
  10. 基于椭圆-最大边缘准则学习的小麦叶片病害及其严重程度识别
  11. Bailian2798 2进制转化为16进制【进制】
  12. SQL Server 本机 Web 服务的使用方案(转载)
  13. 华为面试题:一头牛重 800 公斤,一座桥承重 700 公斤,请问牛怎么过桥?
  14. 机器学习的四种学习方法
  15. TIA protal与SCL从入门到精通(5)——函数终止跳转处理
  16. 【转载】2005中文博客排名报告
  17. 记一次稀里糊涂的面试
  18. arcgis把jpg转成栅格图像,[转载]在ArcGIS中配准(TIF、JPEG)栅格图像并矢量化(转)...
  19. 最全zabbix安装部署
  20. 群联PS3111+7DDL+JMS578转接板,开卡pSLC,附PS3111量产工具

热门文章

  1. SOUI中View类型的控件数据更新的例子
  2. 2021年PMP考试模拟题4(含答案解析)
  3. python与城市规划_读书报告:地理信息系统与城市规划管理
  4. 搭建Visual Studio C语言开发环境
  5. linux蓝牙安装程序,Linux 端蓝牙调试工具
  6. 理解Andriod 硬件加速
  7. github clone加速
  8. mongodb的基本使用
  9. python内置函数type(x)的作用_Python内置函数(43)——type
  10. autosar 与osek 的nm