// Copyright Epic Games, Inc. All Rights Reserved. #include "EditorAnimUtils.h" #include "Modules/ModuleManager.h" #include "Serialization/ArchiveReplaceObjectRef.h" #include "Animation/AnimationAsset.h" #include "Animation/AnimSequence.h" #include "Animation/AnimBlueprint.h" #include "Animation/AnimBlueprintGeneratedClass.h" #include "Engine/SkeletalMesh.h" #include "Kismet2/BlueprintEditorUtils.h" #include "Kismet2/KismetEditorUtilities.h" #include "IAssetTools.h" #include "AssetToolsModule.h" #include "Framework/Notifications/NotificationManager.h" #include "ObjectEditorUtils.h" #include "Widgets/Notifications/SNotificationList.h" #include "IContentBrowserSingleton.h" #include "ContentBrowserModule.h" #include "Subsystems/AssetEditorSubsystem.h" #include "Editor.h" #include "EditorReimportHandler.h" #include "EditorFramework/AssetImportData.h" #include "AnimationBlueprintLibrary.h" #define LOCTEXT_NAMESPACE "EditorAnimUtils" namespace EditorAnimUtils { /** Helper archive class to find all references, used by the cycle finder **/ class FFindAnimAssetRefs : public FArchiveUObject { public: /** * Constructor * * @param Src the object to serialize which may contain a references */ FFindAnimAssetRefs(UObject* Src, TArray& OutAnimationAssets) : AnimationAssets(OutAnimationAssets) { // use the optimized RefLink to skip over properties which don't contain object references ArIsObjectReferenceCollector = true; ArIgnoreArchetypeRef = false; ArIgnoreOuterRef = true; ArIgnoreClassRef = false; Src->Serialize(*this); } virtual FString GetArchiveName() const { return TEXT("FFindAnimAssetRefs"); } private: /** Serialize a reference **/ FArchive& operator<<(class UObject*& Obj) { if (UAnimationAsset* Anim = Cast(Obj)) { AnimationAssets.AddUnique(Anim); } return *this; } TArray& AnimationAssets; }; ////////////////////////////////////////////////////////////////// // FAnimationRetargetContext FAnimationRetargetContext::FAnimationRetargetContext(const TArray& AssetsToRetarget, bool bRetargetReferredAssets, bool bInConvertAnimationDataInComponentSpaces, const FNameDuplicationRule& NameRule) : SingleTargetObject(NULL) , bConvertAnimationDataInComponentSpaces(bInConvertAnimationDataInComponentSpaces) { TArray Objects; for(auto Iter = AssetsToRetarget.CreateConstIterator(); Iter; ++Iter) { Objects.Add((*Iter).GetAsset()); } auto WeakObjectList = FObjectEditorUtils::GetTypedWeakObjectPtrs(Objects); Initialize(WeakObjectList,bRetargetReferredAssets); } FAnimationRetargetContext::FAnimationRetargetContext(TArray> AssetsToRetarget, bool bRetargetReferredAssets, bool bInConvertAnimationDataInComponentSpaces, const FNameDuplicationRule& NameRule) : SingleTargetObject(NULL) , bConvertAnimationDataInComponentSpaces(bInConvertAnimationDataInComponentSpaces) { Initialize(AssetsToRetarget,bRetargetReferredAssets); } void FAnimationRetargetContext::Initialize(TArray> AssetsToRetarget, bool bRetargetReferredAssets) { for(auto Iter = AssetsToRetarget.CreateConstIterator(); Iter; ++Iter) { UObject* Asset = (*Iter).Get(); if( UAnimationAsset* AnimAsset = Cast(Asset) ) { AnimationAssetsToRetarget.AddUnique(AnimAsset); } else if( UAnimBlueprint* AnimBlueprint = Cast(Asset) ) { // Add parent non-template blueprints UAnimBlueprint* ParentBP = Cast(AnimBlueprint->ParentClass->ClassGeneratedBy); while (ParentBP) { // Cant transitively retarget templates if(!(ParentBP->bIsTemplate && ParentBP->TargetSkeleton == nullptr)) { AnimBlueprintsToRetarget.AddUnique(ParentBP); } ParentBP = Cast(ParentBP->ParentClass->ClassGeneratedBy); } AnimBlueprintsToRetarget.AddUnique(AnimBlueprint); } } if(AssetsToRetarget.Num() == 1) { //Only chose one object to retarget, keep track of it SingleTargetObject = AssetsToRetarget[0].Get(); } if(bRetargetReferredAssets) { // Grab assets from the blueprint. Do this first as it can add complex assets to the retarget array // which will need to be processed next. for(auto Iter = AnimBlueprintsToRetarget.CreateConstIterator(); Iter; ++Iter) { GetAllAnimationSequencesReferredInBlueprint( (*Iter), AnimationAssetsToRetarget); } int32 AssetIndex = 0; while (AssetIndex < AnimationAssetsToRetarget.Num()) { UAnimationAsset* AnimAsset = AnimationAssetsToRetarget[AssetIndex++]; AnimAsset->HandleAnimReferenceCollection(AnimationAssetsToRetarget, true); } } } bool FAnimationRetargetContext::HasAssetsToRetarget() const { return AnimationAssetsToRetarget.Num() > 0 || AnimBlueprintsToRetarget.Num() > 0; } bool FAnimationRetargetContext::HasDuplicates() const { return DuplicatedAnimAssets.Num() > 0 || DuplicatedBlueprints.Num() > 0; } TArray FAnimationRetargetContext::GetAllDuplicates() const { TArray Duplicates; if (AnimationAssetsToRetarget.Num() > 0) { Duplicates.Append(AnimationAssetsToRetarget); } if(AnimBlueprintsToRetarget.Num() > 0) { Duplicates.Append(AnimBlueprintsToRetarget); } return Duplicates; } UObject* FAnimationRetargetContext::GetSingleTargetObject() const { return SingleTargetObject; } UObject* FAnimationRetargetContext::GetDuplicate(const UObject* OriginalObject) const { if(HasDuplicates()) { if(const UAnimationAsset* Asset = Cast(OriginalObject)) { if(DuplicatedAnimAssets.Contains(Asset)) { return DuplicatedAnimAssets.FindRef(Asset); } } if(const UAnimBlueprint* AnimBlueprint = Cast(OriginalObject)) { if(DuplicatedBlueprints.Contains(AnimBlueprint)) { return DuplicatedBlueprints.FindRef(AnimBlueprint); } } } return NULL; } void FAnimationRetargetContext::DuplicateAssetsToRetarget(UPackage* DestinationPackage, const FNameDuplicationRule* NameRule) { if(!HasDuplicates()) { TArray AnimationAssetsToDuplicate = AnimationAssetsToRetarget; TArray AnimBlueprintsToDuplicate = AnimBlueprintsToRetarget; // We only want to duplicate unmapped assets, so we remove mapped assets from the list we're duplicating for(TPair& Pair : RemappedAnimAssets) { AnimationAssetsToDuplicate.Remove(Pair.Key); } DuplicatedAnimAssets = DuplicateAssets(AnimationAssetsToDuplicate, DestinationPackage, NameRule); DuplicatedBlueprints = DuplicateAssets(AnimBlueprintsToDuplicate, DestinationPackage, NameRule); // If we are moving the new asset to a different directory we need to fixup the reimport path. This should only effect source FBX paths within the project. if (!NameRule->FolderPath.IsEmpty()) { for (TPair& Pair : DuplicatedAnimAssets) { UAnimSequence* SourceSequence = Cast(Pair.Key); UAnimSequence* DestinationSequence = Cast(Pair.Value); if (SourceSequence && DestinationSequence) { for (int index = 0; index < SourceSequence->AssetImportData->SourceData.SourceFiles.Num(); index++) { const FString& RelativeFilename = SourceSequence->AssetImportData->SourceData.SourceFiles[index].RelativeFilename; const FString OldPackagePath = FPackageName::GetLongPackagePath(SourceSequence->GetPathName()) / TEXT(""); const FString NewPackagePath = FPackageName::GetLongPackagePath(DestinationSequence->GetPathName()) / TEXT(""); if (NewPackagePath != OldPackagePath) { const FString AbsoluteSrcPath = FPaths::ConvertRelativePathToFull(FPackageName::LongPackageNameToFilename(OldPackagePath)); const FString SrcFile = AbsoluteSrcPath / RelativeFilename; if (FPlatformFileManager::Get().GetPlatformFile().FileExists(*SrcFile)) { FString OldSourceFilePath = FPaths::ConvertRelativePathToFull(FPackageName::LongPackageNameToFilename(OldPackagePath), RelativeFilename); TArray Paths; Paths.Add(OldSourceFilePath); // Update the reimport file names FReimportManager::Instance()->UpdateReimportPaths(DestinationSequence, Paths); } } } } } } // Remapped assets needs the duplicated ones added RemappedAnimAssets.Append(DuplicatedAnimAssets); DuplicatedAnimAssets.GenerateValueArray(AnimationAssetsToRetarget); DuplicatedBlueprints.GenerateValueArray(AnimBlueprintsToRetarget); } } void FAnimationRetargetContext::RetargetAnimations(USkeleton* OldSkeleton, USkeleton* NewSkeleton) { check (!bConvertAnimationDataInComponentSpaces || OldSkeleton); check (NewSkeleton); if (bConvertAnimationDataInComponentSpaces) { // we need to update reference pose before retargeting. // this is to ensure the skeleton has the latest pose you're looking at. USkeletalMesh * PreviewMesh = NULL; if (OldSkeleton != NULL) { PreviewMesh = OldSkeleton->GetPreviewMesh(true); if (PreviewMesh) { OldSkeleton->UpdateReferencePoseFromMesh(PreviewMesh); } } PreviewMesh = NewSkeleton->GetPreviewMesh(true); if (PreviewMesh) { NewSkeleton->UpdateReferencePoseFromMesh(PreviewMesh); } } // anim sequences will be retargeted first becauseReplaceSkeleton forces it to change skeleton // @todo: please note that I think we can merge two loops //(without separating two loops - one for AnimSequence and one for everybody else) // but if you have animation asssets that does replace skeleton, it will try fix up internal asset also // so I think you might be doing twice - look at AnimationAsset:ReplaceSkeleton // for safety, I'm doing Sequence first and then everything else // however this can be re-investigated and fixed better in the future for(auto Iter = AnimationAssetsToRetarget.CreateIterator(); Iter; ++Iter) { UAnimSequence* AnimSequenceToRetarget = Cast(*Iter); if (AnimSequenceToRetarget) { // Copy curve data from source asset, preserving data in the target if present. if (OldSkeleton) { UAnimationBlueprintLibrary::CopyAnimationCurveNamesToSkeleton(OldSkeleton, NewSkeleton, AnimSequenceToRetarget, ERawCurveTrackTypes::RCT_Float); // clear transform curves since those curves won't work in new skeleton // since we're deleting curves, mark this rebake flag off IAnimationDataController& Controller = AnimSequenceToRetarget->GetController(); Controller.RemoveAllCurvesOfType(ERawCurveTrackTypes::RCT_Transform); // I can't copy transform curves yet because transform curves need retargeting. //EditorAnimUtils::CopyAnimCurves(OldSkeleton, NewSkeleton, AssetToRetarget, USkeleton::AnimTrackCurveMappingName, FRawCurveTracks::TransformType); } } UAnimationAsset* AssetToRetarget = (*Iter); if (HasDuplicates()) { AssetToRetarget->ReplaceReferredAnimations(RemappedAnimAssets); } AssetToRetarget->ReplaceSkeleton(NewSkeleton, bConvertAnimationDataInComponentSpaces); AssetToRetarget->MarkPackageDirty(); } // convert all Animation Blueprints and compile for ( auto AnimBPIter = AnimBlueprintsToRetarget.CreateIterator(); AnimBPIter; ++AnimBPIter ) { UAnimBlueprint * AnimBlueprint = (*AnimBPIter); AnimBlueprint->TargetSkeleton = NewSkeleton; // We can directly retarget templates (although not transitively via parents) so we need to make sure we clear the flag here AnimBlueprint->bIsTemplate = false; if (HasDuplicates()) { // if they have parent blueprint, make sure to re-link to the new one also UAnimBlueprint* CurrentParentBP = Cast(AnimBlueprint->ParentClass->ClassGeneratedBy); if (CurrentParentBP) { UAnimBlueprint* const * ParentBP = DuplicatedBlueprints.Find(CurrentParentBP); if (ParentBP) { AnimBlueprint->ParentClass = (*ParentBP)->GeneratedClass; } } } if(RemappedAnimAssets.Num() > 0) { ReplaceReferredAnimationsInBlueprint(AnimBlueprint, RemappedAnimAssets); } FBlueprintEditorUtils::RefreshAllNodes(AnimBlueprint); FKismetEditorUtilities::CompileBlueprint(AnimBlueprint, EBlueprintCompileOptions::SkipGarbageCollection); AnimBlueprint->PostEditChange(); AnimBlueprint->MarkPackageDirty(); } } void FAnimationRetargetContext::AddRemappedAsset(UAnimationAsset* OriginalAsset, UAnimationAsset* NewAsset) { RemappedAnimAssets.Add(OriginalAsset, NewAsset); } void OpenAssetFromNotify(UObject* AssetToOpen) { GEditor->GetEditorSubsystem()->OpenEditorForAsset(AssetToOpen); } ////////////////////////////////////////////////////////////////// UObject* RetargetAnimations(USkeleton* OldSkeleton, USkeleton* NewSkeleton, TArray> AssetsToRetarget, bool bRetargetReferredAssets, const FNameDuplicationRule* NameRule, bool bConvertSpace) { FAnimationRetargetContext RetargetContext(AssetsToRetarget, bRetargetReferredAssets, bConvertSpace); return RetargetAnimations(OldSkeleton, NewSkeleton, RetargetContext, bRetargetReferredAssets, NameRule); } UObject* RetargetAnimations(USkeleton* OldSkeleton, USkeleton* NewSkeleton, const TArray& AssetsToRetarget, bool bRetargetReferredAssets, const FNameDuplicationRule* NameRule, bool bConvertSpace) { FAnimationRetargetContext RetargetContext(AssetsToRetarget, bRetargetReferredAssets, bConvertSpace); return RetargetAnimations(OldSkeleton, NewSkeleton, RetargetContext, bRetargetReferredAssets, NameRule); } UObject* RetargetAnimations(USkeleton* OldSkeleton, USkeleton* NewSkeleton, FAnimationRetargetContext& RetargetContext, bool bRetargetReferredAssets, const FNameDuplicationRule* NameRule) { check(NewSkeleton); UObject* OriginalObject = RetargetContext.GetSingleTargetObject(); UPackage* DuplicationDestPackage = NewSkeleton->GetOutermost(); if( RetargetContext.HasAssetsToRetarget() ) { if(NameRule) { RetargetContext.DuplicateAssetsToRetarget(DuplicationDestPackage, NameRule); } RetargetContext.RetargetAnimations(OldSkeleton, NewSkeleton); } FNotificationInfo Notification(FText::GetEmpty()); Notification.ExpireDuration = 5.f; UObject* NotifyLinkObject = OriginalObject; if(OriginalObject && NameRule) { NotifyLinkObject = RetargetContext.GetDuplicate(OriginalObject); } if(!NameRule) { if(OriginalObject) { Notification.Text = FText::Format(LOCTEXT("SingleNonDuplicatedAsset", "'{0}' retargeted to new skeleton '{1}'"), FText::FromString(OriginalObject->GetName()), FText::FromString(NewSkeleton->GetName())); } else { Notification.Text = FText::Format(LOCTEXT("MultiNonDuplicatedAsset", "Assets retargeted to new skeleton '{0}'"), FText::FromString(NewSkeleton->GetName())); } } else { if(OriginalObject) { Notification.Text = FText::Format(LOCTEXT("SingleDuplicatedAsset", "'{0}' duplicated to '{1}' and retargeted"), FText::FromString(OriginalObject->GetName()), FText::FromString(DuplicationDestPackage->GetName())); } else { Notification.Text = FText::Format(LOCTEXT("MultiDuplicatedAsset", "Assets duplicated to '{0}' and retargeted"), FText::FromString(DuplicationDestPackage->GetName())); } } if(NotifyLinkObject) { Notification.Hyperlink = FSimpleDelegate::CreateStatic(&OpenAssetFromNotify, NotifyLinkObject); Notification.HyperlinkText = LOCTEXT("OpenAssetLink", "Open"); } FSlateNotificationManager::Get().AddNotification(Notification); // sync newly created objects on CB if (NotifyLinkObject) { TArray NewObjects = RetargetContext.GetAllDuplicates(); TArray CurrentSelection; for(auto& NewObject : NewObjects) { CurrentSelection.Add(FAssetData(NewObject)); } FContentBrowserModule& ContentBrowserModule = FModuleManager::Get().LoadModuleChecked("ContentBrowser"); ContentBrowserModule.Get().SyncBrowserToAssets(CurrentSelection); } if(OriginalObject && NameRule) { return RetargetContext.GetDuplicate(OriginalObject); } return NULL; } FString CreateDesiredName(UObject* Asset, const FNameDuplicationRule* NameRule) { check(Asset); FString NewName = Asset->GetName(); if(NameRule) { NewName = NameRule->Rename(Asset); } return NewName; } TMap DuplicateAssetsInternal(const TArray& AssetsToDuplicate, UPackage* DestinationPackage, const FNameDuplicationRule* NameRule) { FAssetToolsModule& AssetToolsModule = FModuleManager::LoadModuleChecked("AssetTools"); TMap DuplicateMap; for(UObject* AssetToDuplicate : AssetsToDuplicate) { if(DuplicateMap.Contains(AssetToDuplicate)) { continue; // ignore duplicates (shouldn't happen though) } // create unique name in case of existing asset with same name FString PathName = (NameRule)? NameRule->FolderPath : FPackageName::GetLongPackagePath(DestinationPackage->GetName()); FString ObjectName = CreateDesiredName(AssetToDuplicate, NameRule); FString PackageName; FString BasePackageName = PathName+"/"+ObjectName; AssetToolsModule.Get().CreateUniqueAssetName(BasePackageName, TEXT(""), PackageName, ObjectName); // create a new asset if (UObject* NewAsset = AssetToolsModule.Get().DuplicateAsset(ObjectName, PathName, AssetToDuplicate)) { DuplicateMap.Add(AssetToDuplicate, NewAsset); } } return DuplicateMap; } void GetAllAnimationSequencesReferredInBlueprint(UAnimBlueprint* AnimBlueprint, TArray& AnimationAssets) { UObject* DefaultObject = AnimBlueprint->GetAnimBlueprintGeneratedClass()->GetDefaultObject(); FFindAnimAssetRefs AnimRefFinderObject(DefaultObject, AnimationAssets); // For assets referenced in the event graph (either pin default values or variable-get nodes) // we need to serialize the nodes in that graph for(UEdGraph* GraphPage : AnimBlueprint->UbergraphPages) { for(UEdGraphNode* Node : GraphPage->Nodes) { FFindAnimAssetRefs AnimRefFinderBlueprint(Node, AnimationAssets); } } // Gather references in functions for(UEdGraph* GraphPage : AnimBlueprint->FunctionGraphs) { for(UEdGraphNode* Node : GraphPage->Nodes) { FFindAnimAssetRefs AnimRefFinderBlueprint(Node, AnimationAssets); } } // removes blendspaces that are embedded in the graph so that we don't duplicate them to make new assets AnimationAssets.RemoveAll([](UAnimationAsset*& AnimationAsset) { const bool bIsBlendspace = IsValid(Cast(AnimationAsset)); const bool bIsEmbedded = !AnimationAsset->GetSkeleton(); return bIsBlendspace && bIsEmbedded; }); } void ReplaceReferredAnimationsInBlueprint(UAnimBlueprint* AnimBlueprint, const TMap& AnimAssetReplacementMap) { UObject* DefaultObject = AnimBlueprint->GetAnimBlueprintGeneratedClass()->GetDefaultObject(); FArchiveReplaceObjectRef ReplaceAr(DefaultObject, AnimAssetReplacementMap); FArchiveReplaceObjectRef ReplaceAr2(AnimBlueprint, AnimAssetReplacementMap); // Replace event graph references for(UEdGraph* GraphPage : AnimBlueprint->UbergraphPages) { for(UEdGraphNode* Node : GraphPage->Nodes) { FArchiveReplaceObjectRef ReplaceGraphAr(Node, AnimAssetReplacementMap); } } // Replace references in functions for(UEdGraph* GraphPage : AnimBlueprint->FunctionGraphs) { for(UEdGraphNode* Node : GraphPage->Nodes) { FArchiveReplaceObjectRef ReplaceGraphAr(Node, AnimAssetReplacementMap); } } } void CopyAnimCurves(USkeleton* OldSkeleton, USkeleton* NewSkeleton, UAnimSequenceBase* SequenceBase, const FName ContainerName, ERawCurveTrackTypes CurveType) { // In some circumstances the asset may have already been updated during the retarget process (eg. retargeting of child assets for blendspaces, etc) if (NewSkeleton != SequenceBase->GetSkeleton()) { UAnimationBlueprintLibrary::CopyAnimationCurveNamesToSkeleton(OldSkeleton, NewSkeleton, SequenceBase, CurveType); } } FString FNameDuplicationRule::Rename(const UObject* Asset) const { check(Asset); FString NewName = Asset->GetName(); NewName = NewName.Replace(*ReplaceFrom, *ReplaceTo); return FString::Printf(TEXT("%s%s%s"), *Prefix, *NewName, *Suffix); } } #undef LOCTEXT_NAMESPACE