Files
UnrealEngineUWP/Engine/Source/Editor/AnimationEditor/Private/AnimationEditor.cpp
Jurre deBaare f24003f082 Animation data MVC refactor
#jira UE-104234
#rb Thomas.Sarkanen, Martin.Wilson, Alexis.Matte, Michael.Zyracki

+ Introduced UAnimDataModel, this currently represents the source data for bone and curve animation. It contains:
    + Bone animation tracks (FBoneAnimationTrack)
        + Key data (coarse)
        + Name
        + Bone tree index
    + Curve data (FAnimationCurveData)
        + Float Curves
        + Transform Curves
    + Play length
    + Sampling rate
        + Used for deriving the expected number of keys/frames when combined with the PlayLength
    + Transient data for supporting backwards compatibility APIs
    + (Dynamic) delegate which broadcasts the mutation Notifies

+ Introduced UAnimDataController, this has sole authority over mutating data contained by UAnimDataModel
    + API functionality allows to transform the contained data in all ways currently expected in the engine.
        + Any mutation will add an undo/redo-able FChange object into the transaction buffer
            + FChangeTransactor helper object allows for keeping track of (compounded) FChange's and inserting them into GUndo
        + Broadcasts change notifies alongside a payload object containing details about the change
            + Interested systems/objects can register to these changes through UAnimDataModel::OnModelModified event
        + Almost all functionality is exposed to both Blueprint and Python scripting
    + Allows for opening 'Brackets', these define the beginning and end of a chain of mutations. Allowing anything registered to OnModelModified to halt responding to the notifies until the (top-level) bracket is closed
    + Undo/redo actions
        + Each mutation to the UAnimDataModel is covered by a 'Action' which is based of FChange and is used to insert a undo/redo-able operation into the transactions buffer

+ Introduced EAnimDataModelNotifType and per-notify payload types. These are used to update views, and any model derived data

+ Introduced FAnimationCurveIdentifier, exposed to scripting, this is used for referencing a curve by name and type when passed to the controller
    + Allows for targetting a specific RichCurve as part of a TransformCurve

+ Introduced FAnimationDataNotifyCollectorused for keeping track of which notifies of type EAnimDataModelNotifType are broadcasted between top-level EAnimDataModelNotifType::BracketOpened and EAnimDataModelNotifType::BracketClosed notifies

+ Added CanTransact to UEngine, returns whether or not a transaction can be made

+ Added UAnimCompositeBase::SetCompositeLength, used for runtime changing of play length value

+ Animation Sequence helpers, containing 'helper' functionality for both AnimSequence and AnimDataModel

* Animation Sequence Base
    + virtual PopulateModel, called during upgrade path for converting existing (legacy) data to the new UAnimDataModel data
    + virtual OnModelModified, registered to the Model's OnModified event to handle any Notifies and payloads
    + Added UAnimDataModel sub-object (editor only)
    + Added UAnimDataController instance (transient and editor only)
    + FAnimationDataNotifyCollector to keep track of model bracketed notifies
    * Deprecated
        * SetSequenceLength()
        * RefreshCurveData()
        * MarkRawDataAsModified()
        * RegisterOnAnimCurvesChanged()
        * UnregisterOnAnimCurvesChanged()
        * UnregisterOnAnimTrackCurvesChanged()
        * RegisterOnAnimTrackCurvesChanged()
        * Public access to RawCurveData

* Animation Sequence
    + Implements PopulateModel/OnModelModified for AnimationSequence related data
    + Added TargetFrameRate, currently locked to the initial sampling rate, but allows for resampling the UAnimDataModel data according to the set rate
    + Added NumberOfSampledKeys (editor only), populated by resampling process
    + Added NumberOfSampledFrames (editor only), populated by resampling process
    + Added ResampledAnimationTrackData (editor only and transient), populated by resampling process
    + Added bBlockCompressionRequests (editor only), used for blocking compression during automation tests
    * Deprecated
        * SetNumberOfSampledKeys()
        * MarkRawDataAsModified()
        * GetRawAnimationData()
        * GetAnimationTrackNames()
        * AddNewRawTrack()
        * GetRawTrackToSkeletonMapTable()
        * GetRawAnimationTrack()
        * GetRawAnimationTrack()
        * UpdateFrameRate()
        * ExtractBoneTransform()
        * CompressRawAnimData()
        * GetSkeletonIndexFromRawDataTrackIndex()
        * RecycleAnimSequence()
        * CleanAnimSequenceForImport()
        * CopyNotifies()
        * PostProcessSequence()
        * AddLoopingInterpolation()
        * RemoveAllTracks()
        * DoesContainTransformCurves()
        * InsertFramesToRawAnimData()
        * CropRawAnimData()
        * RemoveTrack()
        * InsertTrack()
        * ResizeSequence()
        * Non-editor access to GetUncompressedRawSize()
        * Non-editor access to GetApproxRawSize()
        * Public access to UpdateCompressedCurveName()
        * NumberOfKeys
        * SamplingFrameRate
        * TrackToSkeletonMapTable
        * RawAnimationData
        * AnimationTrackNames

* Animation Streamable
    * Model from source Sequence is duplicated rather than copying animation data

* Deprecated ERawCurveTrackTypes::RCT_Vector (marked as hidden)

+ Added automated test for
    + All script exposed controller functionality
    + Undo/redo actions
* Updated existing AnimSequence tests with deprecation and new expected data

+ Base data for compression is now populated from the 'resampled' version stored on the AnimSequence
+ Data cleanup and validation is now performed during compression
    + Track sanitization is now performed during compression (normalizing Quats and clamping near-zero values to zero)
    + Key reduction
    + Invalid track (missing bone from skeleton) removal
    + Curve name validation against skeleton
* Replaced RawCurves with Float curves
* Updated DDC key
- Removed all Guid generation related functionality (now part of the model)

* AnimSequenceBase model registers to AnimModel's notify event, used for refreshing the tracks
    * Replaces the in-place calling of RefreshTracks()
* All curve tracks now hold a const CurveType* rather than a CurveType*/& for introspection of its data
    + Any mutations now go through the controller API
    + Uses AnimationCurveIdentifier to set Transform's per-channel tracks
* RichCurveEditorModelNamed conformed to model/controller
    + Any modifications are applied to an temporary CurveType which always contains a copy of the const-ptr after any previous mutation
    + Registers to the outer AnimModel's notify event, used for updating the cached curve data
    + Registers to CurveModifiedDelegate, used for copying the temp curve data to the model using SetCurveKeys
        * Not-ideal but point that I got to for V1
        * Would be replaced with either refactoring FRichCurve to be MVC like, or by calling UAnimDataController functionality from an implementation of FRichCurveEditorModel

* Mark FChange overrides as final for swap/command change permutations
* Changed FCompoundChange GetDescription to return concatenation of all sub-changes
* Added GetDescription to FTransaction which returns the ToString() value of any contained FChange entries.
    * Entries in SUndoHistory tab now have a ToolTip showing the GetDescription() value
+ FChangeTransactor helper object allows for keeping track of (compounded) FChange's and inserting them into GUndo

* Deprecation handling, conforming code to new APIs and exposing structure/objects/properties to scripting
* Conforming AnimSequence importing from FBX to Model/Controller changes

[CL 15106211 by Jurre deBaare in ue5-main branch]
2021-01-15 06:41:11 -04:00

867 lines
29 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "AnimationEditor.h"
#include "Misc/MessageDialog.h"
#include "Modules/ModuleManager.h"
#include "Framework/Application/SlateApplication.h"
#include "Framework/MultiBox/MultiBoxBuilder.h"
#include "EditorStyleSet.h"
#include "EditorReimportHandler.h"
#include "Animation/SmartName.h"
#include "Animation/AnimationAsset.h"
#include "Animation/DebugSkelMeshComponent.h"
#include "AssetData.h"
#include "EdGraph/EdGraphSchema.h"
#include "Animation/AnimSequence.h"
#include "Animation/AnimMontage.h"
#include "Editor/EditorEngine.h"
#include "Factories/AnimSequenceFactory.h"
#include "Factories/PoseAssetFactory.h"
#include "EngineGlobals.h"
#include "Editor.h"
#include "IAnimationEditorModule.h"
#include "IPersonaToolkit.h"
#include "PersonaModule.h"
#include "AnimationEditorMode.h"
#include "IPersonaPreviewScene.h"
#include "AnimationEditorCommands.h"
#include "IDetailsView.h"
#include "ISkeletonTree.h"
#include "ISkeletonEditorModule.h"
#include "IDocumentation.h"
#include "Widgets/Docking/SDockTab.h"
#include "Animation/PoseAsset.h"
#include "AnimPreviewInstance.h"
#include "ScopedTransaction.h"
#include "IContentBrowserSingleton.h"
#include "ContentBrowserModule.h"
#include "AnimationEditorUtils.h"
#include "AssetRegistryModule.h"
#include "IAssetFamily.h"
#include "IAnimationSequenceBrowser.h"
#include "Framework/Notifications/NotificationManager.h"
#include "Widgets/Notifications/SNotificationList.h"
#include "PersonaCommonCommands.h"
#include "Sound/SoundWave.h"
#include "Engine/CurveTable.h"
#include "Developer/AssetTools/Public/IAssetTools.h"
#include "Developer/AssetTools/Public/AssetToolsModule.h"
#include "ISkeletonTreeItem.h"
#include "Algo/Transform.h"
#include "ISequenceRecorder.h"
#include "IAnimSequenceCurveEditor.h"
#include "EditorModeManager.h"
#include "IPersonaEditorModeManager.h"
#include "Toolkits/AssetEditorToolkit.h"
#include "PersonaToolMenuContext.h"
const FName AnimationEditorAppIdentifier = FName(TEXT("AnimationEditorApp"));
const FName AnimationEditorModes::AnimationEditorMode(TEXT("AnimationEditorMode"));
const FName AnimationEditorTabs::DetailsTab(TEXT("DetailsTab"));
const FName AnimationEditorTabs::SkeletonTreeTab(TEXT("SkeletonTreeView"));
const FName AnimationEditorTabs::ViewportTab(TEXT("Viewport"));
const FName AnimationEditorTabs::AdvancedPreviewTab(TEXT("AdvancedPreviewTab"));
const FName AnimationEditorTabs::DocumentTab(TEXT("Document"));
const FName AnimationEditorTabs::CurveEditorTab(TEXT("CurveEditor"));
const FName AnimationEditorTabs::AssetBrowserTab(TEXT("SequenceBrowser"));
const FName AnimationEditorTabs::AssetDetailsTab(TEXT("AnimAssetPropertiesTab"));
const FName AnimationEditorTabs::CurveNamesTab(TEXT("AnimCurveViewerTab"));
const FName AnimationEditorTabs::SlotNamesTab(TEXT("SkeletonSlotNames"));
const FName AnimationEditorTabs::AnimMontageSectionsTab(TEXT("AnimMontageSections"));
DEFINE_LOG_CATEGORY(LogAnimationEditor);
#define LOCTEXT_NAMESPACE "AnimationEditor"
FAnimationEditor::FAnimationEditor()
{
UEditorEngine* Editor = Cast<UEditorEngine>(GEngine);
if (Editor != nullptr)
{
Editor->RegisterForUndo(this);
}
}
FAnimationEditor::~FAnimationEditor()
{
UEditorEngine* Editor = Cast<UEditorEngine>(GEngine);
if (Editor != nullptr)
{
Editor->UnregisterForUndo(this);
}
GEditor->GetEditorSubsystem<UImportSubsystem>()->OnAssetPostImport.RemoveAll(this);
FReimportManager::Instance()->OnPostReimport().RemoveAll(this);
}
void FAnimationEditor::RegisterTabSpawners(const TSharedRef<class FTabManager>& InTabManager)
{
WorkspaceMenuCategory = InTabManager->AddLocalWorkspaceMenuCategory(LOCTEXT("WorkspaceMenu_AnimationEditor", "Animation Editor"));
FAssetEditorToolkit::RegisterTabSpawners( InTabManager );
}
void FAnimationEditor::UnregisterTabSpawners(const TSharedRef<class FTabManager>& InTabManager)
{
FAssetEditorToolkit::UnregisterTabSpawners(InTabManager);
}
void FAnimationEditor::InitAnimationEditor(const EToolkitMode::Type Mode, const TSharedPtr<IToolkitHost>& InitToolkitHost, UAnimationAsset* InAnimationAsset)
{
AnimationAsset = InAnimationAsset;
// Register post import callback to catch animation imports when we have the asset open (we need to reinit)
FReimportManager::Instance()->OnPostReimport().AddRaw(this, &FAnimationEditor::HandlePostReimport);
GEditor->GetEditorSubsystem<UImportSubsystem>()->OnAssetPostImport.AddRaw(this, &FAnimationEditor::HandlePostImport);
FPersonaModule& PersonaModule = FModuleManager::LoadModuleChecked<FPersonaModule>("Persona");
PersonaToolkit = PersonaModule.CreatePersonaToolkit(InAnimationAsset);
PersonaToolkit->GetPreviewScene()->SetDefaultAnimationMode(EPreviewSceneDefaultAnimationMode::Animation);
FSkeletonTreeArgs SkeletonTreeArgs;
SkeletonTreeArgs.OnSelectionChanged = FOnSkeletonTreeSelectionChanged::CreateSP(this, &FAnimationEditor::HandleSelectionChanged);
SkeletonTreeArgs.PreviewScene = PersonaToolkit->GetPreviewScene();
SkeletonTreeArgs.ContextName = GetToolkitFName();
ISkeletonEditorModule& SkeletonEditorModule = FModuleManager::GetModuleChecked<ISkeletonEditorModule>("SkeletonEditor");
SkeletonTree = SkeletonEditorModule.CreateSkeletonTree(PersonaToolkit->GetSkeleton(), SkeletonTreeArgs);
const bool bCreateDefaultStandaloneMenu = true;
const bool bCreateDefaultToolbar = true;
FAssetEditorToolkit::InitAssetEditor(Mode, InitToolkitHost, AnimationEditorAppIdentifier, FTabManager::FLayout::NullLayout, bCreateDefaultStandaloneMenu, bCreateDefaultToolbar, InAnimationAsset);
BindCommands();
AddApplicationMode(
AnimationEditorModes::AnimationEditorMode,
MakeShareable(new FAnimationEditorMode(SharedThis(this), SkeletonTree.ToSharedRef())));
SetCurrentMode(AnimationEditorModes::AnimationEditorMode);
ExtendMenu();
ExtendToolbar();
RegenerateMenusAndToolbars();
OpenNewAnimationDocumentTab(AnimationAsset);
}
FName FAnimationEditor::GetToolkitFName() const
{
return FName("AnimationEditor");
}
FText FAnimationEditor::GetBaseToolkitName() const
{
return LOCTEXT("AppLabel", "AnimationEditor");
}
FString FAnimationEditor::GetWorldCentricTabPrefix() const
{
return LOCTEXT("WorldCentricTabPrefix", "AnimationEditor ").ToString();
}
FLinearColor FAnimationEditor::GetWorldCentricTabColorScale() const
{
return FLinearColor(0.3f, 0.2f, 0.5f, 0.5f);
}
void FAnimationEditor::InitToolMenuContext(FToolMenuContext& MenuContext)
{
FAssetEditorToolkit::InitToolMenuContext(MenuContext);
UPersonaToolMenuContext* Context = NewObject<UPersonaToolMenuContext>();
Context->SetToolkit(GetPersonaToolkit());
MenuContext.AddObject(Context);
}
void FAnimationEditor::Tick(float DeltaTime)
{
GetPersonaToolkit()->GetPreviewScene()->InvalidateViews();
}
TStatId FAnimationEditor::GetStatId() const
{
RETURN_QUICK_DECLARE_CYCLE_STAT(FAnimationEditor, STATGROUP_Tickables);
}
void FAnimationEditor::AddReferencedObjects(FReferenceCollector& Collector)
{
Collector.AddReferencedObject(AnimationAsset);
}
void FAnimationEditor::BindCommands()
{
FAnimationEditorCommands::Register();
ToolkitCommands->MapAction(FAnimationEditorCommands::Get().ApplyCompression,
FExecuteAction::CreateSP(this, &FAnimationEditor::OnApplyCompression),
FCanExecuteAction::CreateSP(this, &FAnimationEditor::HasValidAnimationSequence));
ToolkitCommands->MapAction(FAnimationEditorCommands::Get().SetKey,
FExecuteAction::CreateSP(this, &FAnimationEditor::OnSetKey),
FCanExecuteAction::CreateSP(this, &FAnimationEditor::CanSetKey));
ToolkitCommands->MapAction(FAnimationEditorCommands::Get().ReimportAnimation,
FExecuteAction::CreateSP(this, &FAnimationEditor::OnReimportAnimation),
FCanExecuteAction::CreateSP(this, &FAnimationEditor::HasValidAnimationSequence));
ToolkitCommands->MapAction(FAnimationEditorCommands::Get().ExportToFBX_AnimData,
FExecuteAction::CreateSP(this, &FAnimationEditor::OnExportToFBX, EExportSourceOption::CurrentAnimation_AnimData),
FCanExecuteAction::CreateSP(this, &FAnimationEditor::HasValidAnimationSequence));
ToolkitCommands->MapAction(FAnimationEditorCommands::Get().ExportToFBX_PreviewMesh,
FExecuteAction::CreateSP(this, &FAnimationEditor::OnExportToFBX, EExportSourceOption::CurrentAnimation_PreviewMesh),
FCanExecuteAction::CreateSP(this, &FAnimationEditor::HasValidAnimationSequence));
ToolkitCommands->MapAction(FAnimationEditorCommands::Get().AddLoopingInterpolation,
FExecuteAction::CreateSP(this, &FAnimationEditor::OnAddLoopingInterpolation),
FCanExecuteAction::CreateSP(this, &FAnimationEditor::HasValidAnimationSequence));
ToolkitCommands->MapAction(FAnimationEditorCommands::Get().RemoveBoneTracks,
FExecuteAction::CreateSP(this, &FAnimationEditor::OnRemoveBoneTrack),
FCanExecuteAction::CreateSP(this, &FAnimationEditor::HasValidAnimationSequence));
ToolkitCommands->MapAction(FPersonaCommonCommands::Get().TogglePlay,
FExecuteAction::CreateRaw(&GetPersonaToolkit()->GetPreviewScene().Get(), &IPersonaPreviewScene::TogglePlayback));
}
void FAnimationEditor::ExtendToolbar()
{
// If the ToolbarExtender is valid, remove it before rebuilding it
if (ToolbarExtender.IsValid())
{
RemoveToolbarExtender(ToolbarExtender);
ToolbarExtender.Reset();
}
ToolbarExtender = MakeShareable(new FExtender);
AddToolbarExtender(ToolbarExtender);
IAnimationEditorModule& AnimationEditorModule = FModuleManager::GetModuleChecked<IAnimationEditorModule>("AnimationEditor");
AddToolbarExtender(AnimationEditorModule.GetToolBarExtensibilityManager()->GetAllExtenders(GetToolkitCommands(), GetEditingObjects()));
TArray<IAnimationEditorModule::FAnimationEditorToolbarExtender> ToolbarExtenderDelegates = AnimationEditorModule.GetAllAnimationEditorToolbarExtenders();
for (auto& ToolbarExtenderDelegate : ToolbarExtenderDelegates)
{
if (ToolbarExtenderDelegate.IsBound())
{
AddToolbarExtender(ToolbarExtenderDelegate.Execute(GetToolkitCommands(), SharedThis(this)));
}
}
// extend extra menu/toolbars
ToolbarExtender->AddToolBarExtension(
"Asset",
EExtensionHook::After,
GetToolkitCommands(),
FToolBarExtensionDelegate::CreateLambda([this](FToolBarBuilder& ToolbarBuilder)
{
FPersonaModule& PersonaModule = FModuleManager::LoadModuleChecked<FPersonaModule>("Persona");
FPersonaModule::FCommonToolbarExtensionArgs Args;
Args.bPreviewAnimation = false;
Args.bReferencePose = false;
PersonaModule.AddCommonToolbarExtensions(ToolbarBuilder, PersonaToolkit.ToSharedRef(), Args);
ToolbarBuilder.BeginSection("Animation");
{
ToolbarBuilder.AddToolBarButton(FAnimationEditorCommands::Get().ReimportAnimation);
ToolbarBuilder.AddToolBarButton(FAnimationEditorCommands::Get().ApplyCompression, NAME_None, LOCTEXT("Toolbar_ApplyCompression", "Apply Compression"));
{
ToolbarBuilder.AddComboButton(
FUIAction(),
FOnGetContent::CreateSP(this, &FAnimationEditor::GenerateExportAssetMenu),
LOCTEXT("ExportAsset_Label", "Export Asset"),
LOCTEXT("ExportAsset_ToolTip", "Export Assets for this skeleton."),
FSlateIcon(FEditorStyle::GetStyleSetName(), "Persona.ExportToFBX")
);
}
}
ToolbarBuilder.EndSection();
ToolbarBuilder.BeginSection("Editing");
{
ToolbarBuilder.AddToolBarButton(FAnimationEditorCommands::Get().SetKey, NAME_None, LOCTEXT("Toolbar_SetKey", "Key"));
}
ToolbarBuilder.EndSection();
TSharedRef<class IAssetFamily> AssetFamily = PersonaModule.CreatePersonaAssetFamily(AnimationAsset);
AddToolbarWidget(PersonaModule.CreateAssetFamilyShortcutWidget(SharedThis(this), AssetFamily));
}
));
}
void FAnimationEditor::ExtendMenu()
{
MenuExtender = MakeShareable(new FExtender);
struct Local
{
static void AddAssetMenu(FMenuBuilder& MenuBuilder, FAnimationEditor* InAnimationEditor)
{
MenuBuilder.BeginSection("AnimationEditor", LOCTEXT("AnimationEditorAssetMenu_Animation", "Animation"));
{
MenuBuilder.AddMenuEntry(FAnimationEditorCommands::Get().ApplyCompression);
MenuBuilder.AddSubMenu(
LOCTEXT("ExportToFBX", "Export to FBX"),
LOCTEXT("ExportToFBX_ToolTip", "Export current animation to FBX"),
FNewMenuDelegate::CreateSP(InAnimationEditor, &FAnimationEditor::FillExportAssetMenu),
false,
FSlateIcon(FEditorStyle::GetStyleSetName(), "ClassIcon.")
);
MenuBuilder.AddMenuEntry(FAnimationEditorCommands::Get().AddLoopingInterpolation);
MenuBuilder.AddMenuEntry(FAnimationEditorCommands::Get().RemoveBoneTracks);
MenuBuilder.AddSubMenu(
LOCTEXT("CopyCurvesToSoundWave", "Copy Curves To SoundWave"),
LOCTEXT("CopyCurvesToSoundWave_ToolTip", "Copy curves from this animation to the selected SoundWave"),
FNewMenuDelegate::CreateSP(InAnimationEditor, &FAnimationEditor::FillCopyToSoundWaveMenu),
false,
FSlateIcon(FEditorStyle::GetStyleSetName(), "ClassIcon.")
);
}
MenuBuilder.EndSection();
}
};
MenuExtender->AddMenuExtension(
"AssetEditorActions",
EExtensionHook::After,
GetToolkitCommands(),
FMenuExtensionDelegate::CreateStatic(&Local::AddAssetMenu, this)
);
AddMenuExtender(MenuExtender);
IAnimationEditorModule& AnimationEditorModule = FModuleManager::GetModuleChecked<IAnimationEditorModule>("AnimationEditor");
AddMenuExtender(AnimationEditorModule.GetMenuExtensibilityManager()->GetAllExtenders(GetToolkitCommands(), GetEditingObjects()));
}
void FAnimationEditor::HandleObjectsSelected(const TArray<UObject*>& InObjects)
{
if (DetailsView.IsValid())
{
DetailsView->SetObjects(InObjects);
}
}
void FAnimationEditor::HandleSelectionChanged(const TArrayView<TSharedPtr<ISkeletonTreeItem>>& InSelectedItems, ESelectInfo::Type InSelectInfo)
{
if (DetailsView.IsValid())
{
TArray<UObject*> Objects;
Algo::TransformIf(InSelectedItems, Objects, [](const TSharedPtr<ISkeletonTreeItem>& InItem) { return InItem->GetObject() != nullptr; }, [](const TSharedPtr<ISkeletonTreeItem>& InItem) { return InItem->GetObject(); });
DetailsView->SetObjects(Objects);
}
}
void FAnimationEditor::HandleObjectSelected(UObject* InObject)
{
if (DetailsView.IsValid())
{
DetailsView->SetObject(InObject);
}
}
void FAnimationEditor::PostUndo(bool bSuccess)
{
OnPostUndo.Broadcast();
}
void FAnimationEditor::PostRedo(bool bSuccess)
{
OnPostUndo.Broadcast();
}
void FAnimationEditor::HandleDetailsCreated(const TSharedRef<IDetailsView>& InDetailsView)
{
DetailsView = InDetailsView;
}
TSharedPtr<SDockTab> FAnimationEditor::OpenNewAnimationDocumentTab(UAnimationAsset* InAnimAsset)
{
TSharedPtr<SDockTab> OpenedTab;
if (InAnimAsset != nullptr)
{
FString DocumentLink;
FAnimDocumentArgs Args(PersonaToolkit->GetPreviewScene(), GetPersonaToolkit(), GetSkeletonTree()->GetEditableSkeleton(), OnPostUndo, OnSectionsChanged);
Args.OnDespatchObjectsSelected = FOnObjectsSelected::CreateSP(this, &FAnimationEditor::HandleObjectsSelected);
Args.OnDespatchInvokeTab = FOnInvokeTab::CreateSP(this, &FAssetEditorToolkit::InvokeTab);
Args.OnDespatchSectionsChanged = FSimpleDelegate::CreateSP(this, &FAnimationEditor::HandleSectionsChanged);
FPersonaModule& PersonaModule = FModuleManager::GetModuleChecked<FPersonaModule>("Persona");
TSharedRef<SWidget> TabContents = PersonaModule.CreateEditorWidgetForAnimDocument(SharedThis(this), InAnimAsset, Args, DocumentLink);
if (AnimationAsset)
{
RemoveEditingObject(AnimationAsset);
}
AddEditingObject(InAnimAsset);
AnimationAsset = InAnimAsset;
GetPersonaToolkit()->GetPreviewScene()->SetPreviewAnimationAsset(InAnimAsset);
GetPersonaToolkit()->SetAnimationAsset(InAnimAsset);
// Close existing opened curve tab
if(AnimCurveDocumentTab.IsValid())
{
AnimCurveDocumentTab.Pin()->RequestCloseTab();
}
AnimCurveDocumentTab.Reset();
struct Local
{
static FText GetObjectName(UObject* Object)
{
return FText::FromString(Object->GetName());
}
};
TAttribute<FText> NameAttribute = TAttribute<FText>::Create(TAttribute<FText>::FGetter::CreateStatic(&Local::GetObjectName, (UObject*)InAnimAsset));
const bool bIsReusedEditor = SharedAnimDocumentTab.IsValid();
if (bIsReusedEditor)
{
OpenedTab = SharedAnimDocumentTab.Pin();
OpenedTab->SetContent(TabContents);
OpenedTab->ActivateInParent(ETabActivationCause::SetDirectly);
OpenedTab->SetLabel(NameAttribute);
OpenedTab->SetLeftContent(IDocumentation::Get()->CreateAnchor(DocumentLink));
}
else
{
OpenedTab = SNew(SDockTab)
.Label(NameAttribute)
.TabRole(ETabRole::DocumentTab)
.TabColorScale(GetTabColorScale())
.OnTabClosed_Lambda([this](TSharedRef<SDockTab> InTab)
{
TSharedPtr<SDockTab> CurveTab = AnimCurveDocumentTab.Pin();
if(CurveTab.IsValid())
{
CurveTab->RequestCloseTab();
}
})
[
TabContents
];
OpenedTab->SetLeftContent(IDocumentation::Get()->CreateAnchor(DocumentLink));
TabManager->InsertNewDocumentTab(AnimationEditorTabs::DocumentTab, FTabManager::ESearchPreference::RequireClosedTab, OpenedTab.ToSharedRef());
SharedAnimDocumentTab = OpenedTab;
}
// Invoke the montage sections tab, and make sure the asset browser is there and in focus when we are dealing with a montage.
if(InAnimAsset->IsA<UAnimMontage>())
{
TabManager->TryInvokeTab(AnimationEditorTabs::AnimMontageSectionsTab);
// Only activate the asset browser tab when this is a reused Animation Editor window.
if (bIsReusedEditor)
{
TabManager->TryInvokeTab(AnimationEditorTabs::AssetBrowserTab);
}
OnSectionsChanged.Broadcast();
}
else
{
// Close existing opened montage sections tab
TSharedPtr<SDockTab> OpenMontageSectionsTab = TabManager->FindExistingLiveTab(AnimationEditorTabs::AnimMontageSectionsTab);
if(OpenMontageSectionsTab.IsValid())
{
OpenMontageSectionsTab->RequestCloseTab();
}
}
if (SequenceBrowser.IsValid())
{
SequenceBrowser.Pin()->SelectAsset(InAnimAsset);
}
// let the asset family know too
TSharedRef<IAssetFamily> AssetFamily = PersonaModule.CreatePersonaAssetFamily(InAnimAsset);
AssetFamily->RecordAssetOpened(FAssetData(InAnimAsset));
}
return OpenedTab;
}
void FAnimationEditor::EditCurves(UAnimSequenceBase* InAnimSequence, const TArray<FCurveEditInfo>& InCurveInfo, const TSharedPtr<ITimeSliderController>& InExternalTimeSliderController)
{
FPersonaModule& PersonaModule = FModuleManager::GetModuleChecked<FPersonaModule>("Persona");
if(!AnimCurveDocumentTab.IsValid())
{
TSharedRef<IAnimSequenceCurveEditor> NewCurveEditor = PersonaModule.CreateCurveWidgetForAnimDocument(SharedThis(this), GetPersonaToolkit()->GetPreviewScene(), InAnimSequence, InExternalTimeSliderController, TabManager);
CurveEditor = NewCurveEditor;
TSharedPtr<SDockTab> CurveTab = SNew(SDockTab)
.Label(LOCTEXT("CurveEditorTabTitle", "Curve Editor"))
.TabRole(ETabRole::DocumentTab)
.TabColorScale(GetTabColorScale())
[
NewCurveEditor
];
AnimCurveDocumentTab = CurveTab;
TabManager->InsertNewDocumentTab(AnimationEditorTabs::CurveEditorTab, FTabManager::ESearchPreference::RequireClosedTab, CurveTab.ToSharedRef());
}
else
{
TabManager->DrawAttention(AnimCurveDocumentTab.Pin().ToSharedRef());
}
check(CurveEditor.IsValid());
CurveEditor.Pin()->ResetCurves();
for(const FCurveEditInfo& CurveInfo : InCurveInfo)
{
CurveEditor.Pin()->AddCurve(CurveInfo.CurveDisplayName, CurveInfo.CurveColor, CurveInfo.Name, CurveInfo.Type, CurveInfo.CurveIndex, CurveInfo.OnCurveModified);
}
CurveEditor.Pin()->ZoomToFit();
}
void FAnimationEditor::StopEditingCurves(const TArray<FCurveEditInfo>& InCurveInfo)
{
if(CurveEditor.IsValid())
{
for(const FCurveEditInfo& CurveInfo : InCurveInfo)
{
CurveEditor.Pin()->RemoveCurve(CurveInfo.Name, CurveInfo.Type, CurveInfo.CurveIndex);
}
}
}
void FAnimationEditor::HandleSectionsChanged()
{
OnSectionsChanged.Broadcast();
}
void FAnimationEditor::SetAnimationAsset(UAnimationAsset* AnimAsset)
{
HandleOpenNewAsset(AnimAsset);
}
IAnimationSequenceBrowser* FAnimationEditor::GetAssetBrowser() const
{
return SequenceBrowser.Pin().Get();
}
void FAnimationEditor::HandleOpenNewAsset(UObject* InNewAsset)
{
if (UAnimationAsset* NewAnimationAsset = Cast<UAnimationAsset>(InNewAsset))
{
OpenNewAnimationDocumentTab(NewAnimationAsset);
}
}
UObject* FAnimationEditor::HandleGetAsset()
{
return GetEditingObject();
}
bool FAnimationEditor::HasValidAnimationSequence() const
{
UAnimSequence* AnimSequence = Cast<UAnimSequence>(AnimationAsset);
return (AnimSequence != nullptr);
}
bool FAnimationEditor::CanSetKey() const
{
UDebugSkelMeshComponent* PreviewMeshComponent = PersonaToolkit->GetPreviewMeshComponent();
return (HasValidAnimationSequence() && PreviewMeshComponent->BonesOfInterest.Num() > 0);
}
void FAnimationEditor::OnSetKey()
{
if (AnimationAsset)
{
UDebugSkelMeshComponent* Component = PersonaToolkit->GetPreviewMeshComponent();
Component->PreviewInstance->SetKey();
}
}
void FAnimationEditor::OnReimportAnimation()
{
UAnimSequence* AnimSequence = Cast<UAnimSequence>(AnimationAsset);
if (AnimSequence)
{
FReimportManager::Instance()->Reimport(AnimSequence, true);
}
}
void FAnimationEditor::OnApplyCompression()
{
UAnimSequence* AnimSequence = Cast<UAnimSequence>(AnimationAsset);
if (AnimSequence)
{
TArray<TWeakObjectPtr<UAnimSequence>> AnimSequences;
AnimSequences.Add(AnimSequence);
FPersonaModule& PersonaModule = FModuleManager::GetModuleChecked<FPersonaModule>("Persona");
PersonaModule.ApplyCompression(AnimSequences, false);
}
}
void FAnimationEditor::OnExportToFBX(const EExportSourceOption Option)
{
UAnimSequence* AnimSequenceToRecord = nullptr;
if (Option == EExportSourceOption::CurrentAnimation_AnimData)
{
TArray<UObject*> AssetsToExport;
AssetsToExport.Add(AnimationAsset);
ExportToFBX(AssetsToExport, false);
}
else if (Option == EExportSourceOption::CurrentAnimation_PreviewMesh)
{
TArray<TWeakObjectPtr<UObject>> Skeletons;
Skeletons.Add(PersonaToolkit->GetSkeleton());
AnimationEditorUtils::CreateAnimationAssets(Skeletons, UAnimSequence::StaticClass(), FString("_PreviewMesh"), FAnimAssetCreated::CreateSP(this, &FAnimationEditor::ExportToFBX, true), AnimationAsset, true);
}
else
{
ensure(false);
}
}
bool FAnimationEditor::ExportToFBX(const TArray<UObject*> AssetsToExport, bool bRecordAnimation)
{
bool AnimSequenceExportResult = false;
TArray<TWeakObjectPtr<UAnimSequence>> AnimSequences;
if (AssetsToExport.Num() > 0)
{
UAnimSequence* AnimationToRecord = Cast<UAnimSequence>(AssetsToExport[0]);
if (AnimationToRecord)
{
if (bRecordAnimation)
{
USkeletalMeshComponent* MeshComponent = PersonaToolkit->GetPreviewMeshComponent();
RecordMeshToAnimation(MeshComponent, AnimationToRecord);
}
AnimSequences.Add(AnimationToRecord);
}
}
if (AnimSequences.Num() > 0)
{
FPersonaModule& PersonaModule = FModuleManager::GetModuleChecked<FPersonaModule>("Persona");
AnimSequenceExportResult = PersonaModule.ExportToFBX(AnimSequences, GetPersonaToolkit()->GetPreviewScene()->GetPreviewMeshComponent()->SkeletalMesh);
}
return AnimSequenceExportResult;
}
void FAnimationEditor::OnAddLoopingInterpolation()
{
UAnimSequence* AnimSequence = Cast<UAnimSequence>(AnimationAsset);
if (AnimSequence)
{
TArray<TWeakObjectPtr<UAnimSequence>> AnimSequences;
AnimSequences.Add(AnimSequence);
FPersonaModule& PersonaModule = FModuleManager::GetModuleChecked<FPersonaModule>("Persona");
PersonaModule.AddLoopingInterpolation(AnimSequences);
}
}
void FAnimationEditor::OnRemoveBoneTrack()
{
if ( FMessageDialog::Open(EAppMsgType::YesNo, LOCTEXT("WarningOnRemovingBoneTracks", "This will clear all bone transform of the animation, source data, and edited layer information. This doesn't remove notifies, and curves. Do you want to continue?")) == EAppReturnType::Yes)
{
UAnimSequence* AnimSequence = Cast<UAnimSequence>(AnimationAsset);
if (AnimSequence)
{
UAnimDataController* Controller = AnimSequence->GetController();
UAnimDataController::FScopedBracket ScopedBracket(Controller, LOCTEXT("OnRemoveBoneTrack_Bracket", "Removing all Bone Animation and Transform Curve Tracks"));
Controller->RemoveAllBoneTracks();
Controller->RemoveAllCurvesOfType(ERawCurveTrackTypes::RCT_Transform);
}
}
}
TSharedRef< SWidget > FAnimationEditor::GenerateExportAssetMenu() const
{
const bool bShouldCloseWindowAfterMenuSelection = true;
FMenuBuilder MenuBuilder(bShouldCloseWindowAfterMenuSelection, GetToolkitCommands());
FillExportAssetMenu(MenuBuilder);
return MenuBuilder.MakeWidget();
}
void FAnimationEditor::FillCopyToSoundWaveMenu(FMenuBuilder& MenuBuilder) const
{
FAssetPickerConfig AssetPickerConfig;
AssetPickerConfig.Filter.ClassNames.Add(*USoundWave::StaticClass()->GetName());
AssetPickerConfig.bAllowNullSelection = false;
AssetPickerConfig.OnAssetSelected = FOnAssetSelected::CreateRaw(this, &FAnimationEditor::CopyCurveToSoundWave);
AssetPickerConfig.InitialAssetViewType = EAssetViewType::List;
FContentBrowserModule& ContentBrowserModule = FModuleManager::Get().LoadModuleChecked<FContentBrowserModule>(TEXT("ContentBrowser"));
MenuBuilder.AddWidget(
SNew(SBox)
.WidthOverride(300.0f)
.HeightOverride(300.0f)
[
ContentBrowserModule.Get().CreateAssetPicker(AssetPickerConfig)
],
FText::GetEmpty()
);
}
void FAnimationEditor::FillExportAssetMenu(FMenuBuilder& MenuBuilder) const
{
MenuBuilder.BeginSection("AnimationExport", LOCTEXT("ExportAssetMenuHeading", "Export"));
{
MenuBuilder.AddMenuEntry(FAnimationEditorCommands::Get().ExportToFBX_AnimData);
MenuBuilder.AddMenuEntry(FAnimationEditorCommands::Get().ExportToFBX_PreviewMesh);
}
MenuBuilder.EndSection();
}
FRichCurve* FindOrAddCurve(UCurveTable* CurveTable, FName CurveName)
{
// Grab existing curve (if present)
FRichCurve* Curve = CurveTable->FindRichCurve(CurveName, FString());
if (Curve == nullptr)
{
// Or allocate new curve
Curve = &CurveTable->AddRichCurve(CurveName);
}
return Curve;
}
void FAnimationEditor::CopyCurveToSoundWave(const FAssetData& SoundWaveAssetData) const
{
USoundWave* SoundWave = Cast<USoundWave>(SoundWaveAssetData.GetAsset());
UAnimSequence* Sequence = Cast<UAnimSequence>(AnimationAsset);
if (!SoundWave || !Sequence)
{
return;
}
// If no internal table, create one now
if (!SoundWave->GetInternalCurveData())
{
static const FName InternalCurveTableName("InternalCurveTable");
UCurveTable* NewCurves = NewObject<UCurveTable>(SoundWave, InternalCurveTableName);
NewCurves->ClearFlags(RF_Public);
NewCurves->SetFlags(NewCurves->GetFlags() | RF_Standalone | RF_Transactional);
SoundWave->SetCurveData(NewCurves);
SoundWave->SetInternalCurveData(NewCurves);
}
UCurveTable* CurveTable = SoundWave->GetInternalCurveData();
// iterate over curves in anim data
for (const FFloatCurve& FloatCurve : Sequence->GetDataModel()->GetFloatCurves())
{
FRichCurve* Curve = FindOrAddCurve(CurveTable, FloatCurve.Name.DisplayName);
*Curve = FloatCurve.FloatCurve; // copy data
}
// we will need to add a curve to tell us the time we want to start playing audio
float PreRollTime = 0.f;
static const FName AudioCurveName("Audio");
FRichCurve* AudioCurve = FindOrAddCurve(CurveTable, AudioCurveName);
AudioCurve->Reset();
AudioCurve->AddKey(PreRollTime, 1.0f);
// Mark dirty after
SoundWave->MarkPackageDirty();
FNotificationInfo Notification(FText::Format(LOCTEXT("AddedClassSuccessNotification", "Copied curves to {0}"), FText::FromString(SoundWave->GetName())));
FSlateNotificationManager::Get().AddNotification(Notification);
// Close menu after picking sound
FSlateApplication::Get().DismissAllMenus();
}
void FAnimationEditor::ConditionalRefreshEditor(UObject* InObject)
{
bool bInterestingAsset = true;
if (InObject != GetPersonaToolkit()->GetSkeleton() && (GetPersonaToolkit()->GetSkeleton() && InObject != GetPersonaToolkit()->GetSkeleton()->GetPreviewMesh()) && Cast<UAnimationAsset>(InObject) != AnimationAsset)
{
bInterestingAsset = false;
}
// Check that we aren't a montage that uses an incoming animation
if (UAnimMontage* Montage = Cast<UAnimMontage>(AnimationAsset))
{
for (FSlotAnimationTrack& Slot : Montage->SlotAnimTracks)
{
if (bInterestingAsset)
{
break;
}
for (FAnimSegment& Segment : Slot.AnimTrack.AnimSegments)
{
if (Segment.AnimReference == InObject)
{
bInterestingAsset = true;
break;
}
}
}
}
if (GetPersonaToolkit()->GetSkeleton() == nullptr)
{
bInterestingAsset = false;
}
if (bInterestingAsset)
{
GetPersonaToolkit()->GetPreviewScene()->InvalidateViews();
OpenNewAnimationDocumentTab(Cast<UAnimationAsset>(InObject));
}
}
void FAnimationEditor::HandlePostReimport(UObject* InObject, bool bSuccess)
{
if (bSuccess)
{
ConditionalRefreshEditor(InObject);
}
}
void FAnimationEditor::HandlePostImport(UFactory* InFactory, UObject* InObject)
{
ConditionalRefreshEditor(InObject);
}
void FAnimationEditor::HandleAnimationSequenceBrowserCreated(const TSharedRef<IAnimationSequenceBrowser>& InSequenceBrowser)
{
SequenceBrowser = InSequenceBrowser;
}
bool FAnimationEditor::RecordMeshToAnimation(USkeletalMeshComponent* PreviewComponent, UAnimSequence* NewAsset) const
{
ISequenceRecorder& RecorderModule = FModuleManager::Get().LoadModuleChecked<ISequenceRecorder>("SequenceRecorder");
return RecorderModule.RecordSingleNodeInstanceToAnimation(PreviewComponent, NewAsset);
}
#undef LOCTEXT_NAMESPACE