Files
UnrealEngine/Engine/Source/Editor/UnrealEd/Private/SkeletalMeshEdit.cpp
2025-05-18 13:04:45 +08:00

2332 lines
89 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
/*=============================================================================
SkeletalMeshEdit.cpp: Unreal editor skeletal mesh/anim support
=============================================================================*/
#include "Animation/AnimationSettings.h"
#include "Animation/AnimCurveTypes.h"
#include "Animation/AnimSequence.h"
#include "Animation/AnimTypes.h"
#include "Animation/Skeleton.h"
#include "Animation/SmartName.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "ComponentReregisterContext.h"
#include "Components/SkeletalMeshComponent.h"
#include "CoreMinimal.h"
#include "Curves/KeyHandle.h"
#include "Editor/EditorEngine.h"
#include "Editor/EditorPerProjectUserSettings.h"
#include "Factories/Factory.h"
#include "Factories/FbxAnimSequenceImportData.h"
#include "Factories/FbxImportUI.h"
#include "Factories/FbxSkeletalMeshImportData.h"
#include "FbxAnimUtils.h"
#include "FbxImporter.h"
#include "Logging/TokenizedMessage.h"
#include "Misc/FbxErrors.h"
#include "Misc/FeedbackContext.h"
#include "Misc/PackageName.h"
#include "Misc/Paths.h"
#include "Misc/ScopedSlowTask.h"
#include "ObjectTools.h"
#include "Rendering/SkeletalMeshLODImporterData.h"
#include "UObject/UObjectIterator.h"
#include "UObject/Object.h"
#include "ComponentReregisterContext.h"
#include "Components/SkeletalMeshComponent.h"
#include "FbxAnimUtils.h"
#include "Animation/AnimSequenceHelpers.h"
#include "Animation/BuiltInAttributeTypes.h"
#define LOCTEXT_NAMESPACE "SkeletalMeshEdit"
//The max reference rate is use to cap the maximum rate we support.
//It must be base on DEFAULT_SAMPLERATE*2ExpX where X is a integer with range [1 to 6] because we use KINDA_SMALL_NUMBER(0.0001) we do not want to pass 1920Hz 1/1920 = 0.0005
#define MaxReferenceRate 1920.0f
UAnimSequence * UEditorEngine::ImportFbxAnimation( USkeleton* Skeleton, UObject* Outer, UFbxAnimSequenceImportData* TemplateImportData, const TCHAR* InFilename, const TCHAR* AnimName, bool bImportMorphTracks )
{
check(Skeleton);
UAnimSequence * NewAnimation=nullptr;
UnFbx::FFbxImporter* FFbxImporter = UnFbx::FFbxImporter::GetInstance();
const bool bPrevImportMorph = FFbxImporter->ImportOptions->bImportMorph;
FFbxImporter->ImportOptions->bImportMorph = bImportMorphTracks;
if ( !FFbxImporter->ImportFromFile( InFilename, FPaths::GetExtension( InFilename ), true ) )
{
// Log the error message and fail the import.
FFbxImporter->FlushToTokenizedErrorMessage(EMessageSeverity::Error);
}
else
{
// Log the import message and import the mesh.
FFbxImporter->FlushToTokenizedErrorMessage(EMessageSeverity::Warning);
const FString Filename( InFilename );
// Get Mesh nodes array that bind to the skeleton system, then morph animation is imported.
TArray<FbxNode*> FBXMeshNodeArray;
FbxNode* SkeletonRoot = FFbxImporter->FindFBXMeshesByBone(Skeleton->GetReferenceSkeleton().GetBoneName(0), true, FBXMeshNodeArray);
if (!SkeletonRoot)
{
FFbxImporter->AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Error, FText::Format(LOCTEXT("Error_CouldNotFindFbxTrack", "Mesh contains {0} bone as root but animation doesn't contain the root track.\nImport failed."), FText::FromName(Skeleton->GetReferenceSkeleton().GetBoneName(0)))), FFbxErrors::Animation_CouldNotFindRootTrack);
FFbxImporter->ReleaseScene();
return nullptr;
}
// Check for blend shape curves that are not skinned. Unskinned geometry can still contain morph curves
if( bImportMorphTracks )
{
TArray<FbxNode*> MeshNodes;
FFbxImporter->FillFbxMeshArray( FFbxImporter->Scene->GetRootNode(), MeshNodes, FFbxImporter );
for( int32 NodeIndex = 0; NodeIndex < MeshNodes.Num(); ++NodeIndex )
{
// Its possible the nodes already exist so make sure they are only added once
FBXMeshNodeArray.AddUnique( MeshNodes[NodeIndex] );
}
}
TArray<FbxNode*> SortedLinks;
FFbxImporter->RecursiveBuildSkeleton(SkeletonRoot, SortedLinks);
if(SortedLinks.Num() == 0)
{
FFbxImporter->AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Warning, LOCTEXT("Error_CouldNotBuildValidSkeleton", "Could not create a valid skeleton from the import data that matches the given Skeletal Mesh. Check the bone names of both the Skeletal Mesh for this AnimSet and the animation data you are trying to import.")),
FFbxErrors::Animation_CouldNotBuildSkeleton);
}
else
{
NewAnimation = FFbxImporter->ImportAnimations( Skeleton, Outer, SortedLinks, AnimName, TemplateImportData, FBXMeshNodeArray);
if( NewAnimation )
{
// since to know full path, reimport will need to do same
UFbxAnimSequenceImportData* ImportData = UFbxAnimSequenceImportData::GetImportDataForAnimSequence(NewAnimation, TemplateImportData);
ImportData->Update(UFactory::GetCurrentFilename(), &(FFbxImporter->Md5Hash));
}
}
}
FFbxImporter->ImportOptions->bImportMorph = bPrevImportMorph;
FFbxImporter->ReleaseScene();
return NewAnimation;
}
bool UEditorEngine::ReimportFbxAnimation( USkeleton* Skeleton, UAnimSequence* AnimSequence, UFbxAnimSequenceImportData* ImportData, const TCHAR* InFilename, bool& bOutImportAll, const bool bFactoryShowOptions, UFbxImportUI* ReimportUI)
{
check(Skeleton);
bool bResult = true;
GWarn->BeginSlowTask( LOCTEXT("ImportingFbxAnimations", "Importing FBX animations"), true );
UnFbx::FFbxImporter* FbxImporter = UnFbx::FFbxImporter::GetInstance();
const bool bPrevImportMorph = (AnimSequence->GetDataModel()->GetNumberOfFloatCurves() > 0);
const bool bOverrideImportSettings = ReimportUI != nullptr;
if (!ReimportUI)
{
ReimportUI = NewObject<UFbxImportUI>();
}
ReimportUI->MeshTypeToImport = FBXIT_Animation;
ReimportUI->bOverrideFullName = false;
ReimportUI->bImportAnimations = true;
const bool ShowImportDialogAtReimport = GetDefault<UEditorPerProjectUserSettings>()->bShowImportDialogAtReimport && !GIsAutomationTesting && bFactoryShowOptions;
if (ImportData && !ShowImportDialogAtReimport)
{
// Prepare the import options
if (!bOverrideImportSettings)
{
//Use the asset import data setting only if there was no ReimportUI parameter
ReimportUI->AnimSequenceImportData = ImportData;
}
else
{
ImportData->CopyAnimationValues(ReimportUI->AnimSequenceImportData);
}
ReimportUI->SkeletalMeshImportData->bImportMeshesInBoneHierarchy = ImportData->bImportMeshesInBoneHierarchy;
ApplyImportUIToImportOptions(ReimportUI, *FbxImporter->ImportOptions);
}
else if(ShowImportDialogAtReimport)
{
if (ImportData == nullptr)
{
// An existing import data object was not found, make one here and show the options dialog
ImportData = UFbxAnimSequenceImportData::GetImportDataForAnimSequence(AnimSequence, ReimportUI->AnimSequenceImportData);
AnimSequence->AssetImportData = ImportData;
}
ReimportUI->bIsReimport = true;
ReimportUI->ReimportMesh = AnimSequence;
ReimportUI->AnimSequenceImportData = ImportData;
bool bImportOperationCanceled = false;
bool bShowOptionDialog = true;
bool bForceImportType = true;
bool bIsObjFormat = false;
bool bIsAutomated = false;
// @hack to make sure skeleton is set before opening the dialog
FbxImporter->ImportOptions->SkeletonForAnimation = Skeleton;
GetImportOptions(FbxImporter, ReimportUI, bShowOptionDialog, bIsAutomated, AnimSequence->GetPathName(), bImportOperationCanceled, bOutImportAll, bIsObjFormat, InFilename, bForceImportType, FBXIT_Animation);
if (bImportOperationCanceled)
{
//User cancel the re-import
bResult = false;
GWarn->EndSlowTask();
return bResult;
}
}
else
{
FbxImporter->ImportOptions->ResetForReimportAnimation();
}
if ( !FbxImporter->ImportFromFile( InFilename, FPaths::GetExtension( InFilename ), true ) )
{
// Log the error message and fail the import.
FbxImporter->FlushToTokenizedErrorMessage(EMessageSeverity::Error);
bResult = false;
}
else
{
// Log the import message and import the mesh.
FbxImporter->FlushToTokenizedErrorMessage(EMessageSeverity::Warning);
const FString Filename( InFilename );
// Get Mesh nodes array that bind to the skeleton system, then morph animation is imported.
TArray<FbxNode*> FBXMeshNodeArray;
FbxNode* SkeletonRoot = FbxImporter->FindFBXMeshesByBone(Skeleton->GetReferenceSkeleton().GetBoneName(0), true, FBXMeshNodeArray);
if (!SkeletonRoot)
{
FbxImporter->AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Error, FText::Format(LOCTEXT("Error_CouldNotFindFbxTrack", "Mesh contains {0} bone as root but animation doesn't contain the root track.\nImport failed."), FText::FromName(Skeleton->GetReferenceSkeleton().GetBoneName(0)))), FFbxErrors::Animation_CouldNotFindTrack);
bResult = false;
}
if (bResult)
{
// for now import all the time?
bool bImportMorphTracks = true;
// Check for blend shape curves that are not skinned. Unskinned geometry can still contain morph curves
if (bImportMorphTracks)
{
TArray<FbxNode*> MeshNodes;
FbxImporter->FillFbxMeshArray(FbxImporter->Scene->GetRootNode(), MeshNodes, FbxImporter);
for (int32 NodeIndex = 0; NodeIndex < MeshNodes.Num(); ++NodeIndex)
{
// Its possible the nodes already exist so make sure they are only added once
FBXMeshNodeArray.AddUnique(MeshNodes[NodeIndex]);
}
}
TArray<FbxNode*> SortedLinks;
FbxImporter->RecursiveBuildSkeleton(SkeletonRoot, SortedLinks);
if (SortedLinks.Num() == 0)
{
FbxImporter->AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Warning, LOCTEXT("Error_CouldNotBuildValidSkeleton", "Could not create a valid skeleton from the import data that matches the given Skeletal Mesh. Check the bone names of both the Skeletal Mesh for this AnimSet and the animation data you are trying to import.")), FFbxErrors::Animation_CouldNotBuildSkeleton);
}
else
{
check(ImportData);
// find the correct animation based on import data
FbxAnimStack* CurAnimStack = nullptr;
//ignore the source animation name if there's only one animation in the file.
//this is to make it easier for people who use content creation programs that only export one animation and/or ones that don't allow naming animations
if (FbxImporter->Scene->GetSrcObjectCount(FbxCriteria::ObjectType(FbxAnimStack::ClassId)) > 1 && !ImportData->SourceAnimationName.IsEmpty())
{
CurAnimStack = FbxCast<FbxAnimStack>(FbxImporter->Scene->FindSrcObject(FbxCriteria::ObjectType(FbxAnimStack::ClassId), TCHAR_TO_UTF8(*ImportData->SourceAnimationName), 0));
}
else
{
CurAnimStack = FbxCast<FbxAnimStack>(FbxImporter->Scene->GetSrcObject(FbxCriteria::ObjectType(FbxAnimStack::ClassId), 0));
}
if (CurAnimStack)
{
// set current anim stack
int32 ResampleRate = static_cast<int32>(DEFAULT_SAMPLERATE);
if (FbxImporter->ImportOptions->bResample)
{
if(FbxImporter->ImportOptions->ResampleRate > 0)
{
ResampleRate = FbxImporter->ImportOptions->ResampleRate;
}
else
{
int32 BestResampleRate = FbxImporter->GetMaxSampleRate(SortedLinks);
if(BestResampleRate > 0)
{
ResampleRate = BestResampleRate;
}
}
}
FbxTimeSpan AnimTimeSpan = FbxImporter->GetAnimationTimeSpan(SortedLinks[0], CurAnimStack);
// for now it's not importing morph - in the future, this should be optional or saved with asset
if (FbxImporter->ValidateAnimStack(SortedLinks, FBXMeshNodeArray, CurAnimStack, ResampleRate, bImportMorphTracks, FbxImporter->ImportOptions->bSnapToClosestFrameBoundary, AnimTimeSpan))
{
AnimSequence->ImportResampleFramerate = ResampleRate;
FbxImporter->ImportAnimation(Skeleton, AnimSequence, Filename, SortedLinks, FBXMeshNodeArray, CurAnimStack, ResampleRate, AnimTimeSpan, true);
}
}
else
{
// no track is found
FbxImporter->AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Error, LOCTEXT("Error_CouldNotFindTrack", "Could not find needed track.")), FFbxErrors::Animation_CouldNotFindTrack);
bResult = false;
}
}
}
}
FbxImporter->ImportOptions->bImportMorph = bPrevImportMorph;
FbxImporter->ReleaseScene();
GWarn->EndSlowTask();
return bResult;
}
// The Unroll filter expects only rotation curves, we need to walk the scene and extract the
// rotation curves from the nodes property. This can become time consuming but we have no choice.
static void ApplyUnroll(FbxNode *pNode, FbxAnimLayer* pLayer, FbxAnimCurveFilterUnroll* pUnrollFilter)
{
if (!pNode || !pLayer || !pUnrollFilter)
{
return;
}
FbxAnimCurveNode* lCN = pNode->LclRotation.GetCurveNode(pLayer);
if (lCN)
{
FbxAnimCurve* lRCurve[3];
lRCurve[0] = lCN->GetCurve(0);
lRCurve[1] = lCN->GetCurve(1);
lRCurve[2] = lCN->GetCurve(2);
// Set bone rotation order
EFbxRotationOrder RotationOrder = eEulerXYZ;
pNode->GetRotationOrder(FbxNode::eSourcePivot, RotationOrder);
pUnrollFilter->SetRotationOrder((FbxEuler::EOrder)(RotationOrder));
pUnrollFilter->Apply(lRCurve, 3);
}
for (int32 i = 0; i < pNode->GetChildCount(); i++)
{
ApplyUnroll(pNode->GetChild(i), pLayer, pUnrollFilter);
}
}
void UnFbx::FFbxImporter::MergeAllLayerAnimation(FbxAnimStack* AnimStack, int32 ResampleRate)
{
FbxTime lFramePeriod;
lFramePeriod.SetSecondDouble(1.0 / ResampleRate);
FbxTimeSpan lTimeSpan = AnimStack->GetLocalTimeSpan();
AnimStack->BakeLayers(Scene->GetAnimationEvaluator(), lTimeSpan.GetStart(), lTimeSpan.GetStop(), lFramePeriod);
// always apply unroll filter
FbxAnimCurveFilterUnroll UnrollFilter;
FbxAnimLayer* lLayer = AnimStack->GetMember<FbxAnimLayer>(0);
UnrollFilter.Reset();
ApplyUnroll(Scene->GetRootNode(), lLayer, &UnrollFilter);
}
bool UnFbx::FFbxImporter::IsValidAnimationData(TArray<FbxNode*>& SortedLinks, TArray<FbxNode*>& NodeArray, int32& ValidTakeCount)
{
// If there are no valid links, then we cannot import the anim set
if(SortedLinks.Num() == 0)
{
return false;
}
ValidTakeCount = 0;
int32 AnimStackCount = Scene->GetSrcObjectCount<FbxAnimStack>();
int32 AnimStackIndex;
for (AnimStackIndex = 0; AnimStackIndex < AnimStackCount; AnimStackIndex++ )
{
FbxAnimStack* CurAnimStack = Scene->GetSrcObject<FbxAnimStack>(AnimStackIndex);
// set current anim stack
Scene->SetCurrentAnimationStack(CurAnimStack);
// debug purpose
for (int32 BoneIndex = 0; BoneIndex < SortedLinks.Num(); BoneIndex++)
{
FString BoneName = MakeName(SortedLinks[BoneIndex]->GetName());
UE_LOG(LogFbx, Log, TEXT("SortedLinks :(%d) %s"), BoneIndex, *BoneName );
}
//The animation timespan must use the original fbx framerate so the frame number match the DCC frame number
FbxTimeSpan AnimTimeSpan = GetAnimationTimeSpan(SortedLinks[0], CurAnimStack);
if (AnimTimeSpan.GetDuration() <= 0)
{
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Warning, FText::Format(LOCTEXT("FBXImport_ZeroLength", "Animation Stack {0} does not contain any valid key. Try different time options when import."), FText::FromString(UTF8_TO_TCHAR(CurAnimStack->GetName())))), FFbxErrors::Animation_ZeroLength);
continue;
}
ValidTakeCount++;
{
bool bBlendCurveFound = false;
for ( int32 NodeIndex = 0; !bBlendCurveFound && NodeIndex < NodeArray.Num(); NodeIndex++ )
{
// consider blendshape animation curve
FbxGeometry* Geometry = (FbxGeometry*)NodeArray[NodeIndex]->GetNodeAttribute();
if (Geometry)
{
int32 BlendShapeDeformerCount = Geometry->GetDeformerCount(FbxDeformer::eBlendShape);
for(int32 BlendShapeIndex = 0; BlendShapeIndex<BlendShapeDeformerCount; ++BlendShapeIndex)
{
FbxBlendShape* BlendShape = (FbxBlendShape*)Geometry->GetDeformer(BlendShapeIndex, FbxDeformer::eBlendShape);
int32 BlendShapeChannelCount = BlendShape->GetBlendShapeChannelCount();
for(int32 ChannelIndex = 0; ChannelIndex<BlendShapeChannelCount; ++ChannelIndex)
{
FbxBlendShapeChannel* Channel = BlendShape->GetBlendShapeChannel(ChannelIndex);
if(Channel)
{
// Get the percentage of influence of the shape.
FbxAnimCurve* Curve = Geometry->GetShapeChannel(BlendShapeIndex, ChannelIndex, (FbxAnimLayer*)CurAnimStack->GetMember(0));
if (Curve && Curve->KeyGetCount() > 0)
{
bBlendCurveFound = true;
break;
}
}
}
}
}
}
}
}
return ( ValidTakeCount != 0 );
}
void UnFbx::FFbxImporter::FillAndVerifyBoneNames(USkeleton* Skeleton, TArray<FbxNode*>& SortedLinks, TArray<FName>& OutRawBoneNames, FString Filename)
{
int32 TrackNum = SortedLinks.Num();
OutRawBoneNames.AddUninitialized(TrackNum);
// copy to the data
for (int32 BoneIndex = 0; BoneIndex < TrackNum; BoneIndex++)
{
OutRawBoneNames[BoneIndex] = FName(*FSkeletalMeshImportData::FixupBoneName( MakeName(SortedLinks[BoneIndex]->GetName()) ));
}
const FReferenceSkeleton& RefSkeleton = Skeleton->GetReferenceSkeleton();
// make sure at least root bone matches
if ( OutRawBoneNames[0] != RefSkeleton.GetBoneName(0) )
{
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Error, FText::Format(LOCTEXT("FBXImport_RootMatchFail", "Root bone name does not match (FBX: {0} | Skeleton: {1})"), FText::FromName(OutRawBoneNames[0]), FText::FromName(RefSkeleton.GetBoneName(0)))), FFbxErrors::Animation_RootTrackMismatch);
return;
}
// ensure there are no duplicated names
for (int32 I = 0; I < TrackNum; I++)
{
for ( int32 J = I+1; J < TrackNum; J++ )
{
if (OutRawBoneNames[I] == OutRawBoneNames[J])
{
FString RawBoneName = OutRawBoneNames[J].ToString();
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Warning, FText::Format(LOCTEXT("FBXImport_DupeBone", "Could not import {0}.\nDuplicate bone name found ('{1}'). Each bone must have a unique name."), FText::FromString(Filename), FText::FromString(RawBoneName))), FFbxErrors::Animation_DuplicatedBone);
}
}
}
// make sure all bone names are included, if not warn user
FString BoneNames;
for (int32 I = 0; I < TrackNum; ++I)
{
FName RawBoneName = OutRawBoneNames[I];
if (RefSkeleton.FindBoneIndex(RawBoneName) == INDEX_NONE && !IsUnrealTransformAttribute(SortedLinks[I]))
{
BoneNames += RawBoneName.ToString();
BoneNames += TEXT(" \n");
}
}
if (BoneNames.IsEmpty() == false)
{
// warn user
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Warning, FText::Format(LOCTEXT("FBXImport_MissingBone", "The following bones exist in the imported animation, but not in the Skeleton asset {0}. Any animation on these bones will not be imported: \n\n {1}"), FText::FromString(Skeleton->GetName()), FText::FromString(BoneNames) )), FFbxErrors::Animation_MissingBones);
}
}
//-------------------------------------------------------------------------
//
//-------------------------------------------------------------------------
FbxTimeSpan UnFbx::FFbxImporter::GetAnimationTimeSpan(FbxNode* RootNode, FbxAnimStack* AnimStack)
{
FBXImportOptions* ImportOption = GetImportOptions();
FbxTimeSpan AnimTimeSpan(FBXSDK_TIME_INFINITE, FBXSDK_TIME_MINUS_INFINITE);
if (ImportOption)
{
bool bUseDefault = ImportOption->AnimationLengthImportType == FBXALIT_ExportedTime || FMath::IsNearlyZero(OriginalFbxFramerate, KINDA_SMALL_NUMBER);
if (bUseDefault)
{
AnimTimeSpan = AnimStack->GetLocalTimeSpan();
}
else if (ImportOption->AnimationLengthImportType == FBXALIT_AnimatedKey)
{
RootNode->GetAnimationInterval(AnimTimeSpan, AnimStack);
}
else // then it's range
{
AnimTimeSpan = AnimStack->GetLocalTimeSpan();
FbxTimeSpan AnimatedInterval(FBXSDK_TIME_INFINITE, FBXSDK_TIME_MINUS_INFINITE);
RootNode->GetAnimationInterval(AnimatedInterval, AnimStack);
// find the most range that covers by both method, that'll be used for clamping
FbxTime StartTime = FMath::Min<FbxTime>(AnimTimeSpan.GetStart(), AnimatedInterval.GetStart());
FbxTime StopTime = FMath::Max<FbxTime>(AnimTimeSpan.GetStop(),AnimatedInterval.GetStop());
// make inclusive time between localtimespan and animation interval
AnimTimeSpan.SetStart(StartTime);
AnimTimeSpan.SetStop(StopTime);
FbxTime EachFrame = FBXSDK_TIME_ONE_SECOND/OriginalFbxFramerate;
int32 StartFrame = StartTime.Get()/EachFrame.Get();
int32 StopFrame = StopTime.Get()/EachFrame.Get();
if (StartFrame != StopFrame)
{
FbxTime Duration = AnimTimeSpan.GetDuration();
int32 AnimRangeX = FMath::Clamp<int32>(ImportOption->AnimationRange.X, StartFrame, StopFrame);
int32 AnimRangeY = FMath::Clamp<int32>(ImportOption->AnimationRange.Y, StartFrame, StopFrame);
FbxLongLong Interval = EachFrame.Get();
// now set new time
if (StartFrame != AnimRangeX)
{
FbxTime NewTime(AnimRangeX*Interval);
AnimTimeSpan.SetStart(NewTime);
}
if (StopFrame != AnimRangeY)
{
FbxTime NewTime(AnimRangeY*Interval);
AnimTimeSpan.SetStop(NewTime);
}
}
}
}
return AnimTimeSpan;
}
void UnFbx::FFbxImporter::GetAnimationIntervalMultiLayer(FbxNode* RootNode, FbxAnimStack* AnimStack, FbxTimeSpan& AnimTimeSpan)
{
int NumAnimLayers = AnimStack != nullptr ? AnimStack->GetMemberCount() : 0;
for (int AnimLayerIndex = 0; AnimLayerIndex < NumAnimLayers; ++AnimLayerIndex)
{
FbxTimeSpan LayerAnimTimeSpan(FBXSDK_TIME_INFINITE, FBXSDK_TIME_MINUS_INFINITE);
RootNode->GetAnimationInterval(LayerAnimTimeSpan, AnimStack, AnimLayerIndex);
if (LayerAnimTimeSpan.GetStart() < AnimTimeSpan.GetStart())
{
AnimTimeSpan.SetStart(LayerAnimTimeSpan.GetStart());
}
if (LayerAnimTimeSpan.GetStop() > AnimTimeSpan.GetStop())
{
AnimTimeSpan.SetStop(LayerAnimTimeSpan.GetStop());
}
}
}
/**
* Add to the animation set, the animations contained within the FBX document, for the given skeleton
*/
UAnimSequence * UnFbx::FFbxImporter::ImportAnimations(USkeleton* Skeleton, UObject* Outer, TArray<FbxNode*>& SortedLinks, const FString& Name, UFbxAnimSequenceImportData* TemplateImportData, TArray<FbxNode*>& NodeArray)
{
// we need skeleton to create animsequence
if (Skeleton == nullptr || !CanImportClass(UAnimSequence::StaticClass()))
{
return nullptr;
}
int32 ValidTakeCount = 0;
if (IsValidAnimationData(SortedLinks, NodeArray, ValidTakeCount) == false)
{
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Warning, LOCTEXT("FBXImport_InvalidAnimationData", "This does not contain any valid animation takes.")), FFbxErrors::Animation_InvalidData);
return nullptr;
}
UAnimSequence* LastCreatedAnim = nullptr;
int32 ResampleRate = DEFAULT_SAMPLERATE;
if ( ImportOptions->bResample )
{
if (ImportOptions->ResampleRate > 0)
{
ResampleRate = ImportOptions->ResampleRate;
}
else
{
// For FBX data, "Frame Rate" is just the speed at which the animation is played back. It can change
// arbitrarily, and the underlying data can stay the same. What we really want here is the Sampling Rate,
// ie: the number of animation keys per second. These are the individual animation curve keys
// on the FBX nodes of the skeleton. So we loop through the nodes of the skeleton and find the maximum number
// of keys that any node has, then divide this by the total length (in seconds) of the animation to find the
// sampling rate of this set of data
// we want the maximum resample rate, so that we don't lose any precision of fast anims,
// and don't mind creating lerped frames for slow anims
int32 BestResampleRate = GetMaxSampleRate(SortedLinks);
if (BestResampleRate > 0)
{
ResampleRate = BestResampleRate;
}
}
}
int32 AnimStackCount = Scene->GetSrcObjectCount<FbxAnimStack>();
for( int32 AnimStackIndex = 0; AnimStackIndex < AnimStackCount; AnimStackIndex++ )
{
FbxAnimStack* CurAnimStack = Scene->GetSrcObject<FbxAnimStack>(AnimStackIndex);
FbxTimeSpan AnimTimeSpan = GetAnimationTimeSpan(SortedLinks[0], CurAnimStack);
bool bValidAnimStack = ValidateAnimStack(SortedLinks, NodeArray, CurAnimStack, ResampleRate, ImportOptions->bImportMorph, ImportOptions->bSnapToClosestFrameBoundary, AnimTimeSpan);
// no animation
if (!bValidAnimStack)
{
continue;
}
FString SequenceName = Name;
FString SourceAnimationName = UTF8_TO_TCHAR(CurAnimStack->GetName());
if (ValidTakeCount > 1)
{
SequenceName += "_";
SequenceName += SourceAnimationName;
}
// See if this sequence already exists.
SequenceName = ObjectTools::SanitizeObjectName(SequenceName);
FString ParentPath = FString::Printf(TEXT("%s/%s"), *FPackageName::GetLongPackagePath(*Outer->GetName()), *SequenceName);
UObject* ParentPackage = CreatePackage( *ParentPath);
UObject* Object = LoadObject<UObject>(ParentPackage, *SequenceName, nullptr, (LOAD_Quiet | LOAD_NoWarn), nullptr);
UAnimSequence * DestSeq = Cast<UAnimSequence>(Object);
// if object with same name exists, warn user
if (Object && !DestSeq)
{
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Error, LOCTEXT("Error_AssetExist", "Asset with same name exists. Can't overwrite another asset")), FFbxErrors::Generic_SameNameAssetExists);
continue; // Move on to next sequence...
}
// If not, create new one now.
const bool bCreateAsset = !DestSeq;
if(bCreateAsset)
{
DestSeq = NewObject<UAnimSequence>(ParentPackage, *SequenceName, RF_Public | RF_Standalone);
CreatedObjects.Add(DestSeq);
}
DestSeq->SetSkeleton(Skeleton);
// since to know full path, reimport will need to do same
UFbxAnimSequenceImportData* ImportData = UFbxAnimSequenceImportData::GetImportDataForAnimSequence(DestSeq, TemplateImportData);
ImportData->Update(UFactory::GetCurrentFilename(), &Md5Hash);
ImportData->SourceAnimationName = SourceAnimationName;
DestSeq->ImportFileFramerate = GetOriginalFbxFramerate();
DestSeq->ImportResampleFramerate = ResampleRate;
DestSeq->GetController().InitializeModel();
ImportAnimation(Skeleton, DestSeq, Name, SortedLinks, NodeArray, CurAnimStack, ResampleRate, AnimTimeSpan, false);
if (bCreateAsset)
{
// Notify the asset registry
FAssetRegistryModule::AssetCreated(DestSeq);
}
LastCreatedAnim = DestSeq;
}
return LastCreatedAnim;
}
//Get the smallest sample rate(integer) representing the DeltaTime(time between 0.0f and 1.0f).
//@DeltaTime: the time to find the rate between 0.0f and 1.0f
int32 GetTimeSampleRate(const float DeltaTime)
{
float OriginalSampleRateDivider = 1.0f / DeltaTime;
float SampleRateDivider = OriginalSampleRateDivider;
float SampleRemainder = FPlatformMath::Fractional(SampleRateDivider);
float Multiplier = 2.0f;
float IntegerPrecision = FMath::Min(FMath::Max(KINDA_SMALL_NUMBER*SampleRateDivider, KINDA_SMALL_NUMBER), 0.1f); //The precision is limit between KINDA_SMALL_NUMBER and 0.1f
while (!FMath::IsNearlyZero(SampleRemainder, IntegerPrecision) && !FMath::IsNearlyEqual(SampleRemainder, 1.0f, IntegerPrecision))
{
SampleRateDivider = OriginalSampleRateDivider * Multiplier;
SampleRemainder = FPlatformMath::Fractional(SampleRateDivider);
if (SampleRateDivider > MaxReferenceRate)
{
SampleRateDivider = DEFAULT_SAMPLERATE;
break;
}
Multiplier += 1.0f;
}
return FMath::Min(FPlatformMath::RoundToInt(SampleRateDivider), FPlatformMath::RoundToInt(MaxReferenceRate));
}
int32 GetAnimationCurveRate(FbxAnimCurve* CurrentCurve)
{
if (CurrentCurve == nullptr)
return 0;
int32 KeyCount = CurrentCurve->KeyGetCount();
FbxTimeSpan TimeInterval(FBXSDK_TIME_INFINITE, FBXSDK_TIME_MINUS_INFINITE);
bool bValidTimeInterval = CurrentCurve->GetTimeInterval(TimeInterval);
if (KeyCount > 1 && bValidTimeInterval)
{
double KeyAnimLength = TimeInterval.GetDuration().GetSecondDouble();
if (KeyAnimLength != 0.0)
{
//////////////////////////////////////////////////////////////////////////
// 1. Look if we have high frequency keys(resampling).
//Basic sample rate is compute by dividing the KeyCount by the anim length. This is valid only if
//all keys are time equidistant. But if we find a rate over DEFAULT_SAMPLERATE, we can estimate that
//there is a constant frame rate between the key and simply return the rate.
int32 SampleRate = FPlatformMath::RoundToInt((KeyCount - 1) / KeyAnimLength);
if (SampleRate >= DEFAULT_SAMPLERATE)
{
//We import a curve with more then 30 keys per frame
return SampleRate;
}
//////////////////////////////////////////////////////////////////////////
// 2. Compute the sample rate of every keys with there time. Use the
// least common multiplier to get a sample rate that go through all keys.
SampleRate = 1;
double OldKeyTime = 0.0f;
TSet<int32> DeltaComputed;
//Reserve some space
DeltaComputed.Reserve(30);
const double KeyMultiplier = (1.0f / KINDA_SMALL_NUMBER);
//Find also the smallest delta time between keys
for (int32 KeyIndex = 0; KeyIndex < KeyCount; ++KeyIndex)
{
double KeyTime = (CurrentCurve->KeyGet(KeyIndex).GetTime().GetSecondDouble());
//Collect the smallest delta time, there is no delta in case the first animation key time is negative
double Delta = (KeyTime < 0 && KeyIndex == 0) ? 0.0 : KeyTime - OldKeyTime;
//use the fractional part of the delta to have the delta between 0.0f and 1.0f
Delta = FPlatformMath::Fractional(Delta);
int32 DeltaKey = FPlatformMath::RoundToInt(Delta*KeyMultiplier);
if (!FMath::IsNearlyZero((float)Delta, KINDA_SMALL_NUMBER) && !DeltaComputed.Contains(DeltaKey))
{
int32 ComputeSampleRate = GetTimeSampleRate(Delta);
DeltaComputed.Add(DeltaKey);
//Use the least common multiplier with the new delta entry
int32 LeastCommonMultiplier = FMath::Min(FMath::LeastCommonMultiplier(SampleRate, ComputeSampleRate), FPlatformMath::RoundToInt(MaxReferenceRate));
SampleRate = LeastCommonMultiplier != 0 ? LeastCommonMultiplier : FMath::Max3(FPlatformMath::RoundToInt(DEFAULT_SAMPLERATE), SampleRate, ComputeSampleRate);
}
OldKeyTime = KeyTime;
}
return SampleRate;
}
}
return 0;
}
void GetNodeSampleRate(FbxNode* Node, FbxAnimLayer* AnimLayer, TArray<int32>& NodeAnimSampleRates)
{
const int32 MaxElement = 9;
FbxAnimCurve* Curves[MaxElement];
Curves[0] = Node->LclTranslation.GetCurve(AnimLayer, FBXSDK_CURVENODE_COMPONENT_X, false);
Curves[1] = Node->LclTranslation.GetCurve(AnimLayer, FBXSDK_CURVENODE_COMPONENT_Y, false);
Curves[2] = Node->LclTranslation.GetCurve(AnimLayer, FBXSDK_CURVENODE_COMPONENT_Z, false);
Curves[3] = Node->LclRotation.GetCurve(AnimLayer, FBXSDK_CURVENODE_COMPONENT_X, false);
Curves[4] = Node->LclRotation.GetCurve(AnimLayer, FBXSDK_CURVENODE_COMPONENT_Y, false);
Curves[5] = Node->LclRotation.GetCurve(AnimLayer, FBXSDK_CURVENODE_COMPONENT_Z, false);
Curves[6] = Node->LclScaling.GetCurve(AnimLayer, FBXSDK_CURVENODE_COMPONENT_X, false);
Curves[7] = Node->LclScaling.GetCurve(AnimLayer, FBXSDK_CURVENODE_COMPONENT_Y, false);
Curves[8] = Node->LclScaling.GetCurve(AnimLayer, FBXSDK_CURVENODE_COMPONENT_Z, false);
for (int32 CurveIndex = 0; CurveIndex < MaxElement; ++CurveIndex)
{
FbxAnimCurve* CurrentCurve = Curves[CurveIndex];
if (CurrentCurve)
{
int32 CurveAnimRate = GetAnimationCurveRate(CurrentCurve);
if (CurveAnimRate != 0)
{
NodeAnimSampleRates.AddUnique(CurveAnimRate);
}
}
}
}
int32 UnFbx::FFbxImporter::GetGlobalAnimStackSampleRate(FbxAnimStack* CurAnimStack)
{
int32 ResampleRate = DEFAULT_SAMPLERATE;
if (ImportOptions->bResample)
{
TArray<int32> CurveAnimSampleRates;
int32 MaxStackResampleRate = 0;
int32 AnimStackLayerCount = CurAnimStack->GetMemberCount();
for (int32 LayerIndex = 0; LayerIndex < AnimStackLayerCount; ++LayerIndex)
{
FbxAnimLayer* AnimLayer = (FbxAnimLayer*)CurAnimStack->GetMember(LayerIndex);
for (int32 NodeIndex = 0; NodeIndex < Scene->GetNodeCount(); ++NodeIndex)
{
FbxNode* Node = Scene->GetNode(NodeIndex);
//Get both the transform properties curve and the blend shape animation sample rate
GetNodeSampleRate(Node, AnimLayer, CurveAnimSampleRates);
}
}
MaxStackResampleRate = CurveAnimSampleRates.Num() > 0 ? 1 : MaxStackResampleRate;
//Find the lowest sample rate that will pass by all the keys from all curves
for (int32 CurveSampleRate : CurveAnimSampleRates)
{
if (CurveSampleRate >= MaxReferenceRate && MaxStackResampleRate < CurveSampleRate)
{
MaxStackResampleRate = CurveSampleRate;
}
else if (MaxStackResampleRate < MaxReferenceRate)
{
int32 LeastCommonMultiplier = FMath::LeastCommonMultiplier(MaxStackResampleRate, CurveSampleRate);
MaxStackResampleRate = LeastCommonMultiplier != 0 ? LeastCommonMultiplier : FMath::Max3(FPlatformMath::RoundToInt(DEFAULT_SAMPLERATE), MaxStackResampleRate, CurveSampleRate);
if (MaxStackResampleRate >= MaxReferenceRate)
{
MaxStackResampleRate = MaxReferenceRate;
}
}
}
// Make sure we're not hitting 0 for samplerate
if (MaxStackResampleRate != 0)
{
//Make sure the resample rate is positive
if (!ensure(MaxStackResampleRate >= 0))
{
MaxStackResampleRate *= -1;
}
ResampleRate = MaxStackResampleRate;
}
}
return ResampleRate;
}
int32 UnFbx::FFbxImporter::GetMaxSampleRate(TArray<FbxNode*>& SortedLinks)
{
int32 MaxStackResampleRate = 0;
TArray<int32> CurveAnimSampleRates;
const FBXImportOptions* ImportOption = GetImportOptions();
int32 AnimStackCount = Scene->GetSrcObjectCount<FbxAnimStack>();
for( int32 AnimStackIndex = 0; AnimStackIndex < AnimStackCount; AnimStackIndex++)
{
FbxAnimStack* CurAnimStack = Scene->GetSrcObject<FbxAnimStack>(AnimStackIndex);
FbxTimeSpan AnimStackTimeSpan = GetAnimationTimeSpan(SortedLinks[0], CurAnimStack);
double AnimStackStart = AnimStackTimeSpan.GetStart().GetSecondDouble();
double AnimStackStop = AnimStackTimeSpan.GetStop().GetSecondDouble();
int32 AnimStackLayerCount = CurAnimStack->GetMemberCount();
for (int32 LayerIndex = 0; LayerIndex < AnimStackLayerCount; ++LayerIndex)
{
FbxAnimLayer* AnimLayer = (FbxAnimLayer*)CurAnimStack->GetMember(LayerIndex);
for (int32 LinkIndex = 0; LinkIndex < SortedLinks.Num(); ++LinkIndex)
{
FbxNode* CurrentLink = SortedLinks[LinkIndex];
GetNodeSampleRate(CurrentLink, AnimLayer, CurveAnimSampleRates);
}
}
}
MaxStackResampleRate = CurveAnimSampleRates.Num() > 0 ? 1 : MaxStackResampleRate;
//Find the lowest sample rate that will pass by all the keys from all curves
for (int32 CurveSampleRate : CurveAnimSampleRates)
{
if (CurveSampleRate >= MaxReferenceRate && MaxStackResampleRate < CurveSampleRate)
{
MaxStackResampleRate = CurveSampleRate;
}
else if (MaxStackResampleRate < MaxReferenceRate)
{
int32 LeastCommonMultiplier = FMath::LeastCommonMultiplier(MaxStackResampleRate, CurveSampleRate);
MaxStackResampleRate = LeastCommonMultiplier != 0 ? LeastCommonMultiplier : FMath::Max3(FPlatformMath::RoundToInt(DEFAULT_SAMPLERATE), MaxStackResampleRate, CurveSampleRate);
if (MaxStackResampleRate >= MaxReferenceRate)
{
MaxStackResampleRate = MaxReferenceRate;
}
}
}
// Make sure we're not hitting 0 for samplerate
if ( MaxStackResampleRate != 0 )
{
//Make sure the resample rate is positive
if (!ensure(MaxStackResampleRate >= 0))
{
MaxStackResampleRate *= -1;
}
return MaxStackResampleRate;
}
return DEFAULT_SAMPLERATE;
}
bool UnFbx::FFbxImporter::ValidateAnimStack(TArray<FbxNode*>& SortedLinks, TArray<FbxNode*>& NodeArray, FbxAnimStack* CurAnimStack, int32 ResampleRate, bool bImportMorph, bool bSnapToClosestFrameBoundary, FbxTimeSpan &AnimTimeSpan)
{
// set current anim stack
Scene->SetCurrentAnimationStack(CurAnimStack);
UE_LOG(LogFbx, Log, TEXT("Parsing AnimStack %s"),UTF8_TO_TCHAR(CurAnimStack->GetName()));
bool bValidAnimStack = true;
AnimTimeSpan = GetAnimationTimeSpan(SortedLinks[0], CurAnimStack);
// if no duration is found, return false
if (AnimTimeSpan.GetDuration() <= 0)
{
return false;
}
const double SequenceLengthInSeconds = FGenericPlatformMath::Max<double>(AnimTimeSpan.GetDuration().GetSecondDouble(), MINIMUM_ANIMATION_LENGTH);
const FFrameRate TargetFrameRate(ResampleRate, 1);
const FFrameTime LengthInFrameTime = TargetFrameRate.AsFrameTime(SequenceLengthInSeconds);
const float SubFrame = LengthInFrameTime.GetSubFrame();
if (!FMath::IsNearlyZero(SubFrame, KINDA_SMALL_NUMBER) && !FMath::IsNearlyEqual(SubFrame, 1.0f, KINDA_SMALL_NUMBER))
{
if (bSnapToClosestFrameBoundary)
{
// Figure out whether start or stop has to be adjusted
const FbxTime StartTime = AnimTimeSpan.GetStart();
const FbxTime StopTime = AnimTimeSpan.GetStop();
FbxTime NewStartTime;
FbxTime NewStopTime;
const FFrameTime StartFrameTime = TargetFrameRate.AsFrameTime(StartTime.GetSecondDouble());
const FFrameTime StopFrameTime = TargetFrameRate.AsFrameTime(StopTime.GetSecondDouble());
FFrameNumber StartFrameNumber, StopFrameNumber;
if (!FMath::IsNearlyZero(StartFrameTime.GetSubFrame()))
{
StartFrameNumber = StartFrameTime.RoundToFrame();
NewStartTime.SetSecondDouble(TargetFrameRate.AsSeconds(StartFrameNumber));
AnimTimeSpan.SetStart(NewStartTime);
}
if (!FMath::IsNearlyZero(StopFrameTime.GetSubFrame()))
{
StopFrameNumber = StopFrameTime.RoundToFrame();
NewStopTime.SetSecondDouble(TargetFrameRate.AsSeconds(StopFrameNumber));
AnimTimeSpan.SetStop(NewStopTime);
}
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Info, FText::Format(LOCTEXT("Info_ImportLengthSnap", "Animation length has been adjusted to align with frame borders using import frame-rate {0}.\n\nOriginal timings:\n\t\tStart: {1} ({2})\n\t\tStop: {3} ({4})\nAligned timings:\n\t\tStart: {5} ({6})\n\t\tStop: {7} ({8})"),
TargetFrameRate.ToPrettyText(),
FText::AsNumber(StartTime.GetSecondDouble()),
FText::AsNumber(StartFrameTime.AsDecimal()),
FText::AsNumber(StopTime.GetSecondDouble()),
FText::AsNumber(StopFrameTime.AsDecimal()),
FText::AsNumber(NewStartTime.GetSecondDouble()),
FText::AsNumber(StartFrameNumber.Value),
FText::AsNumber(NewStopTime.GetSecondDouble()),
FText::AsNumber(StopFrameNumber.Value))), FFbxErrors::Animation_InvalidData);
}
else
{
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Error, FText::Format(LOCTEXT("Error_InvalidImportLength", "Animation length {0} is not compatible with import frame-rate {1} (sub frame {2}), animation has to be frame-border aligned. Either re-export animation or enable snap to closest frame boundary import option."), FText::AsNumber(SequenceLengthInSeconds), TargetFrameRate.ToPrettyText(), FText::AsNumber(SubFrame))), FFbxErrors::Animation_InvalidData);
return false;
}
}
const FBXImportOptions* ImportOption = GetImportOptions();
// only add morph time if not setrange. If Set Range there is no reason to override time
if ( bImportMorph && ImportOption->AnimationLengthImportType != FBXALIT_SetRange)
{
for ( int32 NodeIndex = 0; NodeIndex < NodeArray.Num(); NodeIndex++ )
{
// consider blendshape animation curve
FbxGeometry* Geometry = (FbxGeometry*)NodeArray[NodeIndex]->GetNodeAttribute();
if (Geometry)
{
int32 BlendShapeDeformerCount = Geometry->GetDeformerCount(FbxDeformer::eBlendShape);
for(int32 BlendShapeIndex = 0; BlendShapeIndex<BlendShapeDeformerCount; ++BlendShapeIndex)
{
FbxBlendShape* BlendShape = (FbxBlendShape*)Geometry->GetDeformer(BlendShapeIndex, FbxDeformer::eBlendShape);
int32 BlendShapeChannelCount = BlendShape->GetBlendShapeChannelCount();
for(int32 ChannelIndex = 0; ChannelIndex<BlendShapeChannelCount; ++ChannelIndex)
{
FbxBlendShapeChannel* Channel = BlendShape->GetBlendShapeChannel(ChannelIndex);
if(Channel)
{
// Get the percentage of influence of the shape.
FbxAnimCurve* Curve = Geometry->GetShapeChannel(BlendShapeIndex, ChannelIndex, (FbxAnimLayer*)CurAnimStack->GetMember(0));
if (Curve && Curve->KeyGetCount() > 0)
{
FbxTimeSpan TmpAnimSpan;
if (Curve->GetTimeInterval(TmpAnimSpan))
{
bValidAnimStack = true;
// update animation interval to include morph target range
AnimTimeSpan.UnionAssignment(TmpAnimSpan);
}
}
}
}
}
}
}
}
return bValidAnimStack;
}
bool UnFbx::FFbxImporter::ImportCurve(const FbxAnimCurve* FbxCurve, FRichCurve& RichCurve, const FbxTimeSpan &AnimTimeSpan, const bool bNegative/*=false*/, const float ValueScale/*=1.f*/, const bool bAutoSetTangents /*= true*/)
{
const float DefaultCurveWeight = FbxAnimCurveDef::sDEFAULT_WEIGHT;
if ( FbxCurve )
{
//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* NonConstFbxCurve = const_cast<FbxAnimCurve*>(FbxCurve);
int32 KeyCount = FbxCurve->KeyGetCount();
const float AdjustedValueScale = (bNegative ? -ValueScale : ValueScale);
for ( int32 KeyIndex=0; KeyIndex < KeyCount; ++KeyIndex )
{
FbxAnimCurveKey Key = FbxCurve->KeyGet(KeyIndex);
FbxTime KeyTime = Key.GetTime() - AnimTimeSpan.GetStart();
const float KeyTimeValue = static_cast<float>(KeyTime.GetSecondDouble());
float Value = Key.GetValue() * AdjustedValueScale;
FKeyHandle NewKeyHandle = RichCurve.AddKey(KeyTimeValue, Value, false);
const bool bIncludeOverrides = true;
FbxAnimCurveDef::ETangentMode KeyTangentMode = Key.GetTangentMode(bIncludeOverrides);
FbxAnimCurveDef::EInterpolationType KeyInterpMode = Key.GetInterpolation();
FbxAnimCurveDef::EWeightedMode KeyTangentWeightMode = Key.GetTangentWeightMode();
ERichCurveInterpMode NewInterpMode = RCIM_Linear;
ERichCurveTangentMode NewTangentMode = RCTM_Auto;
ERichCurveTangentWeightMode NewTangentWeightMode = RCTWM_WeightedNone;
float RightTangent = NonConstFbxCurve->KeyGetRightDerivative(KeyIndex) * AdjustedValueScale;
float LeftTangent = NonConstFbxCurve->KeyGetLeftDerivative(KeyIndex) * AdjustedValueScale;
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 = FbxCurve->KeyGet(KeyIndex - 1);
FbxTime PreviousKeyTime = PreviousKey.GetTime() - AnimTimeSpan.GetStart();
PreviousKeyTimeValue = static_cast<float>(PreviousKeyTime.GetSecondDouble());
PreviousValue = PreviousKey.GetValue() * AdjustedValueScale;
//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 = FbxCurve->KeyGet(KeyIndex + 1);
FbxTime NextKeyTime = NextKey.GetTime() - AnimTimeSpan.GetStart();
NextKeyTimeValue = static_cast<float>(NextKeyTime.GetSecondDouble());
NextValue = NextKey.GetValue() * AdjustedValueScale;
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 = RCTM_Break;
}
else if (KeyTangentMode & FbxAnimCurveDef::eTangentUser)
{
NewTangentMode = RCTM_User;
}
switch (KeyInterpMode)
{
case FbxAnimCurveDef::eInterpolationConstant://! Constant value until next key.
NewInterpMode = RCIM_Constant;
break;
case FbxAnimCurveDef::eInterpolationLinear://! Linear progression to next key.
NewInterpMode = RCIM_Linear;
break;
case FbxAnimCurveDef::eInterpolationCubic://! Cubic progression to next key.
NewInterpMode = RCIM_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 = RCTM_User;
}
}
if (bIsFlatTangent)
{
RightTangent = 0;
LeftTangent = 0;
//To force flat tangent we need to set the tangent mode to user
NewTangentMode = RCTM_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 == RCTM_Auto && (bLeftWeightActive || bRightWeightActive))
{
NewTangentMode = RCTM_User;
RightTangent = LeftTangent;
}
if (NewTangentMode != RCTM_Auto)
{
const bool bEqualTangents = FMath::IsNearlyEqual(LeftTangent, RightTangent);
//If tangents are different then broken.
if (bEqualTangents)
{
NewTangentMode = RCTM_User;
}
else
{
NewTangentMode = RCTM_Break;
}
}
//Only cubic interpolation allow weighted tangents
if (KeyInterpMode == FbxAnimCurveDef::eInterpolationCubic)
{
if (bLeftWeightActive && bRightWeightActive)
{
NewTangentWeightMode = RCTWM_WeightedBoth;
}
else if (bLeftWeightActive)
{
NewTangentWeightMode = RCTWM_WeightedArrive;
RightTangentWeight = DefaultCurveWeight;
}
else if (bRightWeightActive)
{
NewTangentWeightMode = RCTWM_WeightedLeave;
LeftTangentWeight = DefaultCurveWeight;
}
else
{
NewTangentWeightMode = RCTWM_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.
RichCurve.SetKeyInterpMode(NewKeyHandle, NewInterpMode, bForceDisableTangentRecompute);
RichCurve.SetKeyTangentMode(NewKeyHandle, NewTangentMode, bForceDisableTangentRecompute);
RichCurve.SetKeyTangentWeightMode(NewKeyHandle, NewTangentWeightMode, bForceDisableTangentRecompute);
FRichCurveKey& NewKey = RichCurve.GetKey(NewKeyHandle);
NewKey.ArriveTangent = LeftTangent;
NewKey.LeaveTangent = RightTangent;
NewKey.ArriveTangentWeight = LeftTangentWeight;
NewKey.LeaveTangentWeight = RightTangentWeight;
}
if (bAutoSetTangents)
{
RichCurve.AutoSetTangents();
}
return true;
}
return false;
}
bool UnFbx::FFbxImporter::ImportCurveToAnimSequence(class UAnimSequence * TargetSequence, const FString& CurveName, const FbxAnimCurve* FbxCurve, int32 CurveFlags,const FbxTimeSpan& AnimTimeSpan, const bool bReimport, float ValueScale/*=1.f*/) const
{
if (TargetSequence && FbxCurve)
{
FName Name = *CurveName;
FAnimationCurveIdentifier FloatCurveId(Name, ERawCurveTrackTypes::RCT_Float);
const bool bShouldTransact = bReimport;
IAnimationDataModel* DataModel = TargetSequence->GetDataModel();
IAnimationDataController& Controller = TargetSequence->GetController();
const FFloatCurve* TargetCurve = DataModel->FindFloatCurve(FloatCurveId);
if (TargetCurve == nullptr)
{
// Need to add the curve first
Controller.AddCurve(FloatCurveId, AACF_DefaultCurve | CurveFlags, bShouldTransact);
TargetCurve = DataModel->FindFloatCurve(FloatCurveId);
}
else
{
// Need to update any of the flags
Controller.SetCurveFlags(FloatCurveId, CurveFlags | TargetCurve->GetCurveTypeFlags(), bShouldTransact);
}
// Should be valid at this point
ensure(TargetCurve);
FRichCurve RichCurve;
constexpr bool bNegative = false;
if (ImportCurve(FbxCurve, RichCurve, AnimTimeSpan, bNegative, ValueScale))
{
if (ImportOptions->bRemoveRedundantKeys)
{
RichCurve.RemoveRedundantAutoTangentKeys(SMALL_NUMBER);
}
// Set actual keys on curve within the model
Controller.SetCurveKeys(FloatCurveId, RichCurve.GetConstRefOfKeys(), bShouldTransact);
return true;
}
}
return false;
}
bool UnFbx::FFbxImporter::ImportRichCurvesToAnimSequence(UAnimSequence* TargetSequence, const TArray<FString>& CurveNames, const TArray<FRichCurve> RichCurves, int32 CurveFlags, const bool bReimport) const
{
if (TargetSequence && CurveNames.Num() > 0 && CurveNames.Num() == RichCurves.Num())
{
const bool bShouldTransact = bReimport;
for (int32 CurveIndex = 0; CurveIndex < CurveNames.Num(); ++CurveIndex)
{
FName Name = *CurveNames[CurveIndex];
FAnimationCurveIdentifier FloatCurveId(Name, ERawCurveTrackTypes::RCT_Float);
const IAnimationDataModel* DataModel = TargetSequence->GetDataModel();
IAnimationDataController& Controller = TargetSequence->GetController();
const FFloatCurve* TargetCurve = DataModel->FindFloatCurve(FloatCurveId);
if (TargetCurve == nullptr)
{
// Need to add the curve first
Controller.AddCurve(FloatCurveId, AACF_DefaultCurve | CurveFlags, bShouldTransact);
TargetCurve = DataModel->FindFloatCurve(FloatCurveId);
}
else
{
// Need to update any of the flags
Controller.SetCurveFlags(FloatCurveId, CurveFlags | TargetCurve->GetCurveTypeFlags(), bShouldTransact);
}
// Should be valid at this point
ensure(TargetCurve);
// Set actual keys on curve within the model
Controller.SetCurveKeys(FloatCurveId, RichCurves[CurveIndex].GetConstRefOfKeys(), bShouldTransact);
}
return true;
}
return false;
}
TArray<FRichCurve> UnFbx::FFbxImporter::ResolveWeightsForBlendShapeCurve(FRichCurve& ChannelWeightCurve, const TArray<float>& InbetweenFullWeights) const
{
int32 NumInbetweens = InbetweenFullWeights.Num();
if (NumInbetweens == 0)
{
return { ChannelWeightCurve };
}
TArray<FRichCurve> Result;
Result.SetNum( NumInbetweens + 1 );
TArray<float> ResolvedInbetweenWeightsSample;
ResolvedInbetweenWeightsSample.SetNum( NumInbetweens );
for ( const FRichCurveKey& SourceKey : ChannelWeightCurve.Keys )
{
const float SourceTime = SourceKey.Time;
const float SourceValue = SourceKey.Value;
float ResolvedPrimarySample = 0.0f;
ResolveWeightsForBlendShape(InbetweenFullWeights,SourceValue, ResolvedPrimarySample, ResolvedInbetweenWeightsSample);
FRichCurve& PrimaryCurve = Result[ 0 ];
FKeyHandle PrimaryHandle = PrimaryCurve.AddKey( SourceTime, ResolvedPrimarySample );
PrimaryCurve.SetKeyInterpMode( PrimaryHandle, SourceKey.InterpMode );
for ( int32 InbetweenIndex = 0; InbetweenIndex < NumInbetweens; ++InbetweenIndex )
{
FRichCurve& InbetweenCurve = Result[ InbetweenIndex + 1 ];
FKeyHandle InbetweenHandle = InbetweenCurve.AddKey( SourceTime, ResolvedInbetweenWeightsSample[ InbetweenIndex ] );
InbetweenCurve.SetKeyInterpMode( InbetweenHandle, SourceKey.InterpMode );
}
}
return Result;
}
void UnFbx::FFbxImporter::ResolveWeightsForBlendShape(const TArray<float>& InbetweenFullWeights , float InWeight, float& OutMainWeight, TArray<float>& OutInbetweenWeights) const
{
int32 NumInbetweens = InbetweenFullWeights.Num();
if ( NumInbetweens == 0 )
{
OutMainWeight = InWeight;
return;
}
OutInbetweenWeights.SetNumUninitialized( NumInbetweens );
for ( float& OutInbetweenWeight : OutInbetweenWeights )
{
OutInbetweenWeight = 0.0f;
}
if ( FMath::IsNearlyEqual( InWeight, 0.0f ) )
{
OutMainWeight = 0.0f;
return;
}
else if ( FMath::IsNearlyEqual( InWeight, 1.0f ) )
{
OutMainWeight = 1.0f;
return;
}
// Note how we don't care if UpperIndex/LowerIndex are beyond the bounds of the array here,
// as that signals when we're above/below all inbetweens
int32 UpperIndex = Algo::UpperBoundBy( InbetweenFullWeights, InWeight, []( const double& InbetweenWeight )
{
return InbetweenWeight;
} );
int32 LowerIndex = UpperIndex - 1;
float UpperWeight = 1.0f;
if ( UpperIndex <= NumInbetweens - 1 )
{
UpperWeight = InbetweenFullWeights[ UpperIndex ];
}
float LowerWeight = 0.0f;
if ( LowerIndex >= 0 )
{
LowerWeight = InbetweenFullWeights[ LowerIndex ];
}
UpperWeight = ( InWeight - LowerWeight ) / ( UpperWeight - LowerWeight );
LowerWeight = ( 1.0f - UpperWeight );
// We're between upper inbetween and the 1.0 weight
if ( UpperIndex > NumInbetweens - 1 )
{
OutMainWeight = UpperWeight;
OutInbetweenWeights[ NumInbetweens - 1 ] = LowerWeight;
}
// We're between 0.0 and the first inbetween weight
else if ( LowerIndex < 0 )
{
OutMainWeight = 0;
OutInbetweenWeights[ 0 ] = UpperWeight;
}
// We're between two inbetweens
else
{
OutInbetweenWeights[ UpperIndex ] = UpperWeight;
OutInbetweenWeights[ LowerIndex ] = LowerWeight;
}
}
template<typename AttributeType>
void FillCurveAttributeToBone(TArray<float>& OutFrameTimes, TArray<AttributeType>& OutFrameValues, const FbxAnimCurve* FbxCurve, const FbxTimeSpan& AnimTimeSpan, TFunctionRef<AttributeType(const FbxAnimCurveKey*, const FbxTime*)> EvaluationFunction)
{
const int32 KeyCount = FbxCurve ? FbxCurve->KeyGetCount() : 0;
if (KeyCount > 0)
{
OutFrameTimes.Reserve(KeyCount);
OutFrameValues.Reserve(KeyCount);
const FbxTime StartTime = AnimTimeSpan.GetStart();
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));
}
}
bool UnFbx::FFbxImporter::ImportCustomAttributeToBone(UAnimSequence* TargetSequence, FbxProperty& InProperty, FName BoneName, const FString& CurveName, const FbxAnimCurve* FbxCurve, const FbxTimeSpan& AnimTimeSpan, const bool bReimport, float ValueScale/*=1.f*/)
{
if (TargetSequence)
{
TArray<float> TimeArray;
switch (InProperty.GetPropertyDataType().GetType())
{
case EFbxType::eFbxHalfFloat:
case EFbxType::eFbxFloat:
case EFbxType::eFbxDouble:
{
TArray<float> FloatValues;
FillCurveAttributeToBone<float>(TimeArray, FloatValues, FbxCurve, AnimTimeSpan,
[&ValueScale, &InProperty](const FbxAnimCurveKey* Key, const FbxTime* KeyTime){
if (Key)
{
return Key->GetValue() * ValueScale;
}
else
{
return InProperty.Get<float>() * ValueScale;
}
});
UE::Anim::AddTypedCustomAttribute<FFloatAnimationAttribute, float>(FName(CurveName), BoneName, TargetSequence, MakeArrayView(TimeArray), MakeArrayView(FloatValues));
break;
}
case EFbxType::eFbxBool:
case EFbxType::eFbxShort:
case EFbxType::eFbxUShort:
case EFbxType::eFbxInt:
case EFbxType::eFbxUInt:
case EFbxType::eFbxLongLong:
case EFbxType::eFbxULongLong:
case EFbxType::eFbxChar:
{
TArray<int32> IntValues;
FillCurveAttributeToBone<int32>(TimeArray, IntValues, FbxCurve, AnimTimeSpan,
[&ValueScale, &InProperty](const FbxAnimCurveKey* Key, const FbxTime* KeyTime) {
if (Key)
{
return static_cast<int32>(Key->GetValue() * ValueScale);
}
else
{
return static_cast<int32>(InProperty.Get<int32>() * ValueScale);
}
});
UE::Anim::AddTypedCustomAttribute<FIntegerAnimationAttribute, int32>(FName(CurveName), BoneName, TargetSequence, MakeArrayView(TimeArray), MakeArrayView(IntValues));
break;
}
case EFbxType::eFbxString:
{
TArray<FString> StringValues;
FillCurveAttributeToBone<FString>(TimeArray, StringValues, FbxCurve, AnimTimeSpan,
[&ValueScale, &InProperty](const FbxAnimCurveKey* Key, const FbxTime* KeyTime) {
if (KeyTime)
{
FbxPropertyValue& EvaluatedValue = InProperty.EvaluateValue(*KeyTime);
FbxString StringValue;
EvaluatedValue.Get(&StringValue, EFbxType::eFbxString);
return FString(UTF8_TO_TCHAR(StringValue));
}
else
{
return FString(UTF8_TO_TCHAR(InProperty.Get<FbxString>()));
}
});
UE::Anim::AddTypedCustomAttribute<FStringAnimationAttribute, FString>(FName(CurveName), BoneName, TargetSequence, MakeArrayView(TimeArray), MakeArrayView(StringValues));
break;
}
case EFbxType::eFbxEnum:
{
// Enum-typed properties in FBX are converted to string-typed custom attributes using the string value
// that corresponds to the enum index.
TArray<FString> StringValues;
FillCurveAttributeToBone<FString>(TimeArray, StringValues, FbxCurve, AnimTimeSpan,
[&ValueScale, &InProperty](const FbxAnimCurveKey* Key, const FbxTime* KeyTime) {
int32 EnumIndex = -1;
if (KeyTime)
{
FbxPropertyValue& EvaluatedValue = InProperty.EvaluateValue(*KeyTime);
EvaluatedValue.Get(&EnumIndex, EFbxType::eFbxEnum);
}
else
{
EnumIndex = InProperty.Get<FbxEnum>();
}
if (EnumIndex < 0 || EnumIndex >= InProperty.GetEnumCount())
{
return FString();
}
const char* EnumValue = InProperty.GetEnumValue(EnumIndex);
return FString(UTF8_TO_TCHAR(EnumValue));
});
UE::Anim::AddTypedCustomAttribute<FStringAnimationAttribute, FString>(FName(CurveName), BoneName, TargetSequence, MakeArrayView(TimeArray), MakeArrayView(StringValues));
break;
}
default:
{
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Warning, FText::Format(LOCTEXT("Warning_CustomAttributeTypeNotSupported", "Custom Attribute ({0}) could not be imported on bone, as its type {1} is not supported."), FText::FromString(CurveName), FText::AsNumber((int32)InProperty.GetPropertyDataType().GetType()))), FFbxErrors::Animation_InvalidData);
return false;
}
}
return true;
}
return false;
}
bool ShouldImportCurve(FbxAnimCurve* Curve, bool bDoNotImportWithZeroValues)
{
if (Curve && Curve->KeyGetCount() > 0)
{
if (bDoNotImportWithZeroValues)
{
for (int32 KeyIndex = 0; KeyIndex < Curve->KeyGetCount(); ++KeyIndex)
{
if (!FMath::IsNearlyZero(Curve->KeyGetValue(KeyIndex)))
{
return true;
}
}
}
else
{
return true;
}
}
return false;
}
bool UnFbx::FFbxImporter::ImportAnimation(USkeleton* Skeleton, UAnimSequence * DestSeq, const FString& FileName, TArray<FbxNode*>& SortedLinks, TArray<FbxNode*>& NodeArray, FbxAnimStack* CurAnimStack, const int32 ResampleRate, const FbxTimeSpan AnimTimeSpan, const bool bReimport)
{
// @todo : the length might need to change w.r.t. sampling keys
FbxTime SequenceLength = AnimTimeSpan.GetDuration();
float PreviousSequenceLength = DestSeq->GetPlayLength();
const bool bShouldTransact = bReimport;
IAnimationDataModel::FReimportScope ReimportScope(DestSeq->GetDataModel());
IAnimationDataController& Controller = DestSeq->GetController();
Controller.OpenBracket(LOCTEXT("ImportAnimation_Bracket", "Importing Animation"), bShouldTransact);
//This destroy all previously imported animation raw data
Controller.RemoveAllBoneTracks(bShouldTransact);
// First set frame rate
const FFrameRate ResampleFrameRate(ResampleRate, 1);
Controller.SetFrameRate(ResampleFrameRate, bShouldTransact);
// if you have one pose(thus 0.f duration), it still contains animation, so we'll need to consider that as MINIMUM_ANIMATION_LENGTH time length
const FFrameNumber NumberOfFrames = ResampleFrameRate.AsFrameNumber(SequenceLength.GetSecondDouble());
Controller.SetNumberOfFrames(FGenericPlatformMath::Max<int32>(NumberOfFrames.Value, 1), bShouldTransact);
if(PreviousSequenceLength > MINIMUM_ANIMATION_LENGTH && DestSeq->GetDataModel()->GetNumberOfFloatCurves() > 0)
{
// The sequence already existed when we began the import. We need to scale the key times for all curves to match the new
// duration before importing over them. This is to catch any user-added curves
float ScaleFactor = DestSeq->GetPlayLength() / PreviousSequenceLength;
if (!FMath::IsNearlyEqual(ScaleFactor, 1.f))
{
for (const FFloatCurve& Curve : DestSeq->GetDataModel()->GetFloatCurves())
{
const FAnimationCurveIdentifier CurveId(Curve.GetName(), ERawCurveTrackTypes::RCT_Float);
Controller.ScaleCurve(CurveId, 0.f, ScaleFactor, bShouldTransact);
}
}
}
USkeleton* MySkeleton = DestSeq->GetSkeleton();
check(MySkeleton);
if (ImportOptions->bDeleteExistingMorphTargetCurves || ImportOptions->bDeleteExistingCustomAttributeCurves)
{
TArray<FName> CurveNamesToRemove;
for (const FFloatCurve& Curve : DestSeq->GetDataModel()->GetFloatCurves())
{
const FCurveMetaData* MetaData = MySkeleton->GetCurveMetaData(Curve.GetName());
if (MetaData)
{
bool bDeleteCurve = MetaData->Type.bMorphtarget ? ImportOptions->bDeleteExistingMorphTargetCurves : ImportOptions->bDeleteExistingCustomAttributeCurves;
if (bDeleteCurve)
{
CurveNamesToRemove.Add(Curve.GetName());
}
}
}
for (auto CurveName : CurveNamesToRemove)
{
const FAnimationCurveIdentifier CurveId(CurveName, ERawCurveTrackTypes::RCT_Float);
Controller.RemoveCurve(CurveId, bShouldTransact);
}
}
if (ImportOptions->bDeleteExistingNonCurveCustomAttributes)
{
Controller.RemoveAllAttributes(bShouldTransact);
}
const bool bReimportWarnings = GetDefault<UEditorPerProjectUserSettings>()->bAnimationReimportWarnings;
if (bReimportWarnings && !FMath::IsNearlyZero(PreviousSequenceLength) && !FMath::IsNearlyEqual(DestSeq->GetPlayLength(), PreviousSequenceLength))
{
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Warning, FText::Format(LOCTEXT("Warning_SequenceLengthChanged", "Animation Sequence ({0}) length {1} is different from previous {2}."), FText::FromName(DestSeq->GetFName()), DestSeq->GetPlayLength(), PreviousSequenceLength)), FFbxErrors::Animation_DifferentLength);
}
TArray<FName> FbxRawBoneNames;
FillAndVerifyBoneNames(Skeleton, SortedLinks, FbxRawBoneNames, FileName);
FAnimCurveImportSettings AnimImportSettings(DestSeq, NodeArray, SortedLinks, FbxRawBoneNames, AnimTimeSpan);
//
// import blend shape curves
//
int32 CurveAttributeKeyCount = 0;
ImportBlendShapeCurves(AnimImportSettings, CurAnimStack, CurveAttributeKeyCount, bReimport);
// importing custom attribute START
TArray<FString> CurvesNotFound;
if (ImportOptions->bImportCustomAttribute)
{
int CustomAttributeKeyCount = 0;
ImportAnimationCustomAttribute(AnimImportSettings, CustomAttributeKeyCount, CurvesNotFound, bReimport);
CurveAttributeKeyCount = FMath::Max(CurveAttributeKeyCount, CustomAttributeKeyCount);
}
else
{
// Store float curve tracks which use to exist on the animation
for (const FFloatCurve& Curve : DestSeq->GetDataModel()->GetFloatCurves())
{
const FCurveMetaData* MetaData = MySkeleton->GetCurveMetaData(Curve.GetName());
if (MetaData && !MetaData->Type.bMorphtarget)
{
CurvesNotFound.Add(Curve.GetName().ToString());
}
}
}
if (bReimportWarnings && CurvesNotFound.Num())
{
for (const FString& CurveName : CurvesNotFound)
{
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Warning, FText::Format(LOCTEXT("Warning_NonExistingCurve", "Curve ({0}) was not found in the new Animation."), FText::FromString(CurveName))), FFbxErrors::Animation_CurveNotFound);
}
}
// importing custom attribute END
int32 TotalNumKeys = 0;
const FReferenceSkeleton& RefSkeleton = Skeleton->GetReferenceSkeleton();
// import animation
if (ImportOptions->bImportBoneTracks)
{
FbxNode* SkeletalMeshRootNode = NodeArray.Num() > 0 ? NodeArray[0] : nullptr;
ImportBoneTracks(Skeleton, AnimImportSettings, SkeletalMeshRootNode, ResampleRate, TotalNumKeys, bReimport);
}
else if (CurveAttributeKeyCount > 0)
{
Controller.SetFrameRate(FFrameRate(ResampleRate, 1), bShouldTransact);
}
// Import bone metadata to AnimSequence
for (FbxNode* SkeletonNode : SortedLinks)
{
ImportNodeCustomProperties(DestSeq, SkeletonNode, true);
}
Controller.NotifyPopulated();
Controller.CloseBracket(bShouldTransact);
// Reregister skeletal mesh components so they reflect the updated animation
for (TObjectIterator<USkeletalMeshComponent> Iter; Iter; ++Iter)
{
FComponentReregisterContext ReregisterContext(*Iter);
}
return true;
}
void UnFbx::FFbxImporter::ImportBlendShapeCurves(FAnimCurveImportSettings& AnimImportSettings, FbxAnimStack* CurAnimStack, int32& OutKeyCount, const bool bReimport)
{
FText CurrentExportMessage = LOCTEXT("BeginImportMorphTargetCurves", "Importing Morph Target Curves");
FScopedSlowTask SlowTaskNode(AnimImportSettings.NodeArray.Num(), CurrentExportMessage);
SlowTaskNode.MakeDialog();
OutKeyCount = 0;
USkeleton* MySkeleton = AnimImportSettings.DestSeq->GetSkeleton();
for (int32 NodeIndex = 0; NodeIndex < AnimImportSettings.NodeArray.Num(); NodeIndex++)
{
SlowTaskNode.EnterProgressFrame(1, CurrentExportMessage);
// consider blendshape animation curve
FbxGeometry* Geometry = (FbxGeometry*)AnimImportSettings.NodeArray[NodeIndex]->GetNodeAttribute();
if (Geometry)
{
int32 BlendShapeDeformerCount = Geometry->GetDeformerCount(FbxDeformer::eBlendShape);
FScopedSlowTask SlowTaskBlendShape(BlendShapeDeformerCount);
for (int32 BlendShapeIndex = 0; BlendShapeIndex < BlendShapeDeformerCount; ++BlendShapeIndex)
{
SlowTaskBlendShape.EnterProgressFrame(1);
FbxBlendShape* BlendShape = (FbxBlendShape*)Geometry->GetDeformer(BlendShapeIndex, FbxDeformer::eBlendShape);
const int32 BlendShapeChannelCount = BlendShape->GetBlendShapeChannelCount();
FString BlendShapeName = MakeName(BlendShape->GetName());
// see below where this is used for explanation...
const bool bMightBeBadMAXFile = (BlendShapeName == FString("Morpher"));
FScopedSlowTask SlowTaskChannel(BlendShapeChannelCount);
for (int32 ChannelIndex = 0; ChannelIndex < BlendShapeChannelCount; ++ChannelIndex)
{
FbxBlendShapeChannel* Channel = BlendShape->GetBlendShapeChannel(ChannelIndex);
bool bUpdatedProgress = false;
if (Channel)
{
FString ChannelName = MakeName(Channel->GetName());
// Maya adds the name of the blendshape and an underscore or point to the front of the channel name, so remove it
// Also avoid to endup with a empty name, we prefer having the Blendshapename instead of nothing
if (ChannelName.StartsWith(BlendShapeName) && ChannelName.Len() > BlendShapeName.Len())
{
ChannelName.RightInline(ChannelName.Len() - (BlendShapeName.Len() + 1), EAllowShrinking::No);
}
if (bMightBeBadMAXFile)
{
FbxShape* TargetShape = Channel->GetTargetShapeCount() > 0 ? Channel->GetTargetShape(0) : nullptr;
if (TargetShape)
{
FString TargetShapeName = MakeName(TargetShape->GetName());
ChannelName = TargetShapeName.IsEmpty() ? ChannelName : TargetShapeName;
}
}
FbxAnimCurve* Curve = Geometry->GetShapeChannel(BlendShapeIndex, ChannelIndex, (FbxAnimLayer*)CurAnimStack->GetMember(0));
if (FbxAnimUtils::ShouldImportCurve(Curve, ImportOptions->bDoNotImportCurveWithZero))
{
FFormatNamedArguments Args;
Args.Add(TEXT("BlendShape"), FText::FromString(ChannelName));
CurrentExportMessage = FText::Format(LOCTEXT("ImportingMorphTargetCurvesDetail", "Importing Morph Target Curves [{BlendShape}]"), Args);
SlowTaskNode.FrameMessage = CurrentExportMessage;
SlowTaskChannel.EnterProgressFrame(1);
bUpdatedProgress = true;
const int32 TargetShapeCount = Channel->GetTargetShapeCount();
if (ensure(TargetShapeCount > 0))
{
if (TargetShapeCount == 1)
{
// now see if we have one already exists. If so, just overwrite that. if not, add new one.
if (ImportCurveToAnimSequence(AnimImportSettings.DestSeq, *ChannelName, Curve, 0, AnimImportSettings.AnimTimeSpan, bReimport, 0.01f /** for some reason blend shape values are coming as 100 scaled **/))
{
OutKeyCount = FMath::Max(OutKeyCount, Curve->KeyGetCount());
if(ImportOptions->bAddCurveMetadataToSkeleton)
{
// this one doesn't reset Material curve to false, it just accumulate if true.
MySkeleton->AccumulateCurveMetaData(*ChannelName, false, true);
}
}
}
else
{
// the blend shape channel can have multiple inbetween target shapes
// since the engine does not directly support inbetween morphs at runtime,
// each inbetween is imported as a standalone blendshape.
// as a result we have to create a curve for each one of those inbetweens
// and modify the primary channel curve such that their combined effect preserves
// the original animation
TArray<FString> CurveNames;
CurveNames.Reserve(TargetShapeCount);
// in fbx the primary shape is the last shape, however to make
// the code more similar to usd importer, we deal with the primary shape separately
CurveNames.Add(ChannelName);
// ignoring the last shape because it is not a inbetween, i.e. it is the primary shape
int32 InbetweenCount = TargetShapeCount - 1;
TArrayView<double> FbxInbetweenFullWeights = {Channel->GetTargetShapeFullWeights(), InbetweenCount};
TArray<float> InbetweenFullWeights;
InbetweenFullWeights.Reserve(InbetweenCount);
/** for some reason blend shape values are coming as 100 scaled, so a transform is needed to scale it to 0-1 **/
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()));
}
// first convert fbx curve to rich curve
FRichCurve ChannelWeightCurve;
ImportCurve(Curve, ChannelWeightCurve, AnimImportSettings.AnimTimeSpan, false, 0.01f /** for some reason blend shape values are coming as 100 scaled **/);
if (ensure(AnimImportSettings.DestSeq))
{
#if WITH_EDITORONLY_DATA
ChannelWeightCurve.BakeCurve(1.0f / AnimImportSettings.DestSeq->ImportResampleFramerate);
#endif
}
// use the primary curve to generate inbetween shape curves + a modified primary curve
TArray<FRichCurve> Results = ResolveWeightsForBlendShapeCurve(ChannelWeightCurve, InbetweenFullWeights);
for (FRichCurve& Result : Results)
{
if (ImportOptions->bRemoveRedundantKeys)
{
Result.RemoveRedundantAutoTangentKeys(SMALL_NUMBER);
}
}
if (ImportRichCurvesToAnimSequence(AnimImportSettings.DestSeq, CurveNames, Results, 0, bReimport))
{
OutKeyCount = FMath::Max(OutKeyCount, Curve->KeyGetCount());
if(ImportOptions->bAddCurveMetadataToSkeleton)
{
for (const FString& CurveName : CurveNames)
{
// this one doesn't reset Material curve to false, it just accumulate if true.
MySkeleton->AccumulateCurveMetaData(*CurveName, false, true);
}
}
}
}
}
}
else
{
UE_LOG(LogFbx, Warning, TEXT("CurveName(%s) is skipped because it only contains invalid values."), *ChannelName);
}
}
if (!bUpdatedProgress)
{
SlowTaskChannel.EnterProgressFrame(1);
}
}
}
}
}
}
void UnFbx::FFbxImporter::ImportAnimationCustomAttribute(FAnimCurveImportSettings& AnimImportSettings, int32& OutKeyCount, TArray<FString>& OutCurvesNotFound, const bool bReimport)
{
FScopedSlowTask SlowTask(AnimImportSettings.SortedLinks.Num(), LOCTEXT("BeginImportCustomAttributeCurves", "Importing Custom Attribute Curves"), true);
SlowTask.MakeDialog();
// Store float curve tracks which use to exist on the animation
UAnimSequence* DestSeq = AnimImportSettings.DestSeq;
USkeleton* MySkeleton = DestSeq->GetSkeleton();
const IAnimationDataModel* DataModel = DestSeq->GetDataModel();
const int32 NumFloatCurves = DataModel->GetNumberOfFloatCurves();
const FAnimationCurveData& CurveData = DataModel->GetCurveData();
OutCurvesNotFound.Reset(NumFloatCurves);
for (const FFloatCurve& FloatCurve : CurveData.FloatCurves)
{
const FCurveMetaData* MetaData = MySkeleton->GetCurveMetaData(FloatCurve.GetName());
if (MetaData && !MetaData->Type.bMorphtarget)
{
OutCurvesNotFound.Add(FloatCurve.GetName().ToString());
}
}
OutKeyCount = 0;
for (int32 LinkIndex = 0; LinkIndex < AnimImportSettings.SortedLinks.Num(); ++LinkIndex)
{
FbxNode* Node = AnimImportSettings.SortedLinks[LinkIndex];
FName BoneName = AnimImportSettings.FbxRawBoneNames[LinkIndex];
bool bImportAllAttributesOnBone = UAnimationSettings::Get()->BoneNamesWithCustomAttributes.Contains(BoneName.ToString());
SlowTask.EnterProgressFrame(1);
if (!bImportAllAttributesOnBone)
{
FbxAnimUtils::ExtractAttributeCurves(Node, ImportOptions->bDoNotImportCurveWithZero,
[this, &LinkIndex, &DestSeq, &AnimImportSettings, &OutCurvesNotFound, &OutKeyCount, &SlowTask, bReimport](FbxAnimCurve* InCurve, const FString& InCurveName)
{
FFormatNamedArguments Args;
Args.Add(TEXT("CurveName"), FText::FromString(InCurveName));
const FText StatusUpate = FText::Format(LOCTEXT("ImportingCustomAttributeCurvesDetail", "Importing Custom Attribute [{CurveName}]"), Args);
SlowTask.EnterProgressFrame(0, StatusUpate);
int32 CurveFlags = AACF_DefaultCurve;
if (ImportCurveToAnimSequence(DestSeq, InCurveName, InCurve, CurveFlags, AnimImportSettings.AnimTimeSpan, bReimport))
{
OutKeyCount = FMath::Max(OutKeyCount, InCurve->KeyGetCount());
if(ImportOptions->bAddCurveMetadataToSkeleton)
{
USkeleton* SeqSkeleton = DestSeq->GetSkeleton();
// first let them override material curve if required
if (ImportOptions->bSetMaterialDriveParameterOnCustomAttribute)
{
// now mark this curve as material curve
SeqSkeleton->AccumulateCurveMetaData(FName(*InCurveName), true, false);
}
else
{
// if not material set by default, apply naming convention for material
for (const auto& Suffix : ImportOptions->MaterialCurveSuffixes)
{
int32 TotalSuffix = Suffix.Len();
if (InCurveName.Right(TotalSuffix) == Suffix)
{
SeqSkeleton->AccumulateCurveMetaData(FName(*InCurveName), true, false);
break;
}
}
}
}
OutCurvesNotFound.Remove(InCurveName);
}
});
}
FbxAnimUtils::ExtractNodeAttributes(Node, ImportOptions->bDoNotImportCurveWithZero, bImportAllAttributesOnBone,
[this, &OutKeyCount, &DestSeq, &AnimImportSettings, &BoneName, bReimport](FbxProperty& InProperty, FbxAnimCurve* InCurve, const FString& InCurveName)
{
if (ImportCustomAttributeToBone(DestSeq, InProperty, BoneName, InCurveName, InCurve, AnimImportSettings.AnimTimeSpan, bReimport))
{
OutKeyCount = FMath::Max(OutKeyCount, InCurve ? InCurve->KeyGetCount() : 1);
}
});
}
}
void UnFbx::FFbxImporter::ImportBoneTracks(USkeleton* Skeleton, FAnimCurveImportSettings& AnimImportSettings, FbxNode* SkeletalMeshRootNode, const int32 ResampleRate, int32& OutTotalNumKeys, const bool bReimport)
{
OutTotalNumKeys = 0;
UnFbx::FFbxImporter* FbxImporter = UnFbx::FFbxImporter::GetInstance();
const bool bPreserveLocalTransform = FbxImporter->GetImportOptions()->bPreserveLocalTransform;
UAnimSequence* DestSeq = AnimImportSettings.DestSeq;
const FbxTimeSpan& AnimTimeSpan = AnimImportSettings.AnimTimeSpan;
const bool bShouldTransact = bReimport;
// Build additional transform matrix
UFbxAnimSequenceImportData* TemplateData = Cast<UFbxAnimSequenceImportData>(DestSeq->AssetImportData);
FbxAMatrix FbxAddedMatrix;
BuildFbxMatrixForImportTransform(FbxAddedMatrix, TemplateData);
FMatrix AddedMatrix = Converter.ConvertMatrix(FbxAddedMatrix);
FTransform AddedTransform(AddedMatrix);
bool bIsRigidMeshAnimation = false;
if (ImportOptions->bImportScene && AnimImportSettings.SortedLinks.Num() > 0)
{
for (int32 BoneIdx = 0; BoneIdx < AnimImportSettings.SortedLinks.Num(); ++BoneIdx)
{
FbxNode* Link = AnimImportSettings.SortedLinks[BoneIdx];
if (Link->GetMesh() && Link->GetMesh()->GetDeformerCount(FbxDeformer::eSkin) == 0)
{
bIsRigidMeshAnimation = true;
break;
}
}
}
// Prepare per-track data
const FReferenceSkeleton& RefSkeleton = Skeleton->GetReferenceSkeleton();
int32 SourceTrackNum = AnimImportSettings.FbxRawBoneNames.Num();
int32 KeyTotal = FMath::RoundToInt((AnimTimeSpan.GetDuration().GetSecondDouble() * ResampleRate)) + 1; // Expected Number of Keys
TArray<int32> BoneTreeIndices;
TArray<bool> SourceTrackValid;
TArray<FRawAnimSequenceTrack> RawTracks;
TArray<TArray<float>> TimeKeys;
BoneTreeIndices.SetNum(SourceTrackNum);
SourceTrackValid.SetNum(SourceTrackNum);
RawTracks.SetNum(SourceTrackNum);
TimeKeys.SetNum(SourceTrackNum);
for (int32 SourceTrackIdx = 0; SourceTrackIdx < SourceTrackNum; ++SourceTrackIdx)
{
BoneTreeIndices[SourceTrackIdx] = RefSkeleton.FindBoneIndex(AnimImportSettings.FbxRawBoneNames[SourceTrackIdx]);
SourceTrackValid[SourceTrackIdx] = BoneTreeIndices[SourceTrackIdx] != INDEX_NONE || IsUnrealTransformAttribute(AnimImportSettings.SortedLinks[SourceTrackIdx]);
RawTracks[SourceTrackIdx].PosKeys.Reserve(KeyTotal);
RawTracks[SourceTrackIdx].RotKeys.Reserve(KeyTotal);
RawTracks[SourceTrackIdx].ScaleKeys.Reserve(KeyTotal);
TimeKeys[SourceTrackIdx].Reserve(KeyTotal);
}
// Set the time increment from the re-sample rate
FbxTime TimeIncrement = 0;
TimeIncrement.SetSecondDouble(1.0 / ((double)(ResampleRate)));
// Add a threshold when we compare if we have reach the end of the animation
const FbxTime TimeComparisonThreshold = (KINDA_SMALL_NUMBER * static_cast<float>(FBXSDK_TC_SECOND));
// Loop Over Key Times
FScopedSlowTask SlowTask(KeyTotal, LOCTEXT("BeginImportAnimation", "Importing Animation"), true);
SlowTask.MakeDialog();
int32 KeyNum = 0;
for (FbxTime CurTime = AnimTimeSpan.GetStart(); CurTime < (AnimTimeSpan.GetStop() + TimeComparisonThreshold); CurTime += TimeIncrement)
{
// Update Status
FFormatNamedArguments Args;
Args.Add(TEXT("KeyNum"), FText::AsNumber(KeyNum + 1));
Args.Add(TEXT("KeyTotal"), FText::AsNumber(KeyTotal));
const FText StatusUpate = FText::Format(LOCTEXT("ImportingAnimTrackDetail", "Importing Animation Keys {KeyNum} of {KeyTotal}"), Args);
SlowTask.EnterProgressFrame(1.0f, StatusUpate);
// Loop Over Tracks
for (int32 SourceTrackIdx = 0; SourceTrackIdx < SourceTrackNum; ++SourceTrackIdx)
{
// Skip Invalid Tracks
if (!SourceTrackValid[SourceTrackIdx])
{
continue;
}
// Get Bone name and index.
FName BoneName = AnimImportSettings.FbxRawBoneNames[SourceTrackIdx];
int32 BoneTreeIndex = BoneTreeIndices[SourceTrackIdx];
// Get Links
FbxNode* Link = AnimImportSettings.SortedLinks[SourceTrackIdx];
FbxNode* LinkParent = Link->GetParent();
FTransform LocalTransform;
if (!bPreserveLocalTransform && LinkParent)
{
// get global transform
FbxAMatrix GlobalMatrix = Link->EvaluateGlobalTransform(CurTime) * FFbxDataConverter::GetJointPostConversionMatrix();
// we'd like to verify this before going to Transform.
// currently transform has tons of NaN check, so it will crash there
FMatrix GlobalUEMatrix = Converter.ConvertMatrix(GlobalMatrix);
if (GlobalUEMatrix.ContainsNaN())
{
SourceTrackValid[SourceTrackIdx] = false;
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Error, FText::Format(LOCTEXT("Error_InvalidTransform", "Track {0} contains invalid transform. Could not import the track."), FText::FromName(BoneName))), FFbxErrors::Animation_TransformError);
continue;
}
FTransform GlobalTransform = Converter.ConvertTransform(GlobalMatrix);
if (GlobalTransform.ContainsNaN())
{
SourceTrackValid[SourceTrackIdx] = false;
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Error, FText::Format(LOCTEXT("Error_InvalidUnrealTransform", "Track {0} has invalid transform(NaN). Zero scale transform can cause this issue."), FText::FromName(BoneName))), FFbxErrors::Animation_TransformError);
continue;
}
// I can't rely on LocalMatrix. I need to recalculate quaternion/scale based on global transform if Parent exists
FbxAMatrix ParentGlobalMatrix = Link->GetParent()->EvaluateGlobalTransform(CurTime);
if (BoneTreeIndex != 0)
{
ParentGlobalMatrix = ParentGlobalMatrix * FFbxDataConverter::GetJointPostConversionMatrix();
}
FTransform ParentGlobalTransform = Converter.ConvertTransform(ParentGlobalMatrix);
//In case we do a scene import we need to add the skeletal mesh root node matrix to the parent link.
if (ImportOptions->bImportScene && !ImportOptions->bTransformVertexToAbsolute && BoneTreeIndex == 0 && SkeletalMeshRootNode != nullptr)
{
//In the case of a rigidmesh animation we have to use the skeletalMeshRootNode position at zero since the mesh can be animate.
FbxAMatrix GlobalSkeletalNodeFbx = bIsRigidMeshAnimation ? SkeletalMeshRootNode->EvaluateGlobalTransform(0) : SkeletalMeshRootNode->EvaluateGlobalTransform(CurTime);
FTransform GlobalSkeletalNode = Converter.ConvertTransform(GlobalSkeletalNodeFbx);
ParentGlobalTransform = ParentGlobalTransform * GlobalSkeletalNode;
}
LocalTransform = GlobalTransform.GetRelativeTransform(ParentGlobalTransform);
}
else
{
FbxAMatrix& LocalMatrix = Link->EvaluateLocalTransform(CurTime);
FbxVector4 NewLocalT = LocalMatrix.GetT();
FbxVector4 NewLocalS = LocalMatrix.GetS();
FbxQuaternion NewLocalQ = LocalMatrix.GetQ();
LocalTransform.SetTranslation(Converter.ConvertPos(NewLocalT));
LocalTransform.SetScale3D(Converter.ConvertScale(NewLocalS));
LocalTransform.SetRotation(Converter.ConvertRotToQuat(NewLocalQ));
}
if (TemplateData && BoneTreeIndex == 0)
{
// If we found template data earlier, apply the import transform matrix to
// the root track.
LocalTransform.SetFromMatrix(LocalTransform.ToMatrixWithScale() * AddedMatrix);
}
if (LocalTransform.ContainsNaN())
{
SourceTrackValid[SourceTrackIdx] = false;
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Error, FText::Format(LOCTEXT("Error_InvalidUnrealLocalTransform", "Track {0} has invalid transform(NaN). If you have zero scale transform, that can cause this."), FText::FromName(BoneName))), FFbxErrors::Animation_TransformError);
continue;
}
// Add data to RawTracks, TimeKeys
RawTracks[SourceTrackIdx].ScaleKeys.Add(FVector3f(LocalTransform.GetScale3D()));
RawTracks[SourceTrackIdx].PosKeys.Add(FVector3f(LocalTransform.GetTranslation()));
RawTracks[SourceTrackIdx].RotKeys.Add(FQuat4f(LocalTransform.GetRotation()));
TimeKeys[SourceTrackIdx].Add((CurTime - AnimTimeSpan.GetStart()).GetSecondDouble());
}
// Update Key Num
KeyNum++;
}
// Compute Total Number of Keys
for (int32 SourceTrackIdx = 0; SourceTrackIdx < SourceTrackNum; ++SourceTrackIdx)
{
OutTotalNumKeys = FMath::Max(OutTotalNumKeys, TimeKeys[SourceTrackIdx].Num());
}
// Put raw track data into animation
IAnimationDataController& Controller = DestSeq->GetController();
Controller.SetFrameRate(FFrameRate(ResampleRate, 1), bShouldTransact);
for (int32 SourceTrackIdx = 0; SourceTrackIdx < SourceTrackNum; ++SourceTrackIdx)
{
if (!SourceTrackValid[SourceTrackIdx])
{
continue;
}
FName BoneName = AnimImportSettings.FbxRawBoneNames[SourceTrackIdx];
int32 BoneTreeIndex = BoneTreeIndices[SourceTrackIdx];
FbxNode* Link = AnimImportSettings.SortedLinks[SourceTrackIdx];
FbxNode* LinkParent = Link->GetParent();
const int32 SourceTrackKeyNum = TimeKeys[SourceTrackIdx].Num();
if (BoneTreeIndex != INDEX_NONE)
{
//add new track
if (BoneName.GetStringLength() > 92)
{
//The bone name exceed the maximum length supported by the animation system
//The animation system is adding _CONTROL to the bone name to name the animation controller and
//the maximum total length is cap at 100, so user should not import bone name longer then 92 characters
AddTokenizedErrorMessage(FTokenizedMessage::Create(EMessageSeverity::Warning, FText::Format(LOCTEXT("Error_BoneNameExceed92Characters", "Bone with animation cannot have a name exceeding 92 characters: {0}"), FText::FromName(BoneName))), FFbxErrors::Animation_InvalidData);
continue;
}
Controller.AddBoneCurve(BoneName, bShouldTransact);
Controller.SetBoneTrackKeys(BoneName, RawTracks[SourceTrackIdx].PosKeys, RawTracks[SourceTrackIdx].RotKeys, RawTracks[SourceTrackIdx].ScaleKeys, bShouldTransact);
}
else if (SourceTrackKeyNum > 0) // add transform attribute
{
FbxNode* TargetBoneLink = LinkParent;
while (TargetBoneLink != nullptr && !IsUnrealBone(TargetBoneLink))
{
TargetBoneLink = TargetBoneLink->GetParent();
}
if (TargetBoneLink)
{
int32 TargetBoneTrackIndex = AnimImportSettings.SortedLinks.Find(TargetBoneLink);
if (TargetBoneTrackIndex != INDEX_NONE)
{
FName TargetBoneName = AnimImportSettings.FbxRawBoneNames[TargetBoneTrackIndex];
if (RefSkeleton.FindBoneIndex(TargetBoneName) != INDEX_NONE)
{
FAnimationAttributeIdentifier AttributeIdentifier = UAnimationAttributeIdentifierExtensions::CreateAttributeIdentifier(DestSeq, FName(BoneName), TargetBoneName, FTransformAnimationAttribute::StaticStruct());
if (AttributeIdentifier.IsValid())
{
// remove any existing attribute with the same identifier
if (const IAnimationDataModel* Model = Controller.GetModel())
{
if (Model->FindAttribute(AttributeIdentifier))
{
Controller.RemoveAttribute(AttributeIdentifier, bShouldTransact);
}
}
// pack the separate rot/pos/scale key arrays into a single array
TArray<FTransform> TransformValues;
TransformValues.Reserve(SourceTrackKeyNum);
for (int32 KeyIndex = 0; KeyIndex < SourceTrackKeyNum; ++KeyIndex)
{
const FQuat Q(RawTracks[SourceTrackIdx].RotKeys[KeyIndex]);
const FVector T(RawTracks[SourceTrackIdx].PosKeys[KeyIndex]);
const FVector S(RawTracks[SourceTrackIdx].ScaleKeys[KeyIndex]);
TransformValues.Add(FTransform(Q, T, S));
}
// reduce keys for the common case where all of the keys have the same values
bool bReduceKeys = true;
for (int32 KeyIndex = 1; KeyIndex < SourceTrackKeyNum; ++KeyIndex)
{
if (!TransformValues[KeyIndex].Equals(TransformValues[0]))
{
bReduceKeys = false;
break;
}
}
// create the attribute and add the transform keys
const int32 NumAttributeKeys = (bReduceKeys) ? 1 : SourceTrackKeyNum;
UE::Anim::AddTypedCustomAttribute<FTransformAnimationAttribute, FTransform>(FName(BoneName), TargetBoneName, DestSeq, MakeArrayView(TimeKeys[SourceTrackIdx].GetData(), NumAttributeKeys), MakeArrayView(TransformValues.GetData(), NumAttributeKeys), bShouldTransact);
}
}
}
}
}
}
}
#undef LOCTEXT_NAMESPACE