Background

由于毕设需要,最近在做FBX文件的解析工作,即解析由3dsmax导出的fbx文件模型,并在openGL中重新显示,到目前为止已经断断续续做了半个月了。根据我前期的调研来看,FBX SDK的官方文档的示例并不实用,此外,网上和FBX SDK相关的资料也不是很多。好在我还是在网上扒到了一些有用的信息,如这篇文章:How to Work With FBX SDK,这篇文章对我的帮助很大,使我成功使用OpenGL将该FBX模型显示出来。因此,我决定翻译此文,一是希望能够对FBX SDK的初学者有所帮助(当然,倘若能互相交流共同进步,亦不失为一件美事),二是希望通过翻译此文,加深自己对FBX SDK的理解,为之后的工作打下基础。


- 本文不是FBX SDK的安装使用教程,关于此类入门内容,请参考官方教程或其他博客文章。本文假设读者对FBX文件的树形结构有基本的了解。
- 本文尽可能以通俗、易于理解的文字来翻译原文,此外,对于原文中讲解得不够详细的部分,我则在原文的基础上加以补充,这些部分通过引用的方式标出。
- 由于本人水平有限,文章内容难免有错误纰漏之处,如若读者能不吝告知,则不胜感激。

获取Mesh Data

对于FBX文件,我们一般从网格数据(Mesh Data)入手,因为一旦获取到网格数据,我们就可以将其导入到自己需要的引擎(如OpenGL)以还原模型了。

我们先遍历FBX文件中的网格数据,这样的好处是使得我们对如何获取所有网格数据有一个自上而下的直观理解。一开始你不需要知道这些函数的细节,但是你需要知道我们是如何获取三角面片的顶点的(笔者注:原文只提到三角面片,因为原文作者使用的模型只包含三角面片。但是从我的实践来看,部分模型会包含四角面片),我们将在后面详细介绍这些函数。

代码如下(中文注解是我自行添加的,原文英文注释不变):

void FBXExporter::ProcessMesh(FbxNode* inNode)
{FbxMesh* currMesh = inNode->GetMesh();  //获取当前结点的网格mTriangleCount = currMesh->GetPolygonCount();   //获取Polygon的个数。注意此处的变量命名是mTriangleCount(三角形的个数),事实上在实践中可能会有四角面片,该命名可能会给读者误解int vertexCounter = 0;mTriangles.reserve(mTriangleCount);     //根据上下文推测mTriangles的类型是vector<Triangle>for (unsigned int i = 0; i < mTriangleCount; ++i){XMFLOAT3 normal[3];     //法线,XMFLOAT3应该是原作者自定义的类型,可以用FBX SDK的FBXVector4类型来替换XMFLOAT3 tangent[3];    //切线XMFLOAT3 binormal[3];   //次法线XMFLOAT2 UV[3][2];      //UV坐标,用于UV贴图Triangle currTriangle;mTriangles.push_back(currTriangle);for (unsigned int j = 0; j < 3; ++j){int ctrlPointIndex = currMesh->GetPolygonVertex(i, j);CtrlPoint* currCtrlPoint = mControlPoints[ctrlPointIndex];//自定义类型,用于保存控制点信息ReadNormal(currMesh, ctrlPointIndex, vertexCounter, normal[j]); //读取当前面片的法向量// We only have diffuse texturefor (int k = 0; k < 1; ++k){ReadUV(currMesh, ctrlPointIndex, currMesh->GetTextureUVIndex(i, j), k, UV[j][k]);  //读取UV贴图信息}PNTIWVertex temp;   //原作者自定义的类型,根据上下文推测为结构体,用于保存顶点的坐标、法向量和UV坐标等信息temp.mPosition = currCtrlPoint->mPosition;temp.mNormal = normal[j];temp.mUV = UV[j][0];// Copy the blending info from each control pointfor(unsigned int i = 0; i < currCtrlPoint->mBlendingInfo.size(); ++i){VertexBlendingInfo currBlendingInfo;currBlendingInfo.mBlendingIndex = currCtrlPoint->mBlendingInfo[i].mBlendingIndex;currBlendingInfo.mBlendingWeight = currCtrlPoint->mBlendingInfo[i].mBlendingWeight;temp.mVertexBlendingInfos.push_back(currBlendingInfo);}// Sort the blending info so that later we can remove// duplicated verticestemp.SortBlendingInfoByWeight();mVertices.push_back(temp);mTriangles.back().mIndices.push_back(vertexCounter);//设置该三角面片数组的最后一个元素的索引值++vertexCounter;}}// Now mControlPoints has served its purpose// We can free its memoryfor(auto itr = mControlPoints.begin(); itr != mControlPoints.end(); ++itr){delete itr->second;}mControlPoints.clear();
}

在讲解代码之前,首先要知道FBX文件是怎么保存mesh信息的,这涉及到Control Point(控制点)以及Polygon Vertex(几何顶点)

笔者注:

原文对Control Point的讲解只有寥寥数句,也没有提及Polygon Vertex,这里本人对其加以扩展,希望对不清楚这两者的读者有所帮助。
- Control Point。Control Point指的是模型几何意义上的顶点,例如四边形有4个控制点,立方体有8个控制点等等。
- Polygon Vertex。Polygon Vertex则指示了该模型是怎么构成的。例如,假设一个四边形由A、B、C、D共4个Control Point构成,那么:

若该四边形由两个三角面片(ABD和BCD)构成,则有以下结论:1)构成该四边形的Polygon是三角形;2)该四边形的Polygon Count为2(因为由2个三角面片构成);3)该四边形的Polygon Size为3(因为是三角形);4)该四边形的Polygon Vertex Count为6(分别为A、B、D、B、C、D)。

若该四边形由一个四角面片(ABCD)构成,则有以下结论:1)构成该四边形的Polygon是四边形;2)该四边形的Polygon Count为1(因为由1个四角面片构成);3)该四边形的Polygon Size为4(因为是四角形);4)该四边形的Polygon Vertex Count为4(分别为A、B、C、D)。

若考虑由四角面片构成的立方体,则有:Control Point Count为8,由于立方体共6个面片,每个面片由一个四边形(即Polygon Size为4)构成,则Polygon Vertex Count为4 * 6 = 24

不失一般性,对于任一模型,有:Polygon Vertex Count = (Polygon Count) * (Polygon Size)

原代码如下:

// inNode is the Node in this FBX Scene that contains the mesh
// this is why I can use inNode->GetMesh() on it to get the mesh
void FBXExporter::ProcessControlPoints(FbxNode* inNode)
{FbxMesh* currMesh = inNode->GetMesh();unsigned int ctrlPointCount = currMesh->GetControlPointsCount();    //获取控制点个数for(unsigned int i = 0; i < ctrlPointCount; ++i)    //遍历所有控制点{CtrlPoint* currCtrlPoint = new CtrlPoint();XMFLOAT3 currPosition;  //记录每一个控制点的x、y、z坐标currPosition.x = static_cast<float>(currMesh->GetControlPointAt(i).mData[0]);currPosition.y = static_cast<float>(currMesh->GetControlPointAt(i).mData[1]);currPosition.z = static_cast<float>(currMesh->GetControlPointAt(i).mData[2]);currCtrlPoint->mPosition = currPosition;mControlPoints[i] = currCtrlPoint;}
}

笔者注:

上述代码用于获取当前结点的mesh中每一个Control Point的坐标,其实可以用更为通用的方式来遍历Control Point,见下:

void FBXExporter::ProcessControlPoints(FbxNode* inNode)
{vector<FBXVector4> ctrlPoints;FbxMesh* currMesh = inNode->GetMesh();unsigned int ctrlPointCount = currMesh->GetControlPointsCount();    //获取控制点个数ctrlPoints.reserve(ctrlPointCount);for(unsigned int i = 0; i < ctrlPointCount; ++i)    //遍历所有控制点{FBXVector4 ctrlPoint = currMesh->GetControlPointAt(i);ctrlPoints.push_back(ctrlPoint);}//process all of the control points through ctrPoints
}

获取到mesh的Control Point和Polygon Vertex后,就可以根据这些信息绘制模型了。但是目前得到的仅仅是坐标信息,也就是说,我们已经知道了模型的外观,但是其法线、贴图等信息都没获取到。而要想获取到 UVs, Normals, Tangents, Binormals等信息,就要先知道这些信息放在哪里。

FBX使用“Layer”来保存这些信息。我们可以想象有一个纸盒,然后我们用彩纸包装这个纸盒,那么纸盒的形状就是模型的外观(表面信息),彩纸就是模型的“Layer”,我们可以在这层Layer中获取到UVs, Normals, Tangents, Binormals等信息。

但是,如何将Control Point与Layer中包含的信息联系到一起呢?不失一般性,我们以读取Normal的函数ReadNormal为例,介绍读取每个顶点的法向量的过程。首先请看该函数的参数:
- FbxMesh* inMesh: 我们所要读取信息的mesh。
- int inCtrlPointIndex:Control Point的索引,我们通过该参数将Layer中的信息和Polygon Vertex联系到一起。
- int inVertexCounter: 当前正在处理的Polygon Vertex的索引
- XMFLOAT3& outNormal: 输出的normal,传递引用以在函数内部更新normal的值。

笔者注

读者可能会对inCtrlPointIndex和inVertexCounter有疑惑:这两个参数的含义不是一样的吗?

对于这两个参数,由于原文没有提及Polygon Vertex的概念,因此解释得不够详细,我一开始也有此困惑。根据我的理解,原文中的vertices一词,在部分语境下就是指Polygon Vertex,而在另外的语境下又似乎可以和Control Point同义,这不免产生二义性,为此我试图另作他解,而没有参照原文。

对于inCtrlPointIndex,指的是我们当前遍历到的Control Point的索引,因此inCtrlPointIndex最终的值就是Control Point的个数。假设inCtrlPointIndex = 7,则表明我们现在遍历的是第8个(假设索引从0开始)Control Point。

对于inVertexCounter,指的是当前遍历到的Polygon Vertex的索引(Control Point和Polygon Vertex的区别参见前文)。我是在调试的时候才知道它是Polygon Vertex的索引的。我在对多个模型调试后总结得出:inVertexCount最终的值= (Polygon Size) * (Polygon Count)。假设我们在遍历一个由三角面片构成的模型,那么当inVertexCounter = 7时,则表示我们在遍历第3个Polygon的第2个Polygon Vertex。

void FBXExporter::ReadNormal(FbxMesh* inMesh, int inCtrlPointIndex, int inVertexCounter, XMFLOAT3& outNormal)
{if(inMesh->GetElementNormalCount() < 1){throw std::exception("Invalid Normal Number");}FbxGeometryElementNormal* vertexNormal = inMesh->GetElementNormal(0);   //读取mesh的第一层Layer中保存的法向量信息switch(vertexNormal->GetMappingMode())  {case FbxGeometryElement::eByControlPoint:switch(vertexNormal->GetReferenceMode()){case FbxGeometryElement::eDirect:{outNormal.x = static_cast<float>(vertexNormal->GetDirectArray().GetAt(inCtrlPointIndex).mData[0]);outNormal.y = static_cast<float>(vertexNormal->GetDirectArray().GetAt(inCtrlPointIndex).mData[1]);outNormal.z = static_cast<float>(vertexNormal->GetDirectArray().GetAt(inCtrlPointIndex).mData[2]);}break;case FbxGeometryElement::eIndexToDirect:{int index = vertexNormal->GetIndexArray().GetAt(inCtrlPointIndex);outNormal.x = static_cast<float>(vertexNormal->GetDirectArray().GetAt(index).mData[0]);outNormal.y = static_cast<float>(vertexNormal->GetDirectArray().GetAt(index).mData[1]);outNormal.z = static_cast<float>(vertexNormal->GetDirectArray().GetAt(index).mData[2]);}break;default:throw std::exception("Invalid Reference");}break;case FbxGeometryElement::eByPolygonVertex:switch(vertexNormal->GetReferenceMode()){case FbxGeometryElement::eDirect:{outNormal.x = static_cast<float>(vertexNormal->GetDirectArray().GetAt(inVertexCounter).mData[0]);outNormal.y = static_cast<float>(vertexNormal->GetDirectArray().GetAt(inVertexCounter).mData[1]);outNormal.z = static_cast<float>(vertexNormal->GetDirectArray().GetAt(inVertexCounter).mData[2]);}break;case FbxGeometryElement::eIndexToDirect:{int index = vertexNormal->GetIndexArray().GetAt(inVertexCounter);outNormal.x = static_cast<float>(vertexNormal->GetDirectArray().GetAt(index).mData[0]);outNormal.y = static_cast<float>(vertexNormal->GetDirectArray().GetAt(index).mData[1]);outNormal.z = static_cast<float>(vertexNormal->GetDirectArray().GetAt(index).mData[2]);}break;default:throw std::exception("Invalid Reference");}break;}
}

上述代码主要由两个switch语句构成,第一个为MappingMode(),该函数返回值是一个enum类型,我们只需关注FbxGeometryElement::eByControlPoint 和 FbxGeometryElement::eByPolygonVertex即可。

由前所述,Control Point就是模型几何意义上的三维坐标点,例如,一个立方体由8个控制点组成。但是为了让它看起来“像一个立方体”,每个Control Point可能对应着多条法线。因为如果你想让立方体的边界变得“尖锐”,就必须要给每一个顶点设置多个法向量,例如,一个顶点可能参与了3个面片的构成,那么我们就需要给每个顶点设置3个向量,这时候Polygon Vertex就派上用场了。

综上,如果你的模型没有“锐利”的边缘,就应该使用eByControlPoint ,此时每个Control Point对应1条法线;反之,则应该使用eByPolygonVertex,此时每个Control Point都对应多条法线,或者说每个Polygon Vertex都对应着1条法线。所以当MappingMode为eByControlPoint时,我们可以通过inCtrlPointIndex来读取法向量;当MappingMode为eByPolygonVertex时,就可以通inVertexCounter来读取每一个Polygon Vertex的法向量。

接下来看第二个switch语句:ReferenceMode()。ReferenceMode是FBX文件所做的一种优化技术,类似于计算机图形学中的index buffer(索引缓冲)(笔者注:可能是用于降低计算量)。

  • FbxGeometryElement::eDirect。即直接通过索引来读取Control Point或Polygon Vertex所对应的法向量。
  • FbxGeometryElement::eIndexToDirect。即通过inCtrlPointIndex或inVertexCounter所得到的是指向实际法向量的索引,通过该索引我们才能获取实际的法向量(类比二级指针)。

    获取指向法向量的索引的代码如下:

    int index = vertexNormal->GetIndexArray().GetAt(inVertexCounter);

获取到该索引后,就可以通过该索引读取法向量了。

提取FBX文件中mesh的信息相关推荐

  1. 利用Python提取PDF文件中的文本信息

    如何利用Python提取PDF文件中的文本信息 日常工作中我们经常会用到pdf格式的文件,大多数情况下是浏览或者编辑pdf信息,但有时候需要提取pdf中的文本,如果是单个文件的话还可以通过复制粘贴来直 ...

  2. 提取PDF文件中的文本信息

    转载请注明出处:http://blog.csdn.net/xiaojimanman/article/details/43527755 我们从网上下载的PDF文件有的是加密处理过的,无法复制其中的内容, ...

  3. 提取EPSON机器人示教点位pts文件中的点位信息

    由于操作需要,写了一个程序将爱普生机器人(EPSON)的pts文件中的点位信息进行提取,如下: package mainimport ("bufio""encoding/ ...

  4. gff文件_如何提取gff文件中的基因注释信息

    原标题:如何提取gff文件中的基因注释信息 gff3格式注释文件是最常见的基因注释,(https://archive.broadinstitute.org/annotation/argo/help/g ...

  5. python读json文件中不同的数据类型_怎么使用python提取json文件中的字段

    python中为什么用json有什么作用 python的json模块中如何将变量添加到里面 python的json模块第一个是要打开的文件,第二个是打开的操作,为什么会如果你早认清你在别人心中没那么重 ...

  6. Tips--利用shell脚本批量提取txt文件中任意字段

    利用shell脚本批量提取txt文件中任意字段 前言 0. 一个例子 1. cat命令 2. '|'符号与'>'符号 3. grep命令 4. awk命令 前言 对于测试中出现的log,我们经常 ...

  7. Python办公自动化——提取pdf文件中表格并到Excel

    Python办公自动化--提取pdf文件中表格合并到Excel 需求描述 现有一 pdf 文件内容如下,文件中内容主要是表格形式的获奖名单,共158页.现要读取这些表格信息并保存到 excel 文件中 ...

  8. python提取xml文件中的坐标点(labelimg标记文档)

    LabelImg是深度学习中用来标注图片中物体位置与名称的工具,LabelImg标记数据的xml文档也比较简洁明了. 标记图片: 保存后生成的xml文件: Python提取文档中的标记信息(坐标信息& ...

  9. Python脚本工具,PyMuPDF批量提取PDF文件中的图片

    如何批量快速提取出PDF中的图片文件,你是否遇到这样的一个问题,尤其是PPT文件转换为PDF文件,需要快速提取其中的图片文件,如果你恰好会那么一点py,同时复制粘贴没问题的话,那么相信你也能够很轻松的 ...

最新文章

  1. java 配置及Eclipse安装
  2. 跟我一起写 Makefile(七)
  3. 第六集 MSF构思阶段项目团队的组建
  4. linux 桌面显示视频播放器,Ubuntu 13.10开启媒体播放器VLC桌面通知的步骤
  5. php带参数跳转页面,如何带参数跳转php界面_后端开发
  6. 面试题17: 打印从1到最大的n位数
  7. 记录:Linux 设置文件夹 0777 权限失效问题
  8. 2021泰迪杯B题数据处理4.1
  9. 安卓机被锁屏的6种解锁方法
  10. 【游戏运营】【笔记】 谈谈对游戏运营的理解
  11. 【白皮书】以太坊 (Ethereum ):下一代智能合约和去中心化应用平台
  12. qq邮箱smtp服务器imap,如何配置电子邮件客户端使用IMAP(QQ邮箱账户) 你需要学习了...
  13. 烤仔TVのCCW丨密码学通识(一)密码学基础及常见误区
  14. 夏季养生:夏季养生必备五种中药材
  15. LDdecay计算和做图
  16. 微信小程序 个人收支理财记账本小程序Android hbuilderx App毕业设计
  17. FFmpeg实现音频解码并播放
  18. 学习笔记(1):零基础掌握 Python 入门到实战-用Python操作SQLite数据库
  19. 北航计算机和人大统计学,大学计算机学科排名,清华北大谁是第一,北航表现又如何...
  20. mysql错误码为1045_mysql错误代码1045的原因及解决方案

热门文章

  1. linux文件权限管理实验心得,Linux+文件权限管理实验
  2. HTML+CSS好看的小黄人网页制作(首页部分)
  3. apache kafka技术分享系列(目录索引)
  4. 汉字转拼音和简拼工具类分享
  5. HTML表格,列表,超链接,图片
  6. 大数据面试重点之kafka(七)
  7. 星巴克——starbuck
  8. 从架构到算法到赋能业务,关于国际化电商技术链路的最完整分享【Lazada技术开放日】
  9. 虎年开工第一天,你实现下班自由了吗?
  10. 笔试题——硬币与金币(概率)