深入理解加载FBX模型文件 您所在的位置:网站首页 3d材质与贴图的关系 深入理解加载FBX模型文件

深入理解加载FBX模型文件

2023-08-30 11:45| 来源: 网络整理| 查看: 265

每个模型文件都有自己的格式,有自研引擎的模型格式,有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 = std::make_unique(); scene->m_data.resize(size); memcpy(&scene->m_data[0], data, size); OptionalError 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 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 geom = std::make_unique(scene, element); std::vector vertices; if (!parseDoubleVecData(*vertices_element->first_property, &vertices)) return Error("Failed to parse vertices"); std::vector original_indices; if (!parseBinaryArray(*polys_element->first_property, &original_indices)) return Error("Failed to parse indices"); std::vector 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 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& uvs = geom->uvs[uv_index]; std::vector tmp; std::vector 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 tmp; std::vector 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 tmp; std::vector 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 tmp; std::vector 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 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(*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(*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(*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(*scene, *iter.second.element); } }

对应的实现效果如下所示: 这里写图片描述 上面显示的是Take001动画文件信息,当然并不只限于以上信息,还有动作文件名字,动作播放动画时间信息,实现的效果如下所示: 这里写图片描述 以上关于FBX文件关键信息解释,另外还有很多细节函数实现,在这里就不一一给读者展示了,文章最后会把代码给读者,我们封装的模块可以直接用于DX或者OpenGL的加载的,我们上面用于演示的FBX动作文件在Unity中的显示如下所示: 这里写图片描述 我们的代码是用VS2017实现的,代码下载地址: 链接:https://pan.baidu.com/s/1q5vvdrkr5VBKdtWbw2UUBg 密码:7mvi



【本文地址】

公司简介

联系我们

今日新闻

    推荐新闻

    专题文章
      CopyRight 2018-2019 实验室设备网 版权所有