Files
UnrealEngine/Engine/Plugins/Interchange/Runtime/Source/Pipelines/Private/InterchangeGenericAnimationPipeline.cpp
2025-05-18 13:04:45 +08:00

1021 lines
41 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "InterchangeGenericAnimationPipeline.h"
#include "Animation/AnimationSettings.h"
#include "Animation/AnimSequence.h"
#include "CoreMinimal.h"
#include "Engine/SkeletalMesh.h"
#include "Engine/StaticMesh.h"
#include "InterchangeGenericAssetsPipeline.h"
#include "InterchangeGenericMeshPipeline.h"
#include "InterchangeLevelSequenceFactoryNode.h"
#include "InterchangeAnimationTrackSetNode.h"
#include "InterchangeAnimSequenceFactoryNode.h"
#include "InterchangeHelper.h"
#include "InterchangeMeshNode.h"
#include "InterchangePipelineLog.h"
#include "InterchangePipelineObjectVersion.h"
#include "InterchangeSceneNode.h"
#include "InterchangeSkeletalMeshFactoryNode.h"
#include "InterchangeSkeletalMeshLodDataNode.h"
#include "InterchangeSkeletonFactoryNode.h"
#include "InterchangeSkeletonHelper.h"
#include "InterchangeSourceData.h"
#include "LevelSequence.h"
#include "Nodes/InterchangeBaseNode.h"
#include "Nodes/InterchangeBaseNodeContainer.h"
#include "Nodes/InterchangeSourceNode.h"
#include "Nodes/InterchangeUserDefinedAttribute.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(InterchangeGenericAnimationPipeline)
namespace UE::Interchange::Private
{
const FString ConvertedFromRigidAnimationPrefixIdentifier = TEXT("\\SkeletalAnimation\\ConvertedFromRigidAnimation\\");
bool IsTranslatedDataContainOnlyJointAnimation(const UInterchangeBaseNodeContainer* InBaseNodeContainer, bool bConvertStaticsWithMorphTargetsToSkeletals)
{
//Its valid to call GetMeshesInformationFromTranslatedData with a null container
if (!InBaseNodeContainer)
{
return false;
}
bool bContainOnlyJointAnimation = false;
bool bContainJointAnimation = false;
InBaseNodeContainer->BreakableIterateNodesOfType<UInterchangeSkeletalAnimationTrackNode>([&bContainJointAnimation](const FString& NodeUid, UInterchangeSkeletalAnimationTrackNode* AnimationNode)
{
bContainJointAnimation = true;
return bContainJointAnimation;
});
if (bContainJointAnimation)
{
//if we have bone animation and no skinned mesh, we want to import animation only.
bool bContainSkinnedMeshNode = false;
InBaseNodeContainer->BreakableIterateNodesOfType<UInterchangeMeshNode>([&bContainSkinnedMeshNode, &bConvertStaticsWithMorphTargetsToSkeletals](const FString& NodeUid, UInterchangeMeshNode* MeshNode)
{
if (!MeshNode->IsMorphTarget())
{
if (MeshNode->IsSkinnedMesh())
{
bContainSkinnedMeshNode = true;
}
}
else
{
if (bConvertStaticsWithMorphTargetsToSkeletals)
{
bContainSkinnedMeshNode = true;
}
}
return bContainSkinnedMeshNode;
});
bContainOnlyJointAnimation = !bContainSkinnedMeshNode;
}
return bContainOnlyJointAnimation;
}
//Legacy fbx has a very particular way for naming animation sequences.
void RenameAnimSequenceLikeLegacyFbx(FString& DisplayString, UInterchangeSkeletalAnimationTrackNode& TrackNode, const TArray<const UInterchangeSourceData*>& SourceDatas, UInterchangePipelineBase* OuterMostPipeline, UInterchangeBaseNodeContainer* NodeContainer)
{
if(!SourceDatas.IsValidIndex(0))
{
return;
}
bool bRenameAnimSequenceLegacyFbxWay = false;
TrackNode.GetBooleanAttribute(TEXT("RenameLikeLegacyFbx"), bRenameAnimSequenceLegacyFbxWay);
if (!bRenameAnimSequenceLegacyFbxWay)
{
return;
}
FString SkeletonNodeUid;
TrackNode.GetCustomSkeletonNodeUid(SkeletonNodeUid);
FString BaseFilename = FPaths::GetBaseFilename(SourceDatas[0]->GetFilename());
FString MeshName;
if (UInterchangeGenericAssetsPipeline* GenericAssetPipeline = Cast<UInterchangeGenericAssetsPipeline>(OuterMostPipeline))
{
if (!GenericAssetPipeline->bUseSourceNameForAsset)
{
NodeContainer->BreakableIterateNodesOfType<UInterchangeMeshNode>([&SkeletonNodeUid, &MeshName, &NodeContainer](const FString& NodeUid, UInterchangeMeshNode* MeshNode)
{
if (MeshNode)
{
TArray<FString> SkeletonDependencies;
MeshNode->GetSkeletonDependencies(SkeletonDependencies);
if(SkeletonDependencies.IsValidIndex(0))
{
if(SkeletonDependencies.Contains(SkeletonNodeUid) || NodeContainer->GetIsAncestor(SkeletonDependencies[0], SkeletonNodeUid))
{
int32 MeshReferenceCount = MeshNode->GetSceneInstanceUidsCount();
if (MeshReferenceCount == 1)
{
FString SceneNodeUid;
MeshNode->GetSceneInstanceUid(0, SceneNodeUid);
if (const UInterchangeSceneNode* SceneNode = Cast<UInterchangeSceneNode>(NodeContainer->GetNode(SceneNodeUid)))
{
MeshName = TEXT("_") + SceneNode->GetDisplayLabel();
return true;
}
}
MeshName = TEXT("_") + MeshNode->GetDisplayLabel();
return true;
}
}
}
return false;
});
}
}
DisplayString = BaseFilename + MeshName + TEXT("_Anim");
int32 AnimationCount = 0;
NodeContainer->BreakableIterateNodesOfType<UInterchangeSkeletalAnimationTrackNode>([&AnimationCount](const FString&, UInterchangeSkeletalAnimationTrackNode*) { return (AnimationCount++ > 0); });
if (AnimationCount > 1)
{
DisplayString += TEXT("_") + TrackNode.GetDisplayLabel();
}
constexpr bool bIsJointFalse = false;
UE::Interchange::SanitizeName(DisplayString, bIsJointFalse);
}
//Check Compatibility: (as in does the animation target the skeleton at all)
bool DoesSkeletalAnimationTargetSkeleton(const UInterchangeBaseNodeContainer* BaseNodeContainer, const UInterchangeSkeletalAnimationTrackNode& TrackNode, USkeleton* Skeleton)
{
//Check if the animation is targeting any of the joints on said skeleton:
TMap<FString, FString> SceneNodeAnimationPayloadKeyUids;
TMap<FString, uint8> SceneNodeAnimationPayloadKeyTypes;
TrackNode.GetSceneNodeAnimationPayloadKeys(SceneNodeAnimationPayloadKeyUids, SceneNodeAnimationPayloadKeyTypes);
TSet<FString> SceneNodeUIDs;
SceneNodeAnimationPayloadKeyUids.GetKeys(SceneNodeUIDs);
const FReferenceSkeleton& SkeletonRef = Skeleton->GetReferenceSkeleton();
bool bSkeletonCompatible = false;
for (const FString& SceneNodeUID : SceneNodeUIDs)
{
if (const UInterchangeSceneNode* SceneNode = Cast<UInterchangeSceneNode>(BaseNodeContainer->GetNode(SceneNodeUID)))
{
FString DisplayName = SceneNode->GetDisplayLabel();
int32 BoneIndex = SkeletonRef.FindBoneIndex(*DisplayName);
if (BoneIndex != INDEX_NONE)
{
bSkeletonCompatible = true;
break;
}
}
}
if (!bSkeletonCompatible)
{
USkeletalMesh* PreviewMesh = Skeleton->GetPreviewMesh();
const TArray<TObjectPtr<UMorphTarget>>& SkeletalMeshMorphTargets = PreviewMesh->GetMorphTargets();
TSet<FString> MorphTargetNames;
for (const TObjectPtr<UMorphTarget>& MorphTarget : SkeletalMeshMorphTargets)
{
if (MorphTarget)
{
MorphTargetNames.Add(MorphTarget->GetName());
}
}
TMap<FString, FString> MorphTargetNodeAnimationPayloadKeyUids;
TMap<FString, uint8> MorphTargetNodeAnimationPayloadKeyTypes;
TrackNode.GetMorphTargetNodeAnimationPayloadKeys(MorphTargetNodeAnimationPayloadKeyUids, MorphTargetNodeAnimationPayloadKeyTypes);
TSet<FString> MorphTargetNodeUIDs;
MorphTargetNodeAnimationPayloadKeyUids.GetKeys(MorphTargetNodeUIDs);
for (const FString& MorphTargetNodeUID : MorphTargetNodeUIDs)
{
if (const UInterchangeMeshNode* MeshNode = Cast<UInterchangeMeshNode>(BaseNodeContainer->GetNode(MorphTargetNodeUID)))
{
FString MorphTargetName = MeshNode->GetDisplayLabel();
if (MorphTargetNames.Contains(MorphTargetName))
{
bSkeletonCompatible = true;
break;
}
}
}
if (!bSkeletonCompatible)
{
return false;
}
}
return true;
}
}
void UInterchangeGenericAnimationPipeline::Serialize(FArchive& Ar)
{
Ar.UsingCustomVersion(FFortniteMainInterchangePipelineObjectVersion::GUID);
Super::Serialize(Ar);
//If we load an old pipeline, we must transfer the value of the bAddCurveMetadataToSkeleton hold by
if (Ar.IsLoading() && Ar.CustomVer(FFortniteMainInterchangePipelineObjectVersion::GUID) < FFortniteMainInterchangePipelineObjectVersion::InterchangeAddCurveMetadataToSkeletonPropertyMove)
{
if (ensure(CommonSkeletalMeshesAndAnimationsProperties.IsValid()))
{
CommonSkeletalMeshesAndAnimationsProperties->bAddCurveMetadataToSkeleton = bAddCurveMetadataToSkeleton_DEPRECATED;
}
}
}
FString UInterchangeGenericAnimationPipeline::GetPipelineCategory(UClass* AssetClass)
{
return TEXT("Animations");
}
#if WITH_EDITOR
bool UInterchangeGenericAnimationPipeline::CanEditChange(const FProperty* InProperty) const
{
// If other logic prevents editing, we want to respect that
const bool ParentVal = Super::CanEditChange(InProperty);
// Can we edit flower color?
if (InProperty->GetFName() == GET_MEMBER_NAME_CHECKED(UInterchangeGenericAnimationPipeline, FrameImportRange))
{
return ParentVal && bImportAnimations && bImportBoneTracks && AnimationRange == EInterchangeAnimationRange::SetRange;
}
return ParentVal;
}
#endif
void UInterchangeGenericAnimationPipeline::AdjustSettingsForContext(const FInterchangePipelineContextParams& ContextParams)
{
Super::AdjustSettingsForContext(ContextParams);
#if WITH_EDITOR
check(CommonSkeletalMeshesAndAnimationsProperties.IsValid());
bSceneImport = ContextParams.ContextType == EInterchangePipelineContext::SceneImport
|| ContextParams.ContextType == EInterchangePipelineContext::SceneReimport;
if (ContextParams.ContextType == EInterchangePipelineContext::AssetCustomLODImport
|| ContextParams.ContextType == EInterchangePipelineContext::AssetCustomLODReimport
|| ContextParams.ContextType == EInterchangePipelineContext::AssetAlternateSkinningImport
|| ContextParams.ContextType == EInterchangePipelineContext::AssetAlternateSkinningReimport
|| ContextParams.ContextType == EInterchangePipelineContext::AssetCustomMorphTargetImport
|| ContextParams.ContextType == EInterchangePipelineContext::AssetCustomMorphTargetReImport)
{
bImportAnimations = false;
CommonSkeletalMeshesAndAnimationsProperties->Skeleton = nullptr;
CommonSkeletalMeshesAndAnimationsProperties->bImportOnlyAnimations = false;
}
const FString CommonMeshesCategory = UInterchangeGenericCommonMeshesProperties::GetPipelineCategory(nullptr);
const FString StaticMeshesCategory = UInterchangeGenericMeshPipeline::GetPipelineCategory(UStaticMesh::StaticClass());
const FString SkeletalMeshesCategory = UInterchangeGenericMeshPipeline::GetPipelineCategory(USkeletalMesh::StaticClass());
const FString AnimationCategory = UInterchangeGenericAnimationPipeline::GetPipelineCategory(nullptr);
TArray<FString> HideCategories;
if(ContextParams.ContextType == EInterchangePipelineContext::AssetImport)
{
if(UE::Interchange::Private::IsTranslatedDataContainOnlyJointAnimation(ContextParams.BaseNodeContainer, CommonSkeletalMeshesAndAnimationsProperties->bConvertStaticsWithMorphTargetsToSkeletals))
{
bImportAnimations = true;
CommonSkeletalMeshesAndAnimationsProperties->bImportOnlyAnimations = true;
HideCategories.Add(StaticMeshesCategory);
HideCategories.Add(SkeletalMeshesCategory);
HideCategories.Add(CommonMeshesCategory);
}
}
if (ContextParams.ContextType == EInterchangePipelineContext::AssetReimport)
{
if (UAnimSequence* AnimSequence = Cast<UAnimSequence>(ContextParams.ReimportAsset))
{
//Set the skeleton to the current asset skeleton and re-import only the animation
CommonSkeletalMeshesAndAnimationsProperties->Skeleton = AnimSequence->GetSkeleton();
CommonSkeletalMeshesAndAnimationsProperties->bImportOnlyAnimations = true;
bImportAnimations = true;
}
else
{
HideCategories.Add(AnimationCategory);
}
}
if (CommonSkeletalMeshesAndAnimationsProperties->bImportOnlyAnimations)
{
bImportAnimations = true;
}
if (UInterchangePipelineBase* OuterMostPipeline = GetMostPipelineOuter())
{
for (const FString& HideCategoryName : HideCategories)
{
HidePropertiesOfCategory(OuterMostPipeline, this, HideCategoryName);
}
}
#endif //WITH_EDITOR
}
#if WITH_EDITOR
bool UInterchangeGenericAnimationPipeline::IsPropertyChangeNeedRefresh(const FPropertyChangedEvent& PropertyChangedEvent) const
{
if (PropertyChangedEvent.GetPropertyName() == GET_MEMBER_NAME_CHECKED(UInterchangeGenericAnimationPipeline, bImportAnimations))
{
return true;
}
return Super::IsPropertyChangeNeedRefresh(PropertyChangedEvent);
}
void UInterchangeGenericAnimationPipeline::GetSupportAssetClasses(TArray<UClass*>& PipelineSupportAssetClasses) const
{
PipelineSupportAssetClasses.Add(UAnimSequence::StaticClass());
PipelineSupportAssetClasses.Add(ULevelSequence::StaticClass());
}
#endif //WITH_EDITOR
void UInterchangeGenericAnimationPipeline::ExecutePipeline(UInterchangeBaseNodeContainer* InBaseNodeContainer, const TArray<UInterchangeSourceData*>& InSourceDatas, const FString& ContentBasePath)
{
if (!InBaseNodeContainer)
{
UE_LOG(LogInterchangePipeline, Warning, TEXT("UInterchangeGenericAnimationPipeline: Cannot execute pre-import pipeline because InBaseNodeContainer is null."));
return;
}
BaseNodeContainer = InBaseNodeContainer;
if (CommonSkeletalMeshesAndAnimationsProperties->bImportOnlyAnimations)
{
bImportAnimations = true;
}
if (!bImportAnimations)
{
//Nothing to import
return;
}
TArray<UInterchangeAnimationTrackSetNode*> TrackSetNodes;
BaseNodeContainer->IterateNodesOfType<UInterchangeAnimationTrackSetNode>([&](const FString& NodeUid, UInterchangeAnimationTrackSetNode* Node)
{
TrackSetNodes.Add(Node);
});
//Create AnimSequences(UInterchangeSkeletalAnimationTrackNode) for Mesh Instances having MorphTargetCurveWeights:
{
TArray<UInterchangeSceneNode*> SceneNodesWithMorphTargetCurveWeights;
BaseNodeContainer->IterateNodesOfType<UInterchangeSceneNode>([&SceneNodesWithMorphTargetCurveWeights](const FString& NodeUid, UInterchangeSceneNode* SceneNode)
{
TMap<FString, float> MorphTargetCurveWeights;
SceneNode->GetMorphTargetCurveWeights(MorphTargetCurveWeights);
bool CreateAnimSequence = false;
for (const TTuple<FString, float>& MorphTargetCurveWeight : MorphTargetCurveWeights)
{
if (MorphTargetCurveWeight.Value != 0)
{
CreateAnimSequence = true;
break;
}
}
if (CreateAnimSequence)
{
SceneNodesWithMorphTargetCurveWeights.Add(SceneNode);
}
});
for (UInterchangeSceneNode* SceneNode : SceneNodesWithMorphTargetCurveWeights)
{
UInterchangeSkeletalAnimationTrackNode* SkeletalAnimationNode = NewObject< UInterchangeSkeletalAnimationTrackNode >(BaseNodeContainer);
FString SkeletalAnimationNodeUid = UE::Interchange::Private::ConvertedFromRigidAnimationPrefixIdentifier + SceneNode->GetUniqueID();
BaseNodeContainer->SetupNode(SkeletalAnimationNode, SkeletalAnimationNodeUid, SceneNode->GetDisplayLabel(), EInterchangeNodeContainerType::TranslatedAsset);
SkeletalAnimationNode->SetCustomAnimationSampleRate(30.f);
SkeletalAnimationNode->SetCustomAnimationStartTime(0);
SkeletalAnimationNode->SetCustomAnimationStopTime(1.f / 30.f); //we want a single frame
SkeletalAnimationNode->SetCustomSkeletonNodeUid(SceneNode->GetUniqueID());
TMap<FString, float> MorphTargetCurveWeights;
SceneNode->GetMorphTargetCurveWeights(MorphTargetCurveWeights);
for (const TPair<FString, float>& MorphTargetCurveWeight : MorphTargetCurveWeights)
{
FString PayloadUid = MorphTargetCurveWeight.Key + TEXT(":") + LexToString(MorphTargetCurveWeight.Value);
//add the payload key:
SkeletalAnimationNode->SetAnimationPayloadKeyForMorphTargetNodeUid(MorphTargetCurveWeight.Key, PayloadUid, EInterchangeAnimationPayLoadType::MORPHTARGETCURVEWEIGHTINSTANCE);
}
SceneNode->SetCustomAnimationAssetUidToPlay(SkeletalAnimationNodeUid);
}
}
if (!bSceneImport)
{
//Extract any skeleton node use by the skeletal mesh animation track
TArray<FString> SceneNodesUsedBySkeleton;
BaseNodeContainer->IterateNodesOfType<UInterchangeSkeletalAnimationTrackNode>([&](const FString& NodeUid, UInterchangeSkeletalAnimationTrackNode* Node)
{
if (Node)
{
FString SkeletonNodeUid;
if (Node->GetCustomSkeletonNodeUid(SkeletonNodeUid))
{
SceneNodesUsedBySkeleton.Add(SkeletonNodeUid);
BaseNodeContainer->IterateNodeChildren(SkeletonNodeUid, [&SceneNodesUsedBySkeleton](const UInterchangeBaseNode* Node)
{
if (const UInterchangeSceneNode* SceneNode = Cast<UInterchangeSceneNode>(Node))
{
SceneNodesUsedBySkeleton.Add(Node->GetUniqueID());
}
});
}
}
});
auto IsTrackOverrideBySkeletalMeshAnimation = [this, &SceneNodesUsedBySkeleton](TArray<FString>& AnimationTrackUids)
{
//Skip track node that is using one or more scene node used by any skeletal mesh skeleton.
bool bSomeTrackNodeUsedBySkeletalMesh = false;
for (const FString& AnimationTrackUid : AnimationTrackUids)
{
if (const UInterchangeTransformAnimationTrackNode* TransformTrackNode = Cast<UInterchangeTransformAnimationTrackNode>(BaseNodeContainer->GetNode(AnimationTrackUid)))
{
FString ActorNodeUid;
if (TransformTrackNode->GetCustomActorDependencyUid(ActorNodeUid))
{
if (SceneNodesUsedBySkeleton.Contains(ActorNodeUid))
{
bSomeTrackNodeUsedBySkeletalMesh = true;
break;
}
}
}
}
return bSomeTrackNodeUsedBySkeletalMesh;
};
//Support rigid mesh animation animation data (UAnimSequence for rigid mesh)
for (UInterchangeAnimationTrackSetNode* TrackSetNode : TrackSetNodes)
{
if (!TrackSetNode)
{
continue;
}
TArray<FString> AnimationTrackUids;
TrackSetNode->GetCustomAnimationTrackUids(AnimationTrackUids);
if (IsTrackOverrideBySkeletalMeshAnimation(AnimationTrackUids))
{
continue;
}
UInterchangeSkeletalAnimationTrackNode* SkeletalAnimationNode = NewObject< UInterchangeSkeletalAnimationTrackNode >(BaseNodeContainer);
FString SkeletalAnimationNodeUid = UE::Interchange::Private::ConvertedFromRigidAnimationPrefixIdentifier + TrackSetNode->GetUniqueID();
BaseNodeContainer->SetupNode(SkeletalAnimationNode, SkeletalAnimationNodeUid, TrackSetNode->GetDisplayLabel(), EInterchangeNodeContainerType::TranslatedAsset);
bool bCustomSkeletonNodeUidSet = false;
float CustomFrameRate;
if (!TrackSetNode->GetCustomFrameRate(CustomFrameRate))
{
CustomFrameRate = 30.0f;
}
SkeletalAnimationNode->SetCustomAnimationSampleRate(CustomFrameRate);
SkeletalAnimationNode->SetCustomAnimationStartTime(0);
//stop time will be calculated once we get the curves from the Translators.
//However to avoid error reports we set stoptime for 1 subframe.
SkeletalAnimationNode->SetCustomAnimationStopTime(1./CustomFrameRate);
for (const FString& AnimationTrackUid : AnimationTrackUids)
{
if (const UInterchangeTransformAnimationTrackNode* TransformTrackNode = Cast<UInterchangeTransformAnimationTrackNode>(BaseNodeContainer->GetNode(AnimationTrackUid)))
{
FString ActorNodeUid;
if (TransformTrackNode->GetCustomActorDependencyUid(ActorNodeUid))
{
if (const UInterchangeSceneNode* ActorNode = Cast<UInterchangeSceneNode>(BaseNodeContainer->GetNode(ActorNodeUid)))
{
FInterchangeAnimationPayLoadKey AnimationPayLoadKey;
if (TransformTrackNode->GetCustomAnimationPayloadKey(AnimationPayLoadKey))
{
if (!bCustomSkeletonNodeUidSet)
{
FString SkeletonRootUid;
FString LastSceneNode = ActorNodeUid;
if (const UInterchangeSceneNode* SceneNode = ActorNode)
{
FString ParentUid = SceneNode->GetParentUid();
while (!ParentUid.Equals(UInterchangeBaseNode::InvalidNodeUid()))
{
if (const UInterchangeSceneNode* ParentNode = Cast<UInterchangeSceneNode>(BaseNodeContainer->GetNode(ParentUid)))
{
if(ParentNode->IsSpecializedTypeContains(UE::Interchange::FSceneNodeStaticData::GetJointSpecializeTypeString()))
{
SkeletonRootUid = ParentUid;
}
LastSceneNode = ParentUid;
ParentUid = ParentNode->GetParentUid();
}
}
if (SkeletonRootUid.IsEmpty())
{
SkeletonRootUid = LastSceneNode;
}
}
SkeletalAnimationNode->SetCustomSkeletonNodeUid(SkeletonRootUid);
bCustomSkeletonNodeUidSet = true;
}
//add the payload key:
SkeletalAnimationNode->SetAnimationPayloadKeyForSceneNodeUid(ActorNode->GetUniqueID(), AnimationPayLoadKey.UniqueId, AnimationPayLoadKey.Type);
}
}
}
}
}
if (!bCustomSkeletonNodeUidSet)
{
//if no SkeletonNodeUid can be set, then the conversion failed and should not be added to the BaseNodeContainer.
BaseNodeContainer->RemoveNode(SkeletalAnimationNode->GetUniqueID());
}
}
}
else
{
//Support scene node animation (ULevelSequence, only supported when doing scene import)
for (UInterchangeAnimationTrackSetNode* TrackSetNode : TrackSetNodes)
{
if (TrackSetNode)
{
CreateLevelSequenceFactoryNode(*TrackSetNode);
}
}
}
if (!CommonSkeletalMeshesAndAnimationsProperties.IsValid())
{
return;
}
if (CommonSkeletalMeshesAndAnimationsProperties->bImportOnlyAnimations && !CommonSkeletalMeshesAndAnimationsProperties->Skeleton.IsValid())
{
UE_LOG(LogInterchangePipeline, Warning, TEXT("UInterchangeGenericAnimationPipeline: Cannot execute pre-import pipeline because importing animation only requires a valid skeleton."));
return;
}
SourceDatas.Empty(InSourceDatas.Num());
for (const UInterchangeSourceData* SourceData : InSourceDatas)
{
SourceDatas.Add(SourceData);
}
TArray<UInterchangeSkeletalAnimationTrackNode*> TrackNodes;
BaseNodeContainer->IterateNodesOfType<UInterchangeSkeletalAnimationTrackNode>([&](const FString& NodeUid, UInterchangeSkeletalAnimationTrackNode* Node)
{
TrackNodes.Add(Node);
});
//Support skeletal mesh animation (UAnimSequence)
for (UInterchangeSkeletalAnimationTrackNode* TrackNode : TrackNodes)
{
if (TrackNode)
{
CreateAnimSequenceFactoryNode(*TrackNode);
}
}
}
void UInterchangeGenericAnimationPipeline::CreateLevelSequenceFactoryNode(UInterchangeAnimationTrackSetNode& TranslatedNode)
{
const FString FactoryNodeUid = UInterchangeFactoryBaseNode::BuildFactoryNodeUid(TranslatedNode.GetUniqueID());
UInterchangeLevelSequenceFactoryNode* FactoryNode = NewObject<UInterchangeLevelSequenceFactoryNode>(BaseNodeContainer, NAME_None);
BaseNodeContainer->SetupNode(FactoryNode, FactoryNodeUid, TranslatedNode.GetDisplayLabel(), EInterchangeNodeContainerType::FactoryData);
FactoryNode->SetEnabled(true);
TArray<FString> AnimationTrackUids;
TranslatedNode.GetCustomAnimationTrackUids(AnimationTrackUids);
for (const FString& AnimationTrackUid : AnimationTrackUids)
{
FactoryNode->AddCustomAnimationTrackUid(AnimationTrackUid);
// Update factory's dependencies
if (const UInterchangeAnimationTrackBaseNode* TrackNode = Cast<UInterchangeAnimationTrackBaseNode>(BaseNodeContainer->GetNode(AnimationTrackUid)))
{
if (const UInterchangeAnimationTrackNode* AnimationTrackNode = Cast<UInterchangeAnimationTrackNode>(TrackNode))
{
FString ActorNodeUid;
if (AnimationTrackNode->GetCustomActorDependencyUid(ActorNodeUid))
{
const FString ActorFactoryNodeUid = UInterchangeFactoryBaseNode::BuildFactoryNodeUid(ActorNodeUid);
FactoryNode->AddFactoryDependencyUid(ActorFactoryNodeUid);
}
}
else if (const UInterchangeAnimationTrackSetInstanceNode* InstanceTrackNode = Cast<UInterchangeAnimationTrackSetInstanceNode>(TrackNode))
{
FString TrackSetNodeUid;
if (InstanceTrackNode->GetCustomTrackSetDependencyUid(TrackSetNodeUid))
{
const FString TrackSetFactoryNodeUid = UInterchangeFactoryBaseNode::BuildFactoryNodeUid(TrackSetNodeUid);
FactoryNode->AddFactoryDependencyUid(TrackSetFactoryNodeUid);
}
}
}
}
float FrameRate;
if (TranslatedNode.GetCustomFrameRate(FrameRate))
{
FactoryNode->SetCustomFrameRate(FrameRate);
}
UInterchangeUserDefinedAttributesAPI::DuplicateAllUserDefinedAttribute(&TranslatedNode, FactoryNode, false);
FactoryNode->AddTargetNodeUid(TranslatedNode.GetUniqueID());
TranslatedNode.AddTargetNodeUid(FactoryNode->GetUniqueID());
}
void UInterchangeGenericAnimationPipeline::CreateAnimSequenceFactoryNode(UInterchangeSkeletalAnimationTrackNode& TrackNode)
{
FString SkeletonNodeUid;
if (!ensure(TrackNode.GetCustomSkeletonNodeUid(SkeletonNodeUid)))
{
// TODO: Warn something wrong happened
return;
}
const bool bImportOnlyAnimation = CommonSkeletalMeshesAndAnimationsProperties->bImportOnlyAnimations;
const bool bAddCurveMetadataToSkeleton = CommonSkeletalMeshesAndAnimationsProperties->bAddCurveMetadataToSkeleton;
const UInterchangeSkeletonFactoryNode* SkeletonFactoryNode = nullptr;
const UInterchangeSkeletalMeshFactoryNode* SkeletalMeshFactoryNode = nullptr;
const FString SkeletonFactoryNodeUid = UInterchangeFactoryBaseNode::BuildFactoryNodeUid(SkeletonNodeUid);
SkeletonFactoryNode = Cast<const UInterchangeSkeletonFactoryNode>(BaseNodeContainer->GetFactoryNode(SkeletonFactoryNodeUid));
const bool bRigidAnimationConverted = TrackNode.GetUniqueID().StartsWith(UE::Interchange::Private::ConvertedFromRigidAnimationPrefixIdentifier);
//If we import anim only and we do not have meshes and skeleton. We need to create a skeleton factory node
//base on the specified skeleton
bool bSkeletonCompatible = true;
if (bImportOnlyAnimation && !SkeletonFactoryNode && CommonSkeletalMeshesAndAnimationsProperties->Skeleton.IsValid())
{
TWeakObjectPtr<USkeleton> Skeleton = CommonSkeletalMeshesAndAnimationsProperties->Skeleton;
TPair<int32, FString> SkeletonRootNodeUidAndBoneIndex = TPair<int32, FString>(INDEX_NONE, FString());
BaseNodeContainer->IterateNodesOfType<UInterchangeSceneNode>([&SkeletonRootNodeUidAndBoneIndex, BaseNodeContainerClosure = BaseNodeContainer, Skeleton, bRigidAnimationConverted](const FString& NodeUid, UInterchangeSceneNode* Node)
{
//In case the AnimationTrackNode is created from a RigidAnimation conversion the SceneNodes won't have Joint Specialization:
if (Skeleton.IsValid() && (Node->IsSpecializedTypeContains(UE::Interchange::FSceneNodeStaticData::GetJointSpecializeTypeString()) || bRigidAnimationConverted))
{
const FReferenceSkeleton& ReferenceSkeleton = Skeleton->GetReferenceSkeleton();
int32 RefBoneIndex = ReferenceSkeleton.FindBoneIndex(FName(*Node->GetDisplayLabel()));
if(RefBoneIndex != INDEX_NONE)
{
if (SkeletonRootNodeUidAndBoneIndex.Key == INDEX_NONE || RefBoneIndex < SkeletonRootNodeUidAndBoneIndex.Key)
{
SkeletonRootNodeUidAndBoneIndex = TPair<int32, FString>(RefBoneIndex, NodeUid);
}
}
}
});
FString SkeletonRootUid;
//Use the lower uid we found
if (SkeletonRootNodeUidAndBoneIndex.Key != INDEX_NONE && !SkeletonRootNodeUidAndBoneIndex.Value.IsEmpty())
{
SkeletonRootUid = SkeletonRootNodeUidAndBoneIndex.Value;
}
if(!SkeletonRootUid.IsEmpty())
{
//Create a skeleton node from all the joint in the translated nodes
SkeletonFactoryNode = CommonSkeletalMeshesAndAnimationsProperties->CreateSkeletonFactoryNode(BaseNodeContainer, SkeletonRootUid);
}
bSkeletonCompatible = UE::Interchange::Private::DoesSkeletalAnimationTargetSkeleton(BaseNodeContainer, TrackNode, CommonSkeletalMeshesAndAnimationsProperties->Skeleton.Get());
if (!bSkeletonCompatible)
{
UInterchangeResultDisplay_Generic* Message = AddMessage<UInterchangeResultDisplay_Generic>();
Message->Text = FText::Format(NSLOCTEXT("UInterchangeGenericAnimationPipeline", "IncompatibleSkeleton", "Incompatible skeleton {0} when importing AnimSequence {1}."),
FText::FromString(CommonSkeletalMeshesAndAnimationsProperties->Skeleton->GetName()),
FText::FromString(TrackNode.GetDisplayLabel()));
return;
}
}
if (!SkeletonFactoryNode)
{
// It can happen if we force static mesh import, in that case no skeleton will be create
return;
}
FString SkeletalMeshFactoryNodeUid;
if (SkeletonFactoryNode->GetCustomSkeletalMeshFactoryNodeUid(SkeletalMeshFactoryNodeUid))
{
SkeletalMeshFactoryNode = Cast<const UInterchangeSkeletalMeshFactoryNode>(BaseNodeContainer->GetFactoryNode(SkeletalMeshFactoryNodeUid));
}
double SampleRate = 30.;
double StartTime = 0.;
double StopTime = 0.;
bool bTimeRangeIsValid = false;
if (bImportBoneTracks)
{
int32 Numerator, Denominator;
const UInterchangeSourceNode* SourceNode = UInterchangeSourceNode::GetUniqueInstance(BaseNodeContainer);
//Get the sample rate from the options and the data
if (!bUse30HzToBakeBoneAnimation)
{
if (CustomBoneAnimationSampleRate > 0)
{
SampleRate = static_cast<double>(CustomBoneAnimationSampleRate);
}
else if (SourceNode && SourceNode->GetCustomSourceFrameRateNumerator(Numerator) && SourceNode->GetCustomSourceFrameRateDenominator(Denominator) && Denominator > 0 && Numerator > 0)
{
SampleRate = static_cast<double>(Numerator) / static_cast<double>(Denominator);
}
else
{
TrackNode.GetCustomAnimationSampleRate(SampleRate);
}
}
//Get the animation start and stop time (range) from the options and the data
//Some format don't fill the animation range data we fall back on the trackNode
if (AnimationRange == EInterchangeAnimationRange::Timeline)
{
bTimeRangeIsValid = TrackNode.GetCustomSourceTimelineAnimationStartTime(StartTime) && TrackNode.GetCustomSourceTimelineAnimationStopTime(StopTime);
}
else if (AnimationRange == EInterchangeAnimationRange::SetRange)
{
StartTime = static_cast<double>(FrameImportRange.Min) / SampleRate;
StopTime = static_cast<double>(FrameImportRange.Max) / SampleRate;
bTimeRangeIsValid = true;
}
//No custom time specified, use the track default
if(!bTimeRangeIsValid)
{
StartTime = 0.;
StopTime = 0.;
bTimeRangeIsValid = TrackNode.GetCustomAnimationStartTime(StartTime);
bTimeRangeIsValid &= TrackNode.GetCustomAnimationStopTime(StopTime);
}
FFrameRate FrameRate = UE::Interchange::Animation::ConvertSampleRatetoFrameRate(SampleRate);
const double SequenceLength = FMath::Max<double>(StopTime - StartTime, MINIMUM_ANIMATION_LENGTH);
const float SubFrame = FrameRate.AsFrameTime(SequenceLength).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 FFrameTime StartFrameTime = FrameRate.AsFrameTime(StartTime);
const FFrameTime StopFrameTime = FrameRate.AsFrameTime(StopTime);
FFrameNumber StartFrameNumber = StartFrameTime.GetFrame().Value, StopFrameNumber = StopFrameTime.GetFrame().Value;
double NewStartTime = StartTime, NewStopTime = StopTime;
if (!FMath::IsNearlyZero(StartFrameTime.GetSubFrame()))
{
StartFrameNumber = StartFrameTime.RoundToFrame();
NewStartTime = FrameRate.AsSeconds(StartFrameNumber);
}
if (!FMath::IsNearlyZero(StopFrameTime.GetSubFrame()))
{
StopFrameNumber = StopFrameTime.RoundToFrame();
NewStopTime = FrameRate.AsSeconds(StopFrameNumber);
}
UInterchangeResultWarning_Generic* Message = AddMessage<UInterchangeResultWarning_Generic>();
Message->SourceAssetName = SourceDatas[0]->GetFilename();
Message->DestinationAssetName = TrackNode.GetDisplayLabel();
Message->AssetType = UAnimSequence::StaticClass();
Message->Text = FText::Format(NSLOCTEXT("UInterchangeGenericAnimationPipeline", "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})"),
FrameRate.ToPrettyText(),
FText::AsNumber(StartTime),
FText::AsNumber(StartFrameTime.AsDecimal()),
FText::AsNumber(StopTime),
FText::AsNumber(StopFrameTime.AsDecimal()),
FText::AsNumber(NewStartTime),
FText::AsNumber(StartFrameNumber.Value),
FText::AsNumber(NewStopTime),
FText::AsNumber(StopFrameNumber.Value));
StartTime = NewStartTime;
StopTime = NewStopTime;
}
else
{
UInterchangeResultError_Generic* Message = AddMessage<UInterchangeResultError_Generic>();
Message->SourceAssetName = SourceDatas[0]->GetFilename();
Message->DestinationAssetName = TrackNode.GetDisplayLabel();
Message->AssetType = UAnimSequence::StaticClass();
Message->Text = FText::Format(NSLOCTEXT("UInterchangeGenericAnimationPipeline", "WrongSequenceLength", "Animation length {0} is not compatible with import frame-rate {1} (sub frame {2}). The animation has to be frame-border aligned if the 'Snap to Closest Frame Boundary' pipeline option is disabled."),
FText::AsNumber(SequenceLength), FrameRate.ToPrettyText(), FText::AsNumber(SubFrame));
//Skip this anim sequence factory node
return;
}
}
}
const FString AnimSequenceUid = TEXT("\\AnimSequence") + TrackNode.GetUniqueID();
UInterchangeAnimSequenceFactoryNode* AnimSequenceFactoryNode = NewObject<UInterchangeAnimSequenceFactoryNode>(BaseNodeContainer, NAME_None);
FString DisplayString = TrackNode.GetDisplayLabel();
UE::Interchange::Private::RenameAnimSequenceLikeLegacyFbx(DisplayString, TrackNode, SourceDatas, GetMostPipelineOuter(), BaseNodeContainer);
AnimSequenceFactoryNode->InitializeAnimSequenceNode(AnimSequenceUid, DisplayString, BaseNodeContainer);
if (SkeletonFactoryNode)
{
AnimSequenceFactoryNode->SetCustomSkeletonFactoryNodeUid(SkeletonFactoryNode->GetUniqueID());
}
if (SkeletalMeshFactoryNode)
{
AnimSequenceFactoryNode->AddFactoryDependencyUid(SkeletalMeshFactoryNode->GetUniqueID());
}
AnimSequenceFactoryNode->SetCustomImportBoneTracks(bImportBoneTracks);
AnimSequenceFactoryNode->SetCustomImportBoneTracksSampleRate(SampleRate);
if (bTimeRangeIsValid)
{
AnimSequenceFactoryNode->SetCustomImportBoneTracksRangeStart(StartTime);
AnimSequenceFactoryNode->SetCustomImportBoneTracksRangeStop(StopTime);
}
AnimSequenceFactoryNode->SetCustomImportAttributeCurves(bImportCustomAttribute);
AnimSequenceFactoryNode->SetCustomAddCurveMetadataToSkeleton(bAddCurveMetadataToSkeleton);
AnimSequenceFactoryNode->SetCustomDoNotImportCurveWithZero(bDoNotImportCurveWithZero);
AnimSequenceFactoryNode->SetCustomRemoveCurveRedundantKeys(bRemoveCurveRedundantKeys);
AnimSequenceFactoryNode->SetCustomDeleteExistingMorphTargetCurves(bDeleteExistingMorphTargetCurves);
AnimSequenceFactoryNode->SetCustomDeleteExistingCustomAttributeCurves(bDeleteExistingCustomAttributeCurves);
AnimSequenceFactoryNode->SetCustomDeleteExistingNonCurveCustomAttributes(bDeleteExistingNonCurveCustomAttributes);
AnimSequenceFactoryNode->SetCustomMaterialDriveParameterOnCustomAttribute(bSetMaterialDriveParameterOnCustomAttribute);
for (const FString& MaterialSuffixe : MaterialCurveSuffixes)
{
AnimSequenceFactoryNode->SetAnimatedMaterialCurveSuffixe(MaterialSuffixe);
}
//USkeleton cannot be created without a valid skeletal mesh
FString SkeletonUid;
if(SkeletonFactoryNode)
{
SkeletonUid = SkeletonFactoryNode->GetUniqueID();
AnimSequenceFactoryNode->AddFactoryDependencyUid(SkeletonUid);
}
FString RootJointUid;
if (SkeletonFactoryNode && SkeletonFactoryNode->GetCustomRootJointUid(RootJointUid))
{
// NOTE: Could this be added as an array of FString attributes on the UInterchangeSkeletalAnimationTrackNode
#if WITH_EDITOR
//Iterate all joints to set the meta data value in the anim sequence factory node
//Note: We shouldn't be able to get to this point without the ChildrenCache already set, so there is no need to call ComputeChildrenCache.
// (Also trying to avoid recreating the ChildrenCache for every AnimSequenceFactoryNode creation call.)
UE::Interchange::Private::FSkeletonHelper::RecursiveAddSkeletonMetaDataValues(BaseNodeContainer, AnimSequenceFactoryNode, RootJointUid);
#endif //WITH_EDITOR
const TArray<FString> CustomAttributeNamesToImport = UAnimationSettings::Get()->GetBoneCustomAttributeNamesToImport();
BaseNodeContainer->IterateNodeChildren(RootJointUid, [&AnimSequenceFactoryNode, &CustomAttributeNamesToImport](const UInterchangeBaseNode* Node)
{
if (const UInterchangeSceneNode* SceneNode = Cast<UInterchangeSceneNode>(Node))
{
using namespace UE::Interchange;
FString BoneName = SceneNode->GetDisplayLabel();
bool bImportAllAttributesOnBone = UAnimationSettings::Get()->BoneNamesWithCustomAttributes.Contains(BoneName);
TArray<FInterchangeUserDefinedAttributeInfo> AttributeInfos;
UInterchangeUserDefinedAttributesAPI::GetUserDefinedAttributeInfos(SceneNode, AttributeInfos);
for (const FInterchangeUserDefinedAttributeInfo& AttributeInfo : AttributeInfos)
{
if (AttributeInfo.PayloadKey.IsSet())
{
bool bDecimalType = AttributeInfo.Type == EAttributeTypes::Float ||
AttributeInfo.Type == EAttributeTypes::Float16 ||
AttributeInfo.Type == EAttributeTypes::Double;
const bool bForceImportBoneCustomAttribute = CustomAttributeNamesToImport.Contains(AttributeInfo.Name);
EInterchangeAnimationPayLoadType AnimationPayloadType = EInterchangeAnimationPayLoadType::NONE;
if (SceneNode->GetAnimationCurveTypeForCurveName(AttributeInfo.Name, AnimationPayloadType))
{
if (AnimationPayloadType == EInterchangeAnimationPayLoadType::CURVE)
{
AnimSequenceFactoryNode->SetAnimatedAttributeCurveName(AttributeInfo.Name);
}
else if (AnimationPayloadType == EInterchangeAnimationPayLoadType::STEPCURVE)
{
AnimSequenceFactoryNode->SetAnimatedAttributeStepCurveName(AttributeInfo.Name);
}
}
else
{
//Material attribute curve
if (!bImportAllAttributesOnBone && bDecimalType && !bForceImportBoneCustomAttribute)
{
AnimSequenceFactoryNode->SetAnimatedAttributeCurveName(AttributeInfo.Name);
}
else if (bForceImportBoneCustomAttribute || bImportAllAttributesOnBone)
{
AnimSequenceFactoryNode->SetAnimatedAttributeStepCurveName(AttributeInfo.Name);
}
}
}
}
}
});
}
//Iterate dependencies
{
TArray<FString> SkeletalMeshNodeUids;
BaseNodeContainer->GetNodes(UInterchangeSkeletalMeshFactoryNode::StaticClass(), SkeletalMeshNodeUids);
for (const FString& SkelMeshFactoryNodeUid : SkeletalMeshNodeUids)
{
if (const UInterchangeSkeletalMeshFactoryNode* ConstSkeletalMeshFactoryNode = Cast<const UInterchangeSkeletalMeshFactoryNode>(BaseNodeContainer->GetFactoryNode(SkelMeshFactoryNodeUid)))
{
TArray<FString> SkeletalMeshDependencies;
ConstSkeletalMeshFactoryNode->GetFactoryDependencies(SkeletalMeshDependencies);
for (const FString& SkeletalMeshDependencyUid : SkeletalMeshDependencies)
{
if (SkeletonUid.Equals(SkeletalMeshDependencyUid))
{
AnimSequenceFactoryNode->AddFactoryDependencyUid(SkelMeshFactoryNodeUid);
break;
}
}
}
}
}
if (CommonSkeletalMeshesAndAnimationsProperties->Skeleton.IsValid())
{
//TODO: support skeleton helper in runtime
#if WITH_EDITOR
bSkeletonCompatible = bSkeletonCompatible && UE::Interchange::Private::FSkeletonHelper::IsCompatibleSkeleton(CommonSkeletalMeshesAndAnimationsProperties->Skeleton.Get()
, RootJointUid
, BaseNodeContainer
, CommonSkeletalMeshesAndAnimationsProperties->bConvertStaticsWithMorphTargetsToSkeletals || CommonMeshesProperties->ForceAllMeshAsType == EInterchangeForceMeshType::IFMT_SkeletalMesh
, false /*bCheckForIdenticalSkeleton*/
, CommonMeshesProperties->bImportSockets);
#endif
if (bSkeletonCompatible)
{
FSoftObjectPath SkeletonSoftObjectPath(CommonSkeletalMeshesAndAnimationsProperties->Skeleton.Get());
AnimSequenceFactoryNode->SetCustomSkeletonSoftObjectPath(SkeletonSoftObjectPath);
}
else
{
UInterchangeResultDisplay_Generic* Message = AddMessage<UInterchangeResultDisplay_Generic>();
Message->Text = FText::Format(NSLOCTEXT("UInterchangeGenericAnimationPipeline", "IncompatibleSkeleton", "Incompatible skeleton {0} when importing AnimSequence {1}."),
FText::FromString(CommonSkeletalMeshesAndAnimationsProperties->Skeleton->GetName()),
FText::FromString(TrackNode.GetDisplayLabel()));
}
}
{
TMap<FString, FString> SceneNodeAnimationPayloadKeyUids;
TMap<FString, uint8> SceneNodeAnimationPayloadKeyTypes;
TrackNode.GetSceneNodeAnimationPayloadKeys(SceneNodeAnimationPayloadKeyUids, SceneNodeAnimationPayloadKeyTypes);
AnimSequenceFactoryNode->SetAnimationPayloadKeysForSceneNodeUids(SceneNodeAnimationPayloadKeyUids, SceneNodeAnimationPayloadKeyTypes);
}
{
TMap<FString, FString> MorphTargetNodeAnimationPayloadKeyUids;
TMap<FString, uint8> MorphTargetNodeAnimationPayloadKeyTypes;
TrackNode.GetMorphTargetNodeAnimationPayloadKeys(MorphTargetNodeAnimationPayloadKeyUids, MorphTargetNodeAnimationPayloadKeyTypes);
AnimSequenceFactoryNode->SetAnimationPayloadKeysForMorphTargetNodeUids(MorphTargetNodeAnimationPayloadKeyUids, MorphTargetNodeAnimationPayloadKeyTypes);
}
UInterchangeUserDefinedAttributesAPI::DuplicateAllUserDefinedAttribute(&TrackNode, AnimSequenceFactoryNode, false);
AnimSequenceFactoryNode->AddTargetNodeUid(TrackNode.GetUniqueID());
TrackNode.AddTargetNodeUid(AnimSequenceFactoryNode->GetUniqueID());
}