// Copyright Epic Games, Inc. All Rights Reserved. #include "FbxAnimation.h" #include "Async/ParallelFor.h" #include "CoreMinimal.h" #include "FbxAPI.h" #include "FbxConvert.h" #include "FbxHelper.h" #include "FbxInclude.h" #include "FbxMesh.h" #include "Fbx/InterchangeFbxMessages.h" #include "InterchangeAnimationTrackSetNode.h" #include "InterchangeCommonAnimationPayload.h" #include "InterchangeMeshNode.h" #include "InterchangeResultsContainer.h" #include "InterchangeSceneNode.h" #include "MeshDescription.h" #include "Misc/FileHelper.h" #include "Misc/ScopeLock.h" #include "Nodes/InterchangeBaseNodeContainer.h" #include "Serialization/LargeMemoryWriter.h" #include "SkeletalMeshAttributes.h" #include "StaticMeshAttributes.h" #define LOCTEXT_NAMESPACE "InterchangeFbxMesh" namespace UE::Interchange::Private { bool IsStepCurve(FbxAnimCurveNode* AnimCurveNode) { const uint32 ChannelCount = AnimCurveNode->GetChannelsCount(); for (uint32 ChannelIndex = 0; ChannelIndex < ChannelCount; ++ChannelIndex) { const uint32 ChannelCurveCount = AnimCurveNode->GetCurveCount(ChannelIndex); for (uint32 CurveIndex = 0; CurveIndex < ChannelCurveCount; ++CurveIndex) { if (FbxAnimCurve* CurrentAnimCurve = AnimCurveNode->GetCurve(ChannelIndex, CurveIndex)) { int32 KeyCount = CurrentAnimCurve->KeyGetCount(); for (int32 KeyIndex = 0; KeyIndex < KeyCount; ++KeyIndex) { FbxAnimCurveKey Key = CurrentAnimCurve->KeyGet(KeyIndex); FbxAnimCurveDef::EInterpolationType KeyInterpMode = Key.GetInterpolation(); if (KeyInterpMode != FbxAnimCurveDef::EInterpolationType::eInterpolationConstant) { return false; } } } } } return true; } bool ImportCurve(const FbxAnimCurve* SourceFloatCurves, const float ScaleValue, TArray& DestinationFloatCurve) { if (!SourceFloatCurves) { return true; } const float DefaultCurveWeight = FbxAnimCurveDef::sDEFAULT_WEIGHT; //We use the non const to query the left and right derivative of the key, for whatever reason those FBX API functions are not const FbxAnimCurve* NonConstSourceFloatCurves = const_cast(SourceFloatCurves); int32 KeyCount = SourceFloatCurves->KeyGetCount(); for (int32 KeyIndex = 0; KeyIndex < KeyCount; ++KeyIndex) { FbxAnimCurveKey Key = SourceFloatCurves->KeyGet(KeyIndex); FbxTime KeyTime = Key.GetTime(); const float KeyTimeValue = static_cast(KeyTime.GetSecondDouble()); float Value = Key.GetValue() * ScaleValue; FInterchangeCurveKey& InterchangeCurveKey = DestinationFloatCurve.AddDefaulted_GetRef(); InterchangeCurveKey.Time = KeyTimeValue; InterchangeCurveKey.Value = Value; const bool bIncludeOverrides = true; FbxAnimCurveDef::ETangentMode KeyTangentMode = Key.GetTangentMode(bIncludeOverrides); FbxAnimCurveDef::EInterpolationType KeyInterpMode = Key.GetInterpolation(); FbxAnimCurveDef::EWeightedMode KeyTangentWeightMode = Key.GetTangentWeightMode(); EInterchangeCurveInterpMode NewInterpMode = EInterchangeCurveInterpMode::Linear; EInterchangeCurveTangentMode NewTangentMode = EInterchangeCurveTangentMode::Auto; EInterchangeCurveTangentWeightMode NewTangentWeightMode = EInterchangeCurveTangentWeightMode::WeightedNone; float RightTangent = NonConstSourceFloatCurves->KeyGetRightDerivative(KeyIndex) * ScaleValue; float LeftTangent = NonConstSourceFloatCurves->KeyGetLeftDerivative(KeyIndex) * ScaleValue; float RightTangentWeight = 0.0f; float LeftTangentWeight = 0.0f; //This one is dependent on the previous key. bool bLeftWeightActive = false; bool bRightWeightActive = false; const bool bPreviousKeyValid = KeyIndex > 0; const bool bNextKeyValid = KeyIndex < KeyCount - 1; float PreviousValue = 0.0f; float PreviousKeyTimeValue = 0.0f; float NextValue = 0.0f; float NextKeyTimeValue = 0.0f; if (bPreviousKeyValid) { FbxAnimCurveKey PreviousKey = SourceFloatCurves->KeyGet(KeyIndex - 1); FbxTime PreviousKeyTime = PreviousKey.GetTime(); PreviousKeyTimeValue = static_cast(PreviousKeyTime.GetSecondDouble()); PreviousValue = PreviousKey.GetValue() * ScaleValue; //The left tangent is driven by the previous key. If the previous key have a the NextLeftweight or both flag weighted mode, it mean the next key is weighted on the left side bLeftWeightActive = (PreviousKey.GetTangentWeightMode() & FbxAnimCurveDef::eWeightedNextLeft) > 0; if (bLeftWeightActive) { LeftTangentWeight = PreviousKey.GetDataFloat(FbxAnimCurveDef::eNextLeftWeight); } } if (bNextKeyValid) { FbxAnimCurveKey NextKey = SourceFloatCurves->KeyGet(KeyIndex + 1); FbxTime NextKeyTime = NextKey.GetTime(); NextKeyTimeValue = static_cast(NextKeyTime.GetSecondDouble()); NextValue = NextKey.GetValue() * ScaleValue; bRightWeightActive = (KeyTangentWeightMode & FbxAnimCurveDef::eWeightedRight) > 0; if (bRightWeightActive) { //The right tangent weight should be use only if we are not the last key since the last key do not have a right tangent. //Use the current key to gather the right tangent weight RightTangentWeight = Key.GetDataFloat(FbxAnimCurveDef::eRightWeight); } } // When this flag is true, the tangent is flat if the value has the same value as the previous or next key. const bool bTangentGenericClamp = (KeyTangentMode & FbxAnimCurveDef::eTangentGenericClamp); //Time independent tangent this is consider has a spline tangent key const bool bTangentGenericTimeIndependent = (KeyTangentMode & FbxAnimCurveDef::ETangentMode::eTangentGenericTimeIndependent); // When this flag is true, the tangent is flat if the value is outside of the [previous key, next key] value range. //Clamp progressive is (eTangentGenericClampProgressive |eTangentGenericTimeIndependent) const bool bTangentGenericClampProgressive = (KeyTangentMode & FbxAnimCurveDef::ETangentMode::eTangentGenericClampProgressive) == FbxAnimCurveDef::ETangentMode::eTangentGenericClampProgressive; if (KeyTangentMode & FbxAnimCurveDef::eTangentGenericBreak) { NewTangentMode = EInterchangeCurveTangentMode::Break; } else if (KeyTangentMode & FbxAnimCurveDef::eTangentUser) { NewTangentMode = EInterchangeCurveTangentMode::User; } switch (KeyInterpMode) { case FbxAnimCurveDef::eInterpolationConstant://! Constant value until next key. NewInterpMode = EInterchangeCurveInterpMode::Constant; break; case FbxAnimCurveDef::eInterpolationLinear://! Linear progression to next key. NewInterpMode = EInterchangeCurveInterpMode::Linear; break; case FbxAnimCurveDef::eInterpolationCubic://! Cubic progression to next key. NewInterpMode = EInterchangeCurveInterpMode::Cubic; // get tangents { bool bIsFlatTangent = false; bool bIsComputedTangent = false; if (bTangentGenericClampProgressive) { if (bPreviousKeyValid && bNextKeyValid) { const float PreviousNextHalfDelta = (NextValue - PreviousValue) * 0.5f; const float PreviousNextAverage = PreviousValue + PreviousNextHalfDelta; // If the value is outside of the previous-next value range, the tangent is flat. bIsFlatTangent = FMath::Abs(Value - PreviousNextAverage) >= FMath::Abs(PreviousNextHalfDelta); } else { //Start/End tangent with the ClampProgressive flag are flat. bIsFlatTangent = true; } } else if (bTangentGenericClamp && (bPreviousKeyValid || bNextKeyValid)) { if (bPreviousKeyValid && PreviousValue == Value) { bIsFlatTangent = true; } if (bNextKeyValid) { bIsFlatTangent |= Value == NextValue; } } else if (bTangentGenericTimeIndependent) { //Spline tangent key, because bTangentGenericClampProgressive include bTangentGenericTimeIndependent, we must treat this case after bTangentGenericClampProgressive if (KeyCount == 1) { bIsFlatTangent = true; } else { //Spline tangent key must be User mode since we want to keep the tangents provide by the fbx key left and right derivatives NewTangentMode = EInterchangeCurveTangentMode::User; } } if (bIsFlatTangent) { RightTangent = 0; LeftTangent = 0; //To force flat tangent we need to set the tangent mode to user NewTangentMode = EInterchangeCurveTangentMode::User; } } break; } //auto with weighted give the wrong result, so when auto is weighted we set user mode and set the Right tangent equal to the left tangent. //Auto has only the left tangent set if (NewTangentMode == EInterchangeCurveTangentMode::Auto && (bLeftWeightActive || bRightWeightActive)) { NewTangentMode = EInterchangeCurveTangentMode::User; RightTangent = LeftTangent; } if (NewTangentMode != EInterchangeCurveTangentMode::Auto) { const bool bEqualTangents = FMath::IsNearlyEqual(LeftTangent, RightTangent); //If tangents are different then broken. if (bEqualTangents) { NewTangentMode = EInterchangeCurveTangentMode::User; } else { NewTangentMode = EInterchangeCurveTangentMode::Break; } } //Only cubic interpolation allow weighted tangents if (KeyInterpMode == FbxAnimCurveDef::eInterpolationCubic) { if (bLeftWeightActive && bRightWeightActive) { NewTangentWeightMode = EInterchangeCurveTangentWeightMode::WeightedBoth; } else if (bLeftWeightActive) { NewTangentWeightMode = EInterchangeCurveTangentWeightMode::WeightedArrive; RightTangentWeight = DefaultCurveWeight; } else if (bRightWeightActive) { NewTangentWeightMode = EInterchangeCurveTangentWeightMode::WeightedLeave; LeftTangentWeight = DefaultCurveWeight; } else { NewTangentWeightMode = EInterchangeCurveTangentWeightMode::WeightedNone; LeftTangentWeight = DefaultCurveWeight; RightTangentWeight = DefaultCurveWeight; } auto ComputeWeightInternal = [](float TimeA, float TimeB, const float TangentSlope, const float TangentWeight) { const float X = TimeA - TimeB; const float Y = TangentSlope * X; return FMath::Sqrt(X * X + Y * Y) * TangentWeight; }; if (!FMath::IsNearlyZero(LeftTangentWeight)) { if (bPreviousKeyValid) { LeftTangentWeight = ComputeWeightInternal(KeyTimeValue, PreviousKeyTimeValue, LeftTangent, LeftTangentWeight); } else { LeftTangentWeight = 0.0f; } } if (!FMath::IsNearlyZero(RightTangentWeight)) { if (bNextKeyValid) { RightTangentWeight = ComputeWeightInternal(NextKeyTimeValue, KeyTimeValue, RightTangent, RightTangentWeight); } else { RightTangentWeight = 0.0f; } } } const bool bForceDisableTangentRecompute = false; //No need to recompute all the tangents of the curve every time we change de key. InterchangeCurveKey.InterpMode = NewInterpMode; InterchangeCurveKey.TangentMode = NewTangentMode; InterchangeCurveKey.TangentWeightMode = NewTangentWeightMode; InterchangeCurveKey.ArriveTangent = LeftTangent; InterchangeCurveKey.LeaveTangent = RightTangent; InterchangeCurveKey.ArriveTangentWeight = LeftTangentWeight; InterchangeCurveKey.LeaveTangentWeight = RightTangentWeight; } return true; } template void FillStepCurveAttribute(TArray& OutFrameTimes, TArray& OutFrameValues, const FbxAnimCurve* FbxCurve, TFunctionRef EvaluationFunction) { const int32 KeyCount = FbxCurve ? FbxCurve->KeyGetCount() : 0; if (KeyCount > 0) { OutFrameTimes.Reserve(KeyCount); OutFrameValues.Reserve(KeyCount); const FbxTime StartTime = FbxCurve->KeyGet(0).GetTime(); for (int32 KeyIndex = 0; KeyIndex < KeyCount; ++KeyIndex) { FbxAnimCurveKey Key = FbxCurve->KeyGet(KeyIndex); FbxTime KeyTime = Key.GetTime() - StartTime; OutFrameTimes.Add(KeyTime.GetSecondDouble()); OutFrameValues.Add(EvaluationFunction(&Key, &KeyTime)); } } else { OutFrameTimes.Add(0); OutFrameValues.Add(EvaluationFunction(nullptr, nullptr)); } } template void ImportStepCurve(const FbxAnimCurve* SourceCurves, FbxProperty& Property, FInterchangeStepCurve& DestinationCurve) { TArray StepCurveValues; FillStepCurveAttribute(DestinationCurve.KeyTimes, StepCurveValues, SourceCurves, [&Property](const FbxAnimCurveKey* Key, const FbxTime* KeyTime) { if(Key) { return static_cast(Key->GetValue()); } else { return static_cast(Property.Get()); } }); if constexpr(std::is_same_v) { DestinationCurve.BooleanKeyValues = StepCurveValues; } else if constexpr(std::is_floating_point_v) { check(false); //Float curve payload should be extract as FInterchangeCurve since we can interpolate them } else if constexpr(sizeof(T) == sizeof(uint8)) { DestinationCurve.ByteKeyValues = StepCurveValues; } else if constexpr(std::is_integral_v && sizeof(T) > sizeof(uint8)) { DestinationCurve.IntegerKeyValues = StepCurveValues; } } void ImportStringStepCurve(const FbxAnimCurve* SourceCurves, FbxProperty& Property, FInterchangeStepCurve& DestinationCurve) { TArray StepCurveValues; FillStepCurveAttribute(DestinationCurve.KeyTimes, StepCurveValues, SourceCurves, [&Property](const FbxAnimCurveKey* Key, const FbxTime* KeyTime) { if (KeyTime) { FbxPropertyValue& EvaluatedValue = Property.EvaluateValue(*KeyTime); FbxString StringValue; EvaluatedValue.Get(&StringValue, EFbxType::eFbxString); return FString(UTF8_TO_TCHAR(StringValue)); } else { return FString(UTF8_TO_TCHAR(Property.Get())); } }); DestinationCurve.StringKeyValues = StepCurveValues; } struct FGetFbxTransformCurvesParameters { FGetFbxTransformCurvesParameters(FbxScene* InSDKScene, FbxNode* InNode) { SDKScene = InSDKScene; check(SDKScene); Node = InNode; check(Node); } FbxScene* SDKScene = nullptr; FbxNode* Node = nullptr; bool IsNodeAnimated = false; FbxAnimStack* CurrentAnimStack = nullptr; }; void GetFbxTransformCurves(FGetFbxTransformCurvesParameters& Parameters, const int32& AnimationIndex) { if (!ensure(Parameters.SDKScene) || !ensure(Parameters.Node)) { return; } //Get the node transform curve keys, the transform components are separate into float curve //Translation X //Translation Y //Translation Z //Euler X //Euler Y //Euler Z //Scale X //Scale Y //Scale Z Parameters.IsNodeAnimated = false; int32 NumAnimations = Parameters.SDKScene->GetSrcObjectCount(); if (AnimationIndex >= NumAnimations) { return; } int32 NumberOfChannels = 9; Parameters.CurrentAnimStack = (FbxAnimStack*)Parameters.SDKScene->GetSrcObject(AnimationIndex); int32 NumLayers = Parameters.CurrentAnimStack->GetMemberCount(); for (int LayerIndex = 0; LayerIndex < NumLayers && !Parameters.IsNodeAnimated; LayerIndex++) { FbxAnimLayer* AnimLayer = (FbxAnimLayer*)Parameters.CurrentAnimStack->GetMember(LayerIndex); TArray TransformChannelCurves; TransformChannelCurves.Reserve(NumberOfChannels); // Display curves specific to properties TransformChannelCurves.Add(Parameters.Node->LclTranslation.GetCurve(AnimLayer, FBXSDK_CURVENODE_COMPONENT_X, false)); TransformChannelCurves.Add(Parameters.Node->LclTranslation.GetCurve(AnimLayer, FBXSDK_CURVENODE_COMPONENT_Y, false)); TransformChannelCurves.Add(Parameters.Node->LclTranslation.GetCurve(AnimLayer, FBXSDK_CURVENODE_COMPONENT_Z, false)); TransformChannelCurves.Add(Parameters.Node->LclRotation.GetCurve(AnimLayer, FBXSDK_CURVENODE_COMPONENT_X, false)); TransformChannelCurves.Add(Parameters.Node->LclRotation.GetCurve(AnimLayer, FBXSDK_CURVENODE_COMPONENT_Y, false)); TransformChannelCurves.Add(Parameters.Node->LclRotation.GetCurve(AnimLayer, FBXSDK_CURVENODE_COMPONENT_Z, false)); TransformChannelCurves.Add(Parameters.Node->LclScaling.GetCurve(AnimLayer, FBXSDK_CURVENODE_COMPONENT_X, false)); TransformChannelCurves.Add(Parameters.Node->LclScaling.GetCurve(AnimLayer, FBXSDK_CURVENODE_COMPONENT_Y, false)); TransformChannelCurves.Add(Parameters.Node->LclScaling.GetCurve(AnimLayer, FBXSDK_CURVENODE_COMPONENT_Z, false)); if (!Parameters.IsNodeAnimated) { for (int32 ChannelIndex = 0; ChannelIndex < NumberOfChannels; ++ChannelIndex) { if (TransformChannelCurves[ChannelIndex]) { Parameters.IsNodeAnimated = true; break; } } } } } bool FAnimationPayloadContext::FetchPayloadToFile(FFbxParser& Parser, const FString& PayloadFilepath) { if (AttributeFetchPayloadData.IsSet() || AttributeNodeTransformFetchPayloadData.IsSet()) { return InternalFetchCurveNodePayloadToFile(Parser, PayloadFilepath); } else if (MorphTargetFetchPayloadData.IsSet()) { return InternalFetchMorphTargetCurvePayloadToFile(Parser, PayloadFilepath); } return false; } bool FAnimationPayloadContext::FetchAnimationBakeTransformPayloadForTime(FFbxParser& Parser, const FbxTime Currenttime, FTransform& OutLocalTransform) { if (!ensure(NodeTransformFetchPayloadData.IsSet())) { UInterchangeResultError_Generic* Message = Parser.AddMessage(); Message->Text = LOCTEXT("NodeTransformFetchPayloadData_NotSet", "Cannot fetch FBX animation transform payload because the FBX FNodeTransformFetchPayloadData is not set."); return false; } FNodeTransformFetchPayloadData& FetchPayloadData = NodeTransformFetchPayloadData.GetValue(); if (!ensure(FetchPayloadData.Node)) { UInterchangeResultError_Generic* Message = Parser.AddMessage(); Message->InterchangeKey = Parser.GetFbxHelper()->GetFbxNodeHierarchyName(FetchPayloadData.Node); Message->Text = LOCTEXT("FBXNodeNull", "Cannot fetch FBX animation transform payload because the FBX node is null."); return false; } bool bNanErrorLogged = false; auto LogNanError = [&bNanErrorLogged, &FetchPayloadData, &Parser]() { if (bNanErrorLogged) { return; } UInterchangeResultError_Generic* Message = Parser.AddMessage(); Message->InterchangeKey = Parser.GetFbxHelper()->GetFbxNodeHierarchyName(FetchPayloadData.Node); Message->Text = LOCTEXT("BoneTransformNan", "Error when fetching FBX animation bake transforms payload, some transform contain NAN."); bNanErrorLogged = true; }; FbxNode* ParentNode = FetchPayloadData.Node->GetParent(); if (ParentNode) { FbxAMatrix NodeTransform = FetchPayloadData.Node->EvaluateGlobalTransform(Currenttime); FTransform GlobalTransform = UE::Interchange::Private::FFbxConvert::ConvertTransform(NodeTransform); if (GlobalTransform.ContainsNaN()) { LogNanError(); GlobalTransform.SetIdentity(); } FbxAMatrix ParentTransform = ParentNode->EvaluateGlobalTransform(Currenttime); FTransform ParentGlobalTransform = UE::Interchange::Private::FFbxConvert::ConvertTransform(ParentTransform); if (ParentGlobalTransform.ContainsNaN()) { LogNanError(); ParentGlobalTransform.SetIdentity(); } OutLocalTransform = GlobalTransform.GetRelativeTransform(ParentGlobalTransform); } else { FbxAMatrix& LocalMatrix = FetchPayloadData.Node->EvaluateLocalTransform(Currenttime); FbxVector4 NewLocalT = LocalMatrix.GetT(); FbxVector4 NewLocalS = LocalMatrix.GetS(); FbxQuaternion NewLocalQ = LocalMatrix.GetQ(); OutLocalTransform.SetTranslation(UE::Interchange::Private::FFbxConvert::ConvertPos(NewLocalT)); OutLocalTransform.SetScale3D(UE::Interchange::Private::FFbxConvert::ConvertScale(NewLocalS)); OutLocalTransform.SetRotation(UE::Interchange::Private::FFbxConvert::ConvertRotToQuat(NewLocalQ)); } return true; } FbxAnimStack* FAnimationPayloadContext::GetAnimStack() { if (NodeTransformFetchPayloadData.IsSet()) { return NodeTransformFetchPayloadData->CurrentAnimStack; } return nullptr; } bool FAnimationPayloadContext::InternalFetchCurveNodePayloadToFile(FFbxParser& Parser, const FString& PayloadFilepath) { if (AttributeFetchPayloadData.IsSet()) { FAttributeFetchPayloadData& FetchPayloadData = AttributeFetchPayloadData.GetValue(); if (!ensure(FetchPayloadData.Node != nullptr)) { UInterchangeResultError_Generic* Message = Parser.AddMessage(); Message->InterchangeKey = Parser.GetFbxHelper()->GetFbxNodeHierarchyName(FetchPayloadData.Node); Message->Text = LOCTEXT("InternalFetchCurveNodePayloadToFile_FBXNodeNull", "Cannot fetch FBX animation curve payload because the FBX node is null."); return false; } if (!ensure(FetchPayloadData.AnimCurves != nullptr)) { UInterchangeResultError_Generic* Message = Parser.AddMessage(); Message->InterchangeKey = Parser.GetFbxHelper()->GetFbxNodeHierarchyName(FetchPayloadData.Node); Message->Text = LOCTEXT("InternalFetchCurveNodePayloadToFile_FBXCurveNull", "Cannot fetch FBX user attribute animation curve payload because the FBX anim curve node is null."); return false; } if (FetchPayloadData.bAttributeTypeIsStepCurveAnimation) { //Fetch TArray step curve, no interpolation TArray InterchangeStepCurves; const uint32 ChannelCount = FetchPayloadData.AnimCurves->GetChannelsCount(); for (uint32 ChannelIndex = 0; ChannelIndex < ChannelCount; ++ChannelIndex) { const uint32 ChannelCurveCount = FetchPayloadData.AnimCurves->GetCurveCount(ChannelIndex); for (uint32 CurveIndex = 0; CurveIndex < ChannelCurveCount; ++CurveIndex) { if (FbxAnimCurve* CurrentAnimCurve = FetchPayloadData.AnimCurves->GetCurve(ChannelIndex, CurveIndex)) { switch (FetchPayloadData.PropertyType) { case EFbxType::eFbxBool: ImportStepCurve(CurrentAnimCurve, FetchPayloadData.Property, InterchangeStepCurves.AddDefaulted_GetRef()); case EFbxType::eFbxChar: case EFbxType::eFbxUChar: case EFbxType::eFbxEnum: ImportStepCurve(CurrentAnimCurve, FetchPayloadData.Property, InterchangeStepCurves.AddDefaulted_GetRef()); case EFbxType::eFbxShort: case EFbxType::eFbxUShort: case EFbxType::eFbxInt: case EFbxType::eFbxUInt: case EFbxType::eFbxLongLong: case EFbxType::eFbxULongLong: ImportStepCurve(CurrentAnimCurve, FetchPayloadData.Property, InterchangeStepCurves.AddDefaulted_GetRef()); break; case EFbxType::eFbxHalfFloat: case EFbxType::eFbxFloat: case EFbxType::eFbxDouble: case EFbxType::eFbxDouble2: case EFbxType::eFbxDouble3: case EFbxType::eFbxDouble4: check(false); //Float curve payload should be extract as FInterchangeCurve since we can interpolate them break; case EFbxType::eFbxString: ImportStringStepCurve(CurrentAnimCurve, FetchPayloadData.Property, InterchangeStepCurves.AddDefaulted_GetRef()); break; } } } } { FLargeMemoryWriter Ar; Ar << InterchangeStepCurves; uint8* ArchiveData = Ar.GetData(); int64 ArchiveSize = Ar.TotalSize(); TArray64 Buffer(ArchiveData, ArchiveSize); FFileHelper::SaveArrayToFile(Buffer, *PayloadFilepath); } } else { //Fetch TArray which are float curves with interpolation TArray InterchangeCurves; const uint32 ChannelCount = FetchPayloadData.AnimCurves->GetChannelsCount(); for (uint32 ChannelIndex = 0; ChannelIndex < ChannelCount; ++ChannelIndex) { const uint32 ChannelCurveCount = FetchPayloadData.AnimCurves->GetCurveCount(ChannelIndex); for (uint32 CurveIndex = 0; CurveIndex < ChannelCurveCount; ++CurveIndex) { if (FbxAnimCurve* CurrentAnimCurve = FetchPayloadData.AnimCurves->GetCurve(ChannelIndex, CurveIndex)) { ImportCurve(CurrentAnimCurve, 1.0f, InterchangeCurves.AddDefaulted_GetRef().Keys); } } } { FLargeMemoryWriter Ar; Ar << InterchangeCurves; uint8* ArchiveData = Ar.GetData(); int64 ArchiveSize = Ar.TotalSize(); TArray64 Buffer(ArchiveData, ArchiveSize); FFileHelper::SaveArrayToFile(Buffer, *PayloadFilepath); } } return true; } if (AttributeNodeTransformFetchPayloadData.IsSet()) { FAttributeNodeTransformFetchPayloadData& FetchPayloadData = AttributeNodeTransformFetchPayloadData.GetValue(); //Fetch TArray which are float curves with interpolation TArray InterchangeCurves; auto ParseInterchangeCurveCurve = [&InterchangeCurves](FbxAnimCurveNode* AnimCurveNode, const char* pChannelName, float Scale = 1.0f) { bool bInterchangeCurveAdded = false; if (AnimCurveNode) { int ChannelIndex = AnimCurveNode->GetChannelIndex(pChannelName); if (ChannelIndex != -1) { if (FbxAnimCurve* CurrentAnimCurve = AnimCurveNode->GetCurve(ChannelIndex)) { ImportCurve(CurrentAnimCurve, Scale, InterchangeCurves.AddDefaulted_GetRef().Keys); bInterchangeCurveAdded = true; } } } if (!bInterchangeCurveAdded) { InterchangeCurves.AddDefaulted(); } }; auto ResetPivotsPrePostRotationsAndSetRotationOrder = [](FbxNode* Node, double FrameRate) { //This function now clears out all pivots, post and pre rotations and set's the //RotationOrder to XYZ. //Updated per the latest documentation //https://help.autodesk.com/view/FBX/2017/ENU/?guid=__files_GUID_C35D98CB_5148_4B46_82D1_51077D8970EE_htm if (!ensure(Node)) { return; } // Activate pivot converting Node->SetPivotState(FbxNode::eSourcePivot, FbxNode::ePivotActive); Node->SetPivotState(FbxNode::eDestinationPivot, FbxNode::ePivotActive); FbxVector4 Zero(0, 0, 0); // We want to set all these to 0 and bake them into the transforms. Node->SetPostRotation(FbxNode::eDestinationPivot, Zero); Node->SetPreRotation(FbxNode::eDestinationPivot, Zero); Node->SetRotationOffset(FbxNode::eDestinationPivot, Zero); Node->SetScalingOffset(FbxNode::eDestinationPivot, Zero); Node->SetRotationPivot(FbxNode::eDestinationPivot, Zero); Node->SetScalingPivot(FbxNode::eDestinationPivot, Zero); Node->SetRotationOrder(FbxNode::eDestinationPivot, eEulerXYZ); //When we support other orders do this. //EFbxRotationOrder ro; //Node->GetRotationOrder(FbxNode::eSourcePivot, ro); //Node->SetRotationOrder(FbxNode::eDestinationPivot, ro); //Most DCC's don't have this but 3ds Max does Node->SetGeometricTranslation(FbxNode::eDestinationPivot, Zero); Node->SetGeometricRotation(FbxNode::eDestinationPivot, Zero); Node->SetGeometricScaling(FbxNode::eDestinationPivot, Zero); //NOTE THAT ConvertPivotAnimationRecursive did not seem to work when getting the local transform values!!! Node->ConvertPivotAnimationRecursive(nullptr, FbxNode::eDestinationPivot, FrameRate); }; ResetPivotsPrePostRotationsAndSetRotationOrder(FetchPayloadData.Node, FetchPayloadData.FrameRate); ParseInterchangeCurveCurve(FetchPayloadData.TranlsationCurveNode, FBXSDK_CURVENODE_COMPONENT_X); ParseInterchangeCurveCurve(FetchPayloadData.TranlsationCurveNode, FBXSDK_CURVENODE_COMPONENT_Y, -1.0f); //FBX to UE conversion with scale -1.0 ParseInterchangeCurveCurve(FetchPayloadData.TranlsationCurveNode, FBXSDK_CURVENODE_COMPONENT_Z); ParseInterchangeCurveCurve(FetchPayloadData.RotationCurveNode, FBXSDK_CURVENODE_COMPONENT_X); ParseInterchangeCurveCurve(FetchPayloadData.RotationCurveNode, FBXSDK_CURVENODE_COMPONENT_Y, -1.0f); //FBX to UE conversion with scale -1.0 ParseInterchangeCurveCurve(FetchPayloadData.RotationCurveNode, FBXSDK_CURVENODE_COMPONENT_Z, -1.0f); //FBX to UE conversion with scale -1.0 ParseInterchangeCurveCurve(FetchPayloadData.ScaleCurveNode, FBXSDK_CURVENODE_COMPONENT_X); ParseInterchangeCurveCurve(FetchPayloadData.ScaleCurveNode, FBXSDK_CURVENODE_COMPONENT_Y); ParseInterchangeCurveCurve(FetchPayloadData.ScaleCurveNode, FBXSDK_CURVENODE_COMPONENT_Z); { FLargeMemoryWriter Ar; Ar << InterchangeCurves; uint8* ArchiveData = Ar.GetData(); int64 ArchiveSize = Ar.TotalSize(); TArray64 Buffer(ArchiveData, ArchiveSize); FFileHelper::SaveArrayToFile(Buffer, *PayloadFilepath); } return true; } return false; } bool FAnimationPayloadContext::InternalFetchMorphTargetCurvePayloadToFile(FFbxParser& Parser, const FString& PayloadFilepath) { check(MorphTargetFetchPayloadData.IsSet()); FMorphTargetFetchPayloadData& FetchPayloadData = MorphTargetFetchPayloadData.GetValue(); TArray InterchangeCurves; if (InternalFetchMorphTargetCurvePayload(Parser, InterchangeCurves)) { FLargeMemoryWriter Ar; Ar << InterchangeCurves; Ar << FetchPayloadData.InbetweenCurveNames; Ar << FetchPayloadData.InbetweenFullWeights; uint8* ArchiveData = Ar.GetData(); int64 ArchiveSize = Ar.TotalSize(); TArray64 Buffer(ArchiveData, ArchiveSize); FFileHelper::SaveArrayToFile(Buffer, *PayloadFilepath); return true; } return false; } bool FAnimationPayloadContext::InternalFetchMorphTargetCurvePayload(FFbxParser& Parser, TArray& InterchangeCurves) { check(MorphTargetFetchPayloadData.IsSet()); FMorphTargetFetchPayloadData& FetchPayloadData = MorphTargetFetchPayloadData.GetValue(); if (!FetchPayloadData.SDKScene) { UInterchangeResultError_Generic* Message = Parser.AddMessage(); Message->Text = LOCTEXT("InternalFetchMorphTargetCurvePayloadToFile_FBXSDKSceneNull", "InternalFetchMorphTargetCurvePayloadToFile, fbx sdk is nullptr."); return false; } FbxGeometry* Geometry = FetchPayloadData.SDKScene->GetGeometry(FetchPayloadData.GeometryIndex); if (!Geometry) { UInterchangeResultError_Generic* Message = Parser.AddMessage(); Message->Text = LOCTEXT("InternalFetchMorphTargetCurvePayloadToFile_FBXGeometryNull", "Cannot fetch FBX geometry from the scene."); return false; } FbxAnimCurve* AnimCurve = Geometry->GetShapeChannel(FetchPayloadData.MorphTargetIndex, FetchPayloadData.ChannelIndex, FetchPayloadData.AnimLayer); //Morph target curve in fbx are between 0 and 100, in Unreal we are between 0 and 1, so we must scale //The curve with 0.01 constexpr float ScaleCurve = 0.01f; return ImportCurve(AnimCurve, ScaleCurve, InterchangeCurves.AddDefaulted_GetRef().Keys); } bool FFbxAnimation::AddSkeletalTransformAnimation(UInterchangeBaseNodeContainer& NodeContainer , FbxScene* SDKScene , FFbxParser& Parser , FbxNode* Node , UInterchangeSceneNode* SceneNode , TMap>& PayloadContexts , UInterchangeSkeletalAnimationTrackNode* SkeletalAnimationTrackNode , const int32& AnimationIndex) { FGetFbxTransformCurvesParameters Parameters(SDKScene, Node); GetFbxTransformCurves(Parameters, AnimationIndex); if (!Parameters.IsNodeAnimated) { //If we have a joint under the root skeleton and there is some animation in the parent hierarchy //we have to enable IsNodeAnimated so it get bake correctly and generate the appropriate curves FString RootSkeletonNodeUid; if (SkeletalAnimationTrackNode->GetCustomSkeletonNodeUid(RootSkeletonNodeUid)) { const UInterchangeSceneNode* ParentNode = Cast(NodeContainer.GetNode(SceneNode->GetParentUid())); //Search up the hierarchy if we found any animated parent we have to animate the children while (ParentNode) { if (SkeletalAnimationTrackNode->IsNodeAnimatedWithBakedCurve(ParentNode->GetUniqueID())) { Parameters.IsNodeAnimated = true; break; } if (ParentNode->GetUniqueID().Equals(RootSkeletonNodeUid)) { break; } ParentNode = Cast(NodeContainer.GetNode(ParentNode->GetParentUid())); } } } if (Parameters.IsNodeAnimated) { FString PayLoadKey = Parser.GetFbxHelper()->GetFbxNodeHierarchyName(Node) + TEXT("_") + FString::FromInt(AnimationIndex) + TEXT("_SkeletalAnimationPayloadKey"); if (ensure(!PayloadContexts.Contains(PayLoadKey))) { TSharedPtr AnimPayload = MakeShared(); FNodeTransformFetchPayloadData FetchPayloadData; FetchPayloadData.Node = Node; FetchPayloadData.CurrentAnimStack = Parameters.CurrentAnimStack; AnimPayload->NodeTransformFetchPayloadData = FetchPayloadData; PayloadContexts.Add(PayLoadKey, AnimPayload); } SkeletalAnimationTrackNode->SetAnimationPayloadKeyForSceneNodeUid(SceneNode->GetUniqueID(), PayLoadKey, EInterchangeAnimationPayLoadType::BAKED); } return Parameters.IsNodeAnimated; } void FFbxAnimation::AddNodeAttributeCurvesAnimation(FFbxParser& Parser , FbxNode* Node , FbxProperty& Property , FbxAnimCurveNode* AnimCurveNode , UInterchangeSceneNode* SceneNode , TMap>& PayloadContexts , EFbxType PropertyType , TOptional& OutPayloadKey , TOptional& OutIsStepCurve) { const FString PropertyName = Parser.GetFbxHelper()->GetFbxPropertyName(Property); const FString PayLoadKey = Parser.GetFbxHelper()->GetFbxNodeHierarchyName(Node) + PropertyName + TEXT("_AnimationPayloadKey"); if (ensure(!PayloadContexts.Contains(PayLoadKey))) { TSharedPtr AnimPayload = MakeShared(); FAttributeFetchPayloadData FetchPayloadData; FetchPayloadData.Node = Node; FetchPayloadData.AnimCurves = AnimCurveNode; //Only curves with Constant interpolation on all keys are deemed as StepCurves. OutIsStepCurve = !IsFbxPropertyTypeDecimal(PropertyType) && IsStepCurve(AnimCurveNode); FetchPayloadData.bAttributeTypeIsStepCurveAnimation = OutIsStepCurve.GetValue(); FetchPayloadData.PropertyType = PropertyType; FetchPayloadData.Property = Property; AnimPayload->AttributeFetchPayloadData = FetchPayloadData; PayloadContexts.Add(PayLoadKey, AnimPayload); OutPayloadKey = PayLoadKey; } } void FFbxAnimation::AddRigidTransformAnimation(FFbxParser& Parser , FbxNode* Node , FbxAnimCurveNode* TranslationCurveNode , FbxAnimCurveNode* RotationCurveNode , FbxAnimCurveNode* ScaleCurveNode , TMap>& PayloadContexts , TOptional& OutPayloadKey) { const FString PayLoadKey = Parser.GetFbxHelper()->GetFbxNodeHierarchyName(Node) + TEXT("_RigidAnimationPayloadKey"); if (ensure(!PayloadContexts.Contains(PayLoadKey))) { TSharedPtr AnimPayload = MakeShared(); FAttributeNodeTransformFetchPayloadData FetchPayloadData; FetchPayloadData.FrameRate = Parser.GetFrameRate(); FetchPayloadData.Node = Node; FetchPayloadData.TranlsationCurveNode = TranslationCurveNode; FetchPayloadData.RotationCurveNode = RotationCurveNode; FetchPayloadData.ScaleCurveNode = ScaleCurveNode; //Any property that is not decimal should import with step curve AnimPayload->AttributeNodeTransformFetchPayloadData = FetchPayloadData; PayloadContexts.Add(PayLoadKey, AnimPayload); OutPayloadKey = PayLoadKey; } } void FFbxAnimation::AddMorphTargetCurvesAnimation(FbxScene* SDKScene , FFbxParser& Parser , UInterchangeSkeletalAnimationTrackNode* SkeletalAnimationTrackNode , TMap>& PayloadContexts , const FMorphTargetAnimationBuildingData& MorphTargetAnimationBuildingData) { FString PayLoadKey = MorphTargetAnimationBuildingData.MorphTargetNodeUid + TEXT("\\") + (MorphTargetAnimationBuildingData.InterchangeMeshNode ? MorphTargetAnimationBuildingData.InterchangeMeshNode->GetUniqueID() : FString()) //Same shape can be animated on different mesh node + TEXT("\\") + FString::FromInt(MorphTargetAnimationBuildingData.AnimationIndex) + TEXT("\\") + FString::FromInt(MorphTargetAnimationBuildingData.MorphTargetIndex) + TEXT("\\") + FString::FromInt(MorphTargetAnimationBuildingData.ChannelIndex) + TEXT("_CurveAnimationPayloadKey"); if (ensure(!PayloadContexts.Contains(PayLoadKey))) { TSharedPtr AnimPayload = MakeShared(); FMorphTargetFetchPayloadData FetchPayloadData; FetchPayloadData.SDKScene = SDKScene; FetchPayloadData.GeometryIndex = MorphTargetAnimationBuildingData.GeometryIndex; FetchPayloadData.MorphTargetIndex = MorphTargetAnimationBuildingData.MorphTargetIndex; FetchPayloadData.ChannelIndex = MorphTargetAnimationBuildingData.ChannelIndex; FetchPayloadData.AnimLayer = MorphTargetAnimationBuildingData.AnimLayer; FetchPayloadData.InbetweenCurveNames = MorphTargetAnimationBuildingData.InbetweenCurveNames; FetchPayloadData.InbetweenFullWeights = MorphTargetAnimationBuildingData.InbetweenFullWeights; AnimPayload->MorphTargetFetchPayloadData = FetchPayloadData; PayloadContexts.Add(PayLoadKey, AnimPayload); } SkeletalAnimationTrackNode->SetAnimationPayloadKeyForMorphTargetNodeUid(MorphTargetAnimationBuildingData.MorphTargetNodeUid, PayLoadKey, EInterchangeAnimationPayLoadType::MORPHTARGETCURVE); } bool FFbxAnimation::IsFbxPropertyTypeSupported(EFbxType PropertyType) { switch (PropertyType) { case EFbxType::eFbxBool: case EFbxType::eFbxChar: //!< 8 bit signed integer. case EFbxType::eFbxUChar: //!< 8 bit unsigned integer. case EFbxType::eFbxShort: //!< 16 bit signed integer. case EFbxType::eFbxUShort: //!< 16 bit unsigned integer. case EFbxType::eFbxInt: //!< 32 bit signed integer. case EFbxType::eFbxUInt: //!< 32 bit unsigned integer. case EFbxType::eFbxLongLong: //!< 64 bit signed integer. case EFbxType::eFbxULongLong: //!< 64 bit unsigned integer. case EFbxType::eFbxHalfFloat: //!< 16 bit floating point. case EFbxType::eFbxFloat: //!< Floating point value. case EFbxType::eFbxDouble: //!< Double width floating point value. case EFbxType::eFbxDouble2: //!< Vector of two double values. case EFbxType::eFbxDouble3: //!< Vector of three double values. case EFbxType::eFbxDouble4: //!< Vector of four double values. case EFbxType::eFbxEnum: //!< Enumeration. case EFbxType::eFbxString: //!< String. return true; } return false; }; bool FFbxAnimation::IsFbxPropertyTypeDecimal(EFbxType PropertyType) { switch (PropertyType) { case EFbxType::eFbxHalfFloat: case EFbxType::eFbxFloat: case EFbxType::eFbxDouble: case EFbxType::eFbxDouble2: case EFbxType::eFbxDouble3: case EFbxType::eFbxDouble4: return true; } return false; } //PayloadQueries arriving here should be of the same start/stop and frequency: bool FFbxAnimation::FetchAnimationBakeTransformPayload(FFbxParser& Parser, FbxScene* SDKScene, TMap>& PayloadContexts, const TArray& PayloadQueries, const FString& ResultFolder, FCriticalSection* ResultPayloadsCriticalSection, TAtomic& UniqueIdCounter, TMap& ResultPayloads, TArray& OutErrorMessages) { //Helper Structs: struct FPayloadQueryHelper { const UE::Interchange::FAnimationPayloadQuery* PayloadQuery; TSharedPtr PayloadContext; FPayloadQueryHelper(const UE::Interchange::FAnimationPayloadQuery* InPayloadQuery, TSharedPtr InPayloadContext) : PayloadQuery(InPayloadQuery) , PayloadContext(InPayloadContext) { } }; struct FPayloadDataHelper { FString QueryHashString; //Used for payload file name FAnimationPayloadData PayloadData; FPayloadDataHelper(const FString& InHashString, const FString& InSceneNodeUID, const FInterchangeAnimationPayLoadKey& InPayloadKey) : QueryHashString(InHashString) , PayloadData(InSceneNodeUID, InPayloadKey) { } }; //If no PayloadQueries then return if (PayloadQueries.Num() == 0) { return true; } //Get Timings and Number of Frames for the current PayloadQueries // Note: PayloadQueries arriving here(FetchAnimationBakeTransformPayloadInternal) should be of the same start/stop and frequency, so we can grab the first one's: FAnimationTimeDescription TimeDescription = PayloadQueries[0]->TimeDescription; if (!ensure(!FMath::IsNearlyZero(TimeDescription.BakeFrequency))) { OutErrorMessages.Add(LOCTEXT("BakeFrequencyZero", "Cannot fetch FBX animation bake transforms payload because the bake frequency is zero.")); return false; } FbxTime StartTime; StartTime.SetSecondDouble(TimeDescription.RangeStartSecond); FbxTime EndTime; EndTime.SetSecondDouble(TimeDescription.RangeStopSecond); if (!ensure(TimeDescription.RangeStopSecond > TimeDescription.RangeStartSecond)) { OutErrorMessages.Add(LOCTEXT("InvalidRange", "Cannot fetch FBX animation bake transforms payload because the bake range is invalid.")); return false; } const double TimeStepSecond = 1.0 / TimeDescription.BakeFrequency; FbxTime TimeStep = 0; TimeStep.SetSecondDouble(TimeStepSecond); const double SequenceLength = FMath::Max(TimeDescription.RangeStopSecond - TimeDescription.RangeStartSecond, TimeStepSecond); const int32 NumFrame = FMath::RoundToInt32(SequenceLength * TimeDescription.BakeFrequency); int32 BakeKeyCount = NumFrame + 1; ensure(NumFrame >= 0); //Acquire PayloadContexts and AnimationStacks for the PayloadQueries (also group the FPayloadQueryHelper per AnimationStacks): // PayloadQueryHelpers maps AnimStacks to FPaloayQueryHelper (which contains a PayloadQuery and the PayloadContext that belongs to it) TMap> PayloadQueryHelpers; { for (const UE::Interchange::FAnimationPayloadQuery* PayloadQuery : PayloadQueries) { if (!PayloadContexts.Contains(PayloadQuery->PayloadKey.UniqueId)) { OutErrorMessages.Add(FText::Format(LOCTEXT("CannotRetrievePayload", "Cannot retrieve payload; payload key['{0}'] doesn't have any context."), FText::FromString(PayloadQuery->PayloadKey.UniqueId))); continue; } TSharedPtr PayloadContext = PayloadContexts.FindChecked(PayloadQuery->PayloadKey.UniqueId); FbxAnimStack* AnimStack = PayloadContext->GetAnimStack(); if (!AnimStack) { continue; } FString ResultPayloadUniqueId = PayloadQuery->GetHashString(); { FScopeLock Lock(ResultPayloadsCriticalSection); //If we already have extract this mesh, no need to extract again if (ResultPayloads.Contains(ResultPayloadUniqueId)) { continue; } } TArray& PayloadHelpersPerAnimStack = PayloadQueryHelpers.FindOrAdd(AnimStack); PayloadHelpersPerAnimStack.Add(FPayloadQueryHelper(PayloadQuery, PayloadContexts.FindChecked(PayloadQuery->PayloadKey.UniqueId))); } } //Iterate on the AnimStacked groups: for (TPair>& AnimStackPayloadQueryHelpersPair : PayloadQueryHelpers) { SDKScene->SetCurrentAnimationStack(AnimStackPayloadQueryHelpersPair.Key); TArray& PayloadQueryHelpersForAnimStack = AnimStackPayloadQueryHelpersPair.Value; //Initialize PayloadDataHelpers (contains the HashString for the Query and the FAnimationPayloadData TArray PayloadDataHelpersForAnimStack; PayloadDataHelpersForAnimStack.Empty(PayloadQueryHelpersForAnimStack.Num()); for (FPayloadQueryHelper& PayloadQueryHelper : PayloadQueryHelpersForAnimStack) { FPayloadDataHelper PayloadDataHelper(PayloadQueryHelper.PayloadQuery->GetHashString(), PayloadQueryHelper.PayloadQuery->SceneNodeUniqueID, PayloadQueryHelper.PayloadQuery->PayloadKey); PayloadDataHelper.PayloadData.BakeFrequency = PayloadQueryHelper.PayloadQuery->TimeDescription.BakeFrequency; PayloadDataHelper.PayloadData.RangeStartTime = PayloadQueryHelper.PayloadQuery->TimeDescription.RangeStartSecond; PayloadDataHelper.PayloadData.RangeEndTime = PayloadQueryHelper.PayloadQuery->TimeDescription.RangeStopSecond; PayloadDataHelper.PayloadData.Transforms.SetNum(BakeKeyCount); PayloadDataHelpersForAnimStack.Add(PayloadDataHelper); } //Acquire Bone Transforms: //Time iteration FbxTime CurrentTime = StartTime; for (size_t FrameIndex = 0; FrameIndex < BakeKeyCount; FrameIndex++, CurrentTime+=TimeStep) { //Bone iteration: for (size_t PayloadDataIndex = 0; PayloadDataIndex < PayloadDataHelpersForAnimStack.Num(); PayloadDataIndex++) { FPayloadQueryHelper& PayloadQueryHelper = PayloadQueryHelpersForAnimStack[PayloadDataIndex]; FPayloadDataHelper& PayloadDataHelper = PayloadDataHelpersForAnimStack[PayloadDataIndex]; FTransform Transform; if (PayloadQueryHelper.PayloadContext->FetchAnimationBakeTransformPayloadForTime(Parser, CurrentTime, Transform)) { PayloadDataHelper.PayloadData.Transforms[FrameIndex] = Transform; } else { if (FrameIndex == 0) { PayloadDataHelper.PayloadData.Transforms[FrameIndex] = FTransform::Identity; } else { PayloadDataHelper.PayloadData.Transforms[FrameIndex] = PayloadDataHelper.PayloadData.Transforms[FrameIndex - 1]; } } } } //Write out results: ParallelFor(PayloadDataHelpersForAnimStack.Num(), [&PayloadDataHelpersForAnimStack, &ResultPayloads, &ResultPayloadsCriticalSection, &ResultFolder, &UniqueIdCounter](int32 PayloadDataHelperIndex) { FPayloadDataHelper& PayloadDataHelper = PayloadDataHelpersForAnimStack[PayloadDataHelperIndex]; FString QueryHashString = PayloadDataHelper.QueryHashString; //PayloadQuery's GetHashString() FString PayloadFilepathCopy; { FScopeLock Lock(ResultPayloadsCriticalSection); //If we already have extract this mesh, no need to extract again if (ResultPayloads.Contains(QueryHashString)) { return; } FString& PayloadFilepath = ResultPayloads.FindOrAdd(QueryHashString); //To avoid file path with too many character, we hash the payloadKey so we have a deterministic length for the file path. PayloadFilepath = ResultFolder + TEXT("/") + QueryHashString + FString::FromInt(UniqueIdCounter.IncrementExchange()) + TEXT(".payload"); //Copy the map filename key because we are multithreaded and the TMap can be reallocated PayloadFilepathCopy = PayloadFilepath; } FString PayloadFilepath = PayloadFilepathCopy; FLargeMemoryWriter Ar; PayloadDataHelper.PayloadData.SerializeBaked(Ar); uint8* ArchiveData = Ar.GetData(); int64 ArchiveSize = Ar.TotalSize(); TArray64 Buffer(ArchiveData, ArchiveSize); FFileHelper::SaveArrayToFile(Buffer, *PayloadFilepath); }); } return true; } }//ns UE::Interchange::Private #undef LOCTEXT_NAMESPACE