Refactoring

This commit is contained in:
Ivan Anikin
2026-05-21 12:02:28 +07:00
parent 86baa476f4
commit 46cf6a4396
906 changed files with 2434 additions and 6456 deletions
+1 -1
View File
@@ -8,7 +8,7 @@ public class FunnyGirl : ModuleRules
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "HTTP", "Json", "JsonUtilities", "AssetRegistry", "MediaAssets", "UMG" });
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "HTTP", "Json", "JsonUtilities", "AssetRegistry", "MediaAssets", "UMG", "IKRig" });
PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
@@ -2,6 +2,8 @@
#include "Morph/MorphControllerComponent.h"
#include "Morph/MorphPresetDataAsset.h"
#include "Animation/AnimInstance.h"
#include "Animation/AnimMontage.h"
#include "Components/SkeletalMeshComponent.h"
#include "GameFramework/Character.h"
#include "Engine/World.h"
@@ -71,6 +73,16 @@ USkeletalMeshComponent* UMorphControllerComponent::ResolveMeshComponent() const
return nullptr;
}
USkeletalMeshComponent* UMorphControllerComponent::ResolveAnimationMeshComponent() const
{
if (AnimationMeshComponent)
{
return AnimationMeshComponent;
}
return ResolveMeshComponent();
}
bool UMorphControllerComponent::PresetHasAnyPositiveCurveValue(const UMorphPresetDataAsset& Preset) const
{
for (const FMorphTimelineClip& Clip : Preset.MorphTargets)
@@ -206,6 +218,11 @@ bool UMorphControllerComponent::AddPreset(UMorphPresetDataAsset* Preset, float W
UE_LOG(LogMorphController, Warning, TEXT("AddPreset accepted on %s, but no SkeletalMeshComponent was found. Set TargetMeshComponent or place the component on an actor with a skeletal mesh."), *GetNameSafe(GetOwner()));
}
if (bPlayPresetMontages)
{
PlayPresetMontage(Preset);
}
if (!PresetHasAnyPositiveCurveValue(*Preset) && bLogWarnings)
{
UE_LOG(LogMorphController, Warning, TEXT("AddPreset accepted on %s, but preset %s has no positive curve key values. Default curve keys are 0, so raise keys in the clip curve editor to see morph output."), *GetNameSafe(GetOwner()), *GetNameSafe(Preset));
@@ -250,6 +267,54 @@ bool UMorphControllerComponent::PlayPreset(UMorphPresetDataAsset* Preset, float
return AddPreset(Preset, Weight);
}
bool UMorphControllerComponent::PlayPresetMontage(UMorphPresetDataAsset* Preset)
{
if (!Preset)
{
return false;
}
UAnimMontage* Montage = Preset->GetMontage();
if (!Montage)
{
return false;
}
USkeletalMeshComponent* Mesh = ResolveAnimationMeshComponent();
if (!Mesh)
{
if (bLogWarnings)
{
UE_LOG(LogMorphController, Warning, TEXT("PlayPresetMontage ignored on %s: no animation mesh component was found for montage %s."), *GetNameSafe(GetOwner()), *GetNameSafe(Montage));
}
return false;
}
UAnimInstance* AnimInstance = Mesh->GetAnimInstance();
if (!AnimInstance)
{
if (bLogWarnings)
{
UE_LOG(LogMorphController, Warning, TEXT("PlayPresetMontage ignored on %s: animation mesh %s has no AnimInstance."), *GetNameSafe(GetOwner()), *GetNameSafe(Mesh));
}
return false;
}
const float PlayRate = FMath::Max(Preset->MontagePlayRate, 0.01f);
const float Duration = AnimInstance->Montage_Play(Montage, PlayRate, EMontagePlayReturnType::MontageLength, 0.0f);
return Duration > 0.0f;
}
void UMorphControllerComponent::SetMontageMeshComponent(USkeletalMeshComponent* InMontageMeshComponent)
{
AnimationMeshComponent = InMontageMeshComponent;
}
USkeletalMeshComponent* UMorphControllerComponent::GetMontageMeshComponent() const
{
return ResolveAnimationMeshComponent();
}
void UMorphControllerComponent::RemovePreset(UMorphPresetDataAsset* Preset)
{
if (!Preset) return;
@@ -301,6 +366,10 @@ void UMorphControllerComponent::SetActivePresets(const TArray<FActiveMorphPreset
if (NewSlot.TimelineDuration > 0.0f)
{
ActivePresets.Add(NewSlot);
if (bPlayPresetMontages)
{
PlayPresetMontage(NewSlot.Preset);
}
}
}
}
@@ -2,6 +2,8 @@
#include "Morph/MorphPresetDataAsset.h"
#include "Animation/AnimMontage.h"
namespace
{
constexpr float MorphPresetMinMorphTargetValue = -1.0f;
@@ -64,6 +66,10 @@ float UMorphPresetDataAsset::GetTimelineLength() const
MaxEnd = FMath::Max(MaxEnd, Clip.StartTime + Clip.Duration);
}
}
if (const UAnimMontage* PreviewMontage = GetMontage())
{
MaxEnd = FMath::Max(MaxEnd, FMath::Max(MontageStartTime, 0.0f) + PreviewMontage->GetPlayLength());
}
return MaxEnd;
}
@@ -105,6 +105,22 @@ bool SaveLightSaveObject(const TSharedRef<FJsonObject>& SaveObject, FString& Out
return true;
}
bool DeleteDirectoryIfExists(const FString& Directory, FString& OutError)
{
if (!IFileManager::Get().DirectoryExists(*Directory))
{
return true;
}
if (!IFileManager::Get().DeleteDirectory(*Directory, false, true))
{
OutError = FString::Printf(TEXT("Could not delete save directory: %s"), *Directory);
return false;
}
return true;
}
}
bool UFunnyGirlUtilityBlueprintLibrary::SaveMorphTargetNamesToFile(
@@ -361,6 +377,25 @@ bool UFunnyGirlUtilityBlueprintLibrary::DeleteLightString(const FString& Field,
return SaveLightSaveObject(SaveObject.ToSharedRef(), OutError);
}
bool UFunnyGirlUtilityBlueprintLibrary::ClearFunnyGirlSaves(FString& OutError)
{
OutError.Reset();
const FString ChatDirectory = FPaths::ProjectSavedDir() / TEXT("Chat");
if (!DeleteDirectoryIfExists(ChatDirectory, OutError))
{
return false;
}
const FString LightSaveDirectory = FPaths::GetPath(GetLightSaveFilePath());
if (!DeleteDirectoryIfExists(LightSaveDirectory, OutError))
{
return false;
}
return true;
}
bool UFunnyGirlUtilityBlueprintLibrary::OpenStreamMediaSourceUrl(UMediaPlayer* MediaPlayer, UStreamMediaSource* StreamMediaSource, const FString& Url)
{
if (!MediaPlayer || !StreamMediaSource || !Url.Contains(TEXT("://")))
@@ -8,6 +8,7 @@
#include "MorphControllerComponent.generated.h"
class USkeletalMeshComponent;
class UAnimMontage;
UENUM(BlueprintType)
enum class EMorphLayerBlendMode : uint8
@@ -62,6 +63,18 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Morph Controller")
TObjectPtr<USkeletalMeshComponent> TargetMeshComponent;
/** Mesh that owns body animation montages. Use this when the visible girl mesh retargets/copies motion from a separate animation mesh. Falls back to Target Mesh Component when unset. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Morph Controller|Montage", meta = (
DisplayName = "Montage Mesh Component",
ToolTip = "Skeletal mesh component where preset montages are played. Leave empty to use the morph target mesh."))
TObjectPtr<USkeletalMeshComponent> AnimationMeshComponent;
/** Automatically play a preset's optional Montage on Animation Mesh Component when the preset starts. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Morph Controller|Montage", meta = (
DisplayName = "Play Preset Montages",
ToolTip = "When enabled, Add Preset, Play Morph Preset, and Set Active Presets play the montage stored in each preset."))
bool bPlayPresetMontages = true;
/** Speed used when blending preset weight toward the requested weight. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Morph Controller", meta = (ClampMin = "0.1"))
float BlendSpeed = 4.0f;
@@ -112,6 +125,18 @@ public:
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Morph Controller", meta = (DisplayName = "Play Morph Preset"))
bool PlayPreset(UMorphPresetDataAsset* Preset, float Weight = 1.0f);
/** Play the montage assigned inside a morph preset on the Animation Mesh Component. */
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Morph Controller|Montage", meta = (DisplayName = "Play Preset Montage"))
bool PlayPresetMontage(UMorphPresetDataAsset* Preset);
/** Set the mesh component used for preset montage playback. */
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Morph Controller|Montage", meta = (DisplayName = "Set Montage Mesh Component"))
void SetMontageMeshComponent(USkeletalMeshComponent* InMontageMeshComponent);
/** Get the explicit montage mesh component, or the resolved fallback mesh when none is set. */
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "FunnyGirl|Morph Controller|Montage", meta = (DisplayName = "Get Montage Mesh Component"))
USkeletalMeshComponent* GetMontageMeshComponent() const;
/** Remove a preset from active playback immediately. */
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Morph Controller", meta = (DisplayName = "Remove Preset"))
void RemovePreset(UMorphPresetDataAsset* Preset);
@@ -205,6 +230,7 @@ private:
bool bMissingMeshWarningIssued = false;
USkeletalMeshComponent* ResolveMeshComponent() const;
USkeletalMeshComponent* ResolveAnimationMeshComponent() const;
bool PresetHasAnyPositiveCurveValue(const UMorphPresetDataAsset& Preset) const;
void UpdateMorphTargets(float DeltaTime);
void BlendMorphLayer(const TMap<FName, float>& LayerValues, float LayerWeight, EMorphLayerBlendMode BlendMode, TMap<FName, float>& InOutFinalValues) const;
@@ -3,9 +3,11 @@
#pragma once
#include "CoreMinimal.h"
#include "Animation/AnimMontage.h"
#include "Curves/CurveFloat.h"
#include "Engine/DataAsset.h"
#include "Engine/SkeletalMesh.h"
#include "Retargeter/IKRetargeter.h"
#include "MorphPresetDataAsset.generated.h"
USTRUCT(BlueprintType)
@@ -41,6 +43,11 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Morph Preset|Preview", meta = (DisplayName = "Preview Mesh"))
TSoftObjectPtr<USkeletalMesh> PreviewMesh;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Morph Preset|Preview|Montage", meta = (
DisplayName = "Preview IK Retargeter",
ToolTip = "IK Retargeter used by preview to drive the girl mesh from the animation mesh, matching the Retarget Pose From Mesh AnimBP path."))
TSoftObjectPtr<UIKRetargeter> PreviewIKRetargeter;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Morph Preset", meta = (DisplayName = "Display Name"))
FText DisplayName;
@@ -52,6 +59,23 @@ public:
ToolTip = "Morph clips placed on the preset timeline."))
TArray<FMorphTimelineClip> MorphTargets;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Morph Preset|Montage", meta = (
DisplayName = "Montage",
ToolTip = "Optional body montage associated with this emotion. Runtime should play it on the animation/source mesh, while morphs stay on the girl mesh."))
TSoftObjectPtr<UAnimMontage> Montage;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Morph Preset|Montage", meta = (
DisplayName = "Runtime Montage Play Rate",
ToolTip = "Play rate used when the morph controller plays this montage at runtime. The editor timeline preview keeps the block at the montage asset's real length.",
ClampMin = "0.01"))
float MontagePlayRate = 1.0f;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Morph Preset|Montage", meta = (
DisplayName = "Montage Timeline Start",
ToolTip = "Time on the morph preset timeline where the montage block starts in preview.",
ClampMin = "0"))
float MontageStartTime = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Morph Preset", meta = (
DisplayName = "Duration",
ToolTip = "Preset duration in seconds. 0 means auto-calculate from clips.",
@@ -81,6 +105,9 @@ public:
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Morph Preset")
float GetTimelineLength() const;
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Morph Preset|Montage")
UAnimMontage* GetMontage() const { return Montage.LoadSynchronous(); }
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Morph Preset")
bool HasPlayableContent() const { return IsValidPreset(); }
@@ -56,6 +56,11 @@ public:
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Save")
static bool DeleteLightString(const FString& Field, FString& OutError);
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Save", meta = (
DisplayName = "Clear Funny Girl Saves",
ToolTip = "Deletes runtime save data written by Funny Girl systems: chat/player/semantic memory and light string saves. Exported files are not deleted."))
static bool ClearFunnyGirlSaves(FString& OutError);
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Media")
static bool OpenStreamMediaSourceUrl(UMediaPlayer* MediaPlayer, UStreamMediaSource* StreamMediaSource, const FString& Url);
};
@@ -10,6 +10,6 @@ public class FunnyGirlEditor : ModuleRules
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "FunnyGirl" });
PrivateDependencyModuleNames.AddRange(new string[] { "AdvancedPreviewScene", "AnimationDataController", "AnimGraph", "AssetTools", "AssetRegistry", "ContentBrowser", "EditorWidgets", "InputCore", "Json", "JsonUtilities", "KismetWidgets", "LevelEditor", "Persona", "PropertyEditor", "SequencerWidgets", "Slate", "SlateCore", "ToolMenus", "UnrealEd" });
PrivateDependencyModuleNames.AddRange(new string[] { "AdvancedPreviewScene", "AnimationDataController", "AnimGraph", "AssetTools", "AssetRegistry", "ContentBrowser", "EditorWidgets", "InputCore", "IKRig", "Json", "JsonUtilities", "KismetWidgets", "LevelEditor", "Persona", "PropertyEditor", "SequencerWidgets", "Slate", "SlateCore", "ToolMenus", "UnrealEd" });
}
}
@@ -2,6 +2,8 @@
#include "FunnyGirlEditorModule.h"
#include "AssetRegistry/AssetRegistryModule.h"
#include "AssetRegistry/IAssetRegistry.h"
#include "AssetTypeActions/AssetTypeActions_DirectorPrompt.h"
#include "AssetTypeActions/AssetTypeActions_Identity.h"
#include "AssetTypeActions/AssetTypeActions_LLMProvider.h"
@@ -11,12 +13,16 @@
#include "AssetToolsModule.h"
#include "IAssetTools.h"
#include "IAssetTypeActions.h"
#include "Misc/FileHelper.h"
#include "Misc/MessageDialog.h"
#include "Misc/Paths.h"
#include "Modules/ModuleManager.h"
#include "Morph/Editor/LipSyncMappingAssetTypeActions.h"
#include "Morph/Editor/MorphPresetAssetTypeActions.h"
#include "Morph/MorphPresetDataAsset.h"
#include "Morph/Editor/MorphPresetDetailsCustomization.h"
#include "PropertyEditorModule.h"
#include "ToolMenus.h"
#define LOCTEXT_NAMESPACE "FunnyGirlEditor"
@@ -44,10 +50,18 @@ void FFunnyGirlEditorModule::StartupModule()
FOnGetDetailCustomizationInstance::CreateStatic(&FMorphPresetDetailsCustomization::MakeInstance));
PropertyEditorModule.NotifyCustomizationModuleChanged();
bMorphPresetCustomizationRegistered = true;
UToolMenus::RegisterStartupCallback(FSimpleMulticastDelegate::FDelegate::CreateRaw(this, &FFunnyGirlEditorModule::RegisterMenus));
}
void FFunnyGirlEditorModule::ShutdownModule()
{
if (UObjectInitialized() && FModuleManager::Get().IsModuleLoaded(TEXT("ToolMenus")))
{
UToolMenus::UnRegisterStartupCallback(this);
UToolMenus::UnregisterOwner(this);
}
if (FModuleManager::Get().IsModuleLoaded(TEXT("AssetTools")))
{
IAssetTools& AssetTools = FModuleManager::GetModuleChecked<FAssetToolsModule>("AssetTools").Get();
@@ -88,6 +102,91 @@ void FFunnyGirlEditorModule::RegisterAssetTypeAction(const TSharedRef<IAssetType
RegisteredAssetTypeActions.Add(AssetTypeAction);
}
void FFunnyGirlEditorModule::RegisterMenus()
{
FToolMenuOwnerScoped OwnerScoped(this);
UToolMenu* ToolsMenu = UToolMenus::Get()->ExtendMenu(TEXT("LevelEditor.MainMenu.Tools"));
if (!ToolsMenu)
{
return;
}
FToolMenuSection& Section = ToolsMenu->FindOrAddSection(TEXT("FunnyGirl"));
Section.Label = LOCTEXT("FunnyGirlToolsSection", "Funny Girl");
Section.AddMenuEntry(
TEXT("ExportMorphPresetDescriptions"),
LOCTEXT("ExportMorphPresetDescriptionsLabel", "Export Morph Preset Descriptions"),
LOCTEXT("ExportMorphPresetDescriptionsTooltip", "Export all Morph Preset assets to Saved/Exports/MorphPresetDescriptions.tsv."),
FSlateIcon(),
FUIAction(FExecuteAction::CreateRaw(this, &FFunnyGirlEditorModule::ExportMorphPresetDescriptions)));
}
void FFunnyGirlEditorModule::ExportMorphPresetDescriptions()
{
FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked<FAssetRegistryModule>(TEXT("AssetRegistry"));
IAssetRegistry& AssetRegistry = AssetRegistryModule.Get();
AssetRegistry.SearchAllAssets(true);
FARFilter Filter;
Filter.ClassPaths.Add(UMorphPresetDataAsset::StaticClass()->GetClassPathName());
Filter.bRecursiveClasses = true;
TArray<FAssetData> PresetAssets;
AssetRegistry.GetAssets(Filter, PresetAssets);
PresetAssets.Sort([](const FAssetData& A, const FAssetData& B)
{
return A.GetSoftObjectPath().ToString() < B.GetSoftObjectPath().ToString();
});
FString Output;
Output += TEXT("AssetName\tPresetName\tDescription\n");
for (const FAssetData& AssetData : PresetAssets)
{
const UMorphPresetDataAsset* Preset = Cast<UMorphPresetDataAsset>(AssetData.GetAsset());
if (!Preset)
{
continue;
}
FString AssetName = AssetData.AssetName.ToString();
FString PresetName = Preset->DisplayName.ToString().TrimStartAndEnd();
FString Description = Preset->Description.ToString().TrimStartAndEnd();
if (PresetName.IsEmpty())
{
PresetName = AssetName;
}
AssetName.ReplaceInline(TEXT("\t"), TEXT(" "));
PresetName.ReplaceInline(TEXT("\t"), TEXT(" "));
Description.ReplaceInline(TEXT("\t"), TEXT(" "));
Description.ReplaceInline(TEXT("\r\n"), TEXT(" "));
Description.ReplaceInline(TEXT("\n"), TEXT(" "));
Description.ReplaceInline(TEXT("\r"), TEXT(" "));
Output += FString::Printf(TEXT("%s\t%s\t%s\n"), *AssetName, *PresetName, *Description);
}
const FString ExportDir = FPaths::Combine(FPaths::ProjectSavedDir(), TEXT("Exports"));
IFileManager::Get().MakeDirectory(*ExportDir, true);
const FString ExportPath = FPaths::Combine(ExportDir, TEXT("MorphPresetDescriptions.tsv"));
if (!FFileHelper::SaveStringToFile(Output, *ExportPath, FFileHelper::EEncodingOptions::ForceUTF8WithoutBOM))
{
FMessageDialog::Open(EAppMsgType::Ok, FText::Format(
LOCTEXT("ExportMorphPresetDescriptionsFailed", "Failed to write Morph Preset descriptions to:\n{0}"),
FText::FromString(ExportPath)));
return;
}
FMessageDialog::Open(EAppMsgType::Ok, FText::Format(
LOCTEXT("ExportMorphPresetDescriptionsSuccess", "Exported {0} Morph Preset assets to:\n{1}"),
FText::AsNumber(PresetAssets.Num()),
FText::FromString(ExportPath)));
}
#undef LOCTEXT_NAMESPACE
IMPLEMENT_MODULE(FFunnyGirlEditorModule, FunnyGirlEditor)
@@ -24,4 +24,6 @@ private:
bool bMorphPresetCustomizationRegistered = false;
void RegisterAssetTypeAction(const TSharedRef<IAssetTypeActions>& AssetTypeAction);
void RegisterMenus();
void ExportMorphPresetDescriptions();
};
@@ -60,6 +60,10 @@ namespace
TargetPreset.DisplayName = SourcePreset.DisplayName;
TargetPreset.Description = SourcePreset.Description;
TargetPreset.MorphTargets = SourcePreset.MorphTargets;
TargetPreset.Montage = SourcePreset.Montage;
TargetPreset.MontagePlayRate = SourcePreset.MontagePlayRate;
TargetPreset.MontageStartTime = SourcePreset.MontageStartTime;
TargetPreset.PreviewIKRetargeter = SourcePreset.PreviewIKRetargeter;
TargetPreset.Duration = SourcePreset.Duration;
TargetPreset.bLoop = SourcePreset.bLoop;
TargetPreset.LoopDelay = SourcePreset.LoopDelay;
@@ -3,6 +3,9 @@
#include "Morph/Editor/MorphPresetEditorController.h"
#include "Animation/AnimData/IAnimationDataController.h"
#include "Animation/AnimCompositeBase.h"
#include "Animation/AnimMontage.h"
#include "Animation/AnimSingleNodeInstance.h"
#include "AnimPreviewInstance.h"
#include "Animation/AnimSequence.h"
#include "Animation/DebugSkelMeshComponent.h"
@@ -18,8 +21,10 @@
#include "Morph/LipSyncMappingDataAsset.h"
#include "Morph/MorphControllerComponent.h"
#include "Morph/MorphPresetDataAsset.h"
#include "Morph/Editor/MorphPresetRetargetPreviewAnimInstance.h"
#include "PersonaModule.h"
#include "ReferenceSkeleton.h"
#include "Retargeter/IKRetargeter.h"
#include "Sound/SoundBase.h"
DEFINE_LOG_CATEGORY_STATIC(LogMorphPresetEditorPreview, Log, All);
@@ -31,6 +36,7 @@ namespace
IAnimationDataController& Controller = AnimSequence.GetController();
Controller.SetNumberOfFrames(FMath::Max(Controller.ConvertSecondsToFrameNumber(PlayLength).Value, 1), bShouldTransact);
}
}
FMorphPresetEditorController::FMorphPresetEditorController(UMorphPresetDataAsset* InPreset)
@@ -45,6 +51,7 @@ FMorphPresetEditorController::~FMorphPresetEditorController()
{
StopLipSyncPreviewAudio();
DestroyLipSyncPreviewComponents();
DestroyAnimationPreviewMesh();
if (PersonaToolkit.IsValid())
{
@@ -76,6 +83,36 @@ USkeletalMesh* FMorphPresetEditorController::GetPreviewMesh() const
return nullptr;
}
USkeletalMesh* FMorphPresetEditorController::GetAnimationPreviewMesh() const
{
if (UIKRetargeter* Retargeter = GetPreviewIKRetargeter())
{
return Retargeter->GetPreviewMesh(ERetargetSourceOrTarget::Source);
}
return nullptr;
}
UAnimMontage* FMorphPresetEditorController::GetPreviewMontage() const
{
if (const UMorphPresetDataAsset* PresetAsset = Preset.Get())
{
return PresetAsset->Montage.LoadSynchronous();
}
return nullptr;
}
UIKRetargeter* FMorphPresetEditorController::GetPreviewIKRetargeter() const
{
if (const UMorphPresetDataAsset* PresetAsset = Preset.Get())
{
return PresetAsset->PreviewIKRetargeter.LoadSynchronous();
}
return nullptr;
}
bool FMorphPresetEditorController::SetPreviewMesh(USkeletalMesh* InPreviewMesh)
{
UMorphPresetDataAsset* PresetAsset = Preset.Get();
@@ -93,7 +130,71 @@ bool FMorphPresetEditorController::SetPreviewMesh(USkeletalMesh* InPreviewMesh)
PresetAsset->PreviewMesh = InPreviewMesh;
PresetAsset->MarkPackageDirty();
RebuildPreviewScene();
if (!InPreviewMesh || !GetPreviewScene().IsValid())
{
RebuildPreviewScene();
PresetChanged.Broadcast();
return true;
}
if (TSharedPtr<IPersonaPreviewScene> PreviewScene = GetPreviewScene())
{
StopLipSyncPreviewAudio();
DestroyLipSyncPreviewComponents();
DestroyAnimationPreviewMesh();
PreviewScene->SetPreviewMesh(InPreviewMesh, false);
ActivePreviewMesh = InPreviewMesh;
RebuildPreviewAnimation();
RebuildAnimationPreviewMesh();
RebuildLipSyncPreviewComponents();
SetCurrentTime(CurrentTime);
PresetChanged.Broadcast();
}
return true;
}
bool FMorphPresetEditorController::SetPreviewMontage(UAnimMontage* InMontage)
{
UMorphPresetDataAsset* PresetAsset = Preset.Get();
if (!PresetAsset)
{
return false;
}
if (PresetAsset->Montage.Get() == InMontage)
{
return false;
}
PresetAsset->Modify();
PresetAsset->Montage = InMontage;
PresetAsset->MarkPackageDirty();
RebuildAnimationPreviewMesh();
PlaybackStateChanged.Broadcast();
return true;
}
bool FMorphPresetEditorController::SetPreviewIKRetargeter(UIKRetargeter* InRetargeter)
{
UMorphPresetDataAsset* PresetAsset = Preset.Get();
if (!PresetAsset)
{
return false;
}
if (PresetAsset->PreviewIKRetargeter.Get() == InRetargeter)
{
return false;
}
PresetAsset->Modify();
PresetAsset->PreviewIKRetargeter = InRetargeter;
PresetAsset->MarkPackageDirty();
ConfigureRetargetPreview();
PlaybackStateChanged.Broadcast();
return true;
}
@@ -112,7 +213,12 @@ float FMorphPresetEditorController::GetTimelineLength() const
{
if (const UMorphPresetDataAsset* PresetAsset = Preset.Get())
{
return FMath::Max(PresetAsset->GetTimelineLength(), 0.1f);
float TimelineLength = FMath::Max(PresetAsset->GetTimelineLength(), 0.1f);
if (const UAnimMontage* Montage = PresetAsset->GetMontage())
{
TimelineLength = FMath::Max(TimelineLength, FMath::Max(PresetAsset->MontageStartTime, 0.0f) + Montage->GetPlayLength());
}
return FMath::Max(TimelineLength, 0.1f);
}
return 1.0f;
@@ -134,8 +240,11 @@ void FMorphPresetEditorController::SetCurrentTime(float InTime)
if (PreviewMeshComponent->PreviewInstance)
{
PreviewMeshComponent->PreviewInstance->SetPlaying(false);
PreviewMeshComponent->PreviewInstance->SetPosition(CurrentTime, false);
RefreshPreviewAnimationPose();
if (!HasRetargetPreview())
{
PreviewMeshComponent->PreviewInstance->SetPosition(CurrentTime, false);
RefreshPreviewAnimationPose();
}
}
}
}
@@ -224,6 +333,12 @@ void FMorphPresetEditorController::NotifyPresetEdited(bool bBroadcastPresetChang
return;
}
if (ActiveAnimationPreviewMesh.Get() != GetAnimationPreviewMesh() ||
ActivePreviewIKRetargeter.Get() != GetPreviewIKRetargeter())
{
RebuildAnimationPreviewMesh();
}
if (!PreviewAnimation.IsValid())
{
RebuildPreviewAnimation();
@@ -261,6 +376,12 @@ void FMorphPresetEditorController::TogglePlaying()
void FMorphPresetEditorController::Tick(float DeltaTime)
{
if (LastTickFrameNumber == GFrameCounter)
{
return;
}
LastTickFrameNumber = GFrameCounter;
if (bIsPlaying)
{
float NewTime = CurrentTime + DeltaTime;
@@ -422,8 +543,12 @@ void FMorphPresetEditorController::RebuildPreviewScene()
{
StopLipSyncPreviewAudio();
DestroyLipSyncPreviewComponents();
DestroyAnimationPreviewMesh();
PersonaToolkit.Reset();
PreviewAnimation.Reset();
AnimationPreviewMeshComponent.Reset();
ActiveAnimationPreviewMesh.Reset();
ActivePreviewIKRetargeter.Reset();
USkeletalMesh* PreviewMesh = GetPreviewMesh();
ActivePreviewMesh = PreviewMesh;
@@ -438,6 +563,7 @@ void FMorphPresetEditorController::RebuildPreviewScene()
PersonaToolkit = PersonaModule.CreatePersonaToolkit(PreviewMesh, ToolkitArgs);
RebuildPreviewAnimation();
RebuildAnimationPreviewMesh();
RebuildLipSyncPreviewComponents();
SetCurrentTime(CurrentTime);
PreviewSceneChanged.Broadcast();
@@ -483,6 +609,205 @@ void FMorphPresetEditorController::RebuildPreviewAnimation()
}
}
void FMorphPresetEditorController::RebuildAnimationPreviewMesh()
{
TSharedPtr<IPersonaPreviewScene> PreviewScene = GetPreviewScene();
if (!PreviewScene.IsValid())
{
return;
}
DestroyAnimationPreviewMesh();
UMorphPresetDataAsset* PresetAsset = Preset.Get();
USkeletalMesh* AnimationMesh = GetAnimationPreviewMesh();
ActiveAnimationPreviewMesh = AnimationMesh;
PreviewStatusText = FText::GetEmpty();
if (!PresetAsset || !AnimationMesh)
{
ConfigureRetargetPreview();
return;
}
AActor* PreviewActor = PreviewScene->GetActor();
if (!PreviewActor)
{
return;
}
USkeletalMeshComponent* AnimationMeshComponent = NewObject<USkeletalMeshComponent>(PreviewActor, NAME_None, RF_Transient);
if (!AnimationMeshComponent)
{
return;
}
AnimationMeshComponent->SetSkeletalMesh(AnimationMesh);
AnimationMeshComponent->SetAnimationMode(EAnimationMode::AnimationSingleNode);
AnimationMeshComponent->bSelectable = false;
AnimationMeshComponent->SetVisibility(false, true);
AnimationMeshComponent->VisibilityBasedAnimTickOption = EVisibilityBasedAnimTickOption::AlwaysTickPoseAndRefreshBones;
const FVector SourceMeshOffset = GetPreviewIKRetargeter() ? GetPreviewIKRetargeter()->SourceMeshOffset : FVector(0.0f, 80.0f, 0.0f);
PreviewScene->AddComponent(AnimationMeshComponent, FTransform(SourceMeshOffset));
AnimationPreviewMeshComponent = AnimationMeshComponent;
if (UAnimMontage* Montage = GetPreviewMontage())
{
float MontageAssetTime = 0.0f;
float MontageAssetLength = Montage->GetPlayLength();
UAnimationAsset* PreviewAsset = ResolveMontagePreviewAnimation(MontageAssetTime, MontageAssetLength);
AnimationMeshComponent->SetAnimation(PreviewAsset ? PreviewAsset : Montage);
AnimationMeshComponent->SetPlayRate(1.0f);
AnimationMeshComponent->SetPosition(FMath::Clamp(MontageAssetTime, 0.0f, MontageAssetLength), false);
if (UAnimSingleNodeInstance* SingleNodeInstance = AnimationMeshComponent->GetSingleNodeInstance())
{
SingleNodeInstance->SetLooping(PresetAsset->bLoop);
SingleNodeInstance->SetPlaying(false);
}
}
else
{
PreviewStatusText = FText::FromString(TEXT("Animation Preview Mesh is set, but this preset has no Montage assigned."));
}
ConfigureRetargetPreview();
RefreshAnimationPreviewPose();
}
void FMorphPresetEditorController::DestroyAnimationPreviewMesh()
{
ClearRetargetPreview(false);
if (TSharedPtr<IPersonaPreviewScene> PreviewScene = GetPreviewScene())
{
if (USkeletalMeshComponent* ExistingComponent = AnimationPreviewMeshComponent.Get())
{
PreviewScene->RemoveComponent(ExistingComponent);
ExistingComponent->DestroyComponent();
}
}
AnimationPreviewMeshComponent.Reset();
ActiveAnimationPreviewMesh.Reset();
}
void FMorphPresetEditorController::ConfigureRetargetPreview()
{
TSharedPtr<IPersonaPreviewScene> PreviewScene = GetPreviewScene();
if (!PreviewScene.IsValid())
{
return;
}
UDebugSkelMeshComponent* PreviewMeshComponent = PreviewScene->GetPreviewMeshComponent();
USkeletalMeshComponent* SourceMeshComponent = AnimationPreviewMeshComponent.Get();
UIKRetargeter* Retargeter = GetPreviewIKRetargeter();
if (!PreviewMeshComponent || !SourceMeshComponent || !Retargeter)
{
ClearRetargetPreview(true);
return;
}
ClearRetargetPreview(false);
const FName InstanceName = MakeUniqueObjectName(PreviewMeshComponent, UMorphPresetRetargetPreviewAnimInstance::StaticClass(), TEXT("MorphPresetTargetRetargetPreviewInstance"));
UMorphPresetRetargetPreviewAnimInstance* RetargetAnimInstance = NewObject<UMorphPresetRetargetPreviewAnimInstance>(PreviewMeshComponent, InstanceName, RF_Transient);
if (!RetargetAnimInstance)
{
return;
}
PreviewMeshComponent->SetAnimationMode(EAnimationMode::AnimationCustomMode);
PreviewMeshComponent->AnimScriptInstance = RetargetAnimInstance;
PreviewMeshComponent->VisibilityBasedAnimTickOption = EVisibilityBasedAnimTickOption::AlwaysTickPoseAndRefreshBones;
PreviewMeshComponent->SetProcessRootMotionMode(EProcessRootMotionMode::Ignore);
RetargetAnimInstance->Configure(Retargeter, SourceMeshComponent);
RetargetAnimInstance->InitializeAnimation();
RetargetPreviewInstance = RetargetAnimInstance;
ActivePreviewIKRetargeter = Retargeter;
}
void FMorphPresetEditorController::ClearRetargetPreview(bool bRestorePreviewAnimation)
{
TSharedPtr<IPersonaPreviewScene> PreviewScene = GetPreviewScene();
UDebugSkelMeshComponent* PreviewMeshComponent = PreviewScene.IsValid() ? PreviewScene->GetPreviewMeshComponent() : nullptr;
UMorphPresetRetargetPreviewAnimInstance* RetargetAnimInstance = RetargetPreviewInstance.Get();
if (PreviewMeshComponent && RetargetAnimInstance && PreviewMeshComponent->AnimScriptInstance == RetargetAnimInstance)
{
PreviewMeshComponent->ClearAnimScriptInstance();
}
RetargetPreviewInstance.Reset();
if (ActivePreviewIKRetargeter.IsValid())
{
ActivePreviewIKRetargeter.Reset();
if (bRestorePreviewAnimation)
{
RebuildPreviewAnimation();
}
}
}
UAnimationAsset* FMorphPresetEditorController::ResolveMontagePreviewAnimation(float& OutAssetTime, float& OutAssetLength) const
{
OutAssetTime = 0.0f;
OutAssetLength = 0.0f;
const UMorphPresetDataAsset* PresetAsset = Preset.Get();
UAnimMontage* Montage = GetPreviewMontage();
if (!PresetAsset || !Montage)
{
return nullptr;
}
const float MontageTrackTime = FMath::Clamp(CurrentTime - FMath::Max(PresetAsset->MontageStartTime, 0.0f), 0.0f, Montage->GetPlayLength());
const FAnimSegment* FirstSegment = nullptr;
const FAnimSegment* LastSegment = nullptr;
for (const FSlotAnimationTrack& SlotTrack : Montage->SlotAnimTracks)
{
for (const FAnimSegment& Segment : SlotTrack.AnimTrack.AnimSegments)
{
if (!Segment.GetAnimReference())
{
continue;
}
if (!FirstSegment)
{
FirstSegment = &Segment;
}
LastSegment = &Segment;
if (Segment.IsInRange(MontageTrackTime))
{
OutAssetTime = Segment.ConvertTrackPosToAnimPos(MontageTrackTime);
OutAssetLength = Segment.GetAnimReference()->GetPlayLength();
return Segment.GetAnimReference();
}
}
}
const FAnimSegment* FallbackSegment = MontageTrackTime <= 0.0f ? FirstSegment : LastSegment;
if (FallbackSegment && FallbackSegment->GetAnimReference())
{
const float SegmentTrackTime = FMath::Clamp(MontageTrackTime, FallbackSegment->StartPos, FallbackSegment->GetEndPos());
OutAssetTime = FallbackSegment->ConvertTrackPosToAnimPos(SegmentTrackTime);
OutAssetLength = FallbackSegment->GetAnimReference()->GetPlayLength();
return FallbackSegment->GetAnimReference();
}
OutAssetLength = Montage->GetPlayLength();
OutAssetTime = MontageTrackTime;
return Montage;
}
bool FMorphPresetEditorController::HasRetargetPreview() const
{
return ActivePreviewIKRetargeter.IsValid() && AnimationPreviewMeshComponent.IsValid() && RetargetPreviewInstance.IsValid();
}
void FMorphPresetEditorController::RefreshPreviewAnimationPose()
{
TSharedPtr<IPersonaPreviewScene> PreviewScene = GetPreviewScene();
@@ -503,6 +828,36 @@ void FMorphPresetEditorController::RefreshPreviewAnimationPose()
PreviewScene->InvalidateViews();
}
void FMorphPresetEditorController::RefreshAnimationPreviewPose()
{
const UMorphPresetDataAsset* PresetAsset = Preset.Get();
TSharedPtr<IPersonaPreviewScene> PreviewScene = GetPreviewScene();
USkeletalMeshComponent* AnimationMeshComponent = AnimationPreviewMeshComponent.Get();
if (!PresetAsset || !PreviewScene.IsValid() || !AnimationMeshComponent)
{
return;
}
if (UAnimSingleNodeInstance* SingleNodeInstance = AnimationMeshComponent->GetSingleNodeInstance())
{
float MontageAssetTime = 0.0f;
float MontageAssetLength = 0.0f;
UAnimationAsset* PreviewAsset = ResolveMontagePreviewAnimation(MontageAssetTime, MontageAssetLength);
if (PreviewAsset && SingleNodeInstance->GetCurrentAsset() != PreviewAsset)
{
SingleNodeInstance->SetAnimationAsset(PreviewAsset, PresetAsset->bLoop);
}
SingleNodeInstance->SetPlaying(false);
SingleNodeInstance->SetPosition(FMath::Clamp(MontageAssetTime, 0.0f, MontageAssetLength), false);
}
AnimationMeshComponent->TickAnimation(0.0f, false);
AnimationMeshComponent->RefreshBoneTransforms();
AnimationMeshComponent->MarkRenderStateDirty();
PreviewScene->InvalidateViews();
}
void FMorphPresetEditorController::ApplyPresetToPreview()
{
const UMorphPresetDataAsset* PresetAsset = Preset.Get();
@@ -538,6 +893,7 @@ void FMorphPresetEditorController::ApplyPresetToPreview()
PreviewMeshComponent->SetMorphTarget(Pair.Key, Pair.Value, false);
}
RefreshAnimationPreviewPose();
RefreshPreviewAnimationPose();
}
@@ -0,0 +1,111 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Morph/Editor/MorphPresetRetargetPreviewAnimInstance.h"
#include "Retargeter/IKRetargeter.h"
#include UE_INLINE_GENERATED_CPP_BY_NAME(MorphPresetRetargetPreviewAnimInstance)
FMorphPresetRetargetPreviewAnimInstanceProxy::FMorphPresetRetargetPreviewAnimInstanceProxy(
UAnimInstance* InAnimInstance,
FAnimNode_RetargetPoseFromMesh* InRetargetNode)
: FAnimInstanceProxy(InAnimInstance)
, RetargetNode(InRetargetNode)
{
}
void FMorphPresetRetargetPreviewAnimInstanceProxy::Initialize(UAnimInstance* InAnimInstance)
{
FAnimInstanceProxy::Initialize(InAnimInstance);
FAnimationInitializeContext InitContext(this);
RetargetNode->Initialize_AnyThread(InitContext);
}
void FMorphPresetRetargetPreviewAnimInstanceProxy::PreUpdate(UAnimInstance* InAnimInstance, float DeltaSeconds)
{
FAnimInstanceProxy::PreUpdate(InAnimInstance, DeltaSeconds);
if (RetargetNode && RetargetNode->HasPreUpdate())
{
RetargetNode->PreUpdate(InAnimInstance);
}
}
void FMorphPresetRetargetPreviewAnimInstanceProxy::CacheBones()
{
if (bBoneCachesInvalidated && RetargetNode)
{
FAnimationCacheBonesContext Context(this);
RetargetNode->CacheBones_AnyThread(Context);
bBoneCachesInvalidated = false;
}
}
bool FMorphPresetRetargetPreviewAnimInstanceProxy::Evaluate(FPoseContext& Output)
{
FAnimInstanceProxy::Evaluate(Output);
if (RetargetNode)
{
RetargetNode->Evaluate_AnyThread(Output);
}
return true;
}
void FMorphPresetRetargetPreviewAnimInstanceProxy::UpdateAnimationNode(const FAnimationUpdateContext& InContext)
{
FAnimInstanceProxy::UpdateAnimationNode(InContext);
UpdateCounter.Increment();
if (RetargetNode)
{
RetargetNode->Update_AnyThread(InContext);
}
}
void FMorphPresetRetargetPreviewAnimInstanceProxy::Configure(
UIKRetargeter* InRetargeter,
TWeakObjectPtr<USkeletalMeshComponent> InSourceMeshComponent)
{
if (!RetargetNode)
{
return;
}
RetargetNode->IKRetargeterAsset = InRetargeter;
RetargetNode->RetargetFrom = ERetargetSourceMode::CustomSkeletalMeshComponent;
RetargetNode->SourceMeshComponent = InSourceMeshComponent;
RetargetNode->CustomRetargetProfile = FRetargetProfile();
RetargetNode->bSuppressWarnings = false;
if (FIKRetargetProcessor* Processor = RetargetNode->GetRetargetProcessor())
{
Processor->SetNeedsInitialized();
}
}
UMorphPresetRetargetPreviewAnimInstance::UMorphPresetRetargetPreviewAnimInstance(const FObjectInitializer& ObjectInitializer)
: Super(ObjectInitializer)
{
bUseMultiThreadedAnimationUpdate = false;
}
void UMorphPresetRetargetPreviewAnimInstance::Configure(
UIKRetargeter* InRetargeter,
TWeakObjectPtr<USkeletalMeshComponent> InSourceMeshComponent)
{
FMorphPresetRetargetPreviewAnimInstanceProxy& Proxy = GetProxyOnGameThread<FMorphPresetRetargetPreviewAnimInstanceProxy>();
Proxy.Configure(InRetargeter, InSourceMeshComponent);
}
void UMorphPresetRetargetPreviewAnimInstance::NativeInitializeAnimation()
{
FMorphPresetRetargetPreviewAnimInstanceProxy& Proxy = GetProxyOnGameThread<FMorphPresetRetargetPreviewAnimInstanceProxy>();
Proxy.Initialize(this);
}
FAnimInstanceProxy* UMorphPresetRetargetPreviewAnimInstance::CreateAnimInstanceProxy()
{
return new FMorphPresetRetargetPreviewAnimInstanceProxy(this, &RetargetNode);
}
@@ -9,6 +9,7 @@
#include "Morph/MorphPresetDataAsset.h"
#include "Morph/Editor/MorphPresetEditorController.h"
#include "Morph/AnimTimeline/SAnimEditorBase.h"
#include "Animation/AnimMontage.h"
#include "Animation/MorphTarget.h"
#include "Curves/RichCurve.h"
#include "Framework/Application/SlateApplication.h"
@@ -159,6 +160,152 @@ namespace
TWeakPtr<FMorphPresetEditorController> Controller;
};
class SMorphPresetMontageLane : public SLeafWidget
{
public:
SLATE_BEGIN_ARGS(SMorphPresetMontageLane) {}
SLATE_ARGUMENT(TWeakPtr<FMorphPresetEditorController>, Controller)
SLATE_END_ARGS()
void Construct(const FArguments& InArgs)
{
Controller = InArgs._Controller;
}
virtual FVector2D ComputeDesiredSize(float) const override
{
return FVector2D(320.0f, 44.0f);
}
virtual int32 OnPaint(const FPaintArgs&, const FGeometry& AllottedGeometry, const FSlateRect&, FSlateWindowElementList& OutDrawElements,
int32 LayerId, const FWidgetStyle&, bool) const override
{
const TSharedPtr<FMorphPresetEditorController> PinnedController = Controller.Pin();
const UMorphPresetDataAsset* Preset = PinnedController.IsValid() ? PinnedController->GetPreset() : nullptr;
const UAnimMontage* Montage = Preset ? Preset->GetMontage() : nullptr;
const float TimelineLength = PinnedController.IsValid() ? FMath::Max(PinnedController->GetTimelineLength(), 0.01f) : 1.0f;
const float Width = AllottedGeometry.GetLocalSize().X;
const float Height = AllottedGeometry.GetLocalSize().Y;
FSlateDrawElement::MakeBox(OutDrawElements, LayerId, AllottedGeometry.ToPaintGeometry(), FAppStyle::GetBrush("WhiteBrush"),
ESlateDrawEffect::None, FLinearColor(0.075f, 0.08f, 0.09f, 1.0f));
if (Preset && Montage)
{
const float MontageDuration = Montage->GetPlayLength();
const float StartTime = FMath::Max(Preset->MontageStartTime, 0.0f);
const float StartX = Width * (StartTime / TimelineLength);
const float EndX = Width * ((StartTime + MontageDuration) / TimelineLength);
const float BlockWidth = FMath::Max(EndX - StartX, 2.0f);
const FLinearColor BlockColor = bIsDragging ? FLinearColor(0.55f, 0.38f, 0.11f, 1.0f) : FLinearColor(0.36f, 0.22f, 0.48f, 1.0f);
FSlateDrawElement::MakeBox(OutDrawElements, LayerId + 1,
AllottedGeometry.ToPaintGeometry(FVector2f(BlockWidth, Height - 6.0f), FSlateLayoutTransform(FVector2f(StartX, 3.0f))),
FAppStyle::GetBrush("WhiteBrush"), ESlateDrawEffect::None, BlockColor);
const FString Label = FString::Printf(TEXT("%s %.2fs"), *Montage->GetName(), MontageDuration);
FSlateDrawElement::MakeText(
OutDrawElements,
LayerId + 2,
AllottedGeometry.ToPaintGeometry(FVector2f(FMath::Max(BlockWidth - 10.0f, 1.0f), Height), FSlateLayoutTransform(FVector2f(StartX + 6.0f, 12.0f))),
Label,
FAppStyle::GetFontStyle("SmallFont"),
ESlateDrawEffect::None,
FLinearColor::White);
}
if (PinnedController.IsValid())
{
const float CurrentX = Width * (PinnedController->GetCurrentTime() / TimelineLength);
TArray<FVector2D> Points{ FVector2D(CurrentX, 0.0f), FVector2D(CurrentX, Height) };
FSlateDrawElement::MakeLines(OutDrawElements, LayerId + 3, AllottedGeometry.ToPaintGeometry(), Points,
ESlateDrawEffect::None, FLinearColor(1.0f, 0.5f, 0.25f, 1.0f), true, 2.0f);
}
return LayerId + 3;
}
virtual FReply OnMouseButtonDown(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override
{
if (MouseEvent.GetEffectingButton() != EKeys::LeftMouseButton)
{
return FReply::Unhandled();
}
const TSharedPtr<FMorphPresetEditorController> PinnedController = Controller.Pin();
UMorphPresetDataAsset* Preset = PinnedController.IsValid() ? PinnedController->GetPreset() : nullptr;
if (!Preset || !Preset->GetMontage())
{
return FReply::Unhandled();
}
const float Width = FMath::Max(MyGeometry.GetLocalSize().X, 1.0f);
const float TimelineLength = FMath::Max(PinnedController->GetTimelineLength(), 0.01f);
const float LocalX = MyGeometry.AbsoluteToLocal(MouseEvent.GetScreenSpacePosition()).X;
const float MontageDuration = Preset->GetMontage()->GetPlayLength();
const float MontageStartX = Width * (FMath::Max(Preset->MontageStartTime, 0.0f) / TimelineLength);
const float MontageEndX = Width * ((FMath::Max(Preset->MontageStartTime, 0.0f) + MontageDuration) / TimelineLength);
if (LocalX < MontageStartX || LocalX > MontageEndX)
{
return FReply::Unhandled();
}
Preset->Modify();
InitialStartTime = FMath::Max(Preset->MontageStartTime, 0.0f);
InitialTimelineLength = TimelineLength;
DragStartLocalX = LocalX;
bIsDragging = true;
PinnedController->SetPlaying(false);
return FReply::Handled().CaptureMouse(AsShared()).PreventThrottling();
}
virtual FReply OnMouseMove(const FGeometry& MyGeometry, const FPointerEvent& MouseEvent) override
{
if (!bIsDragging || !HasMouseCapture())
{
return FReply::Unhandled();
}
const TSharedPtr<FMorphPresetEditorController> PinnedController = Controller.Pin();
UMorphPresetDataAsset* Preset = PinnedController.IsValid() ? PinnedController->GetPreset() : nullptr;
if (!Preset)
{
return FReply::Unhandled();
}
const float Width = FMath::Max(MyGeometry.GetLocalSize().X, 1.0f);
const float DeltaX = MyGeometry.AbsoluteToLocal(MouseEvent.GetScreenSpacePosition()).X - DragStartLocalX;
const float DeltaTime = (DeltaX / Width) * InitialTimelineLength;
Preset->MontageStartTime = FMath::Max(InitialStartTime + DeltaTime, 0.0f);
PinnedController->NotifyPresetEdited(false);
Invalidate(EInvalidateWidgetReason::Paint);
return FReply::Handled().PreventThrottling();
}
virtual FReply OnMouseButtonUp(const FGeometry&, const FPointerEvent& MouseEvent) override
{
if (MouseEvent.GetEffectingButton() != EKeys::LeftMouseButton || !bIsDragging)
{
return FReply::Unhandled();
}
bIsDragging = false;
if (const TSharedPtr<FMorphPresetEditorController> PinnedController = Controller.Pin())
{
PinnedController->NotifyPresetEdited(true);
}
return FReply::Handled().ReleaseMouseCapture();
}
private:
TWeakPtr<FMorphPresetEditorController> Controller;
float DragStartLocalX = 0.0f;
float InitialStartTime = 0.0f;
float InitialTimelineLength = 1.0f;
bool bIsDragging = false;
};
class SMorphPresetClipLane : public SLeafWidget
{
public:
@@ -1000,6 +1147,29 @@ namespace
]
]
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(TimelineEdgePadding, 0.0f, TimelineEdgePadding, 6.0f)
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.AutoWidth()
.Padding(FMargin(0.0f, 0.0f, 8.0f, 0.0f))
[
SNew(SBox)
.WidthOverride(TimelineNameColumnWidth)
[
SNew(STextBlock)
.Text(LOCTEXT("HeaderMontage", "Montage"))
]
]
+ SHorizontalBox::Slot()
.FillWidth(1.0f)
[
SNew(SMorphPresetMontageLane)
.Controller(Controller)
]
]
+ SVerticalBox::Slot()
.FillHeight(1.0f)
[
SAssignNew(LocalScrollBox, SScrollBox)
@@ -4,6 +4,7 @@
#include "AssetRegistry/AssetData.h"
#include "AdvancedPreviewScene.h"
#include "Animation/AnimMontage.h"
#include "Animation/DebugSkelMeshComponent.h"
#include "EditorViewportClient.h"
#include "Engine/SkeletalMesh.h"
@@ -11,6 +12,7 @@
#include "Morph/LipSyncMappingDataAsset.h"
#include "Morph/Editor/MorphPresetEditorController.h"
#include "PropertyCustomizationHelpers.h"
#include "Retargeter/IKRetargeter.h"
#include "SEditorViewport.h"
#include "Sound/SoundBase.h"
#include "ToolMenus.h"
@@ -200,7 +202,6 @@ void SMorphPresetViewportPanel::Construct(const FArguments& InArgs)
if (Controller.IsValid())
{
Controller->OnPreviewSceneChanged().AddSP(this, &SMorphPresetViewportPanel::RebuildContent);
Controller->OnPresetChanged().AddSP(this, &SMorphPresetViewportPanel::RebuildContent);
}
ChildSlot
@@ -218,7 +219,6 @@ SMorphPresetViewportPanel::~SMorphPresetViewportPanel()
if (Controller.IsValid())
{
Controller->OnPreviewSceneChanged().RemoveAll(this);
Controller->OnPresetChanged().RemoveAll(this);
}
}
@@ -268,6 +268,51 @@ void SMorphPresetViewportPanel::RebuildContent()
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(0.0f, 0.0f, 0.0f, 8.0f)
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.AutoWidth()
.VAlign(VAlign_Center)
.Padding(0.0f, 0.0f, 8.0f, 0.0f)
[
SNew(STextBlock)
.Text(LOCTEXT("PreviewMontageLabel", "Montage"))
]
+ SHorizontalBox::Slot()
.FillWidth(1.0f)
.Padding(0.0f, 0.0f, 8.0f, 0.0f)
[
SNew(SObjectPropertyEntryBox)
.AllowedClass(UAnimMontage::StaticClass())
.ObjectPath(this, &SMorphPresetViewportPanel::GetSelectedPreviewMontagePath)
.OnObjectChanged(this, &SMorphPresetViewportPanel::OnPreviewMontageChanged)
.DisplayUseSelected(true)
.DisplayBrowse(true)
.AllowClear(true)
]
+ SHorizontalBox::Slot()
.AutoWidth()
.VAlign(VAlign_Center)
.Padding(12.0f, 0.0f, 8.0f, 0.0f)
[
SNew(STextBlock)
.Text(LOCTEXT("PreviewRetargeterLabel", "IK Retargeter"))
]
+ SHorizontalBox::Slot()
.FillWidth(1.0f)
[
SNew(SObjectPropertyEntryBox)
.AllowedClass(UIKRetargeter::StaticClass())
.ObjectPath(this, &SMorphPresetViewportPanel::GetSelectedPreviewIKRetargeterPath)
.OnObjectChanged(this, &SMorphPresetViewportPanel::OnPreviewIKRetargeterChanged)
.DisplayUseSelected(true)
.DisplayBrowse(true)
.AllowClear(true)
]
]
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(0.0f, 0.0f, 0.0f, 8.0f)
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
@@ -380,6 +425,22 @@ void SMorphPresetViewportPanel::OnPreviewMeshChanged(const FAssetData& AssetData
}
}
void SMorphPresetViewportPanel::OnPreviewMontageChanged(const FAssetData& AssetData)
{
if (Controller.IsValid())
{
Controller->SetPreviewMontage(Cast<UAnimMontage>(AssetData.GetAsset()));
}
}
void SMorphPresetViewportPanel::OnPreviewIKRetargeterChanged(const FAssetData& AssetData)
{
if (Controller.IsValid())
{
Controller->SetPreviewIKRetargeter(Cast<UIKRetargeter>(AssetData.GetAsset()));
}
}
void SMorphPresetViewportPanel::OnLipSyncSoundChanged(const FAssetData& AssetData)
{
if (Controller.IsValid())
@@ -409,6 +470,32 @@ FString SMorphPresetViewportPanel::GetSelectedPreviewMeshPath() const
return FString();
}
FString SMorphPresetViewportPanel::GetSelectedPreviewMontagePath() const
{
if (Controller.IsValid())
{
if (UAnimMontage* Montage = Controller->GetPreviewMontage())
{
return Montage->GetPathName();
}
}
return FString();
}
FString SMorphPresetViewportPanel::GetSelectedPreviewIKRetargeterPath() const
{
if (Controller.IsValid())
{
if (UIKRetargeter* Retargeter = Controller->GetPreviewIKRetargeter())
{
return Retargeter->GetPathName();
}
}
return FString();
}
FString SMorphPresetViewportPanel::GetSelectedLipSyncSoundPath() const
{
if (Controller.IsValid())
@@ -8,7 +8,12 @@ class FUICommandList;
class AActor;
class IPersonaPreviewScene;
class IPersonaToolkit;
class UAnimationAsset;
class UAnimMontage;
class UAnimSequence;
class UDebugSkelMeshComponent;
class UMorphPresetRetargetPreviewAnimInstance;
class UIKRetargeter;
class ULipSyncComponent;
class ULipSyncMappingDataAsset;
class UMorphControllerComponent;
@@ -27,6 +32,11 @@ public:
UMorphPresetDataAsset* GetPreset() const;
USkeletalMesh* GetPreviewMesh() const;
bool SetPreviewMesh(USkeletalMesh* InPreviewMesh);
USkeletalMesh* GetAnimationPreviewMesh() const;
UAnimMontage* GetPreviewMontage() const;
bool SetPreviewMontage(UAnimMontage* InMontage);
UIKRetargeter* GetPreviewIKRetargeter() const;
bool SetPreviewIKRetargeter(UIKRetargeter* InRetargeter);
TSharedPtr<IPersonaToolkit> GetPersonaToolkit() const { return PersonaToolkit; }
TSharedPtr<IPersonaPreviewScene> GetPreviewScene() const;
@@ -65,8 +75,15 @@ public:
private:
void RebuildPreviewScene();
void RebuildPreviewAnimation();
void RebuildAnimationPreviewMesh();
void DestroyAnimationPreviewMesh();
void ApplyPresetToPreview();
void RefreshPreviewAnimationPose();
void RefreshAnimationPreviewPose();
void ConfigureRetargetPreview();
void ClearRetargetPreview(bool bRestorePreviewAnimation);
UAnimationAsset* ResolveMontagePreviewAnimation(float& OutAssetTime, float& OutAssetLength) const;
bool HasRetargetPreview() const;
void RebuildLipSyncPreviewComponents();
void DestroyLipSyncPreviewComponents();
void TickLipSyncPreviewComponents(float DeltaTime);
@@ -79,6 +96,10 @@ private:
TStrongObjectPtr<UAnimSequence> PreviewAnimation;
TStrongObjectPtr<ULipSyncMappingDataAsset> DefaultLipSyncPreviewMapping;
TWeakObjectPtr<USkeletalMesh> ActivePreviewMesh;
TWeakObjectPtr<USkeletalMesh> ActiveAnimationPreviewMesh;
TWeakObjectPtr<USkeletalMeshComponent> AnimationPreviewMeshComponent;
TWeakObjectPtr<UMorphPresetRetargetPreviewAnimInstance> RetargetPreviewInstance;
TWeakObjectPtr<UIKRetargeter> ActivePreviewIKRetargeter;
TWeakObjectPtr<ULipSyncMappingDataAsset> LipSyncPreviewMapping;
TWeakObjectPtr<USoundBase> LipSyncPreviewSound;
TWeakObjectPtr<AActor> LipSyncPreviewActor;
@@ -87,6 +108,7 @@ private:
bool bIsPlaying = false;
bool bLipSyncPreviewAudioActive = false;
float CurrentTime = 0.0f;
uint64 LastTickFrameNumber = MAX_uint64;
float LipSyncPreviewSmoothness = 0.55f;
FText LipSyncPreviewTranscript;
FText PreviewStatusText;
@@ -0,0 +1,48 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "Animation/AnimInstanceProxy.h"
#include "Animation/AnimInstance.h"
#include "AnimNodes/AnimNode_RetargetPoseFromMesh.h"
#include "MorphPresetRetargetPreviewAnimInstance.generated.h"
class UIKRetargeter;
class USkeletalMeshComponent;
USTRUCT()
struct FMorphPresetRetargetPreviewAnimInstanceProxy : public FAnimInstanceProxy
{
GENERATED_BODY()
FMorphPresetRetargetPreviewAnimInstanceProxy() = default;
FMorphPresetRetargetPreviewAnimInstanceProxy(UAnimInstance* InAnimInstance, FAnimNode_RetargetPoseFromMesh* InRetargetNode);
virtual void Initialize(UAnimInstance* InAnimInstance) override;
virtual void PreUpdate(UAnimInstance* InAnimInstance, float DeltaSeconds) override;
virtual void CacheBones() override;
virtual bool Evaluate(FPoseContext& Output) override;
virtual void UpdateAnimationNode(const FAnimationUpdateContext& InContext) override;
void Configure(UIKRetargeter* InRetargeter, TWeakObjectPtr<USkeletalMeshComponent> InSourceMeshComponent);
FAnimNode_RetargetPoseFromMesh* RetargetNode = nullptr;
};
UCLASS(Transient, NotBlueprintable)
class FUNNYGIRLEDITOR_API UMorphPresetRetargetPreviewAnimInstance : public UAnimInstance
{
GENERATED_BODY()
public:
UMorphPresetRetargetPreviewAnimInstance(const FObjectInitializer& ObjectInitializer);
void Configure(UIKRetargeter* InRetargeter, TWeakObjectPtr<USkeletalMeshComponent> InSourceMeshComponent);
protected:
virtual void NativeInitializeAnimation() override;
virtual FAnimInstanceProxy* CreateAnimInstanceProxy() override;
UPROPERTY(Transient)
FAnimNode_RetargetPoseFromMesh RetargetNode;
};
@@ -6,6 +6,8 @@
#include "Widgets/SCompoundWidget.h"
class FMorphPresetEditorController;
class UAnimMontage;
class UIKRetargeter;
class ULipSyncMappingDataAsset;
class USoundBase;
class USkeletalMesh;
@@ -24,9 +26,13 @@ public:
private:
void RebuildContent();
void OnPreviewMeshChanged(const struct FAssetData& AssetData);
void OnPreviewMontageChanged(const struct FAssetData& AssetData);
void OnPreviewIKRetargeterChanged(const struct FAssetData& AssetData);
void OnLipSyncSoundChanged(const struct FAssetData& AssetData);
void OnLipSyncMappingChanged(const struct FAssetData& AssetData);
FString GetSelectedPreviewMeshPath() const;
FString GetSelectedPreviewMontagePath() const;
FString GetSelectedPreviewIKRetargeterPath() const;
FString GetSelectedLipSyncSoundPath() const;
FString GetSelectedLipSyncMappingPath() const;
FReply OnPlayLipSyncClicked();