How are FBX files Imported in Unreal Engine

This post briefly introduces how FBX files are imported into Unreal Engine, particularly for animation sequences.

In SkeletalMeshEdit.cpp, you can find the function bool UnFbx::UnFFbxImporter::ImportAnimation, and this is where FBX import takes place.

Find Valid Bone Names

1
2
3
TArray<FName> FbxRawBoneNames;
FillAndVerifyBoneNames(Skeleton, SortedLinks, FbxRawBoneNames, FileName);
FAnimCurveImportSettings AnimImportSettings(DestSeq, NodeArray, SortedLinks, FbxRawBoneNames, AnimTimeSpan);

Function FillAndVerifyBoneNames will copy all bone names into FbxRawBoneNames and verify whether they are duplicate or invalid. FbxRawBoneNames now contains all valid bone names.

AnimImportSettings is a helper structure to pass around the common animation parameters including the AnimationSequence asset reference, FBX nodes, bone names, and animation time span.

Import Blend Shapes

Having everything prepared, we can import raw animation curves into the AnimationSequence asset. The first type of data we would like to import is the blend shape (morph target) curves. This is implemented by function ImportBlendShapeCurves.

The function process can be described using the following pseudo code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
foreach (FBXNode : FBXNodeArray) 
foreach (BlendShape : FBXNode->BlendShapes)
foreach (Channel : BlendShape->BlendShapeChannels)
{
FbxAnimCurve* Curve = GetShapeChannel (BlendShape, Channel)
if (ShouldImportCurve (Curve))
{
int TargetShapeCount = Channel->GetTargetShapeCount()
if (TargetShapeCount > 0)
{
if (TargetShapeCount == 1)
{
ImportCurveToAnimSequence (AnimSeq, Channel, Curve)
}
else
{
// Scale blend shapes values by 0.01
Algo::Transform(FbxInbetweenFullWeights, InbetweenFullWeights, [](double Input){ return Input * 0.01f; })

// Collect inbetween shape names
for (int32 InbetweenIndex = 0; InbetweenIndex < InbetweenCount; ++InbetweenIndex)
{
FbxShape* Shape = Channel->GetTargetShape(InbetweenIndex)
CurveNames.Add(MakeName(Shape->GetName()))
}

// Convert FBX curve into rich curve
FRichCurve ChannelWeightCurve;
ImportCurve(Curve, ChannelWeightCurve)
if (AnimSeq)
{
ChannelWeightCurve.BakeCurve(1.0f / AnimSeq->ImportResampleFramerate)
}

// Use the primary curve to generate inbetween shape curves + a modified primary curve
TArray<FRichCurve> Results = ResolveWeightsForBlendShapeCurve(ChannelWeightCurve, InbetweenFullWeights)
if (ImportRichCurvesToAnimSequence(AnimSeq, CurveNames, Results, 0))
{
for (const FString& CurveName : CurveNames)
MySkeleton->AccumulateCurveMetaData(*CurveName, false, true)
}
}
}
}
}

The function ImportRichCurvesToAnimSequence is worth particular attention. It practically updates the AnimationSequence with the curves created and modified in function ImportBlendShapeCurves.

Let's first examine the function ImportRichCurvesToAnimSequence.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
foreach (CurveName : CurveNames)
{
// Add or retrieve curve
if (!SkeletonCurveMapping->Exists(CurveName))
{
// Make skeleton dirty
Skeleton->Modify()
}

// Get corresponding curve
FAnimationCurveIdentifier FloatCurveId (CurveName, ERawCurveTrackTypes::RCT_Float)
const FFloatCurve* TargetCurve = AnimSeq->GetDataModel()->FindFloatCurve(FloatCurveId)

// Update curve and set keys
AnimSeq->Controller.SetCurveKeys(FloatCurveId, RichCurves[CurveIndex].GetConstRefOfKeys())
}

Then, let's examine function SetCurveKeys, which is defined in AnimDataContyroller.cpp.

1
2
3
4
5
6
7
8
9
10
11
12
13
// Get the curve according to curve id
FRichCurve* RichCurve = Model->GetMutableRichCurve(CurveId)

if (RichCurve)
{
// Set rich curve values
RichCurve->SetKeys(CurveKeys);

// On curve changed notify
FCurveChangedPayload Payload;
Payload.Identifier = CurveId;
Model->Notify(EAnimDataModelNotifyType::CurveChanged, Payload);
}

Finally, go to RichCurve.cpp and you will see the definition of function SetKeys:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void FRichCurve::SetKeys(const TArray<FRichCurveKey>& InKeys)
{
Reset();

Keys.SetNum(InKeys.Num());
KeyHandlesToIndices.SetKeyHandles(InKeys.Num());

for (int32 Index = 0; Index < InKeys.Num(); ++Index)
{
Keys[Index] = InKeys[Index];
}

AutoSetTangents();
}

which is pretty straightforward.

Import Animation

Function UnFbx::FFbxImporter::ImportBoneTracks is responsible for importing bone animation tracks. Its pseudo code is as follows.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
foreach (BoneName : FbxRawBoneNames)
{
CheckValidity (BoneName)

FRawAnimSequenceTrack RawTrack
for (CurTime = StartTime; CurTime < EndTime; CurTime += TimeInc)
{
LocalTransform = GetLocalTransformFromRawMatrixData (CurTime)
RawTrack.ScaleKeys.Add(FVector3f(LocalTransform.GetScale3D()))
RawTrack.PosKeys.Add(FVector3f(LocalTransform.GetTranslation()))
RawTrack.RotKeys.Add(FQuat4f(LocalTransform.GetRotation()))

if (Success)
{
if (SkeletonHasBone (BoneName))
{
// Add a new track
Controller.AddBoneTrack(BoneName)
Controller.SetBoneTrackKeys(BoneName, RawTrack.PosKeys, RawTrack.RotKeys, RawTrack.ScaleKeys)
}
else
{
// Create animation attribute and add the transform keys
UE::Anim::AddTypedCustomAttribute<FTransformAnimationAttribute, FTransform>(...)
}
}
}
}

Import Bone Metadata

The final step is to import the bone metadata. It simply calls the ImportNodeCustomProperties(AnimSeq, SkeletonNode) for each skeleton node.

1
2
3
4
5
6
7
8
9
10
11
foreach (Property : NodeProperties)
{
MetadataTag = "FBX." + NodeName + "." + Property.GetName()
MetadataValue = GetFbxPropertyStringValue(Property)
AnimSeq->GetOutermost()->GetMetaData()->SetValue(AnimSeq, *MetadataTag, *MetadataValue)

foreach (ChildNode : NodeChildren)
{
ImportNodeCustomProperties (AnimSeq, ChildNode)
}
}