FBX SDK开发快速上手指南

一段时间以来,我一直想制作一个 FBX Exporter 将 FBX 文件转换为我自己的格式。 整个过程不是很顺利,主要是FBX的官方文档不是很清楚。 另外,由于 FBX 格式被许多应用程序使用,而不仅仅是游戏引擎,因此提供的示例代码没有使用我们在游戏开发中使用的更常见的技术术语。 我搜索了互联网上几乎所有的角落来澄清事情,以便我可以从 FBX SDK 的数据到我在游戏引擎中需要的数据有一个清晰的映射。

在这里插入图片描述

推荐:用 NSDT设计器 快速搭建可编程3D场景。

因为我认为没有人发布过关于如何将 FBX 文件转换为自定义格式的清晰而详尽的教程,所以我会这样做。 我希望这能帮助人们。 本教程将专门介绍游戏引擎。 基本上我会告诉读者如何获取他们的游戏引擎所需的数据。 对于诸如“如何初始化 FBX SDK”之类的内容,请自行查看示例代码,“ImportScene”示例在这方面非常有用。

如果你不了解骨骼动画如何工作以及需要哪些数据来使骨骼动画发生,请查看 Buckeye 的文章“使用矩阵的蒙皮网格动画”。 这会很有帮助。 链接在这里。

1、网格数据

你要做的第一件事是获取网格数据; 如果你可以将静态网格物体导入到引擎中,那感觉已经相当不错了。 为了使本节更加清晰,我选择首先向你展示如何遍历 FBX 文件中的网格。 这使我能够让你自上而下地了解收集网格数据需要做什么。

你不知道每个函数具体做什么,但应该知道我们正在遍历网格每个三角形上的 3 个顶点。 稍后我将回到每个功能。 请注意,有一些与动画混合信息相关的代码。 现在可以忽略它。 我们稍后会再讨论这个问题。

Exporter::ProcessMesh(FbxNode* inNode) 
{ FbxMesh* currMesh = inNode->GetMesh(); mTriangleCount = currMesh->GetPolygonCount(); int vertexCounter = 0; mTriangles.reserve(mTriangleCount); for (unsigned int i = 0; i < mTriangleCount; ++i) { XMFLOAT3 normal[3]; XMFLOAT3 tangent[3]; XMFLOAT3 binormal[3]; XMFLOAT2 UV[3][2]; 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]); } PNTIWVertex temp; 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.mBlendingIndex; currBlendingInfo.mBlendingWeight = currCtrlPoint->mBlendingInfo.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 如何存储有关网格的所有信息。 在 FBX 中,我们有术语控制点(Control Point),基本上控制点是一个物理顶点。 例如,你有一个立方体,那么你有 8 个顶点(Vertex)。 这8个顶点是FBX文件中仅有的8个“控制点”。 因此,如果需要,你可以互换使用“顶点”和“控制点”。 位置信息存储在控制点中。 以下代码将提供网格所有顶点的位置:

// 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; currPosition.x = static_cast(currMesh->GetControlPointAt(i).mData[0]); currPosition.y = static_cast(currMesh->GetControlPointAt(i).mData[1]); currPosition.z = static_cast(currMesh->GetControlPointAt(i).mData[2]); currCtrlPoint->mPosition = currPosition; mControlPoints = currCtrlPoint; } 
} 

然后你会问“我如何获得 UV、法线、切线、副法线?” 好吧,请想象一下这样的网格:你有网格的主体,但这只是它的几何形状。 该物体没有任何关于其表面的信息。 换句话说,你有这个形状,但你没有任何关于这个形状的表面看起来如何的信息。 FBX 引入了这种层(Layer)的感觉,它覆盖了网格体的主体。

这就像你有一个盒子,然后用礼品纸包裹它。 这张礼品纸是FBX中网格的图层。 在图层中,你可以获取UV、法线、切线、副法线的信息。 然而,你可能已经问过我了。 如何将控制点与图层中的信息关联起来? 好吧,这是非常棘手的部分,请让我展示一些代码,然后逐行解释它。 不失一般性,我以 Binormal 为例:

在查看该函数之前,我们先回顾一下它的参数。

  • FbxMesh* inMesh:我们尝试导出的网格
  • int inCtrlPointIndex:控制点的索引。 我们需要这个,因为我们想要将图层信息与顶点(控制点)相关联
  • int inVertexCounter:这是我们正在处理的当前顶点的索引。
  • XMFLOAT3& outNormal:输出。 我们通过引用传递,以便我们可以在该函数内修改此变量并将其用作我们的输出看到这些参数后,你可能会问我“既然你说 控制带你基本上是 FBX SDK 中的顶点,为什么有 inCtrlPointIndex 和 inVertexCounter? 他们不是同一个东西吗?” 不,它们不一样。

正如我之前所解释的,控制点是几何体上的物理顶点。 让我们以四边形为例。 给定一个四边形(2 个三角形),有多少个控制点? 答案是 4。但是我们基于三角形的游戏引擎中有多少个顶点呢? 答案是 6,因为我们有 2 个三角形,每个三角形有 3 个顶点。 2 * 3 = 6 FBXSDK 的控制点和我们的顶点之间的主要区别在于,我们的顶点具有“每个三角形”的感觉,但 FBXSDK 的控制点没有。 我们将在下面的代码解释中回到这一点。 因此,如果你仍然对游戏引擎中 FBXSDK 的控制点和顶点没有非常清晰的了解,请不要担心。

要记住的一件事是,在该函数之外,我们使用循环来遍历该网格中所有三角形的所有顶点。 如果你感到困惑并且不知道“我们正在使用循环遍历此网格中所有三角形的所有顶点”是什么意思,请查看本文开头的内容。 这就是为什么我们可以有像 inCtrlPointIndex 和 inVertexCounter 这样的参数。

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); switch(vertexNormal->GetMappingMode()) { case FbxGeometryElement::eByControlPoint: switch(vertexNormal->GetReferenceMode()) { case FbxGeometryElement::eDirect: { outNormal.x = static_cast(vertexNormal->GetDirectArray().GetAt(inCtrlPointIndex).mData[0]); outNormal.y = static_cast(vertexNormal->GetDirectArray().GetAt(inCtrlPointIndex).mData[1]); outNormal.z = static_cast(vertexNormal->GetDirectArray().GetAt(inCtrlPointIndex).mData[2]); } break; case FbxGeometryElement::eIndexToDirect: { int index = vertexNormal->GetIndexArray().GetAt(inCtrlPointIndex); outNormal.x = static_cast(vertexNormal->GetDirectArray().GetAt(index).mData[0]); outNormal.y = static_cast(vertexNormal->GetDirectArray().GetAt(index).mData[1]); outNormal.z = static_cast(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(vertexNormal->GetDirectArray().GetAt(inVertexCounter).mData[0]); outNormal.y = static_cast(vertexNormal->GetDirectArray().GetAt(inVertexCounter).mData[1]); outNormal.z = static_cast(vertexNormal->GetDirectArray().GetAt(inVertexCounter).mData[2]); } break; case FbxGeometryElement::eIndexToDirect: { int index = vertexNormal->GetIndexArray().GetAt(inVertexCounter); outNormal.x = static_cast(vertexNormal->GetDirectArray().GetAt(index).mData[0]); outNormal.y = static_cast(vertexNormal->GetDirectArray().GetAt(index).mData[1]); outNormal.z = static_cast(vertexNormal->GetDirectArray().GetAt(index).mData[2]); } break; default: throw std::exception("Invalid Reference"); } break; } } 

好吧,这很长,但请不要害怕。 其实很简单。 这为我们获取了图层中的法线信息 FbxGeometryElementNormal* vertexNormal = inMesh->GetElementNormal(0);

第一个 switch 语句是关于 MappingMode() 的。 对于游戏引擎,我认为我们只需要考虑 FbxGeometryElement::eByControlPoint 和 FbxGeometryElement::eByPolygonVertex 。 让我解释一下这两种模式。

正如我所说,控制点基本上就是顶点。 然而,有一个问题。 尽管立方体有 8 个控制点,但如果你希望立方体看起来正确,则其法线将超过 8 个。 原因是,如果你有锐利的边缘,我们必须为同一控制点分配多个法线,以保证锐利的感觉。 这就是我们的游戏引擎中的顶点概念出现的时候,因为即使立方体的顶点具有相同的位置,在游戏引擎中,你也很可能最终得到 3 个位置相同但 3 个顶点 不同的法线。

因此,当你没有锋利边缘的情况时, FbxGeometryElement::eByControlPoint 因此每个控制点只有一个法线。 FbxGeometryElement::eByPolygonVertex 是当你有锐边并且需要获取每个面上每个顶点的法线时,因为每个面都为同一控制点分配了不同的法线。

因此, FbxGeometryElement::eByControlPoint 意味着我们可以通过控制点的索引来精确定位控制点的法线,而 FbxGeometryElement::eByPolygonVertex 意味着我们可以通过顶点的索引来精确定位面上顶点的法线。

这是一个更具体、更深入的例子,说明了FBX SDK的控制点和顶点在游戏引擎中的区别以及为什么当我谈论这个函数的参数时,我说我们必须同时传入 inCtrlPointIndex和 inVertexCounter。 因为我们不知道需要哪一个来获取我们需要的信息,所以我们最好将两者都传入。

现在我们有另一个 switch 语句嵌套在里面,我们在 ReferenceMode() 上“切换”。 这是 FBX 正在做的某种优化,与计算机图形学中的索引缓冲区相同。 你不想多次使用相同的 Vector3; 相反,可以使用其索引来引用它。

FbxGeometryElement::eDirect 意味着你可以直接使用控制点索引或面顶点索引来引用我们的法线。

FbxGeometryElement::eIndexToDirect 意味着使用控制点索引或面顶点索引只会给我们一个指向我们想要的法线的索引,我们必须使用这个索引来找到实际的法线。 这行代码为我们提供了所需的索引 :

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

这些是提取网格的位置和“层”信息的主要步骤。

现在我们转向动画,这是 FBX 导出的难点。

2、动画数据

因此,让我们考虑一下 FBX 需要什么才能使动画在我们的渲染器(游戏引擎)中正常工作。

  • 骨骼层次结构。 哪个关节是哪个关节的父关节
  • 对于每个顶点,我们需要 4 个 SkinningWeight-JointIndex 对
  • 每个关节的绑定姿势矩阵,用于计算全局绑定姿势的逆矩阵
  • 时间 t 时的变换矩阵,以便我们可以将网格变换为该姿势以实现动画

获得骨骼层次结构非常简单:基本上,我们从场景的根节点执行递归深度优先搜索,然后逐层深入。 节点是 FBX 场景的构建块。 FBX 文件中有许多节点,每种类型的节点都包含某种类型的信息。 如果一个节点是骨架类型,我们将它添加到我们的关节列表中,并且它的索引将只是列表的大小。

因此,我们可以保证父级的索引始终小于子级的索引。 如果你想要存储本地变换并手动计算子项在时间 t 的变换,这是必要的。 但如果你像我一样使用全局转换,你不一定需要这样。 注意:如果你不熟悉深度优先搜索的概念。

看看这个页面 和这个页面。

阅读这些页面后,你可能会问“为什么我们不需要跟踪访问过的节点?” 答案是:骨骼层次结构是一棵树,而不是一张图。

void FBXExporter::ProcessSkeletonHierarchy(FbxNode* inRootNode) 
{ for (int childIndex = 0; childIndex < inRootNode->GetChildCount(); ++childIndex) { FbxNode* currNode = inRootNode->GetChild(childIndex); ProcessSkeletonHierarchyRecursively(currNode, 0, 0, -1); } 
} // inDepth is not needed here, I used it for debug but forgot to remove it 
void FBXExporter::ProcessSkeletonHierarchyRecursively(FbxNode* inNode, int inDepth, int myIndex, int inParentIndex) 
{ if(inNode->GetNodeAttribute() && inNode->GetNodeAttribute()->GetAttributeType() && inNode->GetNodeAttribute()->GetAttributeType() == FbxNodeAttribute::eSkeleton) { Joint currJoint; currJoint.mParentIndex = inParentIndex; currJoint.mName = inNode->GetName(); mSkeleton.mJoints.push_back(currJoint); } for (int i = 0; i < inNode->GetChildCount(); i++) { ProcessSkeletonHierarchyRecursively(inNode->GetChild(i), inDepth + 1, mSkeleton.mJoints.size(), myIndex); } 
} 

现在我们需要获取每个顶点的 SkinningWeight-JointIndex 对。 不幸的是,我的动画代码不是很干净,所以下面的函数同时执行步骤 2、3、4。 我将仔细检查代码,请不要失去耐心。 这主要是因为 FBX 存储信息的方式阻止我有效地在单独的函数中获取数据。 如果我想分离我的代码,我需要多次遍历相同的数据。

在看任何代码之前,请让我解释一下 FBX SDK 中使用的术语。 我认为这是大多数人感到困惑的部分,因为 FBX SDK 的关键字与我们(游戏开发人员)的关键字不匹配。 在FBX中,有一个叫做变形器(Deformer)的东西。 我将变形器视为使网格变形的一种方式。

在 Maya 中,你可以使用骨骼变形器,但也可以使用约束(Constraints)来使网格变形。 我认为你可以将“变形器”视为网格的整个骨架。 在每个“变形器”(我认为通常网格只有一个)内,都有簇(Cluster)。 每个簇既是又不是一个关节……你可以把一个簇看成一个关节,但实际上,每个簇内部都有一个“链接”。 这个“链接”实际上是真正的关节,它包含了我需要的有用信息。 现在我们深入研究代码:

void FBXExporter::ProcessJointsAndAnimations(FbxNode* inNode) 
{ FbxMesh* currMesh = inNode->GetMesh(); unsigned int numOfDeformers = currMesh->GetDeformerCount(); // This geometry transform is something I cannot understand // I think it is from MotionBuilder // If you are using Maya for your models, 99% this is just an // identity matrix // But I am taking it into account anyways...... FbxAMatrix geometryTransform = Utilities::GetGeometryTransformation(inNode); // A deformer is a FBX thing, which contains some clusters // A cluster contains a link, which is basically a joint // Normally, there is only one deformer in a mesh for (unsigned int deformerIndex = 0; deformerIndex < numOfDeformers; ++deformerIndex) {// There are many types of deformers in Maya, // We are using only skins, so we see if this is a skin FbxSkin* currSkin = reinterpret_cast(currMesh->GetDeformer(deformerIndex, FbxDeformer::eSkin)); if (!currSkin) { continue; } unsigned int numOfClusters = currSkin->GetClusterCount(); for (unsigned int clusterIndex = 0; clusterIndex < numOfClusters; ++clusterIndex) { FbxCluster* currCluster = currSkin->GetCluster(clusterIndex); std::string currJointName = currCluster->GetLink()->GetName(); unsigned int currJointIndex = FindJointIndexUsingName(currJointName); FbxAMatrix transformMatrix; FbxAMatrix transformLinkMatrix; FbxAMatrix globalBindposeInverseMatrix; currCluster->GetTransformMatrix(transformMatrix); // The transformation of the mesh at binding time currCluster->GetTransformLinkMatrix(transformLinkMatrix); // The transformation of the cluster(joint) at binding time from joint space to world space globalBindposeInverseMatrix = transformLinkMatrix.Inverse() * transformMatrix * geometryTransform; // Update the information in mSkeleton mSkeleton.mJoints[currJointIndex].mGlobalBindposeInverse = globalBindposeInverseMatrix; mSkeleton.mJoints[currJointIndex].mNode = currCluster->GetLink(); // Associate each joint with the control points it affects unsigned int numOfIndices = currCluster->GetControlPointIndicesCount(); for (unsigned int i = 0; i < numOfIndices; ++i) { BlendingIndexWeightPair currBlendingIndexWeightPair; currBlendingIndexWeightPair.mBlendingIndex = currJointIndex; currBlendingIndexWeightPair.mBlendingWeight = currCluster->GetControlPointWeights(); mControlPoints[currCluster->GetControlPointIndices()]->mBlendingInfo.push_back(currBlendingIndexWeightPair); } // Get animation information // Now only supports one take FbxAnimStack* currAnimStack = mFBXScene->GetSrcObject(0); FbxString animStackName = currAnimStack->GetName(); mAnimationName = animStackName.Buffer(); FbxTakeInfo* takeInfo = mFBXScene->GetTakeInfo(animStackName); FbxTime start = takeInfo->mLocalTimeSpan.GetStart(); FbxTime end = takeInfo->mLocalTimeSpan.GetStop(); mAnimationLength = end.GetFrameCount(FbxTime::eFrames24) - start.GetFrameCount(FbxTime::eFrames24) + 1; Keyframe** currAnim = &mSkeleton.mJoints[currJointIndex].mAnimation; for (FbxLongLong i = start.GetFrameCount(FbxTime::eFrames24); i <= end.GetFrameCount(FbxTime::eFrames24); ++i) { FbxTime currTime; currTime.SetFrame(i, FbxTime::eFrames24);*currAnim = new Keyframe(); (*currAnim)->mFrameNum = i; FbxAMatrix currentTransformOffset = inNode->EvaluateGlobalTransform(currTime) * geometryTransform; (*currAnim)->mGlobalTransform = currentTransformOffset.Inverse() * currCluster->GetLink()->EvaluateGlobalTransform(currTime); currAnim = &((*currAnim)->mNext); } } } // Some of the control points only have less than 4 joints // affecting them. // For a normal renderer, there are usually 4 joints // I am adding more dummy joints if there isn't enough BlendingIndexWeightPair currBlendingIndexWeightPair; currBlendingIndexWeightPair.mBlendingIndex = 0; currBlendingIndexWeightPair.mBlendingWeight = 0; for(auto itr = mControlPoints.begin(); itr != mControlPoints.end(); ++itr) { for(unsigned int i = itr->second->mBlendingInfo.size(); i <= 4; ++i) { itr->second->mBlendingInfo.push_back(currBlendingIndexWeightPair); } } 
} 

一开始我有这个:

// This geometry transform is something I cannot understand 
// I think it is from MotionBuilder 
// If you are using Maya for your models, 99% this is just an 
// identity matrix 
// But I am taking it into account anyways...... 
FbxAMatrix geometryTransform = Utilities::GetGeometryTransformation(inNode); 

嗯,这是我在 FBX SDK 论坛上看到的。 那里的官员告诉我们应该考虑“几何变换”。 但根据我的经验,大多数时候,这个“GeometricTransform”只是一个单位矩阵。 无论如何,要获得这个“GeometricTransform”,请使用以下函数:

FbxAMatrix Utilities::GetGeometryTransformation(FbxNode* inNode) 
{ if (!inNode) { throw std::exception("Null for mesh geometry"); } const FbxVector4 lT = inNode->GetGeometricTranslation(FbxNode::eSourcePivot); const FbxVector4 lR = inNode->GetGeometricRotation(FbxNode::eSourcePivot); const FbxVector4 lS = inNode->GetGeometricScaling(FbxNode::eSourcePivot); return FbxAMatrix(lT, lR, lS); 
} 

这段代码中最重要的事情是如何获得每个关节的全局绑定姿势的倒数。 这部分非常棘手并且搞砸了很多人。 我将详细解释这一点。

FbxAMatrix transformMatrix; 
FbxAMatrix transformLinkMatrix; 
FbxAMatrix globalBindposeInverseMatrix;
currCluster->GetTransformMatrix(transformMatrix); // The transformation of the mesh at binding time 
currCluster->GetTransformLinkMatrix(transformLinkMatrix); // The transformation of the cluster(joint) at binding time from joint space to world space 
globalBindposeInverseMatrix = transformLinkMatrix.Inverse() * transformMatrix * geometryTransform; // Update the information in mSkeleton 
mSkeleton.mJoints[currJointIndex].mGlobalBindposeInverse = globalBindposeInverseMatrix;  

那么我们就从这个 GetTransformMatrix开始吧。 TransformMatrix 实际上是一个遗留的东西。 它是整个网格在绑定时的全局变换,并且所有簇都具有完全相同的变换矩阵。 如果你的艺术家有良好的习惯,并且在装配模型之前,他们在模型的所有通道上“冻结变换”,则不需要此矩阵。 如果你的艺术家执行“冻结变换”,那么这个矩阵将只是一个单位矩阵。 现在我们继续 GetTransformLinkMatrix。 这就是动画导出代码的本质。 这是 Maya 中绑定时簇(关节)从关节空间到世界空间的变换。

现在我们已经准备好了,我们可以得到每个关节的全局绑定姿势的逆。 我们最终想要的是下面代码中的 InverseOfGlobalBindPoseMatrix:

VertexAtTimeT = TransformationOfPoseAtTimeT * InverseOfGlobalBindPoseMatrix * VertexAtBindingTime

为了得到这个,我们这样做:

transformLinkMatrix.Inverse() * transformMatrix * GeometryTransform

现在我们距离动画还有两步。 我们需要获取每个顶点的 SkinningWeight-JointIndex 对,并且仍然需要获取动画中不同时间的变换。让我们首先处理 SkinningWeight-JointIndex 对。 在我们的游戏引擎中,我们有这样的关系: Vertex -> 4 SkinningWeight-JointIndex 对。

然而,在 FBX SDK 中,这种关系是相反的。 每个簇都有一个其影响的所有控制点(顶点)及其影响程度的列表。 下面的代码以我们喜欢的格式获取关系,但请记住,当我处理控制点时,我根据控制点的索引将所有控制点存储到地图中。 这就是我们可以获利的地方。

有了这个映射,我们可以在 O(1) 内查找并更新集群影响的控制点。

// Associate each joint with the control points it affects 
unsigned int numOfIndices = currCluster->GetControlPointIndicesCount(); 
for (unsigned int i = 0; i < numOfIndices; ++i) 
{ BlendingIndexWeightPair currBlendingIndexWeightPair; currBlendingIndexWeightPair.mBlendingIndex = currJointIndex; currBlendingIndexWeightPair.mBlendingWeight = currCluster->GetControlPointWeights(); mControlPoints[currCluster->GetControlPointIndices()]->mBlendingInfo.push_back(currBlendingIndexWeightPair); 
} 

现在我们只需要拼图中的最后一块:动画中时间 t 的变换。 请注意,这部分是我做得不好的地方,我的方式不是很优化,因为我得到了每个关键帧。 理想情况下应该做的是获取密钥并在它们之间进行插值,但我想这是空间和速度之间的权衡。 另外,我没有认真研究 FBX 的动画层次结构。 实际上,FBX 文件中存储了一条动画曲线,通过一些工作,你可以访问它并获得所需的精简和干净。

// Now only supports one take 
FbxAnimStack* currAnimStack = mFBXScene->GetSrcObject(0); 
FbxString animStackName = currAnimStack->GetName(); 
mAnimationName = animStackName.Buffer(); 
FbxTakeInfo* takeInfo = mFBXScene->GetTakeInfo(animStackName); 
FbxTime start = takeInfo->mLocalTimeSpan.GetStart(); 
FbxTime end = takeInfo->mLocalTimeSpan.GetStop(); 
mAnimationLength = end.GetFrameCount(FbxTime::eFrames24) - start.GetFrameCount(FbxTime::eFrames24) + 1; 
Keyframe** currAnim = &mSkeleton.mJoints[currJointIndex].mAnimation; for (FbxLongLong i = start.GetFrameCount(FbxTime::eFrames24); i <= end.GetFrameCount(FbxTime::eFrames24); ++i) 
{ FbxTime currTime; currTime.SetFrame(i, FbxTime::eFrames24); *currAnim = new Keyframe(); (*currAnim)->mFrameNum = i; FbxAMatrix currentTransformOffset = inNode->EvaluateGlobalTransform(currTime) * geometryTransform; (*currAnim)->mGlobalTransform = currentTransformOffset.Inverse() * currCluster->GetLink()->EvaluateGlobalTransform(currTime); currAnim = &((*currAnim)->mNext); 
} 

这部分非常简单 - 唯一需要注意的是 Maya 目前不支持多镜头动画(也许 MotionBuilder 支持)。 我会根据有多少人阅读这篇文章来决定是否写导出材料,但这很简单,可以通过“ImportScene”示例来学习

3、DirectX 和 OpenGL 转换

我对此 FBX 导出器的目标是提供一种从 FBX 文件中提取数据的方法,并以自定义格式输出数据,以便阅读器的渲染器可以获取数据并渲染它。 渲染器内部不需要任何转换,因为所有转换工作都落在导出器本身上。 在我说任何内容之前,我需要澄清一下,只有当你在 Maya 中制作模型/动画并使用其默认坐标系(X-Right、Y-Up、 Z-超出屏幕)。

如果你想将模型/动画导入OpenGL,那么更有可能你不需要执行任何额外的转换步骤,因为我认为默认情况下OpenGL具有与Maya相同的右手坐标系,即(X-Right, Y 向上,Z 超出屏幕)。 在FBX SDK的示例代码“ViewScene”中,没有对数据进行任何转换,它使用OpenGL作为渲染器,并使用OpenGL中的默认坐标系。 因此,如果你确实遇到麻烦,请查看该代码。

但是,如果你指定自己的坐标系,则可能需要进行一些转换。 现在是 DirectX 的时代了,我在网上看到大多数问题都来自于人们想要在 DirectX 中渲染 FBX 模型/动画的情况。 因此,如果你想将模型/动画导入 DirectX,很可能需要进行一些转换。

我只会解决左手“X-Right,Y-Up,Z-Into Screen”坐标系与背面剔除的情况,因为从我读到的帖子来看,大多数人在使用时都使用这个系统 DirectX。 这确实意味着任何一般性的事情; 这只是我的经验观察。 你需要执行以下操作将坐标从右手坐标系统“X-Right,Y-Up,Z-Out Of Screen”转换为左手“X-Right,Y-Up,Z-Into Screen”系统 :

  • 位置、法线、副法线、切线
  • 我们需要对 UV 向量的 Z 分量求反 ,使 V = 1.0f - V
  • 三角形的顶点顺序,从 Vertex0、Vertex1、Vertex2 更改为 Vertex0, Vertex2、Vertex1(基本上颠倒剔除顺序)矩阵
  • 获取矩阵的平移分量,对其 Z 分量取反
  • 获取矩阵的旋转分量,对其 X 和 Y 分量取反
  • 我认为如果你使用 XMMath 库,则不需要进行转置。

要使用我的转换方式,你需要分解矩阵并分别更改其平移、旋转和缩放。 幸运的是,FBX SDK 提供了分解矩阵的方法,只要你的矩阵是 FbxAMatrix(FBX 仿射矩阵)。 下面的示例代码向你展示了如何操作:

FbxAMatrix input; //Assume this matrix is the one to be converted. 
FbxVector4 translation = input.GetT(); 
FbxVector4 rotation = input.GetR(); 
translation.Set(translation.mData[0], translation.mData[1], -translation.mData[2]); // This negate Z of Translation Component of the matrix 
rotation.Set(-rotation.mData[0], -rotation.mData[1], rotation.mData[2]); // This negate X,Y of Rotation Component of the matrix 
// These 2 lines finally set "input" to the eventual converted result 
input.SetT(translation); 
input.SetR(rotation); 

如果你的动画有Scaling,你需要自己弄清楚需要做什么转换,因为我没有遇到过Scaling发生的情况。

4、局限性及超越

因此,本教程仅旨在帮助你开始使用 FBXSDK。 我自己是个菜鸟,所以我的很多技术可能效率很低。 在这里我把我认为存在的问题列出来。 在此过程中,读者可以自行决定是否使用我的技术以及需要注意什么。

  • 该转换方法仅适用于从 Maya 导出的具有 Maya 右手 X-Right、Y-Up、Z-Out 坐标系的模型/动画。 我的转换技术很可能不适用于其他建模软件(Blender、MotionBuilder、3ds Max)
  • 我提取动画的方式效率低下。 我需要在导出动画之前烘焙动画,然后以 24 帧/秒的速率获取所有关键帧。 这可能会导致巨大的内存消耗。 如果你知道如何使用关键帧而不是关键帧,请通过下面的评论告诉我。
  • 我的转换方法不处理动画中的缩放。 正如你从我的代码中看到的,当我提取动画时,我从不处理转换矩阵中的缩放分量。 因此,你需要自己弄清楚你的动画是否具有缩放功能。
  • 在本教程中,我没有包含删除重复顶点的代码,但实际上,如果你使用我的方法在不进行任何优化的情况下导出 FBX 文件,最终会得到很多重复顶点。 我做了一个比较,优化导出可以将文件大小减少 2/3…你会出现重复的原因是:如果你遍历网格中每个三角形的每个顶点,尽管相同的 Control 法线不同的点会很好处理,法线相同的同一个控制点会被计数1次以上!

原文链接:FBX SDK简明教程 — BimAnt

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/76891.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

什么是注意力机制?注意力机制的计算规则

我们观察事物时&#xff0c;之所以能够快速判断一种事物(当然允许判断是错误的)&#xff0c;是因为我们大脑能够很快把注意力放在事物最具有辨识度的部分从而作出判断&#xff0c;而并非是从头到尾的观察一遍事物后&#xff0c;才能有判断结果&#xff0c;正是基于这样的理论&a…

广州银行信用卡中心:强化数字引擎安全,实现业务稳步增长

广州银行信用卡中心是全国城商行中仅有的两家信用卡专营机构之一&#xff0c;拥有从金融产品研发至销售及后期风险控制、客户服务完整业务链条&#xff0c;曾获“2016年度最佳创新信用卡银行”。 数字引擎驱动业务增长 安全左移降低开发风险 近年来&#xff0c;广州银行信用卡…

java中使用Jsoup和Itext实现将html转换为PDF

1.在build.gradle中安装所需依赖&#xff1a; implementation group: com.itextpdf, name: itextpdf, version: 5.5.13 implementation group: com.itextpdf.tool, name: xmlworker, version: 5.5.13 implementation group: org.jsoup, name: jsoup, version: 1.15.32.创建工具…

贝业新兄弟:企业级应用在供应链物流领域的实践

一、老板的需求 先简单介绍一下我们公司&#xff0c;公司全称是贝业新兄弟&#xff0c;是一家供应链物流企业。现在我们服务的客户中有很多世界 500 强&#xff0c;比如科勒、惠氏、宜家等。我们公司的信息化分为两部分&#xff0c;一部分是核心业务系统OTWB&#xff0c;它是专…

智能卡通用安全检测指南 思度文库

范围 本标准规定了智能卡类产品进行安全性检测的一般性过程和方法。 本标准适用于智能卡安全性检测评估和认证。 规范性引用文件 下列文件对于本文件的应用是必不可少的。凡是注日期的引用文件&#xff0c;仅注日期的版本适用于本文件。凡是不注日期的引用文件&#xff0c;…

互联网宠物医院系统开发:数字化时代下宠物医疗的革新之路

随着人们对宠物关爱意识的提高&#xff0c;宠物医疗服务的需求也日益增加。传统的宠物医院存在排队等待、预约难、信息不透明等问题&#xff0c;给宠物主人带来了诸多不便。而互联网宠物医院系统的开发&#xff0c;则可以带来许多便利和好处。下面将介绍互联网宠物医院系统开发…

docker常用命令

docker常用命令 1.镜像与容器的关系2. 基本命令3. 容器操作4. 镜像操作5. 文件传输6. docker 登录与退出 1.镜像与容器的关系 镜像&#xff1a; 相当于一个类不可修改内容 容器&#xff1a; 对镜像类的实例&#xff0c;可以在环境中更新库容器可以保存为一个新的镜像再根据保存…

java+springboot+mysql法律咨询网

项目介绍&#xff1a; 使用javassmmysql开发的法律咨询网&#xff0c;系统包含超级管理员&#xff0c;系统管理员、用户角色&#xff0c;功能如下&#xff1a; 用户&#xff1a;主要是前台功能使用&#xff0c;包括注册、登录&#xff1b;查看法律领域&#xff1b;法律法规&a…

基于SpringBoot+Vue的在线考试系统设计与实现(源码+LW+部署文档等)

博主介绍&#xff1a; 大家好&#xff0c;我是一名在Java圈混迹十余年的程序员&#xff0c;精通Java编程语言&#xff0c;同时也熟练掌握微信小程序、Python和Android等技术&#xff0c;能够为大家提供全方位的技术支持和交流。 我擅长在JavaWeb、SSH、SSM、SpringBoot等框架…

推荐一款老化测试软件 Monitor.Analog

1. 数据采集模块&#xff1a; 该模块负责与下位机设备通信&#xff0c;实时采集模拟量数据。支持多种通信协议&#xff0c;如Modbus、OPC等&#xff0c;以适应不同类型的设备。数据采集模块还需要具备异常数据处理功能&#xff0c;例如数据丢失、错误数据等。 2. 数据存储模块…

HttpServletRequest和HttpServletResponse的获取与使用

相关笔记&#xff1a;【JavaWeb之Servlet】 文章目录 1、Servlet复习2、HttpServletRequest的使用3、HttpServletResponse的使用4、获取HttpServletRequest和HttpServletResponse 1、Servlet复习 Servlet是JavaWeb的三大组件之一&#xff1a; ServletFilter 过滤器Listener 监…

【maven】构建项目前clean和不clean的区别

其实很简单&#xff0c;但是百度搜了一下&#xff0c;还是没人能简单说明白。 搬用之前做C项目时总结结论&#xff1a; 所以自己在IDE里一遍遍测试程序能否跑通的时候&#xff0c;不需要clean&#xff0c;因为反正还要改嘛。 但是这个项目测试好了&#xff0c;你要打成jar包给…

Python系统学习1-3-变量,运算符

1、变量 变量&#xff1a;关联一个对象的标识符 学习目标&#xff1a;学会画变量的内存图 命名规则:字母数字下划线&#xff0c;所有单词小写&#xff0c;单词之间下划线隔开 赋值&#xff1a;创建一个变量或改变一个变量关联的数据。 语法&#xff1a;变量名数据&#xf…

carla中lka实现(一)

前言&#xff1a; 对于之前项目中工作内容进行总结&#xff0c;使用Carla中的车辆进行lka算法调试&#xff0c;整体技术路线&#xff1a; ①在Carla中生成车辆&#xff0c;并在车辆上搭载camera&#xff0c;通过camera采集图像数据&#xff1b; ②使用图像处理lka算法&#…

探索Streamlit中强大而灵活的 st.write() 函数(五):构建丰富多样的应用界面

文章目录 1 前言2 显示HTML的内容3 显示Markdown内容4 显示代码块5 显示DataFrame的交互式表格6 显示音频和视频7 显示图表8 显示图片9 显示地图10 显示PDF文件11 显示文件下载链接12 结语 1 前言 在这篇博文中&#xff0c;我们将着重介绍Streamlit中一个核心而重要的函数&…

Docker 容器化学习

文章目录 前言Docker架构 1、 docker安装2、启动docker服务3、设置docker随机器一起启动4、docker体验5、docker常规命令5.1、容器操作docker [run|start|stop|restart|kill|rm|pause|unpause]docker [ps|inspect|exec|logs|export|import] 5.2、镜像操作docker images|rmi|tag…

数据结构--图的遍历 DFS

数据结构–图的遍历 DFS 树的深度优先遍历 //树的先根遍历 void PreOrder(TreeNode *R) {if(R ! NULL){visit(R); //访问根节点while(R还有下一个子树T)PreOrder(T);//先根遍历下一棵子树} }图的深度优先遍历 bool visited [MAX_VERTEX_NUM]; //访问标记数组 void DFS(Grap…

【雕爷学编程】MicroPython动手做(31)——物联网之Easy IoT 2

1、物联网的诞生 美国计算机巨头微软(Microsoft)创办人、世界首富比尔盖茨&#xff0c;在1995年出版的《未来之路》一书中&#xff0c;提及“物物互联”。1998年麻省理工学院提出&#xff0c;当时被称作EPC系统的物联网构想。2005年11月&#xff0c;国际电信联盟发布《ITU互联网…

医学影像PACS系统源码:多功能服务器和阅片系统

PACS系统是以最新的IT技术为基础&#xff0c;遵循医疗卫生行业IHE/DICOM3.0和HL7标准&#xff0c;开发的多功能服务器和阅片系统。通过简单高性能的阅片功能&#xff0c;支持繁忙时的影像诊断业务&#xff0c;拥有保存影像的院内Web传输及离线影像等功能&#xff0c;同时具有备…

DP(背包模型)

01背包问题 有 N 件物品和一个容量是 V 的背包。每件物品只能使用一次。 第 i 件物品的体积是 vi&#xff0c;价值是 wi。 求解将哪些物品装入背包&#xff0c;可使这些物品的总体积不超过背包容量&#xff0c;且总价值最大。 输出最大价值。 输入格式 第一行两个整数&…