Refactoring

This commit is contained in:
Ivan Anikin
2026-05-20 15:33:00 +07:00
parent 8732ce99e3
commit 86baa476f4
41 changed files with 287 additions and 1513 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", "UMG" });
PublicDependencyModuleNames.AddRange(new string[] { "Core", "CoreUObject", "Engine", "InputCore", "EnhancedInput", "HTTP", "Json", "JsonUtilities", "AssetRegistry", "MediaAssets", "UMG" });
PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
@@ -169,6 +169,87 @@ bool UPersonaBrainComponent::GetLastSavedLLMMessage(FString& OutMessage) const
return false;
}
TArray<FPersonaChatMessage> UPersonaBrainComponent::GetSavedChatMessages() const
{
TArray<FPersonaChatMessage> ChatMessages;
const FString FilePath = GetChatFilePath();
FString ChatJson;
if (!FFileHelper::LoadFileToString(ChatJson, *FilePath))
{
return ChatMessages;
}
TSharedPtr<FJsonObject> RootObject;
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(ChatJson);
if (!FJsonSerializer::Deserialize(Reader, RootObject) || !RootObject.IsValid())
{
return ChatMessages;
}
const TArray<TSharedPtr<FJsonValue>>* SavedMessages = nullptr;
if (!RootObject->TryGetArrayField(TEXT("messages"), SavedMessages) || !SavedMessages)
{
return ChatMessages;
}
const FString GirlSpeaker = Identity && !Identity->Name.IsEmpty() && !Identity->Name.ToString().TrimStartAndEnd().IsEmpty()
? Identity->Name.ToString().TrimStartAndEnd()
: FString(TEXT("Girl"));
for (const TSharedPtr<FJsonValue>& MessageValue : *SavedMessages)
{
const TSharedPtr<FJsonObject>* MessageObject = nullptr;
if (!MessageValue.IsValid() || !MessageValue->TryGetObject(MessageObject) || !MessageObject || !MessageObject->IsValid())
{
continue;
}
FString Role;
FString Content;
if (!(*MessageObject)->TryGetStringField(TEXT("role"), Role) || !(*MessageObject)->TryGetStringField(TEXT("content"), Content))
{
continue;
}
if ((Role != TEXT("user") && Role != TEXT("assistant")) || Content.TrimStartAndEnd().IsEmpty())
{
continue;
}
FPersonaChatMessage ChatMessage;
(*MessageObject)->TryGetStringField(TEXT("timestamp"), ChatMessage.Timestamp);
ChatMessage.Role = Role;
ChatMessage.Speaker = Role == TEXT("assistant") ? GirlSpeaker : FString(TEXT("Player"));
ChatMessage.Content = Content;
const TSharedPtr<FJsonValue> InteractActionValue = (*MessageObject)->TryGetField(TEXT("interact_action"));
if (InteractActionValue.IsValid() && InteractActionValue->Type != EJson::Null)
{
if (InteractActionValue->Type == EJson::Object)
{
const TSharedPtr<FJsonObject>* InteractActionObject = nullptr;
if (InteractActionValue->TryGetObject(InteractActionObject) && InteractActionObject && InteractActionObject->IsValid())
{
FString InteractActionJson;
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&InteractActionJson);
FJsonSerializer::Serialize(InteractActionObject->ToSharedRef(), Writer);
ChatMessage.InteractActionJson = InteractActionJson;
}
}
else
{
InteractActionValue->TryGetString(ChatMessage.InteractActionJson);
}
}
(*MessageObject)->TryGetStringField(TEXT("emotion"), ChatMessage.Emotion);
ChatMessages.Add(ChatMessage);
}
return ChatMessages;
}
void UPersonaBrainComponent::BroadcastBrainError(const FString& ErrorMessage) const
{
OnLLMError.Broadcast(ErrorMessage);
@@ -2,8 +2,6 @@
#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"
@@ -37,6 +35,12 @@ void UMorphControllerComponent::BeginPlay()
Super::BeginPlay();
}
void UMorphControllerComponent::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
ClearMorphsImmediately();
Super::EndPlay(EndPlayReason);
}
void UMorphControllerComponent::TickComponent(float DeltaTime, ELevelTick TickType, FActorComponentTickFunction* ThisTickFunction)
{
Super::TickComponent(DeltaTime, TickType, ThisTickFunction);
@@ -212,7 +216,6 @@ bool UMorphControllerComponent::AddPreset(UMorphPresetDataAsset* Preset, float W
if (Slot.Preset == Preset)
{
Slot.TargetWeight = FMath::Clamp(Weight, 0.0f, 1.0f);
Slot.CurrentWeight = Slot.TargetWeight;
Slot.PlaybackTime = 0.0f;
Slot.LoopDelayRemaining = 0.0f;
Slot.TimelineDuration = TimelineDuration;
@@ -224,7 +227,7 @@ bool UMorphControllerComponent::AddPreset(UMorphPresetDataAsset* Preset, float W
FActiveMorphPresetSlot NewSlot;
NewSlot.Preset = Preset;
NewSlot.TargetWeight = FMath::Clamp(Weight, 0.0f, 1.0f);
NewSlot.CurrentWeight = NewSlot.TargetWeight;
NewSlot.CurrentWeight = 0.0f;
NewSlot.PlaybackTime = 0.0f;
NewSlot.LoopDelayRemaining = 0.0f;
NewSlot.TimelineDuration = TimelineDuration;
@@ -239,78 +242,32 @@ bool UMorphControllerComponent::PlayPreset(UMorphPresetDataAsset* Preset, float
{
if (bLogWarnings)
{
UE_LOG(LogMorphController, Warning, TEXT("PlayPreset ignored on %s: preset is null or has no morph clips or animation montage."), *GetNameSafe(GetOwner()));
UE_LOG(LogMorphController, Warning, TEXT("PlayPreset ignored on %s: preset is null or has no morph clips."), *GetNameSafe(GetOwner()));
}
return false;
}
const bool bPlayedMorphs = Preset->IsValidPreset() ? AddPreset(Preset, Weight) : false;
const bool bPlayedAnimation = PlayPresetAnimation(Preset);
return bPlayedMorphs || bPlayedAnimation;
}
bool UMorphControllerComponent::PlayPresetAnimation(UMorphPresetDataAsset* Preset)
{
if (!Preset || !Preset->AnimationMontage)
{
return false;
}
USkeletalMeshComponent* Mesh = ResolveMeshComponent();
if (!Mesh)
{
if (bLogWarnings)
{
UE_LOG(LogMorphController, Warning, TEXT("Preset animation ignored on %s: no SkeletalMeshComponent was found."), *GetNameSafe(GetOwner()));
}
return false;
}
UAnimInstance* AnimInstance = Mesh->GetAnimInstance();
if (!AnimInstance)
{
if (bLogWarnings)
{
UE_LOG(LogMorphController, Warning, TEXT("Preset animation ignored on %s: mesh %s has no AnimInstance."), *GetNameSafe(GetOwner()), *GetNameSafe(Mesh));
}
return false;
}
UAnimMontage* Montage = Preset->AnimationMontage;
if (Preset->bRestartAnimationOnReplay && AnimInstance->Montage_IsPlaying(Montage))
{
AnimInstance->Montage_Stop(0.0f, Montage);
}
const float MontageLength = AnimInstance->Montage_Play(Montage, FMath::Max(Preset->AnimationPlayRate, 0.01f));
if (MontageLength <= 0.0f)
{
if (bLogWarnings)
{
UE_LOG(LogMorphController, Warning, TEXT("Preset animation failed on %s: montage %s could not play. Check the Anim Blueprint slot setup."), *GetNameSafe(GetOwner()), *GetNameSafe(Montage));
}
return false;
}
if (!Preset->AnimationStartSection.IsNone())
{
AnimInstance->Montage_JumpToSection(Preset->AnimationStartSection, Montage);
}
return true;
return AddPreset(Preset, Weight);
}
void UMorphControllerComponent::RemovePreset(UMorphPresetDataAsset* Preset)
{
if (!Preset) return;
bool bRemovedAny = false;
for (int32 i = ActivePresets.Num() - 1; i >= 0; --i)
{
if (ActivePresets[i].Preset == Preset)
{
ActivePresets.RemoveAt(i);
bRemovedAny = true;
}
}
if (bRemovedAny)
{
UpdateMorphTargets(0.0f);
}
}
void UMorphControllerComponent::SetPresetWeight(UMorphPresetDataAsset* Preset, float Weight)
@@ -336,6 +293,7 @@ void UMorphControllerComponent::SetActivePresets(const TArray<FActiveMorphPreset
if (Slot.Preset && Slot.Preset->IsValidPreset())
{
FActiveMorphPresetSlot NewSlot = Slot;
NewSlot.TargetWeight = FMath::Clamp(NewSlot.TargetWeight, 0.0f, 1.0f);
NewSlot.CurrentWeight = 0.0f;
NewSlot.PlaybackTime = 0.0f;
NewSlot.LoopDelayRemaining = 0.0f;
@@ -346,11 +304,14 @@ void UMorphControllerComponent::SetActivePresets(const TArray<FActiveMorphPreset
}
}
}
UpdateMorphTargets(0.0f);
}
void UMorphControllerComponent::ClearAllPresets()
{
ActivePresets.Empty();
UpdateMorphTargets(0.0f);
}
bool UMorphControllerComponent::IsPresetActive(UMorphPresetDataAsset* Preset) const
@@ -382,17 +343,27 @@ TArray<FActiveMorphPresetSlot> UMorphControllerComponent::GetActivePresets() con
void UMorphControllerComponent::SetMorphTarget(FName MorphTargetName, float Value)
{
if (MorphTargetName.IsNone())
{
return;
}
DirectMorphOverrides.Add(MorphTargetName, ClampMorphTargetValue(Value));
UpdateMorphTargets(0.0f);
}
void UMorphControllerComponent::RemoveMorphTargetOverride(FName MorphTargetName)
{
DirectMorphOverrides.Remove(MorphTargetName);
if (DirectMorphOverrides.Remove(MorphTargetName) > 0)
{
UpdateMorphTargets(0.0f);
}
}
void UMorphControllerComponent::ClearDirectMorphOverrides()
{
DirectMorphOverrides.Empty();
UpdateMorphTargets(0.0f);
}
void UMorphControllerComponent::SetLipSyncMorphValues(const TMap<FName, float>& MorphValues)
@@ -405,11 +376,13 @@ void UMorphControllerComponent::SetLipSyncMorphValues(const TMap<FName, float>&
LipSyncMorphValues.Add(Pair.Key, ClampMorphTargetValue(Pair.Value));
}
}
UpdateMorphTargets(0.0f);
}
void UMorphControllerComponent::ClearLipSyncMorphValues()
{
LipSyncMorphValues.Empty();
UpdateMorphTargets(0.0f);
}
void UMorphControllerComponent::UpdateMorphTargetsNow(float DeltaTime)
@@ -501,6 +474,19 @@ void UMorphControllerComponent::UpdateMorphTargets(float DeltaTime)
// 2. Gather final morph values. Overlapping clips use the strongest contribution.
TMap<FName, float> FinalValues;
float ActivePresetWeightSum = 0.0f;
for (const FActiveMorphPresetSlot& Slot : ActivePresets)
{
if (Slot.Preset && Slot.CurrentWeight >= 0.001f && Slot.LoopDelayRemaining <= 0.0f)
{
ActivePresetWeightSum += Slot.CurrentWeight;
}
}
const float PresetLayerScale = bNormalizePresetLayerWeights && MaxCombinedPresetWeight > 0.0f && ActivePresetWeightSum > MaxCombinedPresetWeight
? MaxCombinedPresetWeight / ActivePresetWeightSum
: 1.0f;
for (const FActiveMorphPresetSlot& Slot : ActivePresets)
{
if (!Slot.Preset || Slot.CurrentWeight < 0.001f || Slot.LoopDelayRemaining > 0.0f)
@@ -508,7 +494,7 @@ void UMorphControllerComponent::UpdateMorphTargets(float DeltaTime)
continue;
}
MorphPresetRuntime::AccumulatePresetMorphValuesAtTime(*Slot.Preset, Slot.PlaybackTime, Slot.CurrentWeight, FinalValues);
MorphPresetRuntime::AccumulatePresetMorphValuesAtTime(*Slot.Preset, Slot.PlaybackTime, Slot.CurrentWeight * PresetLayerScale, FinalValues);
}
// 3. Apply direct overrides with the same max-based priority rule.
@@ -9,11 +9,13 @@
#include "Engine/SkeletalMesh.h"
#include "Engine/Texture2D.h"
#include "HAL/FileManager.h"
#include "MediaPlayer.h"
#include "Misc/Base64.h"
#include "Misc/FileHelper.h"
#include "Misc/Paths.h"
#include "Serialization/JsonSerializer.h"
#include "Serialization/JsonWriter.h"
#include "StreamMediaSource.h"
namespace
{
@@ -358,3 +360,15 @@ bool UFunnyGirlUtilityBlueprintLibrary::DeleteLightString(const FString& Field,
SaveObject->RemoveField(EncodedField);
return SaveLightSaveObject(SaveObject.ToSharedRef(), OutError);
}
bool UFunnyGirlUtilityBlueprintLibrary::OpenStreamMediaSourceUrl(UMediaPlayer* MediaPlayer, UStreamMediaSource* StreamMediaSource, const FString& Url)
{
if (!MediaPlayer || !StreamMediaSource || !Url.Contains(TEXT("://")))
{
return false;
}
StreamMediaSource->StreamUrl = Url;
MediaPlayer->Close();
return MediaPlayer->OpenSource(StreamMediaSource);
}
@@ -28,6 +28,30 @@ struct FUNNYGIRL_API FLLMReplyResponse
FString Emotion;
};
USTRUCT(BlueprintType)
struct FUNNYGIRL_API FPersonaChatMessage
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|History")
FString Timestamp;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|History")
FString Role;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|History")
FString Speaker;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|History")
FString Content;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|History|Interaction")
FString InteractActionJson;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "LLM|History|Emotion")
FString Emotion;
};
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FLLMResponseDelegate, const FLLMReplyResponse&, Response);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FLLMReplyStreamChunkDelegate, const FString&, ReplyChunk);
DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FLLMErrorDelegate, const FString&, ErrorMessage);
@@ -85,6 +109,9 @@ public:
UFUNCTION(BlueprintPure, Category = "LLM|History")
bool GetLastSavedLLMMessage(FString& OutMessage) const;
UFUNCTION(BlueprintPure, Category = "LLM|History")
TArray<FPersonaChatMessage> GetSavedChatMessages() const;
virtual void CancelRequest() override;
protected:
@@ -74,6 +74,14 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Morph Controller")
bool bLogWarnings = true;
/** Keep layered presets from overdriving the face when several expressions are active at once. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Morph Controller|Layering")
bool bNormalizePresetLayerWeights = true;
/** Maximum total expression weight shared by active presets when normalization is enabled. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Morph Controller|Layering", meta = (ClampMin = "0", ClampMax = "5"))
float MaxCombinedPresetWeight = 1.0f;
/** Emotion presets available for AI/director prompting. Use these as the character's facial expression vocabulary. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Morph Controller|Emotions")
TArray<TObjectPtr<UMorphPresetDataAsset>> EmotionPresets;
@@ -178,6 +186,7 @@ public:
protected:
virtual void BeginPlay() override;
virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
private:
UPROPERTY()
@@ -197,7 +206,6 @@ private:
USkeletalMeshComponent* ResolveMeshComponent() const;
bool PresetHasAnyPositiveCurveValue(const UMorphPresetDataAsset& Preset) const;
bool PlayPresetAnimation(UMorphPresetDataAsset* Preset);
void UpdateMorphTargets(float DeltaTime);
void BlendMorphLayer(const TMap<FName, float>& LayerValues, float LayerWeight, EMorphLayerBlendMode BlendMode, TMap<FName, float>& InOutFinalValues) const;
void ApplyFinalMorphValues(const TMap<FName, float>& FinalValues);
@@ -8,8 +8,6 @@
#include "Engine/SkeletalMesh.h"
#include "MorphPresetDataAsset.generated.h"
class UAnimMontage;
USTRUCT(BlueprintType)
struct FUNNYGIRL_API FMorphTimelineClip
{
@@ -77,26 +75,6 @@ public:
ClampMin = "0"))
float BlendSpeedOverride = 0.0f;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Morph Preset|Animation", meta = (
DisplayName = "Animation Montage",
ToolTip = "Optional animation montage played when this preset starts. The montage must use a slot that is present in the character Anim Blueprint."))
TObjectPtr<UAnimMontage> AnimationMontage = nullptr;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Morph Preset|Animation", meta = (
DisplayName = "Animation Play Rate",
ClampMin = "0.01"))
float AnimationPlayRate = 1.0f;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Morph Preset|Animation", meta = (
DisplayName = "Start Section",
ToolTip = "Optional montage section to jump to after playback starts."))
FName AnimationStartSection;
UPROPERTY(EditAnywhere, BlueprintReadOnly, Category = "Morph Preset|Animation", meta = (
DisplayName = "Restart Animation On Replay",
ToolTip = "If enabled, an already playing montage is stopped and restarted when this preset is triggered again."))
bool bRestartAnimationOnReplay = true;
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Morph Preset")
int32 GetMorphCount() const;
@@ -104,10 +82,7 @@ public:
float GetTimelineLength() const;
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Morph Preset")
bool HasAnimation() const { return AnimationMontage != nullptr; }
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Morph Preset")
bool HasPlayableContent() const { return IsValidPreset() || HasAnimation(); }
bool HasPlayableContent() const { return IsValidPreset(); }
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Morph Preset")
bool IsValidPreset() const { return GetMorphCount() > 0; }
@@ -8,6 +8,8 @@
class USkeletalMesh;
class UButton;
class UMediaPlayer;
class UStreamMediaSource;
class UTexture2D;
UENUM(BlueprintType)
@@ -53,4 +55,7 @@ public:
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Save")
static bool DeleteLightString(const FString& Field, FString& OutError);
UFUNCTION(BlueprintCallable, Category = "FunnyGirl|Media")
static bool OpenStreamMediaSourceUrl(UMediaPlayer* MediaPlayer, UStreamMediaSource* StreamMediaSource, const FString& Url);
};
@@ -64,10 +64,6 @@ namespace
TargetPreset.bLoop = SourcePreset.bLoop;
TargetPreset.LoopDelay = SourcePreset.LoopDelay;
TargetPreset.BlendSpeedOverride = SourcePreset.BlendSpeedOverride;
TargetPreset.AnimationMontage = SourcePreset.AnimationMontage;
TargetPreset.AnimationPlayRate = SourcePreset.AnimationPlayRate;
TargetPreset.AnimationStartSection = SourcePreset.AnimationStartSection;
TargetPreset.bRestartAnimationOnReplay = SourcePreset.bRestartAnimationOnReplay;
}
}
@@ -4,10 +4,7 @@
#include "Animation/AnimData/IAnimationDataController.h"
#include "AnimPreviewInstance.h"
#include "Animation/AnimMontage.h"
#include "Animation/AnimSequence.h"
#include "Animation/AnimSequenceBase.h"
#include "Animation/AnimationAsset.h"
#include "Animation/DebugSkelMeshComponent.h"
#include "Animation/Skeleton.h"
#include "Components/SkeletalMeshComponent.h"
@@ -115,15 +112,7 @@ float FMorphPresetEditorController::GetTimelineLength() const
{
if (const UMorphPresetDataAsset* PresetAsset = Preset.Get())
{
float TimelineLength = PresetAsset->GetTimelineLength();
if (PresetAsset->AnimationMontage)
{
const float PlayRate = FMath::Max(PresetAsset->AnimationPlayRate, 0.01f);
const UAnimSequenceBase* PreviewSequence = PresetAsset->AnimationMontage->GetFirstAnimReference();
const float AnimationLength = PreviewSequence ? PreviewSequence->GetPlayLength() : PresetAsset->AnimationMontage->GetPlayLength();
TimelineLength = FMath::Max(TimelineLength, AnimationLength / PlayRate);
}
return FMath::Max(TimelineLength, 0.1f);
return FMath::Max(PresetAsset->GetTimelineLength(), 0.1f);
}
return 1.0f;
@@ -145,7 +134,7 @@ void FMorphPresetEditorController::SetCurrentTime(float InTime)
if (PreviewMeshComponent->PreviewInstance)
{
PreviewMeshComponent->PreviewInstance->SetPlaying(false);
PreviewMeshComponent->PreviewInstance->SetPosition(GetPreviewAnimationPosition(), false);
PreviewMeshComponent->PreviewInstance->SetPosition(CurrentTime, false);
RefreshPreviewAnimationPose();
}
}
@@ -225,20 +214,21 @@ void FMorphPresetEditorController::NotifyPresetEdited(bool bBroadcastPresetChang
PresetAsset->MarkPackageDirty();
}
UAnimationAsset* DesiredPreviewAsset = ResolvePreviewAnimationAsset();
UAnimationAsset* CurrentPreviewAsset = nullptr;
if (TSharedPtr<IPersonaPreviewScene> PreviewScene = GetPreviewScene())
if (ActivePreviewMesh.Get() != GetPreviewMesh())
{
CurrentPreviewAsset = PreviewScene->GetPreviewAnimationAsset();
RebuildPreviewScene();
if (bBroadcastPresetChanged)
{
PresetChanged.Broadcast();
}
return;
}
if (DesiredPreviewAsset != CurrentPreviewAsset ||
(DesiredPreviewAsset && PreviewAnimation.IsValid()) ||
(!DesiredPreviewAsset && !PreviewAnimation.IsValid()))
if (!PreviewAnimation.IsValid())
{
RebuildPreviewAnimation();
}
else if (PreviewAnimation.IsValid())
else
{
SetAnimSequencePlayLength(*PreviewAnimation, GetTimelineLength(), false);
}
@@ -436,6 +426,7 @@ void FMorphPresetEditorController::RebuildPreviewScene()
PreviewAnimation.Reset();
USkeletalMesh* PreviewMesh = GetPreviewMesh();
ActivePreviewMesh = PreviewMesh;
if (PreviewMesh == nullptr)
{
PreviewSceneChanged.Broadcast();
@@ -464,56 +455,29 @@ void FMorphPresetEditorController::RebuildPreviewAnimation()
PreviewAnimation.Reset();
PreviewStatusText = FText::GetEmpty();
UAnimationAsset* PreviewAsset = ResolvePreviewAnimationAsset();
if (PreviewAsset && !IsPreviewAnimationCompatible(PreviewAsset))
PreviewAnimation = TStrongObjectPtr<UAnimSequence>(AnimSequence);
SetAnimSequencePlayLength(*AnimSequence, GetTimelineLength(), false);
if (USkeletalMesh* PreviewMesh = GetPreviewMesh())
{
const USkeletalMesh* PreviewMesh = GetPreviewMesh();
const FString PreviewMeshSkeletonName = PreviewMesh && PreviewMesh->GetSkeleton()
? PreviewMesh->GetSkeleton()->GetPathName()
: TEXT("None");
const FString AnimationSkeletonName = PreviewAsset->GetSkeleton()
? PreviewAsset->GetSkeleton()->GetPathName()
: TEXT("None");
PreviewStatusText = FText::FromString(FString::Printf(
TEXT("Animation preview is disabled: montage sequence '%s' uses skeleton '%s', but Preview Mesh uses '%s'. Retarget the animation to the Preview Mesh skeleton."),
*GetNameSafe(PreviewAsset),
*AnimationSkeletonName,
*PreviewMeshSkeletonName));
UE_LOG(LogMorphPresetEditorPreview, Warning, TEXT("%s"), *PreviewStatusText.ToString());
PreviewAsset = nullptr;
}
if (!PreviewAsset)
{
PreviewAnimation = TStrongObjectPtr<UAnimSequence>(AnimSequence);
PreviewAsset = AnimSequence;
SetAnimSequencePlayLength(*AnimSequence, GetTimelineLength(), false);
if (USkeletalMesh* PreviewMesh = GetPreviewMesh())
if (USkeleton* Skeleton = PreviewMesh->GetSkeleton())
{
if (USkeleton* Skeleton = PreviewMesh->GetSkeleton())
{
AnimSequence->SetSkeleton(Skeleton);
}
AnimSequence->SetSkeleton(Skeleton);
}
}
PreviewScene->SetPreviewAnimationAsset(PreviewAsset, true);
PreviewScene->SetPreviewAnimationAsset(AnimSequence, true);
if (UDebugSkelMeshComponent* PreviewMeshComponent = PreviewScene->GetPreviewMeshComponent())
{
PreviewMeshComponent->SetAnimationMode(EAnimationMode::AnimationSingleNode);
if (PreviewMeshComponent->PreviewInstance)
{
const UMorphPresetDataAsset* PresetAsset = Preset.Get();
const float PlayRate = PresetAsset ? FMath::Max(PresetAsset->AnimationPlayRate, 0.01f) : 1.0f;
PreviewMeshComponent->PreviewInstance->SetAnimationAsset(PreviewAsset, false, PlayRate);
PreviewMeshComponent->PreviewInstance->SetAnimationAsset(AnimSequence, false, 1.0f);
PreviewMeshComponent->PreviewInstance->EnableControllers(true);
PreviewMeshComponent->PreviewInstance->SetSkeletalControlAlpha(1.0f);
PreviewMeshComponent->PreviewInstance->SetPlaying(false);
PreviewMeshComponent->PreviewInstance->SetLooping(false);
PreviewMeshComponent->PreviewInstance->SetPosition(GetPreviewAnimationPosition(), false);
PreviewMeshComponent->PreviewInstance->SetPosition(CurrentTime, false);
RefreshPreviewAnimationPose();
}
}
@@ -539,52 +503,6 @@ void FMorphPresetEditorController::RefreshPreviewAnimationPose()
PreviewScene->InvalidateViews();
}
UAnimationAsset* FMorphPresetEditorController::ResolvePreviewAnimationAsset() const
{
const UMorphPresetDataAsset* PresetAsset = Preset.Get();
if (PresetAsset && PresetAsset->AnimationMontage)
{
if (UAnimSequenceBase* FirstAnimReference = PresetAsset->AnimationMontage->GetFirstAnimReference())
{
return FirstAnimReference;
}
}
return nullptr;
}
float FMorphPresetEditorController::GetPreviewAnimationPosition() const
{
const UMorphPresetDataAsset* PresetAsset = Preset.Get();
UAnimationAsset* PreviewAsset = ResolvePreviewAnimationAsset();
if (!PresetAsset || !PreviewAsset)
{
return CurrentTime;
}
const float PlayRate = FMath::Max(PresetAsset->AnimationPlayRate, 0.01f);
float PreviewPosition = CurrentTime * PlayRate;
if (const UAnimSequenceBase* PreviewSequence = Cast<UAnimSequenceBase>(PreviewAsset))
{
return FMath::Clamp(PreviewPosition, 0.0f, PreviewSequence->GetPlayLength());
}
return PreviewPosition;
}
bool FMorphPresetEditorController::IsPreviewAnimationCompatible(UAnimationAsset* PreviewAsset) const
{
if (!PreviewAsset)
{
return true;
}
const USkeletalMesh* PreviewMesh = GetPreviewMesh();
const USkeleton* PreviewSkeleton = PreviewMesh ? PreviewMesh->GetSkeleton() : nullptr;
const USkeleton* AnimationSkeleton = PreviewAsset->GetSkeleton();
return PreviewSkeleton && AnimationSkeleton && PreviewSkeleton->IsCompatibleForEditor(AnimationSkeleton);
}
void FMorphPresetEditorController::ApplyPresetToPreview()
{
const UMorphPresetDataAsset* PresetAsset = Preset.Get();
@@ -13,7 +13,9 @@
#include "PropertyCustomizationHelpers.h"
#include "SEditorViewport.h"
#include "Sound/SoundBase.h"
#include "ToolMenus.h"
#include "Toolkits/AssetEditorToolkit.h"
#include "ViewportToolbar/UnrealEdViewportToolbar.h"
#include "Widgets/Input/SButton.h"
#include "Widgets/Input/SEditableTextBox.h"
#include "Widgets/Input/SSlider.h"
@@ -118,11 +120,13 @@ namespace
public:
SLATE_BEGIN_ARGS(SMorphPresetPreviewViewport) {}
SLATE_ARGUMENT(TSharedPtr<FMorphPresetEditorController>, Controller)
SLATE_ARGUMENT(TWeakPtr<FAssetEditorToolkit>, AssetEditorToolkit)
SLATE_END_ARGS()
void Construct(const FArguments& InArgs)
{
Controller = InArgs._Controller;
AssetEditorToolkit = InArgs._AssetEditorToolkit;
SEditorViewport::Construct(SEditorViewport::FArguments());
}
@@ -146,11 +150,45 @@ namespace
virtual TSharedPtr<SWidget> BuildViewportToolbar() override
{
return SNullWidget::NullWidget;
const FName ViewportToolbarName = TEXT("MorphPresetEditor.ViewportToolbar");
if (!UToolMenus::Get()->IsMenuRegistered(ViewportToolbarName))
{
UToolMenu* const ViewportToolbarMenu = UToolMenus::Get()->RegisterMenu(
ViewportToolbarName,
NAME_None,
EMultiBoxType::SlimHorizontalToolBar);
ViewportToolbarMenu->StyleName = TEXT("ViewportToolbar");
FToolMenuSection& LeftSection = ViewportToolbarMenu->AddSection(TEXT("Left"));
LeftSection.AddEntry(UE::UnrealEd::CreateTransformsSubmenu());
LeftSection.AddEntry(UE::UnrealEd::CreateSnappingSubmenu());
FToolMenuSection& RightSection = ViewportToolbarMenu->AddSection(TEXT("Right"));
RightSection.Alignment = EToolMenuSectionAlign::Last;
RightSection.AddEntry(UE::UnrealEd::CreateCameraSubmenu(UE::UnrealEd::FViewportCameraMenuOptions().ShowAll()));
RightSection.AddEntry(UE::UnrealEd::CreateViewModesSubmenu());
RightSection.AddEntry(UE::UnrealEd::CreatePerformanceAndScalabilitySubmenu());
}
FToolMenuContext ViewportToolbarContext;
if (TSharedPtr<IPersonaPreviewScene> PreviewScene = Controller.IsValid() ? Controller->GetPreviewScene() : nullptr)
{
ViewportToolbarContext.AppendCommandList(PreviewScene->GetCommandList());
}
ViewportToolbarContext.AppendCommandList(GetCommandList());
UUnrealEdViewportToolbarContext* const ContextObject =
UE::UnrealEd::CreateViewportToolbarDefaultContext(SharedThis(this));
ContextObject->AssetEditorToolkit = AssetEditorToolkit;
ViewportToolbarContext.AddObject(ContextObject);
return UToolMenus::Get()->GenerateWidget(ViewportToolbarName, ViewportToolbarContext);
}
private:
TSharedPtr<FMorphPresetEditorController> Controller;
TWeakPtr<FAssetEditorToolkit> AssetEditorToolkit;
};
}
@@ -330,6 +368,7 @@ void SMorphPresetViewportPanel::RebuildContent()
[
SNew(SMorphPresetPreviewViewport)
.Controller(Controller)
.AssetEditorToolkit(AssetEditorToolkit)
]);
}
@@ -8,7 +8,6 @@ class FUICommandList;
class AActor;
class IPersonaPreviewScene;
class IPersonaToolkit;
class UAnimationAsset;
class UAnimSequence;
class ULipSyncComponent;
class ULipSyncMappingDataAsset;
@@ -68,9 +67,6 @@ private:
void RebuildPreviewAnimation();
void ApplyPresetToPreview();
void RefreshPreviewAnimationPose();
UAnimationAsset* ResolvePreviewAnimationAsset() const;
float GetPreviewAnimationPosition() const;
bool IsPreviewAnimationCompatible(UAnimationAsset* PreviewAsset) const;
void RebuildLipSyncPreviewComponents();
void DestroyLipSyncPreviewComponents();
void TickLipSyncPreviewComponents(float DeltaTime);
@@ -82,6 +78,7 @@ private:
TSharedPtr<FUICommandList> CommandList;
TStrongObjectPtr<UAnimSequence> PreviewAnimation;
TStrongObjectPtr<ULipSyncMappingDataAsset> DefaultLipSyncPreviewMapping;
TWeakObjectPtr<USkeletalMesh> ActivePreviewMesh;
TWeakObjectPtr<ULipSyncMappingDataAsset> LipSyncPreviewMapping;
TWeakObjectPtr<USoundBase> LipSyncPreviewSound;
TWeakObjectPtr<AActor> LipSyncPreviewActor;