当前位置: 首页 > news >正文

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

相关文章:

FBX SDK开发快速上手指南

一段时间以来&#xff0c;我一直想制作一个 FBX Exporter 将 FBX 文件转换为我自己的格式。 整个过程不是很顺利&#xff0c;主要是FBX的官方文档不是很清楚。 另外&#xff0c;由于 FBX 格式被许多应用程序使用&#xff0c;而不仅仅是游戏引擎&#xff0c;因此提供的示例代码没…...

探讨|使用或不使用机器学习

动动发财的小手&#xff0c;点个赞吧&#xff01; 机器学习擅长解决某些复杂问题&#xff0c;通常涉及特征和结果之间的困难关系&#xff0c;这些关系不能轻易地硬编码为启发式或 if-else 语句。然而&#xff0c;在决定 ML 是否是当前给定问题的良好解决方案时&#xff0c;有一…...

Git笔记--Ubuntu上传本地项目到github

目录 1--基本配置 2--本地上传 1--基本配置 ① 创建ssh-key cd ~/.sshssh-keygen -t rsa -C "邮箱地址"② 查看并关联ssh-key gedit id_rsa.pub 复制内容&#xff0c;在 GitHub 中依次点击 Settings -> SSH and GPG keys -> New SSH key&#xff0c;将 id…...

基于Go编写一个可视化Navicat本地密码解析器

前提 开发小组在测试环境基于docker构建和迁移一个MySQL8.x实例&#xff0c;过程中大意没有记录对应的用户密码&#xff0c;然后发现某开发同事本地Navicat记录了根用户&#xff0c;于是搜索是否能够反解析Navicat中的密码掩码&#xff08;这里可以基本断定Navicat对密码是采用…...

Maven【入门笔记】

Maven 解决版本依赖的问题 https://www.liaoxuefeng.com/wiki/1252599548343744/1309301146648610 如果没有项目管理工具&#xff0c;在开发项目的时候&#xff0c;我们需要手动管理依赖包&#xff0c;需要管理依赖包的版本、去找到并下载依赖包、还有依赖包所依赖的包 等等。…...

Android Studio中使用cmake开发JNI实战

JNI学习大纲 一、JNI编程入门 二、Android Studio中使用cmake开发JNI实战 第一章节我们介绍了JNI的开发步骤&#xff0c;那这一章节我们就开始在Android Studio中实战一下吧&#xff0c;Lets Start。 1. Android Studio中安装CMake插件 AS中菜单栏选择Tools>SDK Manager在…...

第七章 图论

第七章 图论 一、数据结构定义 图的邻接矩阵存储法#define MaxVertexNum 100 // 节点数目的最大值// 无边权&#xff0c;只用0或1表示边是否存在 bool graph[MaxVertexNum][MaxVertexNum];// 有边权 int graph[MaxVertexNum][MaxVertexNum];图的邻接表存储法 把所有节点存储为…...

IEEE SystemVerilog Chapter13 : Tasks and functions (subroutines)

13.2 Overview 任务和函数提供了从描述中的几个不同位置执行通用过程的能力。它们还提供了一种将大型过程分解为小型过程的方法&#xff0c;以便更容易地阅读和调试源代码描述。本小节讨论了任务和函数之间的区别&#xff0c;描述了如何定义和调用任务和函数&#xff0c;并给出…...

day39反转字符串总结

反转字符串原理其实就是交换位置&#xff0c;以中间为分隔点&#xff1b; 基本套路&#xff1a;遍历前一般字符&#xff0c;互换位置&#xff1b; for循环模板 void reverseString(char* s, int sSize){char temp;for (int i 0, j sSize - 1; i < sSize/2; i, j--) {temp…...

使用Socket实现TCP版的回显服务器

文章目录 1. Socket简介2. ServerSocket3. Socket4. 服务器端代码5. 客户端代码 1. Socket简介 Socket&#xff08;Java套接字&#xff09;是Java编程语言提供的一组类和接口&#xff0c;用于实现网络通信。它基于Socket编程接口&#xff0c;提供了一种简单而强大的方式来实现…...

【Nacos篇】Nacos基本操作及配置

官方文档&#xff1a;https://nacos.io/zh-cn/docs/v2/ecology/use-nacos-with-spring-cloud.html 前置条件&#xff1a;SpringCloud脚手架 单机模式下的Nacos控制台&#xff1a; <dependencies><!-- Registry 注册中心相关 --><dependency><groupId>…...

Dockerfile构建Tomcat镜像

准备apache包和jdk并解压 [rootlocalhost tomcat]# ll 总用量 196728 -rw-r--r--. 1 root root 9690027 7月 17 2020 apache-tomcat-8.5.40.tar.gz -rw-r--r--. 1 root root 674 8月 2 20:19 Dockerfile -rw-r--r--. 1 root root 191753373 7月 17 2020 jdk-8u191-…...

k8s的介绍

简介 Kubernetes,简称K8s,是用8代替名字中间的8个字符“ubernete”而成的缩写。是一个开源的,用于管理云平台中多个主机上的容器化的应用, K8s的目标是让部署容器化的应用简单并且高效,K8s提供了应用部署,规划,更新,维护的一种机制。 K8s是Google开源的一个容器编排引…...

mysql sql语句 需要使用like 场景,解决方案

mysql 多重like 解决方案 方案一、使用like 方案二、使用REGEXP 正则匹配 方案三、使用group_concat多重模糊匹配 方案一、使用like 查询user包含小李并且小王的相关数据 SELECT * FROM user WHERE name LIKE %小王% or name like %小王% 方案二、使用REGEXP 正则匹配 查询use…...

通过C语言设计的贪吃蛇游戏(控制台终端)

一、项目介绍 当前通过控制台终端实现一个贪吃蛇小游戏&#xff0c;实现游戏的绘制、更新、控制等功能。 二、实现效果 三、完整代码 下面贴出的代码在Windows系统上编译运行&#xff0c;需要使用conio.h头文件中的getch()函数来获取键盘输入&#xff0c;用于控制蛇的移动。…...

c++实现Qt信号和槽机制

文章目录 简介信号槽信号与槽的连接 特点观察者模式定义观察者模式结构图 实现简单的信号和槽 简介 信号槽机制与Windows下消息机制类似&#xff0c;消息机制是基于回调函数&#xff0c;Qt中用信号与槽来代替函数指针&#xff0c;使程序更安全简洁。  信号和槽机制是 Qt 的核心…...

【Linux】五、进程

一、冯诺依曼体系结构 存储器&#xff1a;指的是内存&#xff1b; 输入设备&#xff1a;键盘、摄像头、话筒&#xff0c;磁盘&#xff0c;网卡&#xff1b; 输出设备&#xff1a;显示器、音响、磁盘、网卡&#xff1b; 中央处理器&#xff08;CPU&#xff09;&#xff1a;运算器…...

使用 OpenCV 和 Python 卡通化图像-附源码

介绍 在本文中,我们将构建一个有趣的应用程序,它将卡通化提供给它的图像。为了构建这个卡通化器应用程序,我们将使用 python 和 OpenCV。这是机器学习令人兴奋的应用之一。在构建此应用程序时,我们还将了解如何使用 easygui、Tkinter 等库。在这里,您必须选择图像,然后应…...

GitLab不同角色对应的权限

Owner&#xff08;拥有者&#xff09;&#xff1a; 拥有者是项目或组的创建者&#xff0c;拥有最高级别的权限。他们可以添加、删除项目成员&#xff0c;修改项目设置&#xff0c;管理访问权限&#xff0c;并进行项目转让。在组级别&#xff0c;他们还可以添加或删除子组和项目…...

手写一个简易的布隆过滤器

1.什么是布隆过滤器 布隆过滤器&#xff08;Bloom Filter&#xff09;是1970年由布隆(人名)提出的。它实际上是一个很长的二进制向量和一系列随机映射函数。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都比一般的算法要好的多&#xff0c;…...

关于nvm与node.js

1 安装nvm 安装过程中手动修改 nvm的安装路径&#xff0c; 以及修改 通过nvm安装node后正在使用的node的存放目录【这句话可能难以理解&#xff0c;但接着往下看你就了然了】 2 修改nvm中settings.txt文件配置 nvm安装成功后&#xff0c;通常在该文件中会出现以下配置&…...

YSYX学习记录(八)

C语言&#xff0c;练习0&#xff1a; 先创建一个文件夹&#xff0c;我用的是物理机&#xff1a; 安装build-essential 练习1&#xff1a; 我注释掉了 #include <stdio.h> 出现下面错误 在你的文本编辑器中打开ex1文件&#xff0c;随机修改或删除一部分&#xff0c;之后…...

鸿蒙中用HarmonyOS SDK应用服务 HarmonyOS5开发一个医院挂号小程序

一、开发准备 ​​环境搭建​​&#xff1a; 安装DevEco Studio 3.0或更高版本配置HarmonyOS SDK申请开发者账号 ​​项目创建​​&#xff1a; File > New > Create Project > Application (选择"Empty Ability") 二、核心功能实现 1. 医院科室展示 /…...

剑指offer20_链表中环的入口节点

链表中环的入口节点 给定一个链表&#xff0c;若其中包含环&#xff0c;则输出环的入口节点。 若其中不包含环&#xff0c;则输出null。 数据范围 节点 val 值取值范围 [ 1 , 1000 ] [1,1000] [1,1000]。 节点 val 值各不相同。 链表长度 [ 0 , 500 ] [0,500] [0,500]。 …...

linux 错误码总结

1,错误码的概念与作用 在Linux系统中,错误码是系统调用或库函数在执行失败时返回的特定数值,用于指示具体的错误类型。这些错误码通过全局变量errno来存储和传递,errno由操作系统维护,保存最近一次发生的错误信息。值得注意的是,errno的值在每次系统调用或函数调用失败时…...

A2A JS SDK 完整教程:快速入门指南

目录 什么是 A2A JS SDK?A2A JS 安装与设置A2A JS 核心概念创建你的第一个 A2A JS 代理A2A JS 服务端开发A2A JS 客户端使用A2A JS 高级特性A2A JS 最佳实践A2A JS 故障排除 什么是 A2A JS SDK? A2A JS SDK 是一个专为 JavaScript/TypeScript 开发者设计的强大库&#xff…...

在Mathematica中实现Newton-Raphson迭代的收敛时间算法(一般三次多项式)

考察一般的三次多项式&#xff0c;以r为参数&#xff1a; p[z_, r_] : z^3 (r - 1) z - r; roots[r_] : z /. Solve[p[z, r] 0, z]&#xff1b; 此多项式的根为&#xff1a; 尽管看起来这个多项式是特殊的&#xff0c;其实一般的三次多项式都是可以通过线性变换化为这个形式…...

加密通信 + 行为分析:运营商行业安全防御体系重构

在数字经济蓬勃发展的时代&#xff0c;运营商作为信息通信网络的核心枢纽&#xff0c;承载着海量用户数据与关键业务传输&#xff0c;其安全防御体系的可靠性直接关乎国家安全、社会稳定与企业发展。随着网络攻击手段的不断升级&#xff0c;传统安全防护体系逐渐暴露出局限性&a…...

数据库——redis

一、Redis 介绍 1. 概述 Redis&#xff08;Remote Dictionary Server&#xff09;是一个开源的、高性能的内存键值数据库系统&#xff0c;具有以下核心特点&#xff1a; 内存存储架构&#xff1a;数据主要存储在内存中&#xff0c;提供微秒级的读写响应 多数据结构支持&…...

Springboot 高校报修与互助平台小程序

一、前言 随着我国经济迅速发展&#xff0c;人们对手机的需求越来越大&#xff0c;各种手机软件也都在被广泛应用&#xff0c;但是对于手机进行数据信息管理&#xff0c;对于手机的各种软件也是备受用户的喜爱&#xff0c;高校报修与互助平台小程序被用户普遍使用&#xff0c;为…...