// Copyright Epic Games, Inc. All Rights Reserved. /*============================================================================= Implementation of animation export related functionality from FbxExporter =============================================================================*/ #include "CoreMinimal.h" #include "Misc/MessageDialog.h" #include "Misc/FeedbackContext.h" #include "Misc/ScopedSlowTask.h" #include "Animation/AnimTypes.h" #include "Components/SkeletalMeshComponent.h" #include "Matinee/InterpData.h" #include "Matinee/InterpTrackAnimControl.h" #include "Animation/AnimSequence.h" #include "Editor/EditorPerProjectUserSettings.h" #include "Matinee/MatineeActor.h" #include "Animation/SkeletalMeshActor.h" #include "FbxExporter.h" #include "Exporters/FbxExportOption.h" #include "Animation/CustomAttributesRuntime.h" DEFINE_LOG_CATEGORY_STATIC(LogFbxAnimationExport, Log, All); namespace UnFbx { bool FFbxExporter::SetupAnimStack(const UAnimSequence* AnimSeq) { if (AnimSeq->SequenceLength == 0.f) { // something is wrong return false; } const float FrameRate = FMath::TruncToFloat(((AnimSeq->GetRawNumberOfFrames() - 1) / AnimSeq->SequenceLength) + 0.5f); // Configure the scene time line { FbxGlobalSettings& SceneGlobalSettings = Scene->GetGlobalSettings(); double CurrentSceneFrameRate = FbxTime::GetFrameRate(SceneGlobalSettings.GetTimeMode()); if (!bSceneGlobalTimeLineSet || FrameRate > CurrentSceneFrameRate) { FbxTime::EMode ComputeTimeMode = FbxTime::ConvertFrameRateToTimeMode(FrameRate); FbxTime::SetGlobalTimeMode(ComputeTimeMode, ComputeTimeMode == FbxTime::eCustom ? FrameRate : 0.0); SceneGlobalSettings.SetTimeMode(ComputeTimeMode); if (ComputeTimeMode == FbxTime::eCustom) { SceneGlobalSettings.SetCustomFrameRate(FrameRate); } bSceneGlobalTimeLineSet = true; } } // set time correctly FbxTime ExportedStartTime, ExportedStopTime; ExportedStartTime.SetSecondDouble(0.f); ExportedStopTime.SetSecondDouble(AnimSeq->SequenceLength); FbxTimeSpan ExportedTimeSpan; ExportedTimeSpan.Set(ExportedStartTime, ExportedStopTime); AnimStack->SetLocalTimeSpan(ExportedTimeSpan); return true; } void FFbxExporter::ExportAnimSequenceToFbx(const UAnimSequence* AnimSeq, const USkeletalMesh* SkelMesh, TArray& BoneNodes, FbxAnimLayer* InAnimLayer, float AnimStartOffset, float AnimEndOffset, float AnimPlayRate, float StartTime) { // stack allocator for extracting curve FMemMark Mark(FMemStack::Get()); USkeleton* Skeleton = AnimSeq->GetSkeleton(); if (Skeleton == nullptr || !SetupAnimStack(AnimSeq)) { // something is wrong return; } // Prepare root anim curves data to be exported TArray AnimCurveNames; TMap CustomCurveMap; if (BoneNodes.Num() > 0) { const FSmartNameMapping* AnimCurveMapping = Skeleton->GetSmartNameContainer(USkeleton::AnimCurveMappingName); if (AnimCurveMapping) { AnimCurveMapping->FillNameArray(AnimCurveNames); const UFbxExportOption* ExportOptions = GetExportOptions(); const bool bExportMorphTargetCurvesInMesh = ExportOptions && ExportOptions->bExportPreviewMesh && ExportOptions->bExportMorphTargets; for (auto AnimCurveName: AnimCurveNames) { const FCurveMetaData* CurveMetaData = AnimCurveMapping->GetCurveMetaData(AnimCurveName); // Only export the custom curve if it is not used in a MorphTarget that will be exported latter on. if (!(bExportMorphTargetCurvesInMesh && CurveMetaData && CurveMetaData->Type.bMorphtarget)) { FbxProperty AnimCurveFbxProp = FbxProperty::Create(BoneNodes[0], FbxDoubleDT, TCHAR_TO_ANSI(*AnimCurveName.ToString())); AnimCurveFbxProp.ModifyFlag(FbxPropertyFlags::eAnimatable, true); AnimCurveFbxProp.ModifyFlag(FbxPropertyFlags::eUserDefined, true); FbxAnimCurve* AnimFbxCurve = AnimCurveFbxProp.GetCurve(InAnimLayer, true); CustomCurveMap.Add(AnimCurveName, AnimFbxCurve); } } } } ExportCustomAnimCurvesToFbx(CustomCurveMap, AnimSeq, AnimStartOffset, AnimEndOffset, AnimPlayRate, StartTime); TArray CustomAttributes; // Add the animation data to the bone nodes for (int32 BoneIndex = 0; BoneIndex < BoneNodes.Num(); ++BoneIndex) { FbxNode* CurrentBoneNode = BoneNodes[BoneIndex]; int32 BoneTreeIndex = Skeleton->GetSkeletonBoneIndexFromMeshBoneIndex(SkelMesh, BoneIndex); int32 BoneTrackIndex = Skeleton->GetRawAnimationTrackIndex(BoneTreeIndex, AnimSeq); FName BoneName = Skeleton->GetReferenceSkeleton().GetBoneName(BoneTreeIndex); CustomAttributes.Reset(); AnimSeq->GetCustomAttributesForBone(BoneName, CustomAttributes); TArray> FloatCustomAttributeIndices; TArray> IntCustomAttributeIndices; // Setup custom attribute properties and curves for (int32 AttributeIndex = 0; AttributeIndex < CustomAttributes.Num(); ++AttributeIndex) { const FCustomAttribute& Attribute = CustomAttributes[AttributeIndex]; const FName& AttributeName = Attribute.Name; const EVariantTypes VariantType = static_cast(Attribute.VariantType); if (VariantType == EVariantTypes::Int32) { FbxProperty AnimCurveFbxProp = FbxProperty::Create(CurrentBoneNode, FbxIntDT, TCHAR_TO_UTF8(*AttributeName.ToString())); AnimCurveFbxProp.ModifyFlag(FbxPropertyFlags::eAnimatable, true); AnimCurveFbxProp.ModifyFlag(FbxPropertyFlags::eUserDefined, true); FbxAnimCurve* AnimFbxCurve = AnimCurveFbxProp.GetCurve(InAnimLayer, true); AnimFbxCurve->KeyModifyBegin(); IntCustomAttributeIndices.Emplace(AttributeIndex, AnimFbxCurve); } else if (VariantType == EVariantTypes::Float) { FbxProperty AnimCurveFbxProp = FbxProperty::Create(CurrentBoneNode, FbxFloatDT, TCHAR_TO_UTF8(*AttributeName.ToString())); AnimCurveFbxProp.ModifyFlag(FbxPropertyFlags::eAnimatable, true); AnimCurveFbxProp.ModifyFlag(FbxPropertyFlags::eUserDefined, true); FbxAnimCurve* AnimFbxCurve = AnimCurveFbxProp.GetCurve(InAnimLayer, true); AnimFbxCurve->KeyModifyBegin(); FloatCustomAttributeIndices.Emplace(AttributeIndex, AnimFbxCurve); } else if (VariantType == EVariantTypes::String) { FbxProperty AnimCurveFbxProp = FbxProperty::Create(CurrentBoneNode, FbxStringDT, TCHAR_TO_UTF8(*AttributeName.ToString())); AnimCurveFbxProp.ModifyFlag(FbxPropertyFlags::eUserDefined, true); // String attributes can't be keyed, simply set a normal value. FString AttributeValue; FCustomAttributesRuntime::GetAttributeValue(Attribute, 0.f, AttributeValue); FbxString FbxValueString(TCHAR_TO_UTF8(*AttributeValue)); AnimCurveFbxProp.Set(FbxValueString); } else { ensureMsgf(false, TEXT("Trying to export unsupported custom attribte (float, int32 and FString are currently supported)")); } } // Create the transform AnimCurves const uint32 NumberOfCurves = 9; FbxAnimCurve* Curves[NumberOfCurves]; // Individual curves for translation, rotation and scaling Curves[0] = CurrentBoneNode->LclTranslation.GetCurve(InAnimLayer, FBXSDK_CURVENODE_COMPONENT_X, true); Curves[1] = CurrentBoneNode->LclTranslation.GetCurve(InAnimLayer, FBXSDK_CURVENODE_COMPONENT_Y, true); Curves[2] = CurrentBoneNode->LclTranslation.GetCurve(InAnimLayer, FBXSDK_CURVENODE_COMPONENT_Z, true); Curves[3] = CurrentBoneNode->LclRotation.GetCurve(InAnimLayer, FBXSDK_CURVENODE_COMPONENT_X, true); Curves[4] = CurrentBoneNode->LclRotation.GetCurve(InAnimLayer, FBXSDK_CURVENODE_COMPONENT_Y, true); Curves[5] = CurrentBoneNode->LclRotation.GetCurve(InAnimLayer, FBXSDK_CURVENODE_COMPONENT_Z, true); Curves[6] = CurrentBoneNode->LclScaling.GetCurve(InAnimLayer, FBXSDK_CURVENODE_COMPONENT_X, true); Curves[7] = CurrentBoneNode->LclScaling.GetCurve(InAnimLayer, FBXSDK_CURVENODE_COMPONENT_Y, true); Curves[8] = CurrentBoneNode->LclScaling.GetCurve(InAnimLayer, FBXSDK_CURVENODE_COMPONENT_Z, true); if (BoneTrackIndex == INDEX_NONE) { // If this sequence does not have a track for the current bone, then skip it continue; } for (FbxAnimCurve* Curve: Curves) { Curve->KeyModifyBegin(); } auto ExportLambda = [&](float AnimTime, FbxTime ExportTime, bool bLastKey) { FTransform BoneAtom; AnimSeq->GetBoneTransform(BoneAtom, BoneTrackIndex, AnimTime, true); FbxAMatrix FbxMatrix = Converter.ConvertMatrix(BoneAtom.ToMatrixWithScale()); FbxVector4 Translation = FbxMatrix.GetT(); FbxVector4 Rotation = FbxMatrix.GetR(); FbxVector4 Scale = FbxMatrix.GetS(); FbxVector4 Vectors[3] = {Translation, Rotation, Scale}; // Loop over each curve and channel to set correct values for (uint32 CurveIndex = 0; CurveIndex < 3; ++CurveIndex) { for (uint32 ChannelIndex = 0; ChannelIndex < 3; ++ChannelIndex) { uint32 OffsetCurveIndex = (CurveIndex * 3) + ChannelIndex; int32 lKeyIndex = Curves[OffsetCurveIndex]->KeyAdd(ExportTime); Curves[OffsetCurveIndex]->KeySetValue(lKeyIndex, Vectors[CurveIndex][ChannelIndex]); Curves[OffsetCurveIndex]->KeySetInterpolation(lKeyIndex, bLastKey ? FbxAnimCurveDef::eInterpolationConstant : FbxAnimCurveDef::eInterpolationCubic); if (bLastKey) { Curves[OffsetCurveIndex]->KeySetConstantMode(lKeyIndex, FbxAnimCurveDef::eConstantStandard); } } } for (TPair& CurrentAttributeCurve: FloatCustomAttributeIndices) { float AttributeValue = 0.f; FCustomAttributesRuntime::GetAttributeValue(CustomAttributes[CurrentAttributeCurve.Key], AnimTime, AttributeValue); int32 KeyIndex = CurrentAttributeCurve.Value->KeyAdd(ExportTime); CurrentAttributeCurve.Value->KeySetValue(KeyIndex, AttributeValue); } for (TPair& CurrentAttributeCurve: IntCustomAttributeIndices) { int32 AttributeValue = 0; FCustomAttributesRuntime::GetAttributeValue(CustomAttributes[CurrentAttributeCurve.Key], AnimTime, AttributeValue); int32 KeyIndex = CurrentAttributeCurve.Value->KeyAdd(ExportTime); CurrentAttributeCurve.Value->KeySetValue(KeyIndex, static_cast(AttributeValue)); } }; IterateInsideAnimSequence(AnimSeq, AnimStartOffset, AnimEndOffset, AnimPlayRate, StartTime, ExportLambda); for (FbxAnimCurve* Curve: Curves) { Curve->KeyModifyEnd(); } auto MarkCurveEnd = [](auto& CurvesArray) { for (auto& CurvePair: CurvesArray) { CurvePair.Value->KeyModifyEnd(); } }; MarkCurveEnd(FloatCustomAttributeIndices); MarkCurveEnd(IntCustomAttributeIndices); } } void FFbxExporter::ExportCustomAnimCurvesToFbx(const TMap& CustomCurves, const UAnimSequence* AnimSeq, float AnimStartOffset, float AnimEndOffset, float AnimPlayRate, float StartTime, float ValueScale) { // stack allocator for extracting curve FMemMark Mark(FMemStack::Get()); const USkeleton* Skeleton = AnimSeq->GetSkeleton(); const FSmartNameMapping* SmartNameMapping = Skeleton ? Skeleton->GetSmartNameContainer(USkeleton::AnimCurveMappingName) : nullptr; if (!Skeleton || !SmartNameMapping || !SetupAnimStack(AnimSeq)) { // Something is wrong. return; } TArray AnimCurveUIDs; { // We need to recreate the UIDs array manually so that we keep the empty entries otherwise the BlendedCurve won't have the correct mapping. TArray UID_ToNameArray; SmartNameMapping->FillUIDToNameArray(UID_ToNameArray); AnimCurveUIDs.Reserve(UID_ToNameArray.Num()); for (int32 NameIndex = 0; NameIndex < UID_ToNameArray.Num(); ++NameIndex) { AnimCurveUIDs.Add(NameIndex); } } for (auto CustomCurve: CustomCurves) { CustomCurve.Value->KeyModifyBegin(); } auto ExportLambda = [&](float AnimTime, FbxTime ExportTime, bool bLastKey) { FBlendedCurve BlendedCurve; BlendedCurve.InitFrom(&AnimCurveUIDs); AnimSeq->EvaluateCurveData(BlendedCurve, AnimTime, true); if (BlendedCurve.IsValid()) { // Loop over the custom curves and add the actual keys for (auto CustomCurve: CustomCurves) { SmartName::UID_Type NameUID = Skeleton->GetUIDByName(USkeleton::AnimCurveMappingName, CustomCurve.Key); if (NameUID != SmartName::MaxUID) { float CurveValueAtTime = BlendedCurve.Get(NameUID) * ValueScale; int32 KeyIndex = CustomCurve.Value->KeyAdd(ExportTime); CustomCurve.Value->KeySetValue(KeyIndex, CurveValueAtTime); } } } }; IterateInsideAnimSequence(AnimSeq, AnimStartOffset, AnimEndOffset, AnimPlayRate, StartTime, ExportLambda); for (auto CustomCurve: CustomCurves) { CustomCurve.Value->KeyModifyEnd(); } } void FFbxExporter::IterateInsideAnimSequence(const UAnimSequence* AnimSeq, float AnimStartOffset, float AnimEndOffset, float AnimPlayRate, float StartTime, TFunctionRef IterationLambda) { float AnimTime = AnimStartOffset; float AnimEndTime = (AnimSeq->SequenceLength - AnimEndOffset); // Subtracts 1 because NumFrames includes an initial pose for 0.0 second double TimePerKey = (AnimSeq->SequenceLength / (AnimSeq->GetRawNumberOfFrames() - 1)); const float AnimTimeIncrement = TimePerKey * AnimPlayRate; uint32 AnimFrameIndex = 0; FbxTime ExportTime; ExportTime.SetSecondDouble(StartTime); FbxTime ExportTimeIncrement; ExportTimeIncrement.SetSecondDouble(TimePerKey); // Step through each frame and add custom curve data bool bLastKey = false; while (!bLastKey) { bLastKey = (AnimTime + KINDA_SMALL_NUMBER) > AnimEndTime; IterationLambda(AnimTime, ExportTime, bLastKey); ExportTime += ExportTimeIncrement; AnimFrameIndex++; AnimTime = AnimStartOffset + ((float)AnimFrameIndex * AnimTimeIncrement); } } // The curve code doesn't differentiate between angles and other data, so an interpolation from 179 to -179 // will cause the bone to rotate all the way around through 0 degrees. So here we make a second pass over the // rotation tracks to convert the angles into a more interpolation-friendly format. void FFbxExporter::CorrectAnimTrackInterpolation(TArray& BoneNodes, FbxAnimLayer* InAnimLayer) { // Add the animation data to the bone nodes for (int32 BoneIndex = 0; BoneIndex < BoneNodes.Num(); ++BoneIndex) { FbxNode* CurrentBoneNode = BoneNodes[BoneIndex]; // Fetch the AnimCurves FbxAnimCurve* Curves[3]; Curves[0] = CurrentBoneNode->LclRotation.GetCurve(InAnimLayer, FBXSDK_CURVENODE_COMPONENT_X, true); Curves[1] = CurrentBoneNode->LclRotation.GetCurve(InAnimLayer, FBXSDK_CURVENODE_COMPONENT_Y, true); Curves[2] = CurrentBoneNode->LclRotation.GetCurve(InAnimLayer, FBXSDK_CURVENODE_COMPONENT_Z, true); for (int32 CurveIndex = 0; CurveIndex < 3; ++CurveIndex) { FbxAnimCurve* CurrentCurve = Curves[CurveIndex]; CurrentCurve->KeyModifyBegin(); float CurrentAngleOffset = 0.f; for (int32 KeyIndex = 1; KeyIndex < CurrentCurve->KeyGetCount(); ++KeyIndex) { float PreviousOutVal = CurrentCurve->KeyGetValue(KeyIndex - 1); float CurrentOutVal = CurrentCurve->KeyGetValue(KeyIndex); float DeltaAngle = (CurrentOutVal + CurrentAngleOffset) - PreviousOutVal; if (DeltaAngle >= 180) { CurrentAngleOffset -= 360; } else if (DeltaAngle <= -180) { CurrentAngleOffset += 360; } CurrentOutVal += CurrentAngleOffset; CurrentCurve->KeySetValue(KeyIndex, CurrentOutVal); } CurrentCurve->KeyModifyEnd(); } } } FbxNode* FFbxExporter::ExportAnimSequence(const UAnimSequence* AnimSeq, const USkeletalMesh* SkelMesh, bool bExportSkelMesh, const TCHAR* MeshName, FbxNode* ActorRootNode, const TArray* OverrideMaterials /*= nullptr*/) { if (Scene == NULL || AnimSeq == NULL || SkelMesh == NULL) { return NULL; } FbxNode* RootNode = (ActorRootNode) ? ActorRootNode : Scene->GetRootNode(); // Create a temporary node attach to the scene root. // This will allow us to do the binding without the scene transform (non uniform scale is not supported when binding the skeleton) // We then detach from the temp node and attach to the parent and remove the temp node FString FbxNodeName = FGuid::NewGuid().ToString(EGuidFormats::Digits); FbxNode* TmpNodeNoTransform = FbxNode::Create(Scene, TCHAR_TO_UTF8(*FbxNodeName)); Scene->GetRootNode()->AddChild(TmpNodeNoTransform); // Create the Skeleton TArray BoneNodes; FbxNode* SkeletonRootNode = CreateSkeleton(SkelMesh, BoneNodes); TmpNodeNoTransform->AddChild(SkeletonRootNode); // Export the anim sequence { ExportAnimSequenceToFbx(AnimSeq, SkelMesh, BoneNodes, AnimLayer, 0.f, // AnimStartOffset 0.f, // AnimEndOffset 1.f, // AnimPlayRate 0.f); // StartTime CorrectAnimTrackInterpolation(BoneNodes, AnimLayer); } // Optionally export the mesh if (bExportSkelMesh) { FString MeshNodeName; if (MeshName) { MeshNodeName = MeshName; } else { SkelMesh->GetName(MeshNodeName); } FbxNode* MeshRootNode = nullptr; if (GetExportOptions()->LevelOfDetail && SkelMesh->GetLODNum() > 1) { FString LodGroup_MeshName = MeshNodeName + TEXT("_LodGroup"); MeshRootNode = FbxNode::Create(Scene, TCHAR_TO_UTF8(*LodGroup_MeshName)); TmpNodeNoTransform->AddChild(MeshRootNode); LodGroup_MeshName = MeshNodeName + TEXT("_LodGroupAttribute"); FbxLODGroup* FbxLodGroupAttribute = FbxLODGroup::Create(Scene, TCHAR_TO_UTF8(*LodGroup_MeshName)); MeshRootNode->AddNodeAttribute(FbxLodGroupAttribute); FbxLodGroupAttribute->ThresholdsUsedAsPercentage = true; // Export an Fbx Mesh Node for every LOD and child them to the fbx node (LOD Group) for (int CurrentLodIndex = 0; CurrentLodIndex < SkelMesh->GetLODNum(); ++CurrentLodIndex) { FString FbxLODNodeName = MeshNodeName + TEXT("_LOD") + FString::FromInt(CurrentLodIndex); if (CurrentLodIndex + 1 < SkelMesh->GetLODNum()) { // Convert the screen size to a threshold, it is just to be sure that we set some threshold, there is no way to convert this precisely double LodScreenSize = (double)(10.0f / SkelMesh->GetLODInfo(CurrentLodIndex)->ScreenSize.Default); FbxLodGroupAttribute->AddThreshold(LodScreenSize); } FbxNode* FbxActorLOD = CreateMesh(SkelMesh, *FbxLODNodeName, CurrentLodIndex, AnimSeq, OverrideMaterials); if (FbxActorLOD) { MeshRootNode->AddChild(FbxActorLOD); if (SkeletonRootNode) { // Bind the mesh to the skeleton BindMeshToSkeleton(SkelMesh, FbxActorLOD, BoneNodes, CurrentLodIndex); // Add the bind pose CreateBindPose(FbxActorLOD); } } } } else { const int32 LodIndex = 0; MeshRootNode = CreateMesh(SkelMesh, *MeshNodeName, LodIndex, AnimSeq, OverrideMaterials); if (MeshRootNode) { TmpNodeNoTransform->AddChild(MeshRootNode); if (SkeletonRootNode) { // Bind the mesh to the skeleton BindMeshToSkeleton(SkelMesh, MeshRootNode, BoneNodes, LodIndex); // Add the bind pose CreateBindPose(MeshRootNode); } } } if (MeshRootNode) { TmpNodeNoTransform->RemoveChild(MeshRootNode); RootNode->AddChild(MeshRootNode); } } if (SkeletonRootNode) { TmpNodeNoTransform->RemoveChild(SkeletonRootNode); RootNode->AddChild(SkeletonRootNode); } Scene->GetRootNode()->RemoveChild(TmpNodeNoTransform); Scene->RemoveNode(TmpNodeNoTransform); return SkeletonRootNode; } void FFbxExporter::ExportAnimSequencesAsSingle(USkeletalMesh* SkelMesh, const ASkeletalMeshActor* SkelMeshActor, const FString& ExportName, const TArray& AnimSeqList, const TArray& TrackKeys) { if (Scene == NULL || SkelMesh == NULL || AnimSeqList.Num() == 0 || AnimSeqList.Num() != TrackKeys.Num()) return; FbxNode* BaseNode = FbxNode::Create(Scene, Converter.ConvertToFbxString(ExportName)); Scene->GetRootNode()->AddChild(BaseNode); if (SkelMeshActor) { // Set the default position of the actor on the transforms // The Unreal transformation is different from FBX's Z-up: invert the Y-axis for translations and the Y/Z angle values in rotations. BaseNode->LclTranslation.Set(Converter.ConvertToFbxPos(SkelMeshActor->GetActorLocation())); BaseNode->LclRotation.Set(Converter.ConvertToFbxRot(SkelMeshActor->GetActorRotation().Euler())); BaseNode->LclScaling.Set(Converter.ConvertToFbxScale(SkelMeshActor->GetRootComponent()->GetRelativeScale3D())); } // Create the Skeleton TArray BoneNodes; FbxNode* SkeletonRootNode = CreateSkeleton(SkelMesh, BoneNodes); BaseNode->AddChild(SkeletonRootNode); bool bAnyObjectMissingSourceData = false; float ExportStartTime = 0.f; for (int32 AnimSeqIndex = 0; AnimSeqIndex < AnimSeqList.Num(); ++AnimSeqIndex) { const UAnimSequence* AnimSeq = AnimSeqList[AnimSeqIndex]; const FAnimControlTrackKey& TrackKey = TrackKeys[AnimSeqIndex]; // Shift the anim sequences so the first one is at time zero in the FBX file const float CurrentStartTime = TrackKey.StartTime - ExportStartTime; ExportAnimSequenceToFbx(AnimSeq, SkelMesh, BoneNodes, AnimLayer, TrackKey.AnimStartOffset, TrackKey.AnimEndOffset, TrackKey.AnimPlayRate, CurrentStartTime); } CorrectAnimTrackInterpolation(BoneNodes, AnimLayer); if (bAnyObjectMissingSourceData) { FMessageDialog::Open(EAppMsgType::Ok, NSLOCTEXT("UnrealEd", "Exporter_Error_SourceDataUnavailable", "No source data available for some objects. See the log for details.")); } } /** * Exports all the animation sequences part of a single Group in a Matinee sequence * as a single animation in the FBX document. The animation is created by sampling the * sequence at DEFAULT_SAMPLERATE updates/second and extracting the resulting bone transforms from the given * skeletal mesh */ void FFbxExporter::ExportMatineeGroup(class AMatineeActor* MatineeActor, USkeletalMeshComponent* SkeletalMeshComponent) { if (Scene == NULL || MatineeActor == NULL || SkeletalMeshComponent == NULL || MatineeActor->MatineeData->InterpLength == 0) { return; } FbxString NodeName("MatineeSequence"); FbxNode* BaseNode = FbxNode::Create(Scene, NodeName); Scene->GetRootNode()->AddChild(BaseNode); AActor* Owner = SkeletalMeshComponent->GetOwner(); if (Owner && Owner->GetRootComponent()) { // Set the default position of the actor on the transforms // The UE3 transformation is different from FBX's Z-up: invert the Y-axis for translations and the Y/Z angle values in rotations. BaseNode->LclTranslation.Set(Converter.ConvertToFbxPos(Owner->GetActorLocation())); BaseNode->LclRotation.Set(Converter.ConvertToFbxRot(Owner->GetActorRotation().Euler())); BaseNode->LclScaling.Set(Converter.ConvertToFbxScale(Owner->GetRootComponent()->GetRelativeScale3D())); } // Create the Skeleton TArray BoneNodes; FbxNode* SkeletonRootNode = CreateSkeleton(SkeletalMeshComponent->SkeletalMesh, BoneNodes); FbxSkeletonRoots.Add(SkeletalMeshComponent, SkeletonRootNode); BaseNode->AddChild(SkeletonRootNode); static const float SamplingRate = 1.f / DEFAULT_SAMPLERATE; FMatineeAnimTrackAdapter AnimTrackAdapter(MatineeActor); ExportAnimTrack(AnimTrackAdapter, Owner, SkeletalMeshComponent, SamplingRate); } void FFbxExporter::ExportAnimTrack(IAnimTrackAdapter& AnimTrackAdapter, AActor* Actor, USkeletalMeshComponent* InSkeletalMeshComponent, float SamplingRate) { // show a status update every 1 second worth of samples const float UpdateFrequency = 1.0f; float NextUpdateTime = UpdateFrequency; // find root and find the bone array TArray BoneNodes; if (FindSkeleton(InSkeletalMeshComponent, BoneNodes) == false) { UE_LOG(LogFbx, Warning, TEXT("Error FBX Animation Export, no root skeleton found.")); return; } // if we have no allocated bone space transforms something wrong so try to recalc them if (InSkeletalMeshComponent->GetBoneSpaceTransforms().Num() <= 0) { InSkeletalMeshComponent->RecalcRequiredBones(0); if (InSkeletalMeshComponent->GetBoneSpaceTransforms().Num() <= 0) { UE_LOG(LogFbx, Warning, TEXT("Error FBX Animation Export, no bone transforms.")); return; } } TArray CustomAttributes; FTransform InitialInvParentTransform; FTransform ComponentRelativeTransform = FTransform::Identity; int32 LocalStartFrame = AnimTrackAdapter.GetLocalStartFrame(); int32 StartFrame = AnimTrackAdapter.GetStartFrame(); int32 AnimationLength = AnimTrackAdapter.GetLength(); float FrameRate = AnimTrackAdapter.GetFrameRate(); TArray SkeletalMeshComponents; Actor->GetComponents(SkeletalMeshComponents); const float TickRate = 1.0f / FrameRate; FScopedSlowTask SlowTask(AnimationLength, NSLOCTEXT("UnrealEd", "ExportAnimationProgress", "Exporting Animation")); SlowTask.MakeDialog(true); for (int32 FrameCount = 0; FrameCount <= AnimationLength; ++FrameCount) { SlowTask.EnterProgressFrame(); int32 LocalFrame = LocalStartFrame + FrameCount; float SampleTime = (StartFrame + FrameCount) / FrameRate; // This will call UpdateSkelPose on the skeletal mesh component to move bones based on animations in the matinee group AnimTrackAdapter.UpdateAnimation(LocalFrame); if (FrameCount == 0) { InitialInvParentTransform = Actor->GetRootComponent()->GetComponentTransform().Inverse(); if (InSkeletalMeshComponent->GetAttachParent()) { if (InSkeletalMeshComponent->GetAttachParent() == Actor->GetRootComponent()) { ComponentRelativeTransform = InSkeletalMeshComponent->GetRelativeTransform().Inverse(); } else { ComponentRelativeTransform = InSkeletalMeshComponent->GetAttachParent()->GetRelativeTransform().Inverse(); } } } // This will retrieve the currently active anim sequence (topmost) for custom attributes const UAnimSequence* AnimSeq = AnimTrackAdapter.GetAnimSequence(LocalFrame); float AnimTime = AnimTrackAdapter.GetAnimTime(LocalFrame); // Update space bases so new animation position has an effect. // @todo - hack - this will be removed at some point for (USkeletalMeshComponent* SkeletalMeshComponent: SkeletalMeshComponents) { USceneComponent* Child = SkeletalMeshComponent; while (Child) { if (USkeletalMeshComponent* ChildSkeletalMeshComponent = Cast(Child)) { SkeletalMeshComponent->TickAnimation(TickRate, false); SkeletalMeshComponent->RefreshBoneTransforms(); SkeletalMeshComponent->RefreshSlaveComponents(); SkeletalMeshComponent->UpdateComponentToWorld(); SkeletalMeshComponent->FinalizeBoneTransform(); SkeletalMeshComponent->MarkRenderTransformDirty(); SkeletalMeshComponent->MarkRenderDynamicDataDirty(); } if (Child->GetOwner()) { // Child->GetOwner()->Tick(TickRate); } Child = Child->GetAttachParent(); } } FbxTime ExportTime; ExportTime.SetSecondDouble(GetExportOptions()->bExportLocalTime ? LocalFrame / FrameRate : SampleTime); NextUpdateTime -= SamplingRate; if (NextUpdateTime <= 0.0f) { NextUpdateTime = UpdateFrequency; GWarn->StatusUpdate(FMath::RoundToInt(SampleTime), FMath::RoundToInt(AnimationLength), NSLOCTEXT("FbxExporter", "ExportingToFbxStatus", "Exporting to FBX")); } TArray LocalBoneTransforms = InSkeletalMeshComponent->GetBoneSpaceTransforms(); if (LocalBoneTransforms.Num() == 0) { continue; } // Add the animation data to the bone nodes for (int32 BoneIndex = 0; BoneIndex < BoneNodes.Num(); ++BoneIndex) { FName BoneName = InSkeletalMeshComponent->SkeletalMesh->GetRefSkeleton().GetBoneName(BoneIndex); FbxNode* CurrentBoneNode = BoneNodes[BoneIndex]; // Create the AnimCurves FbxAnimCurve* Curves[6]; Curves[0] = CurrentBoneNode->LclTranslation.GetCurve(AnimLayer, FBXSDK_CURVENODE_COMPONENT_X, true); Curves[1] = CurrentBoneNode->LclTranslation.GetCurve(AnimLayer, FBXSDK_CURVENODE_COMPONENT_Y, true); Curves[2] = CurrentBoneNode->LclTranslation.GetCurve(AnimLayer, FBXSDK_CURVENODE_COMPONENT_Z, true); Curves[3] = CurrentBoneNode->LclRotation.GetCurve(AnimLayer, FBXSDK_CURVENODE_COMPONENT_X, true); Curves[4] = CurrentBoneNode->LclRotation.GetCurve(AnimLayer, FBXSDK_CURVENODE_COMPONENT_Y, true); Curves[5] = CurrentBoneNode->LclRotation.GetCurve(AnimLayer, FBXSDK_CURVENODE_COMPONENT_Z, true); for (int32 i = 0; i < 6; ++i) { Curves[i]->KeyModifyBegin(); } FTransform BoneTransform = LocalBoneTransforms[BoneIndex]; if (GetExportOptions()->MapSkeletalMotionToRoot && BoneIndex == 0) { BoneTransform = InSkeletalMeshComponent->GetSocketTransform(BoneName) * InitialInvParentTransform * ComponentRelativeTransform; } FbxVector4 Translation = Converter.ConvertToFbxPos(BoneTransform.GetLocation()); FbxVector4 Rotation = Converter.ConvertToFbxRot(BoneTransform.GetRotation().Euler()); int32 lKeyIndex; for (int32 i = 0, j = 3; i < 3; ++i, ++j) { lKeyIndex = Curves[i]->KeyAdd(ExportTime); Curves[i]->KeySetValue(lKeyIndex, Translation[i]); Curves[i]->KeySetInterpolation(lKeyIndex, FbxAnimCurveDef::eInterpolationCubic); lKeyIndex = Curves[j]->KeyAdd(ExportTime); Curves[j]->KeySetValue(lKeyIndex, Rotation[i]); Curves[j]->KeySetInterpolation(lKeyIndex, FbxAnimCurveDef::eInterpolationCubic); } for (int32 i = 0; i < 6; ++i) { Curves[i]->KeyModifyEnd(); } // Custom attributes if (!AnimSeq) { continue; } CustomAttributes.Reset(); AnimSeq->GetCustomAttributesForBone(BoneName, CustomAttributes); TArray> FloatCustomAttributeIndices; TArray> IntCustomAttributeIndices; // Setup custom attribute properties and curves for (int32 AttributeIndex = 0; AttributeIndex < CustomAttributes.Num(); ++AttributeIndex) { const FCustomAttribute& Attribute = CustomAttributes[AttributeIndex]; const FName& AttributeName = Attribute.Name; const EVariantTypes VariantType = static_cast(Attribute.VariantType); if (VariantType == EVariantTypes::Int32) { FbxProperty AnimCurveFbxProp = FbxProperty::Create(CurrentBoneNode, FbxIntDT, TCHAR_TO_UTF8(*AttributeName.ToString())); AnimCurveFbxProp.ModifyFlag(FbxPropertyFlags::eAnimatable, true); AnimCurveFbxProp.ModifyFlag(FbxPropertyFlags::eUserDefined, true); FbxAnimCurve* AnimFbxCurve = AnimCurveFbxProp.GetCurve(AnimLayer, true); AnimFbxCurve->KeyModifyBegin(); IntCustomAttributeIndices.Emplace(AttributeIndex, AnimFbxCurve); } else if (VariantType == EVariantTypes::Float) { FbxProperty AnimCurveFbxProp = FbxProperty::Create(CurrentBoneNode, FbxFloatDT, TCHAR_TO_UTF8(*AttributeName.ToString())); AnimCurveFbxProp.ModifyFlag(FbxPropertyFlags::eAnimatable, true); AnimCurveFbxProp.ModifyFlag(FbxPropertyFlags::eUserDefined, true); FbxAnimCurve* AnimFbxCurve = AnimCurveFbxProp.GetCurve(AnimLayer, true); AnimFbxCurve->KeyModifyBegin(); FloatCustomAttributeIndices.Emplace(AttributeIndex, AnimFbxCurve); } else if (VariantType == EVariantTypes::String) { FbxProperty AnimCurveFbxProp = FbxProperty::Create(CurrentBoneNode, FbxStringDT, TCHAR_TO_UTF8(*AttributeName.ToString())); AnimCurveFbxProp.ModifyFlag(FbxPropertyFlags::eUserDefined, true); // String attributes can't be keyed, simply set a normal value. FString AttributeValue; FCustomAttributesRuntime::GetAttributeValue(Attribute, 0.f, AttributeValue); FbxString FbxValueString(TCHAR_TO_UTF8(*AttributeValue)); AnimCurveFbxProp.Set(FbxValueString); } else { ensureMsgf(false, TEXT("Trying to export unsupported custom attribte (float, int32 and FString are currently supported)")); } for (TPair& CurrentAttributeCurve: FloatCustomAttributeIndices) { float AttributeValue = 0.f; FCustomAttributesRuntime::GetAttributeValue(CustomAttributes[CurrentAttributeCurve.Key], AnimTime, AttributeValue); int32 KeyIndex = CurrentAttributeCurve.Value->KeyAdd(ExportTime); CurrentAttributeCurve.Value->KeySetValue(KeyIndex, AttributeValue); } for (TPair& CurrentAttributeCurve: IntCustomAttributeIndices) { int32 AttributeValue = 0; FCustomAttributesRuntime::GetAttributeValue(CustomAttributes[CurrentAttributeCurve.Key], AnimTime, AttributeValue); int32 KeyIndex = CurrentAttributeCurve.Value->KeyAdd(ExportTime); CurrentAttributeCurve.Value->KeySetValue(KeyIndex, static_cast(AttributeValue)); } } auto MarkCurveEnd = [](auto& CurvesArray) { for (auto& CurvePair: CurvesArray) { CurvePair.Value->KeyModifyEnd(); } }; MarkCurveEnd(FloatCustomAttributeIndices); MarkCurveEnd(IntCustomAttributeIndices); } } CorrectAnimTrackInterpolation(BoneNodes, AnimLayer); } } // namespace UnFbx