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 texture
for (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 point
for(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 vertices
temp.SortBlendingInfoByWeight();
mVertices.push_back(temp);
mTriangles.back().mIndices.push_back(vertexCounter);//设置该三角面片数组的最后一个元素的索引值
++vertexCounter;
}
}
// Now mControlPoints has served its purpose
// We can free its memory
for(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);
获取到该索引后,就可以通过该索引读取法向量了。