I have wanted to make an FBX Exporter to convert FBX files to my own format for a while. The entire process is not very smooth, mainly because FBX's official documentation is not very clear. Plus, since FBX format is utilized by a number of applications, rather than just game engines, the sample code provided is not using the more common technical terms we use in game development.
I have searched almost all the corners on the Internet to clarify things so that I can have a clear mapping from FBX SDK's data to what I need in a game engine. Since I don't think anyone has ever posted a clear and thorough tutorial on how to convert FBX files to custom formats, I will do it. I hope this will help people.
This tutorial would be specifically about game engines. Basically I will tell the reader how to get the data they need for their game engine. For things like "how to initialize FBX SDK", please check the sample code yourself, the "ImportScene" sample would be very useful in this respect.
If you have no knowledge about how skeletal animation works and what data you need to make skeletal animation happen, please look at Buckeye's article "Skinned Mesh Animation Using Matrices". It would be very helpful.
The link is here:
http://www.gamedev.net/page/resources/_/technical/graphics-programming-and-theory/skinned-mesh-animation-using-matrices-r3577
Mesh Data(position, UV, normal, tangent, binormal)
The first thing you want to do is to get the mesh data; it already feels pretty good if you can import your static mesh into your engine.
For the clarity of this section, I choose to show you how I traverse the mesh in a FBX file first. This allows me to give you a Top-Down understanding of what you need to do to gather mesh data. You don't know what each function does specifically, but you should get the idea that I am traversing the 3 vertices on each triangle of the mesh. I will come back to each function later.
Note that there is some code related to blending info for animation. You can ignore it for now. We will come back to it later.
void FBXExporter::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[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();
}
First please let me explain how FBX stores all its information about a mesh. In FBX we have the term "Control Point", basically a control point is a physical vertex. For example, you have a cube, then you have 8 vertices. These 8 vertices are the only 8 "control points" in the FBX file. As a result, if you want, you can use "Vertex" and "Control Point" interchangeably. The position information is stored in the control points.
The following code would get you the positions of all the vertices of your mesh:
// 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<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;
}
}
Then you ask "how can I get the UVs, Normals, Tangents, Binormals?" Well, please think of a mesh like this for a moment: You have this body of the mesh, but this is only the geometry, the shape of it. This body does not have any information about its surface. In other words, you have this shape, but you don't have any information on how the surface of this shape looks.
FBX introduces this sense of "Layer", which covers the body of the mesh. It is like you have a box, and you wrap it with gift paper. This gift paper is the layer of the mesh in FBX. In the layer, you can acquire the information of UVs, Normals, Tangents, Binormals.
However, you might have already asked me. How can I relate the Control Points to the information in the layer? Well, this is the pretty tricky part and please let me show you some code and then explain it line by line. Without loss of generality, I will use Binormal as an example:
Before we take a look at the function, let's go over its parameters first.
FbxMesh* inMesh: the mesh that we are trying to export
int inCtrlPointIndex: the index of the Control Point. We need this because we want to relate our layer information with our vertices (Control Points)
int inVertexCounter: this is the index of the current vertex that we are processing.
XMFLOAT3& outNormal: the output. We are passing by reference so that we can modify this variable inside this function and use it as our output
After seeing these parameters, you may ask me "Since you said ControlPoints are basically Vertices in FBXSDK. Why do you have inCtrlPointIndex and inVertexCounter? Aren't they the same thing?"
No, they are not the same. As I explained before, Control Points are physical vertices on your geometry. Let's use a quad as an example.
Given a quad(2 triangles), how many Control Points are there?
The answer is 4.
But how many vertices are there in our triangle-based game engine?
The answer is 6 because we have 2 triangles and each triangle has 3 vertices. 2 * 3 = 6
The main difference between FBXSDK's Control Point and our Vertex is that our Vertex has this sense of "per-triangle" but FBXSDK's Control Point does not.
We will come back to this point in the explanation of the code below. So don't worry if you still do not have a crystal-clear understanding of FBXSDK's Control Point and Vertex in your game engine.
One thing to keep in mind is that outside of this function, we are using a loop to traverse all the vertices of all the triangles in this mesh. If you are confused and do not know what mean by "we are using a loop to traverse all the vertices of all the triangles in this mesh", look at the very top of this "Mesh Data(position, UV, normal, tangent, binormal)" section. That is why we can have parameters like
inCtrlPointIndex and
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<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;
}
}
Well, this is pretty long but please don't be scared. Actually it is very simple.
This gets us the normal information in the layer
FbxGeometryElementNormal* vertexNormal = inMesh->GetElementNormal(0);
The first switch statement is about
MappingMode(). For a game engine, I think we only need to worry about
FbxGeometryElement::eByControlPoint and
FbxGeometryElement::eByPolygonVertex. Let me explain the 2 modes. As I said, Control Points are basically the vertices. However, there is a problem. Although a cube has 8 Control Points, it will have more than 8 normals if you want your cube to look correct. The reason is if you have a sharp edge, we have to assign more than one normal to the same control point to guarantee that feeling of sharpness. This is when the concept of Vertex in our game engine comes in, because even if you have the same position for a vertex of the cube, in a game engine, you are very likely to end up with 3 vertices with the same position but 3 different normals.
As a result,
FbxGeometryElement::eByControlPoint is when you don't have sharp edged situations so each control point only has one normal.
FbxGeometryElement::eByPolygonVertex is when you have sharp edges and you need to get the normals of each vertex on each face because each face has a different normal assigned for the same control point. So
FbxGeometryElement::eByControlPoint means we can pinpoint the normal of a control point by the index of the control point while
FbxGeometryElement::eByPolygonVertex means we cna pinpoint the normal of a vertex on a face by the index of the vertex
This is a more concrete and deep example of the difference of FBXSDK's ControlPoint and Vertex in a game engine and why when I talk about the parameters of this function, I said we have to pass in both
inCtrlPointIndex and
inVertexCounter. Because we don't know which one we need to get the information we need, we better pass in both.
Now we have another switch statement nested inside, and we are "switching" on
ReferenceMode(). This is some kind of optimization FBX is doing, same idea like index buffer in computer graphics. You don't want to have the same Vector3 many times; instead, you refer to it using its index.
FbxGeometryElement::eDirect means you can refer to our normal using the index of control point or index of face-vertex directly
FbxGeometryElement::eIndexToDirect means using the index of control point or index of face-vertex would only gives us an index pointing to the normal we want, we have to use this index to find the actual normal.
This line of code gives us the index we need
int index = vertexNormal->GetIndexArray().GetAt(inVertexCounter);
So these are the main steps to extract position and "layer" information of a mesh.
Now we move onto animation and this is the hard part of FBX exporting.
Animation Data
So let's think about what we need from FBX to make animation work in our renderer (game engine).
- The skeleton hierarchy. Which joint is which joint's parent
- For each vertex, we need 4 SkinningWeight-JointIndex pairs
- The Bind pose matrix for each joint to calculate the inverse of global bind pose
- The transformation matrix at time t so that we can transform our mesh to that pose to achieve animation
To get the skeleton hierarchy is pretty easy: basically we perform a recursive Depth-First-Search from the root node of the scene and we go down levels.
A Node is the the building block of a FBX Scene. There are many nodes in a FBX file and each type of node contains some type of information.
If a node is of skeleton type, we add it into our list of joints and its index will just be the size of the list. Therefore, we can guarantee that the index of the parent is always going to be less than that of the child. This is necessary if you want to store local transform and calculate the transformation of a child at time
t manually. But if you are using global transformation like I do, you don't necessarily need it like this.
Note: if you are not familiar with the concept of Depth-First-Search.
Look at this page:
http://www.geeksforgeeks.org/depth-first-traversal-for-a-graph/
and this page:
http://en.wikipedia.org/wiki/Depth-first_search
After reading those pages, you may ask "Why don't we need to keep track of visited nodes?"
The answer is: The Skeleton Hierarchy is a tree, not a graph.
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);
}
}
Now we need to get the SkinningWeight-JointIndex pairs of each vertex. Unfortunately, my code is not very clean on animation so the function below does steps 2,3,4 all at once. I will go over the code so please do not lose patience. This is mainly because the way FBX stores information prevents me from getting data in separate functions efficiently. I need to traverse the same data in multiple-passes if I want to separate my code.
Before seeing any code, please let me explain the terms used in FBX SDK. This is the part where I think most people get confused because FBX SDK's keywords do not match ours (game developers).
In FBX, there is such a thing called a "Deformer". I see a deformer as a way to deform a mesh. In Maya, you can have skeletal deformers but you can also have "contraints" to deform your mesh. I think you can think of "Deformers" as the entire skeleton of a mesh. Inside each "Deformer" (I think usually a mesh only has one), you have "Clusters". Each cluster is and is not a joint...... You can see a cluster as a joint, but actually, inside each cluster, there is a "link". This "link" is actually the real joint, and it contains the useful information I need.
Now we delve into the code:
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<FbxSkin*>(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()[i];
mControlPoints[currCluster->GetControlPointIndices()[i]]->mBlendingInfo.push_back(currBlendingIndexWeightPair);
}
// Get animation information
// Now only supports one take
FbxAnimStack* currAnimStack = mFBXScene->GetSrcObject<FbxAnimStack>(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);
}
}
}
At the beginning I have this:
// 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);
Well, this is what I saw on the FBX SDK Forum. The officials there told us we should take into account the "GeometricTransform". But according to my experience, most of the times, this "GeometricTransform" is just an identity matrix. Anyways, to get this "GeometricTransform", use this function:
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);
}
The
very most important thing in this code is how I get the inverse of global bind pose of each joint. This part is very tricky and screwed up many people. I will explain this in details.
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;
So let's start from this
GetTransformMatrix. The
TransformMatrix is actually a legacy thing. It is the Global Transform of the entire mesh at binding time and all the clusters have exactly the same
TransformMatrix. This matrix would not be needed if your artists have good habits and before they rig the model, they "Freeze Transformations" on all channels of the model. If your artists do "Freeze Transformations", then this matrix would just be an identity matrix.
Now we go on to
GetTransformLinkMatrix. This is the very essence of the animation exporting code. This is the transformation of the cluster (joint) at binding time from joint space to world space in Maya.
So now we are all set and we can get our inverse of global bind pose of each joint. What we want eventually is the
InverseOfGlobalBindPoseMatrix in
VertexAtTimeT = TransformationOfPoseAtTimeT * InverseOfGlobalBindPoseMatrix * VertexAtBindingTime
To get this, we do this:
transformLinkMatrix.Inverse() * transformMatrix * geometryTransform
Now we are 2 steps away from animation. We need to get the SkinningWeight-JointIndex pair for each vertex and we still need to get the transformations at different times in the animation
Let's deal with SkinningWeight-JointIndex pair first.
In our game engine, we have this relationship: Vertex -> 4 SkinningWeight-JointIndex pairs. However, in FBX SDK the relationship is inverted. Each cluster has a list of all the control points (vertices) it affects and how much it affects. The code below gets the relationship in the format we favor but please recall that when I process control points, I stored all the control points into a map based on their indices. This is where we can profit. With this map, here we can lookup and update the control point a cluster affects in 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()[i];
mControlPoints[currCluster->GetControlPointIndices()[i]]->mBlendingInfo.push_back(currBlendingIndexWeightPair);
}
Now we only need the last piece in the puzzle: the Transformations at time
t in the animation. Note that this part is something I did not do well, my way is not very optimized since I get every keyframe. What should ideally be done is to get the keys and interpolate between them, but I guess this is a trade-off between space and speed. Also, I did not get down to my feet and study the animation hierarchy of FBX. There is actually an animation curve stored inside FBX file and with some work, you can access it and get lean and clean what you need.
// Get animation information
// Now only supports one take
FbxAnimStack* currAnimStack = mFBXScene->GetSrcObject<FbxAnimStack>(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);
}
This part is pretty straightforward - the only thing to be noted is that Maya currently does not support multi-take animations (Perhaps MotionBuilder does). I will decide if I write about exporting materials based on how many people read this article, but it is pretty easy and can be learnt through the "ImportScene" example
DirectX and OpenGL Conversions
My goal for this FBX exporter is to provide a way to extract data from FBX file, and output the data in a custom format such that the reader's renderer can just take the data and render it. No need for any conversion inside the renderer because all the work of conversion falls on the exporter itself.
Before I say anything, I need to clarify that my way of conversion is only guaranteed to work if you make the model/animation in Maya and export the model/animation from Maya using its default coordinate system(X-Right, Y-Up, Z-Out Of Screen).
If you want to import your model/animation into OpenGL, then more likely you do not need to do any extra steps for conversion because I think by default OpenGL has the same right-handed coordinate system as Maya, which is (X-Right, Y-Up, Z-Out Of Screen). In FBXSDK's sample code "ViewScene", there is no conversion for the data and it uses OpenGL as its renderer with default coordinate system in OpenGL. So if you do run into trouble, take a look at that code. However, if you specify your own coordinate system, then some conversions might be needed.
Now it is time for DirectX and I saw online that most problems come from the case where people want to renderer FBX model/animation in DirectX. So, if you want to import the model/animation into DirectX, you are very likely to need to make some conversions.
I will only address the case where there is a left-handed "X-Right, Y-Up, Z-Into Screen" coordinate system with back-face culling, because from the posts I read, most people use this system when they use DirectX. This does mean anything in general; it is only an observation from my experience.
You need to do the following to convert the coordinates from the right-handed "X-Right, Y-Up, Z-Out Of Screen" to the left-handed "X-Right, Y-Up, Z-Into Screen" system:
Position, Normal, Binormal, Tangent -> we need to negate the Z component of the Vector
UV -> we need to make V = 1.0f - V
Vertex order of a triangle -> change from Vertex0, Vertex1, Vertex2 to Vertex0, Vertex2, Vertex1 (Basically invert the culling order)
Matrices:
- Get translation component of the matrix, negate its Z component
- Get rotation component of the matrix, negate its X and Y component
- I think if you are using XMMath library, you don't need to take the transpose. But don't quote me on that.
To use my way of conversion, you need to decompose the matrix and change its Translation, Rotation and Scale respectively.
Fortunately, FBXSDK provides ways to decompose matrices as long as your matrix is a FbxAMatrix(FBX Affine Matrix). The sample code below shows you how:
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);
If your animation has Scaling, you need to figure out yourself what conversion needs to be done since I have not encountered the case where Scaling happens.
Limitations and Beyond
So this tutorial is only intended to get you started on FBXSDK. I myself am quite a noob so many of my techniques are probably very inefficient. Here I will list out the problems that I think I have. In doing so, the reader can decide themselves whether to use my technique and what needs to be careful about.
1. The conversion method is only for model/animation exported from Maya with Maya's right-handed X-Right, Y-Up, Z-Out coordinate system. It is very likely that my conversion technique will NOT work in other modeling softwares(Blender, MotionBuilder, 3ds Max)
2. The way I extract animation is inefficient. I need to bake the animation before I export the animation, then I get all keyframes at a rate of 24 frames/sec. This can lead to huge memory consumption. If you know how to play with keys instead of keyframes, please let me know by commenting below.
3. My conversion method does not handle scaling in the animation. As you can see from my code, I never deal with scale component in the transformation matrix when I extract animation. As a result, you need to figure it out on your own if your animation has scaling in it.
4. In this tutorial I did not include the code to remove duplicated vertices, but in reality you will end up a lot of duplicates if you use my way to export FBX file without some optimization. I did a comparison and an optimized export can cut the file size by 2/3.......The reason why you will have duplicates is: if you are traversing each vertex of each triangle in your mesh, although the same Control Point with different normals would be handled well, the same Control Point with the same normal will be counted more than 1 times!
Corrections, Improvements, Advice
Well, I am actually quite a noob on FBXSDK and game programming in general. If you see any mistakes, or you see any space for improvements, please comment on this article and help me get better. I know there are a lot of pros on this forum, they just don't have enough time to write an article like this to teach people step by step.
Conclusion
Well, FBXSDK can be pretty nasty to work with. However, once you know what data in FBX means, it is actually very easy to use. I think my article is enough to get people started on using FBXSDK. Please leave a comment if you have any questions.
Source Code
On demand, I decided to provide the github repo because some readers told me it would be more clear if they have access to my structs.
So here it is.
Please be nice and do not mess up the git repo.
Advice on my coding habit and efficiency is very welcomed.
git@github.com:lang1991/FBXExporter.git
Article Update Log
3.10.2014 Added the link to my GitHub Repo
3.03.2014 Added a section for limitations of this tutorial
3.03.2014 Corrected misunderstanding about handedness of coordinate system
3.01.2014 Added more explanations on coordinate conversions
3.01.2014 Changed the order of some paragraphs to make the article more clear
2.19.2014 First Version Submitted