Add Games File

This commit is contained in:
Ivan Anikin
2026-05-19 13:42:41 +07:00
parent 7908479e0c
commit 12e7e22c1a
3672 changed files with 125922 additions and 0 deletions
@@ -0,0 +1,15 @@
// Copyright Epic Games, Inc. All Rights Reserved.
using UnrealBuildTool;
public class FunnyGirlEditor : ModuleRules
{
public FunnyGirlEditor(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
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" });
}
}
@@ -0,0 +1,30 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "AssetTypeActions/AssetTypeActions_DirectorPrompt.h"
#include "FunnyGirlEditorModule.h"
#include "LLM/DirectorPromptDataAsset.h"
#define LOCTEXT_NAMESPACE "AssetTypeActions_DirectorPrompt"
FText FAssetTypeActions_DirectorPrompt::GetName() const
{
return LOCTEXT("Name", "Director Prompt");
}
FColor FAssetTypeActions_DirectorPrompt::GetTypeColor() const
{
return FColor(120, 90, 210);
}
UClass* FAssetTypeActions_DirectorPrompt::GetSupportedClass() const
{
return UDirectorPromptDataAsset::StaticClass();
}
uint32 FAssetTypeActions_DirectorPrompt::GetCategories()
{
return FFunnyGirlEditorModule::GetNarrativeAssetCategory();
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,14 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "AssetTypeActions_Base.h"
class FAssetTypeActions_DirectorPrompt : public FAssetTypeActions_Base
{
public:
virtual FText GetName() const override;
virtual FColor GetTypeColor() const override;
virtual UClass* GetSupportedClass() const override;
virtual uint32 GetCategories() override;
};
@@ -0,0 +1,30 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "AssetTypeActions/AssetTypeActions_Identity.h"
#include "FunnyGirlEditorModule.h"
#include "LLM/IdentityDataAsset.h"
#define LOCTEXT_NAMESPACE "AssetTypeActions_Identity"
FText FAssetTypeActions_Identity::GetName() const
{
return LOCTEXT("Name", "Identity");
}
FColor FAssetTypeActions_Identity::GetTypeColor() const
{
return FColor(48, 190, 150);
}
UClass* FAssetTypeActions_Identity::GetSupportedClass() const
{
return UIdentityDataAsset::StaticClass();
}
uint32 FAssetTypeActions_Identity::GetCategories()
{
return FFunnyGirlEditorModule::GetNarrativeAssetCategory();
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,14 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "AssetTypeActions_Base.h"
class FAssetTypeActions_Identity : public FAssetTypeActions_Base
{
public:
virtual FText GetName() const override;
virtual FColor GetTypeColor() const override;
virtual UClass* GetSupportedClass() const override;
virtual uint32 GetCategories() override;
};
@@ -0,0 +1,30 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "AssetTypeActions/AssetTypeActions_LLMProvider.h"
#include "FunnyGirlEditorModule.h"
#include "LLM/LLMProviderDataAsset.h"
#define LOCTEXT_NAMESPACE "AssetTypeActions_LLMProvider"
FText FAssetTypeActions_LLMProvider::GetName() const
{
return LOCTEXT("Name", "LLM Provider");
}
FColor FAssetTypeActions_LLMProvider::GetTypeColor() const
{
return FColor(72, 128, 255);
}
UClass* FAssetTypeActions_LLMProvider::GetSupportedClass() const
{
return ULLMProviderDataAsset::StaticClass();
}
uint32 FAssetTypeActions_LLMProvider::GetCategories()
{
return FFunnyGirlEditorModule::GetLLMAssetCategory();
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,14 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "AssetTypeActions_Base.h"
class FAssetTypeActions_LLMProvider : public FAssetTypeActions_Base
{
public:
virtual FText GetName() const override;
virtual FColor GetTypeColor() const override;
virtual UClass* GetSupportedClass() const override;
virtual uint32 GetCategories() override;
};
@@ -0,0 +1,30 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "AssetTypeActions/AssetTypeActions_PersonalityTrait.h"
#include "FunnyGirlEditorModule.h"
#include "Identity/PersonalityTraitDataAsset.h"
#define LOCTEXT_NAMESPACE "AssetTypeActions_PersonalityTrait"
FText FAssetTypeActions_PersonalityTrait::GetName() const
{
return LOCTEXT("Name", "Personality Trait");
}
FColor FAssetTypeActions_PersonalityTrait::GetTypeColor() const
{
return FColor(225, 150, 65);
}
UClass* FAssetTypeActions_PersonalityTrait::GetSupportedClass() const
{
return UPersonalityTraitDataAsset::StaticClass();
}
uint32 FAssetTypeActions_PersonalityTrait::GetCategories()
{
return FFunnyGirlEditorModule::GetNarrativeAssetCategory();
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,14 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "AssetTypeActions_Base.h"
class FAssetTypeActions_PersonalityTrait : public FAssetTypeActions_Base
{
public:
virtual FText GetName() const override;
virtual FColor GetTypeColor() const override;
virtual UClass* GetSupportedClass() const override;
virtual uint32 GetCategories() override;
};
@@ -0,0 +1,30 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "AssetTypeActions/AssetTypeActions_SystemEvent.h"
#include "FunnyGirlEditorModule.h"
#include "LLM/SystemEventDataAsset.h"
#define LOCTEXT_NAMESPACE "AssetTypeActions_SystemEvent"
FText FAssetTypeActions_SystemEvent::GetName() const
{
return LOCTEXT("Name", "System Event");
}
FColor FAssetTypeActions_SystemEvent::GetTypeColor() const
{
return FColor(205, 95, 205);
}
UClass* FAssetTypeActions_SystemEvent::GetSupportedClass() const
{
return USystemEventDataAsset::StaticClass();
}
uint32 FAssetTypeActions_SystemEvent::GetCategories()
{
return FFunnyGirlEditorModule::GetNarrativeAssetCategory();
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,14 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "AssetTypeActions_Base.h"
class FAssetTypeActions_SystemEvent : public FAssetTypeActions_Base
{
public:
virtual FText GetName() const override;
virtual FColor GetTypeColor() const override;
virtual UClass* GetSupportedClass() const override;
virtual uint32 GetCategories() override;
};
@@ -0,0 +1,30 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "AssetTypeActions/AssetTypeActions_WorldContext.h"
#include "FunnyGirlEditorModule.h"
#include "World/WorldContextDataAsset.h"
#define LOCTEXT_NAMESPACE "AssetTypeActions_WorldContext"
FText FAssetTypeActions_WorldContext::GetName() const
{
return LOCTEXT("Name", "World Context");
}
FColor FAssetTypeActions_WorldContext::GetTypeColor() const
{
return FColor(80, 170, 220);
}
UClass* FAssetTypeActions_WorldContext::GetSupportedClass() const
{
return UWorldContextDataAsset::StaticClass();
}
uint32 FAssetTypeActions_WorldContext::GetCategories()
{
return FFunnyGirlEditorModule::GetNarrativeAssetCategory();
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,14 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "AssetTypeActions_Base.h"
class FAssetTypeActions_WorldContext : public FAssetTypeActions_Base
{
public:
virtual FText GetName() const override;
virtual FColor GetTypeColor() const override;
virtual UClass* GetSupportedClass() const override;
virtual uint32 GetCategories() override;
};
@@ -0,0 +1,37 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Factories/DirectorPromptDataAssetFactory.h"
#include "FunnyGirlEditorModule.h"
#include "LLM/DirectorPromptDataAsset.h"
#define LOCTEXT_NAMESPACE "DirectorPromptDataAssetFactory"
UDirectorPromptDataAssetFactory::UDirectorPromptDataAssetFactory()
{
SupportedClass = UDirectorPromptDataAsset::StaticClass();
bCreateNew = true;
bEditAfterNew = true;
}
UObject* UDirectorPromptDataAssetFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
{
return NewObject<UDirectorPromptDataAsset>(InParent, InClass, InName, Flags);
}
FText UDirectorPromptDataAssetFactory::GetDisplayName() const
{
return LOCTEXT("DisplayName", "Director Prompt");
}
uint32 UDirectorPromptDataAssetFactory::GetMenuCategories() const
{
return FFunnyGirlEditorModule::GetNarrativeAssetCategory();
}
bool UDirectorPromptDataAssetFactory::ShouldShowInNewMenu() const
{
return true;
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,20 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "Factories/Factory.h"
#include "DirectorPromptDataAssetFactory.generated.h"
UCLASS(hidecategories = Object)
class UDirectorPromptDataAssetFactory : public UFactory
{
GENERATED_BODY()
public:
UDirectorPromptDataAssetFactory();
virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
virtual FText GetDisplayName() const override;
virtual uint32 GetMenuCategories() const override;
virtual bool ShouldShowInNewMenu() const override;
};
@@ -0,0 +1,40 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Factories/DirectorToolBlueprintFactory.h"
#include "Engine/Blueprint.h"
#include "Engine/BlueprintGeneratedClass.h"
#include "FunnyGirlEditorModule.h"
#include "Kismet2/KismetEditorUtilities.h"
#include "LLM/BrainToolBase.h"
#define LOCTEXT_NAMESPACE "DirectorToolBlueprintFactory"
UDirectorToolBlueprintFactory::UDirectorToolBlueprintFactory()
{
SupportedClass = UBlueprint::StaticClass();
bCreateNew = true;
bEditAfterNew = true;
}
UObject* UDirectorToolBlueprintFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
{
return FKismetEditorUtilities::CreateBlueprint(UBrainToolBase::StaticClass(), InParent, InName, BPTYPE_Normal, UBlueprint::StaticClass(), UBlueprintGeneratedClass::StaticClass());
}
FText UDirectorToolBlueprintFactory::GetDisplayName() const
{
return LOCTEXT("DisplayName", "Brain Tool");
}
uint32 UDirectorToolBlueprintFactory::GetMenuCategories() const
{
return FFunnyGirlEditorModule::GetNarrativeAssetCategory();
}
bool UDirectorToolBlueprintFactory::ShouldShowInNewMenu() const
{
return true;
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,20 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "Factories/Factory.h"
#include "DirectorToolBlueprintFactory.generated.h"
UCLASS(hidecategories = Object)
class UDirectorToolBlueprintFactory : public UFactory
{
GENERATED_BODY()
public:
UDirectorToolBlueprintFactory();
virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
virtual FText GetDisplayName() const override;
virtual uint32 GetMenuCategories() const override;
virtual bool ShouldShowInNewMenu() const override;
};
@@ -0,0 +1,37 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Factories/IdentityDataAssetFactory.h"
#include "FunnyGirlEditorModule.h"
#include "LLM/IdentityDataAsset.h"
#define LOCTEXT_NAMESPACE "IdentityDataAssetFactory"
UIdentityDataAssetFactory::UIdentityDataAssetFactory()
{
SupportedClass = UIdentityDataAsset::StaticClass();
bCreateNew = true;
bEditAfterNew = true;
}
UObject* UIdentityDataAssetFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
{
return NewObject<UIdentityDataAsset>(InParent, InClass, InName, Flags);
}
FText UIdentityDataAssetFactory::GetDisplayName() const
{
return LOCTEXT("DisplayName", "Identity");
}
uint32 UIdentityDataAssetFactory::GetMenuCategories() const
{
return FFunnyGirlEditorModule::GetNarrativeAssetCategory();
}
bool UIdentityDataAssetFactory::ShouldShowInNewMenu() const
{
return true;
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,20 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "Factories/Factory.h"
#include "IdentityDataAssetFactory.generated.h"
UCLASS(hidecategories = Object)
class UIdentityDataAssetFactory : public UFactory
{
GENERATED_BODY()
public:
UIdentityDataAssetFactory();
virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
virtual FText GetDisplayName() const override;
virtual uint32 GetMenuCategories() const override;
virtual bool ShouldShowInNewMenu() const override;
};
@@ -0,0 +1,37 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Factories/LLMProviderDataAssetFactory.h"
#include "FunnyGirlEditorModule.h"
#include "LLM/LLMProviderDataAsset.h"
#define LOCTEXT_NAMESPACE "LLMProviderDataAssetFactory"
ULLMProviderDataAssetFactory::ULLMProviderDataAssetFactory()
{
SupportedClass = ULLMProviderDataAsset::StaticClass();
bCreateNew = true;
bEditAfterNew = true;
}
UObject* ULLMProviderDataAssetFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
{
return NewObject<ULLMProviderDataAsset>(InParent, InClass, InName, Flags);
}
FText ULLMProviderDataAssetFactory::GetDisplayName() const
{
return LOCTEXT("DisplayName", "LLM Provider");
}
uint32 ULLMProviderDataAssetFactory::GetMenuCategories() const
{
return FFunnyGirlEditorModule::GetLLMAssetCategory();
}
bool ULLMProviderDataAssetFactory::ShouldShowInNewMenu() const
{
return true;
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,20 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "Factories/Factory.h"
#include "LLMProviderDataAssetFactory.generated.h"
UCLASS(hidecategories = Object)
class ULLMProviderDataAssetFactory : public UFactory
{
GENERATED_BODY()
public:
ULLMProviderDataAssetFactory();
virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
virtual FText GetDisplayName() const override;
virtual uint32 GetMenuCategories() const override;
virtual bool ShouldShowInNewMenu() const override;
};
@@ -0,0 +1,37 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Factories/PersonalityTraitDataAssetFactory.h"
#include "FunnyGirlEditorModule.h"
#include "Identity/PersonalityTraitDataAsset.h"
#define LOCTEXT_NAMESPACE "PersonalityTraitDataAssetFactory"
UPersonalityTraitDataAssetFactory::UPersonalityTraitDataAssetFactory()
{
SupportedClass = UPersonalityTraitDataAsset::StaticClass();
bCreateNew = true;
bEditAfterNew = true;
}
UObject* UPersonalityTraitDataAssetFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
{
return NewObject<UPersonalityTraitDataAsset>(InParent, InClass, InName, Flags);
}
FText UPersonalityTraitDataAssetFactory::GetDisplayName() const
{
return LOCTEXT("DisplayName", "Personality Trait");
}
uint32 UPersonalityTraitDataAssetFactory::GetMenuCategories() const
{
return FFunnyGirlEditorModule::GetNarrativeAssetCategory();
}
bool UPersonalityTraitDataAssetFactory::ShouldShowInNewMenu() const
{
return true;
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,20 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "Factories/Factory.h"
#include "PersonalityTraitDataAssetFactory.generated.h"
UCLASS(hidecategories = Object)
class UPersonalityTraitDataAssetFactory : public UFactory
{
GENERATED_BODY()
public:
UPersonalityTraitDataAssetFactory();
virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
virtual FText GetDisplayName() const override;
virtual uint32 GetMenuCategories() const override;
virtual bool ShouldShowInNewMenu() const override;
};
@@ -0,0 +1,37 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Factories/SystemEventDataAssetFactory.h"
#include "FunnyGirlEditorModule.h"
#include "LLM/SystemEventDataAsset.h"
#define LOCTEXT_NAMESPACE "SystemEventDataAssetFactory"
USystemEventDataAssetFactory::USystemEventDataAssetFactory()
{
SupportedClass = USystemEventDataAsset::StaticClass();
bCreateNew = true;
bEditAfterNew = true;
}
UObject* USystemEventDataAssetFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
{
return NewObject<USystemEventDataAsset>(InParent, InClass, InName, Flags);
}
FText USystemEventDataAssetFactory::GetDisplayName() const
{
return LOCTEXT("DisplayName", "System Event");
}
uint32 USystemEventDataAssetFactory::GetMenuCategories() const
{
return FFunnyGirlEditorModule::GetNarrativeAssetCategory();
}
bool USystemEventDataAssetFactory::ShouldShowInNewMenu() const
{
return true;
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,20 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "Factories/Factory.h"
#include "SystemEventDataAssetFactory.generated.h"
UCLASS(hidecategories = Object)
class USystemEventDataAssetFactory : public UFactory
{
GENERATED_BODY()
public:
USystemEventDataAssetFactory();
virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
virtual FText GetDisplayName() const override;
virtual uint32 GetMenuCategories() const override;
virtual bool ShouldShowInNewMenu() const override;
};
@@ -0,0 +1,37 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Factories/WorldContextDataAssetFactory.h"
#include "FunnyGirlEditorModule.h"
#include "World/WorldContextDataAsset.h"
#define LOCTEXT_NAMESPACE "WorldContextDataAssetFactory"
UWorldContextDataAssetFactory::UWorldContextDataAssetFactory()
{
SupportedClass = UWorldContextDataAsset::StaticClass();
bCreateNew = true;
bEditAfterNew = true;
}
UObject* UWorldContextDataAssetFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
{
return NewObject<UWorldContextDataAsset>(InParent, InClass, InName, Flags);
}
FText UWorldContextDataAssetFactory::GetDisplayName() const
{
return LOCTEXT("DisplayName", "World Context");
}
uint32 UWorldContextDataAssetFactory::GetMenuCategories() const
{
return FFunnyGirlEditorModule::GetNarrativeAssetCategory();
}
bool UWorldContextDataAssetFactory::ShouldShowInNewMenu() const
{
return true;
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,20 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "Factories/Factory.h"
#include "WorldContextDataAssetFactory.generated.h"
UCLASS(hidecategories = Object)
class UWorldContextDataAssetFactory : public UFactory
{
GENERATED_BODY()
public:
UWorldContextDataAssetFactory();
virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
virtual FText GetDisplayName() const override;
virtual uint32 GetMenuCategories() const override;
virtual bool ShouldShowInNewMenu() const override;
};
@@ -0,0 +1,93 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "FunnyGirlEditorModule.h"
#include "AssetTypeActions/AssetTypeActions_DirectorPrompt.h"
#include "AssetTypeActions/AssetTypeActions_Identity.h"
#include "AssetTypeActions/AssetTypeActions_LLMProvider.h"
#include "AssetTypeActions/AssetTypeActions_PersonalityTrait.h"
#include "AssetTypeActions/AssetTypeActions_SystemEvent.h"
#include "AssetTypeActions/AssetTypeActions_WorldContext.h"
#include "AssetToolsModule.h"
#include "IAssetTools.h"
#include "IAssetTypeActions.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"
#define LOCTEXT_NAMESPACE "FunnyGirlEditor"
EAssetTypeCategories::Type FFunnyGirlEditorModule::LLMAssetCategory = EAssetTypeCategories::Misc;
EAssetTypeCategories::Type FFunnyGirlEditorModule::NarrativeAssetCategory = EAssetTypeCategories::Misc;
void FFunnyGirlEditorModule::StartupModule()
{
IAssetTools& AssetTools = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get();
LLMAssetCategory = AssetTools.RegisterAdvancedAssetCategory(FName(TEXT("LLM")), LOCTEXT("LLMAssetCategory", "LLM"));
NarrativeAssetCategory = AssetTools.RegisterAdvancedAssetCategory(FName(TEXT("Narrative")), LOCTEXT("NarrativeAssetCategory", "Narrative"));
RegisterAssetTypeAction(MakeShared<FAssetTypeActions_LLMProvider>());
RegisterAssetTypeAction(MakeShared<FAssetTypeActions_DirectorPrompt>());
RegisterAssetTypeAction(MakeShared<FAssetTypeActions_Identity>());
RegisterAssetTypeAction(MakeShared<FAssetTypeActions_PersonalityTrait>());
RegisterAssetTypeAction(MakeShared<FAssetTypeActions_SystemEvent>());
RegisterAssetTypeAction(MakeShared<FAssetTypeActions_WorldContext>());
RegisterAssetTypeAction(MakeShared<FMorphPresetAssetTypeActions>());
RegisterAssetTypeAction(MakeShared<FLipSyncMappingAssetTypeActions>());
FPropertyEditorModule& PropertyEditorModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>(TEXT("PropertyEditor"));
PropertyEditorModule.RegisterCustomClassLayout(
UMorphPresetDataAsset::StaticClass()->GetFName(),
FOnGetDetailCustomizationInstance::CreateStatic(&FMorphPresetDetailsCustomization::MakeInstance));
PropertyEditorModule.NotifyCustomizationModuleChanged();
bMorphPresetCustomizationRegistered = true;
}
void FFunnyGirlEditorModule::ShutdownModule()
{
if (FModuleManager::Get().IsModuleLoaded(TEXT("AssetTools")))
{
IAssetTools& AssetTools = FModuleManager::GetModuleChecked<FAssetToolsModule>("AssetTools").Get();
for (const TSharedPtr<IAssetTypeActions>& AssetTypeAction : RegisteredAssetTypeActions)
{
if (AssetTypeAction.IsValid())
{
AssetTools.UnregisterAssetTypeActions(AssetTypeAction.ToSharedRef());
}
}
}
RegisteredAssetTypeActions.Empty();
if (bMorphPresetCustomizationRegistered && FModuleManager::Get().IsModuleLoaded(TEXT("PropertyEditor")))
{
FPropertyEditorModule& PropertyEditorModule = FModuleManager::GetModuleChecked<FPropertyEditorModule>(TEXT("PropertyEditor"));
PropertyEditorModule.UnregisterCustomClassLayout(UMorphPresetDataAsset::StaticClass()->GetFName());
PropertyEditorModule.NotifyCustomizationModuleChanged();
bMorphPresetCustomizationRegistered = false;
}
}
EAssetTypeCategories::Type FFunnyGirlEditorModule::GetLLMAssetCategory()
{
return LLMAssetCategory;
}
EAssetTypeCategories::Type FFunnyGirlEditorModule::GetNarrativeAssetCategory()
{
return NarrativeAssetCategory;
}
void FFunnyGirlEditorModule::RegisterAssetTypeAction(const TSharedRef<IAssetTypeActions>& AssetTypeAction)
{
IAssetTools& AssetTools = FModuleManager::LoadModuleChecked<FAssetToolsModule>("AssetTools").Get();
AssetTools.RegisterAssetTypeActions(AssetTypeAction);
RegisteredAssetTypeActions.Add(AssetTypeAction);
}
#undef LOCTEXT_NAMESPACE
IMPLEMENT_MODULE(FFunnyGirlEditorModule, FunnyGirlEditor)
@@ -0,0 +1,27 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "AssetTypeCategories.h"
#include "Modules/ModuleInterface.h"
class IAssetTypeActions;
class FFunnyGirlEditorModule : public IModuleInterface
{
public:
virtual void StartupModule() override;
virtual void ShutdownModule() override;
static EAssetTypeCategories::Type GetLLMAssetCategory();
static EAssetTypeCategories::Type GetNarrativeAssetCategory();
private:
static EAssetTypeCategories::Type LLMAssetCategory;
static EAssetTypeCategories::Type NarrativeAssetCategory;
TArray<TSharedPtr<IAssetTypeActions>> RegisteredAssetTypeActions;
bool bMorphPresetCustomizationRegistered = false;
void RegisterAssetTypeAction(const TSharedRef<IAssetTypeActions>& AssetTypeAction);
};
@@ -0,0 +1,101 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Morph/AnimTimeline/SAnimEditorBase.h"
#include "AnimPreviewInstance.h"
#include "Animation/AnimSequenceBase.h"
#include "Animation/BlendSpace.h"
#include "Animation/DebugSkelMeshComponent.h"
#include "Widgets/SBoxPanel.h"
#include "Widgets/SOverlay.h"
void SAnimEditorBase::Construct(const FArguments& InArgs, const TSharedPtr<IPersonaPreviewScene>& InPreviewScene)
{
PreviewScenePtr = InPreviewScene;
OnObjectsSelectedDelegate = InArgs._OnObjectsSelected;
SetInputViewRange(0.0f, GetSequenceLength());
const TSharedRef<SWidget> TimelineWidget = InArgs._DisplayAnimTimeline
? CreateAnimTimelineWidget()
: SNullWidget::NullWidget;
TSharedPtr<SVerticalBox> RootBox;
ChildSlot
[
SAssignNew(RootBox, SVerticalBox)
+ SVerticalBox::Slot()
.FillHeight(1.0f)
[
SNew(SOverlay)
+ SOverlay::Slot()
[
SAssignNew(NonScrollEditorPanels, SVerticalBox)
]
+ SOverlay::Slot()
[
TimelineWidget
]
]
];
if (InArgs._DisplayAnimScrubBar)
{
RootBox->AddSlot()
.AutoHeight()
[
ConstructAnimScrubPanel(InArgs._DisplayAnimScrubBarEditing)
];
}
}
void SAnimEditorBase::SetInputViewRange(float InViewMinInput, float InViewMaxInput)
{
ViewMaxInput = FMath::Min(InViewMaxInput, GetSequenceLength());
ViewMinInput = FMath::Max(InViewMinInput, 0.0f);
}
float SAnimEditorBase::GetScrubValue() const
{
if (PreviewScenePtr.IsValid())
{
if (UDebugSkelMeshComponent* PreviewMesh = PreviewScenePtr.Pin()->GetPreviewMeshComponent())
{
if (PreviewMesh->PreviewInstance)
{
return PreviewMesh->PreviewInstance->GetCurrentTime();
}
}
}
return 0.0f;
}
void SAnimEditorBase::OnSelectionChanged(const TArray<UObject*>& SelectedItems)
{
OnObjectsSelectedDelegate.ExecuteIfBound(SelectedItems);
}
TSharedRef<SWidget> SAnimEditorBase::CreateDocumentAnchor()
{
return SNullWidget::NullWidget;
}
TSharedRef<SWidget> SAnimEditorBase::ConstructAnimScrubPanel(bool bDisplayAnimScrubBarEditing)
{
return SNullWidget::NullWidget;
}
float SAnimEditorBase::GetSequenceLength() const
{
if (const UAnimSequenceBase* AnimSequence = Cast<UAnimSequenceBase>(GetEditorObject()))
{
return AnimSequence->GetPlayLength();
}
if (const UBlendSpace* BlendSpace = Cast<UBlendSpace>(GetEditorObject()))
{
return 1.0f;
}
return 0.0f;
}
@@ -0,0 +1,22 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Morph/Editor/LipSyncMappingAssetFactory.h"
#include "Morph/LipSyncMappingDataAsset.h"
ULipSyncMappingAssetFactory::ULipSyncMappingAssetFactory()
{
SupportedClass = ULipSyncMappingDataAsset::StaticClass();
bCreateNew = true;
bEditAfterNew = true;
}
UObject* ULipSyncMappingAssetFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
{
return NewObject<ULipSyncMappingDataAsset>(InParent, InClass, InName, Flags);
}
FString ULipSyncMappingAssetFactory::GetDefaultNewAssetName() const
{
return TEXT("LipSyncMapping");
}
@@ -0,0 +1,34 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Morph/Editor/LipSyncMappingAssetTypeActions.h"
#include "Morph/LipSyncMappingDataAsset.h"
#define LOCTEXT_NAMESPACE "LipSyncMapping"
FText FLipSyncMappingAssetTypeActions::GetName() const
{
return LOCTEXT("AssetTypeActions_LipSyncMapping", "Lip Sync Mapping");
}
FColor FLipSyncMappingAssetTypeActions::GetTypeColor() const
{
return FColor(120, 210, 255);
}
UClass* FLipSyncMappingAssetTypeActions::GetSupportedClass() const
{
return ULipSyncMappingDataAsset::StaticClass();
}
uint32 FLipSyncMappingAssetTypeActions::GetCategories()
{
return EAssetTypeCategories::Animation;
}
FText FLipSyncMappingAssetTypeActions::GetAssetDescription(const FAssetData& AssetData) const
{
return LOCTEXT("LipSyncMappingDescription", "Maps lip sync viseme names to character morph targets.");
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,169 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Morph/Editor/MorphPresetAssetEditor.h"
#include "IDetailsView.h"
#include "Morph/MorphPresetDataAsset.h"
#include "Morph/Editor/MorphPresetEditorController.h"
#include "Morph/Editor/MorphPresetTimelinePanel.h"
#include "Morph/Editor/MorphPresetViewportPanel.h"
#include "PropertyEditorModule.h"
#include "Widgets/Docking/SDockTab.h"
#define LOCTEXT_NAMESPACE "MorphPresetAssetEditor"
const FName FMorphPresetAssetEditor::EditorName(TEXT("MorphPresetAssetEditor"));
const FName FMorphPresetAssetEditor::ViewportTabId(TEXT("MorphPresetEditor_Viewport"));
const FName FMorphPresetAssetEditor::TimelineTabId(TEXT("MorphPresetEditor_Timeline"));
const FName FMorphPresetAssetEditor::DetailsTabId(TEXT("MorphPresetEditor_Details"));
void FMorphPresetAssetEditor::InitMorphPresetAssetEditor(
const EToolkitMode::Type Mode,
const TSharedPtr<IToolkitHost>& InitToolkitHost,
UMorphPresetDataAsset* InAsset)
{
Preset = InAsset;
EditorController = MakeShared<FMorphPresetEditorController>(InAsset);
EditorController->OnPresetChanged().AddSP(this, &FMorphPresetAssetEditor::HandlePresetChanged);
EditorController->Initialize();
TSharedRef<FTabManager::FLayout> Layout = FTabManager::NewLayout("MorphPresetEditorLayout_V2")
->AddArea(
FTabManager::NewPrimaryArea()
->SetOrientation(Orient_Horizontal)
->Split(
FTabManager::NewSplitter()
->SetOrientation(Orient_Vertical)
->Split(
FTabManager::NewStack()
->SetSizeCoefficient(0.68f)
->AddTab(ViewportTabId, ETabState::OpenedTab)
)
->Split(
FTabManager::NewStack()
->SetSizeCoefficient(0.32f)
->AddTab(TimelineTabId, ETabState::OpenedTab)
)
)
->Split(
FTabManager::NewStack()
->SetSizeCoefficient(0.25f)
->AddTab(DetailsTabId, ETabState::OpenedTab)
)
);
FAssetEditorToolkit::InitAssetEditor(Mode, InitToolkitHost, EditorName, Layout, true, true, InAsset);
RegenerateMenusAndToolbars();
}
void FMorphPresetAssetEditor::RegisterTabSpawners(const TSharedRef<FTabManager>& InTabManager)
{
FAssetEditorToolkit::RegisterTabSpawners(InTabManager);
InTabManager->RegisterTabSpawner(ViewportTabId, FOnSpawnTab::CreateSP(this, &FMorphPresetAssetEditor::SpawnTab_Viewport))
.SetDisplayName(LOCTEXT("ViewportTab", "Viewport"));
InTabManager->RegisterTabSpawner(TimelineTabId, FOnSpawnTab::CreateSP(this, &FMorphPresetAssetEditor::SpawnTab_Timeline))
.SetDisplayName(LOCTEXT("TimelineTab", "Timeline"));
InTabManager->RegisterTabSpawner(DetailsTabId, FOnSpawnTab::CreateSP(this, &FMorphPresetAssetEditor::SpawnTab_Details))
.SetDisplayName(LOCTEXT("DetailsTab", "Details"));
}
void FMorphPresetAssetEditor::UnregisterTabSpawners(const TSharedRef<FTabManager>& InTabManager)
{
FAssetEditorToolkit::UnregisterTabSpawners(InTabManager);
InTabManager->UnregisterTabSpawner(ViewportTabId);
InTabManager->UnregisterTabSpawner(TimelineTabId);
InTabManager->UnregisterTabSpawner(DetailsTabId);
}
FName FMorphPresetAssetEditor::GetToolkitFName() const
{
return EditorName;
}
FText FMorphPresetAssetEditor::GetBaseToolkitName() const
{
return LOCTEXT("ToolkitName", "Morph Target Preset Editor");
}
FString FMorphPresetAssetEditor::GetWorldCentricTabPrefix() const
{
return TEXT("Morph Preset ");
}
FLinearColor FMorphPresetAssetEditor::GetWorldCentricTabColorScale() const
{
return FLinearColor(0.18f, 0.47f, 0.83f);
}
TSharedRef<SDockTab> FMorphPresetAssetEditor::SpawnTab_Viewport(const FSpawnTabArgs& Args)
{
ViewportWidget = SNew(SMorphPresetViewportPanel)
.Controller(EditorController)
.AssetEditorToolkit(SharedThis(this));
return SNew(SDockTab)
.Label(LOCTEXT("ViewportTabLabel", "Viewport"))
[
ViewportWidget.ToSharedRef()
];
}
TSharedRef<SDockTab> FMorphPresetAssetEditor::SpawnTab_Timeline(const FSpawnTabArgs& Args)
{
TimelineWidget = SNew(SMorphPresetTimelinePanel)
.Controller(EditorController);
return SNew(SDockTab)
.Label(LOCTEXT("TimelineTabLabel", "Timeline"))
[
TimelineWidget.ToSharedRef()
];
}
TSharedRef<SDockTab> FMorphPresetAssetEditor::SpawnTab_Details(const FSpawnTabArgs& Args)
{
FPropertyEditorModule& PropertyModule = FModuleManager::LoadModuleChecked<FPropertyEditorModule>("PropertyEditor");
FDetailsViewArgs DetailsArgs;
DetailsArgs.bAllowSearch = true;
DetailsArgs.bHideSelectionTip = true;
DetailsView = PropertyModule.CreateDetailView(DetailsArgs);
DetailsView->OnFinishedChangingProperties().AddSP(this, &FMorphPresetAssetEditor::HandleDetailsChanged);
DetailsView->SetObjects(TArray<UObject*>{ Preset.Get() });
return SNew(SDockTab)
.Label(LOCTEXT("DetailsTabLabel", "Details"))
[
DetailsView.ToSharedRef()
];
}
void FMorphPresetAssetEditor::HandleDetailsChanged(const FPropertyChangedEvent&)
{
if (EditorController.IsValid())
{
EditorController->NotifyPresetEdited();
}
if (TimelineWidget.IsValid())
{
TimelineWidget->Refresh();
}
}
void FMorphPresetAssetEditor::HandlePresetChanged()
{
if (DetailsView.IsValid())
{
DetailsView->ForceRefresh();
}
if (TimelineWidget.IsValid())
{
TimelineWidget->Refresh();
}
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,21 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Morph/Editor/MorphPresetAssetFactory.h"
#include "Morph/MorphPresetDataAsset.h"
UMorphPresetAssetFactory::UMorphPresetAssetFactory()
{
SupportedClass = UMorphPresetDataAsset::StaticClass();
bCreateNew = true;
bEditAfterNew = true;
}
UObject* UMorphPresetAssetFactory::FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn)
{
return NewObject<UMorphPresetDataAsset>(InParent, InClass, InName, Flags);
}
FString UMorphPresetAssetFactory::GetDefaultNewAssetName() const
{
return TEXT("MorphPreset");
}
@@ -0,0 +1,46 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Morph/Editor/MorphPresetAssetTypeActions.h"
#include "Morph/Editor/MorphPresetAssetEditor.h"
#include "Morph/MorphPresetDataAsset.h"
#include "Morph/Editor/MorphPresetAssetFactory.h"
#include "Toolkits/IToolkitHost.h"
#define LOCTEXT_NAMESPACE "MorphPreset"
FText FMorphPresetAssetTypeActions::GetName() const
{
return LOCTEXT("AssetTypeActions_MorphPreset", "Morph Preset");
}
FColor FMorphPresetAssetTypeActions::GetTypeColor() const
{
return FColor(200, 150, 255); // Светло-фиолетовый
}
UClass* FMorphPresetAssetTypeActions::GetSupportedClass() const
{
return UMorphPresetDataAsset::StaticClass();
}
uint32 FMorphPresetAssetTypeActions::GetCategories()
{
return EAssetTypeCategories::Animation;
}
FText FMorphPresetAssetTypeActions::GetAssetDescription(const FAssetData& AssetData) const
{
return LOCTEXT("MorphPresetDescription", "Пресет морф таргетов для персонажа. Улыбка, грусть, удивление и т.д.");
}
void FMorphPresetAssetTypeActions::OpenAssetEditor(const TArray<UObject*>& InObjects, TSharedPtr<IToolkitHost> EditWithinLevelEditor)
{
if (InObjects.Num() == 0) return;
UMorphPresetDataAsset* Preset = Cast<UMorphPresetDataAsset>(InObjects[0]);
if (!Preset) return;
TSharedRef<FMorphPresetAssetEditor> Editor = MakeShared<FMorphPresetAssetEditor>();
Editor->InitMorphPresetAssetEditor(EToolkitMode::Standalone, EditWithinLevelEditor, Preset);
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,245 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Morph/Editor/MorphPresetDetailsCustomization.h"
#include "DetailCategoryBuilder.h"
#include "DetailLayoutBuilder.h"
#include "DetailWidgetRow.h"
#include "IPropertyUtilities.h"
#include "JsonObjectConverter.h"
#include "Dom/JsonObject.h"
#include "Policies/PrettyJsonPrintPolicy.h"
#include "Serialization/JsonSerializer.h"
#include "Morph/MorphPresetDataAsset.h"
#include "Widgets/Input/SMultiLineEditableTextBox.h"
#include "Widgets/Layout/SBox.h"
#include "Widgets/SBoxPanel.h"
#include "Widgets/Text/STextBlock.h"
#define LOCTEXT_NAMESPACE "MorphPresetDetailsCustomization"
namespace
{
FString SerializePresetToJson(const UMorphPresetDataAsset* Preset)
{
if (!Preset)
{
return TEXT("{}");
}
TSharedRef<FJsonObject> JsonObject = MakeShared<FJsonObject>();
if (!FJsonObjectConverter::UStructToJsonObject(Preset->GetClass(), Preset, JsonObject, 0, 0))
{
return TEXT("{}");
}
JsonObject->RemoveField(TEXT("PreviewMesh"));
FString JsonString;
const TSharedRef<TJsonWriter<TCHAR, TPrettyJsonPrintPolicy<TCHAR>>> Writer =
TJsonWriterFactory<TCHAR, TPrettyJsonPrintPolicy<TCHAR>>::Create(&JsonString);
FJsonSerializer::Serialize(JsonObject, Writer);
return JsonString;
}
bool DeserializePresetFromJson(const FString& JsonString, UMorphPresetDataAsset& OutPreset)
{
TSharedPtr<FJsonObject> JsonObject;
const TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(JsonString);
if (!FJsonSerializer::Deserialize(Reader, JsonObject) || !JsonObject.IsValid())
{
return false;
}
JsonObject->RemoveField(TEXT("PreviewMesh"));
return FJsonObjectConverter::JsonObjectToUStruct(JsonObject.ToSharedRef(), &OutPreset, 0, 0);
}
void CopyPresetValues(const UMorphPresetDataAsset& SourcePreset, UMorphPresetDataAsset& TargetPreset)
{
TargetPreset.DisplayName = SourcePreset.DisplayName;
TargetPreset.Description = SourcePreset.Description;
TargetPreset.MorphTargets = SourcePreset.MorphTargets;
TargetPreset.Duration = SourcePreset.Duration;
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;
}
}
TSharedRef<IDetailCustomization> FMorphPresetDetailsCustomization::MakeInstance()
{
return MakeShared<FMorphPresetDetailsCustomization>();
}
void FMorphPresetDetailsCustomization::CustomizeDetails(IDetailLayoutBuilder& DetailBuilder)
{
const TArray<TWeakObjectPtr<UObject>>& CustomizedObjects = DetailBuilder.GetSelectedObjects();
for (const TWeakObjectPtr<UObject>& Object : CustomizedObjects)
{
if (UMorphPresetDataAsset* MorphPreset = Cast<UMorphPresetDataAsset>(Object.Get()))
{
Preset = MorphPreset;
break;
}
}
PropertyUtilities = DetailBuilder.GetPropertyUtilities();
RefreshJsonFromPreset();
IDetailCategoryBuilder& JsonCategory = DetailBuilder.EditCategory("JSON", LOCTEXT("JsonCategory", "JSON"), ECategoryPriority::Uncommon);
JsonCategory.AddCustomRow(LOCTEXT("PresetJsonFilter", "Preset JSON"))
.WholeRowContent()
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(0.0f, 0.0f, 0.0f, 6.0f)
[
SNew(STextBlock)
.Text(LOCTEXT("JsonHelpText", "JSON representation of the current preset. Pasted valid JSON is applied immediately."))
.AutoWrapText(true)
]
+ SVerticalBox::Slot()
.FillHeight(1.0f)
[
SNew(SBox)
.MinDesiredHeight(260.0f)
.MaxDesiredHeight(420.0f)
[
SAssignNew(JsonTextBox, SMultiLineEditableTextBox)
.Text(FText::FromString(CachedJsonText))
.OnTextChanged(this, &FMorphPresetDetailsCustomization::HandleJsonTextChanged)
.AutoWrapText(false)
]
]
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(0.0f, 6.0f, 0.0f, 0.0f)
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.AutoWidth()
.Padding(0.0f, 0.0f, 8.0f, 0.0f)
[
SNew(SButton)
.Text(LOCTEXT("ApplyJsonButton", "Apply JSON"))
.OnClicked(this, &FMorphPresetDetailsCustomization::OnApplyJsonClicked)
]
+ SHorizontalBox::Slot()
.AutoWidth()
.Padding(0.0f, 0.0f, 12.0f, 0.0f)
[
SNew(SButton)
.Text(LOCTEXT("ResetJsonButton", "Reset From Preset"))
.OnClicked(this, &FMorphPresetDetailsCustomization::OnResetJsonClicked)
]
+ SHorizontalBox::Slot()
.FillWidth(1.0f)
.VAlign(VAlign_Center)
[
SNew(STextBlock)
.Text(this, &FMorphPresetDetailsCustomization::GetStatusText)
.ColorAndOpacity(this, &FMorphPresetDetailsCustomization::GetStatusColor)
.AutoWrapText(true)
]
]
];
}
void FMorphPresetDetailsCustomization::RefreshJsonFromPreset()
{
CachedJsonText = SerializePresetToJson(Preset.Get());
bIsUpdatingJsonText = true;
if (JsonTextBox.IsValid())
{
JsonTextBox->SetText(FText::FromString(CachedJsonText));
}
bIsUpdatingJsonText = false;
}
void FMorphPresetDetailsCustomization::ApplyJsonToPreset(const FString& JsonString)
{
UMorphPresetDataAsset* PresetAsset = Preset.Get();
if (!PresetAsset)
{
bLastApplySucceeded = false;
StatusText = LOCTEXT("NoPresetStatus", "Preset is no longer available.");
return;
}
UMorphPresetDataAsset* TempPreset = NewObject<UMorphPresetDataAsset>(GetTransientPackage(), UMorphPresetDataAsset::StaticClass(), NAME_None, RF_Transient);
if (!DeserializePresetFromJson(JsonString, *TempPreset))
{
bLastApplySucceeded = false;
StatusText = LOCTEXT("InvalidJsonStatus", "JSON is invalid or does not match the preset structure.");
return;
}
PresetAsset->Modify();
CopyPresetValues(*TempPreset, *PresetAsset);
PresetAsset->MarkPackageDirty();
PresetAsset->PostEditChange();
bLastApplySucceeded = true;
StatusText = LOCTEXT("AppliedJsonStatus", "JSON applied to the preset.");
RefreshJsonFromPreset();
if (PropertyUtilities.IsValid())
{
const FPropertyChangedEvent ChangeEvent(nullptr, EPropertyChangeType::ValueSet);
PropertyUtilities->NotifyFinishedChangingProperties(ChangeEvent);
PropertyUtilities->ForceRefresh();
}
}
FText FMorphPresetDetailsCustomization::GetStatusText() const
{
return StatusText;
}
FSlateColor FMorphPresetDetailsCustomization::GetStatusColor() const
{
return bLastApplySucceeded ? FSlateColor(FLinearColor(0.35f, 0.8f, 0.45f, 1.0f)) : FSlateColor(FLinearColor(0.95f, 0.4f, 0.35f, 1.0f));
}
FReply FMorphPresetDetailsCustomization::OnApplyJsonClicked()
{
if (JsonTextBox.IsValid())
{
ApplyJsonToPreset(JsonTextBox->GetText().ToString());
}
return FReply::Handled();
}
FReply FMorphPresetDetailsCustomization::OnResetJsonClicked()
{
RefreshJsonFromPreset();
bLastApplySucceeded = true;
StatusText = LOCTEXT("ResetJsonStatus", "JSON reset to the current preset state.");
return FReply::Handled();
}
void FMorphPresetDetailsCustomization::HandleJsonTextChanged(const FText& NewText)
{
if (bIsUpdatingJsonText)
{
return;
}
const FString NewJsonText = NewText.ToString();
if (NewJsonText == CachedJsonText)
{
return;
}
CachedJsonText = NewJsonText;
ApplyJsonToPreset(NewJsonText);
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,733 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Morph/Editor/MorphPresetEditorController.h"
#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"
#include "Editor.h"
#include "Framework/Commands/UICommandList.h"
#include "GameFramework/Actor.h"
#include "IPersonaPreviewScene.h"
#include "IPersonaToolkit.h"
#include "Modules/ModuleManager.h"
#include "Morph/LipSyncComponent.h"
#include "Morph/LipSyncMappingDataAsset.h"
#include "Morph/MorphControllerComponent.h"
#include "Morph/MorphPresetDataAsset.h"
#include "PersonaModule.h"
#include "ReferenceSkeleton.h"
#include "Sound/SoundBase.h"
DEFINE_LOG_CATEGORY_STATIC(LogMorphPresetEditorPreview, Log, All);
namespace
{
void SetAnimSequencePlayLength(UAnimSequence& AnimSequence, float PlayLength, bool bShouldTransact)
{
IAnimationDataController& Controller = AnimSequence.GetController();
Controller.SetNumberOfFrames(FMath::Max(Controller.ConvertSecondsToFrameNumber(PlayLength).Value, 1), bShouldTransact);
}
}
FMorphPresetEditorController::FMorphPresetEditorController(UMorphPresetDataAsset* InPreset)
: Preset(InPreset)
{
CommandList = MakeShared<FUICommandList>();
DefaultLipSyncPreviewMapping = TStrongObjectPtr<ULipSyncMappingDataAsset>(
NewObject<ULipSyncMappingDataAsset>(GetTransientPackage(), NAME_None, RF_Transient));
}
FMorphPresetEditorController::~FMorphPresetEditorController()
{
StopLipSyncPreviewAudio();
DestroyLipSyncPreviewComponents();
if (PersonaToolkit.IsValid())
{
if (TSharedPtr<IPersonaPreviewScene> PreviewScene = PersonaToolkit->GetPreviewScene())
{
PreviewScene->ClearSelectedBone();
}
}
}
void FMorphPresetEditorController::Initialize()
{
RebuildPreviewScene();
SetCurrentTime(0.0f);
}
UMorphPresetDataAsset* FMorphPresetEditorController::GetPreset() const
{
return Preset.Get();
}
USkeletalMesh* FMorphPresetEditorController::GetPreviewMesh() const
{
if (const UMorphPresetDataAsset* PresetAsset = Preset.Get())
{
return PresetAsset->PreviewMesh.LoadSynchronous();
}
return nullptr;
}
bool FMorphPresetEditorController::SetPreviewMesh(USkeletalMesh* InPreviewMesh)
{
UMorphPresetDataAsset* PresetAsset = Preset.Get();
if (!PresetAsset)
{
return false;
}
if (PresetAsset->PreviewMesh.Get() == InPreviewMesh)
{
return false;
}
PresetAsset->Modify();
PresetAsset->PreviewMesh = InPreviewMesh;
PresetAsset->MarkPackageDirty();
RebuildPreviewScene();
return true;
}
TSharedPtr<IPersonaPreviewScene> FMorphPresetEditorController::GetPreviewScene() const
{
TSharedPtr<IPersonaPreviewScene> PreviewScene;
if (PersonaToolkit.IsValid())
{
PreviewScene = PersonaToolkit->GetPreviewScene();
}
return PreviewScene;
}
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 1.0f;
}
float FMorphPresetEditorController::GetCurrentTime() const
{
return CurrentTime;
}
void FMorphPresetEditorController::SetCurrentTime(float InTime)
{
CurrentTime = FMath::Clamp(InTime, 0.0f, GetTimelineLength());
if (TSharedPtr<IPersonaPreviewScene> PreviewScene = GetPreviewScene())
{
if (UDebugSkelMeshComponent* PreviewMeshComponent = PreviewScene->GetPreviewMeshComponent())
{
if (PreviewMeshComponent->PreviewInstance)
{
PreviewMeshComponent->PreviewInstance->SetPlaying(false);
PreviewMeshComponent->PreviewInstance->SetPosition(GetPreviewAnimationPosition(), false);
RefreshPreviewAnimationPose();
}
}
}
ApplyPresetToPreview();
PlaybackStateChanged.Broadcast();
}
bool FMorphPresetEditorController::TryGetHeadLocation(FVector& OutHeadLocation) const
{
USkeletalMesh* PreviewMesh = GetPreviewMesh();
if (!PreviewMesh)
{
return false;
}
const FReferenceSkeleton& RefSkeleton = PreviewMesh->GetRefSkeleton();
int32 HeadBoneIndex = RefSkeleton.FindBoneIndex(TEXT("head"));
if (HeadBoneIndex == INDEX_NONE)
{
HeadBoneIndex = RefSkeleton.FindBoneIndex(TEXT("Head"));
}
if (HeadBoneIndex == INDEX_NONE)
{
return false;
}
const TArray<FTransform>& RefPose = RefSkeleton.GetRefBonePose();
FTransform HeadTransform = FTransform::Identity;
int32 BoneIndex = HeadBoneIndex;
while (BoneIndex != INDEX_NONE)
{
HeadTransform = RefPose[BoneIndex] * HeadTransform;
BoneIndex = RefSkeleton.GetParentIndex(BoneIndex);
}
OutHeadLocation = HeadTransform.GetLocation();
return true;
}
bool FMorphPresetEditorController::ResolvePreviewBoneName(FName InBoneName, FName& OutBoneName) const
{
USkeletalMesh* PreviewMesh = GetPreviewMesh();
if (!PreviewMesh || InBoneName.IsNone())
{
return false;
}
const FReferenceSkeleton& RefSkeleton = PreviewMesh->GetRefSkeleton();
if (RefSkeleton.FindBoneIndex(InBoneName) != INDEX_NONE)
{
OutBoneName = InBoneName;
return true;
}
const FString RequestedBoneName = InBoneName.ToString();
for (int32 BoneIndex = 0; BoneIndex < RefSkeleton.GetNum(); ++BoneIndex)
{
const FName CandidateBoneName = RefSkeleton.GetBoneName(BoneIndex);
if (CandidateBoneName.ToString().Equals(RequestedBoneName, ESearchCase::IgnoreCase))
{
OutBoneName = CandidateBoneName;
return true;
}
}
return false;
}
void FMorphPresetEditorController::NotifyPresetEdited(bool bBroadcastPresetChanged)
{
if (UMorphPresetDataAsset* PresetAsset = Preset.Get())
{
PresetAsset->MarkPackageDirty();
}
UAnimationAsset* DesiredPreviewAsset = ResolvePreviewAnimationAsset();
UAnimationAsset* CurrentPreviewAsset = nullptr;
if (TSharedPtr<IPersonaPreviewScene> PreviewScene = GetPreviewScene())
{
CurrentPreviewAsset = PreviewScene->GetPreviewAnimationAsset();
}
if (DesiredPreviewAsset != CurrentPreviewAsset ||
(DesiredPreviewAsset && PreviewAnimation.IsValid()) ||
(!DesiredPreviewAsset && !PreviewAnimation.IsValid()))
{
RebuildPreviewAnimation();
}
else if (PreviewAnimation.IsValid())
{
SetAnimSequencePlayLength(*PreviewAnimation, GetTimelineLength(), false);
}
CurrentTime = FMath::Clamp(CurrentTime, 0.0f, GetTimelineLength());
// Re-apply morph weights immediately so viewport reflects drag/resize edits without stalling.
ApplyPresetToPreview();
if (bBroadcastPresetChanged)
{
PresetChanged.Broadcast();
}
}
void FMorphPresetEditorController::SetPlaying(bool bInPlaying)
{
if (bIsPlaying == bInPlaying)
{
return;
}
bIsPlaying = bInPlaying;
PlaybackStateChanged.Broadcast();
}
void FMorphPresetEditorController::TogglePlaying()
{
SetPlaying(!bIsPlaying);
}
void FMorphPresetEditorController::Tick(float DeltaTime)
{
if (bIsPlaying)
{
float NewTime = CurrentTime + DeltaTime;
const UMorphPresetDataAsset* PresetAsset = Preset.Get();
const float Length = GetTimelineLength();
if (PresetAsset && PresetAsset->bLoop && Length > KINDA_SMALL_NUMBER)
{
NewTime = FMath::Fmod(NewTime, Length);
if (NewTime < 0.0f)
{
NewTime += Length;
}
}
else if (NewTime >= Length)
{
NewTime = Length;
bIsPlaying = false;
}
SetCurrentTime(NewTime);
}
else
{
ApplyPresetToPreview();
}
TickLipSyncPreviewComponents(DeltaTime);
if (bLipSyncPreviewAudioActive && !IsLipSyncPreviewAudioPlaying())
{
bLipSyncPreviewAudioActive = false;
PlaybackStateChanged.Broadcast();
}
}
USoundBase* FMorphPresetEditorController::GetLipSyncPreviewSound() const
{
return LipSyncPreviewSound.Get();
}
bool FMorphPresetEditorController::SetLipSyncPreviewSound(USoundBase* InSound)
{
if (LipSyncPreviewSound.Get() == InSound)
{
return false;
}
StopLipSyncPreviewAudio();
LipSyncPreviewSound = InSound;
PlaybackStateChanged.Broadcast();
return true;
}
bool FMorphPresetEditorController::PlayLipSyncPreviewAudio(float StartTime)
{
USoundBase* Sound = LipSyncPreviewSound.Get();
if (!Sound)
{
return false;
}
RebuildLipSyncPreviewComponents();
ULipSyncComponent* PreviewLipSync = LipSyncPreviewComponent.Get();
if (!PreviewLipSync || !GEditor)
{
return false;
}
UAudioComponent* PreviewAudioComponent = GEditor->ResetPreviewAudioComponent(Sound);
if (!PreviewAudioComponent)
{
return false;
}
PreviewLipSync->Mapping = ResolveLipSyncPreviewMapping();
PreviewLipSync->AudioDrivenSound = Sound;
PreviewLipSync->AudioDrivenMode = LipSyncPreviewTranscript.IsEmpty()
? ELipSyncAudioDrivenMode::EnvelopeMouthOpen
: ELipSyncAudioDrivenMode::TranscriptVisemes;
PreviewLipSync->AudioDrivenTranscript = LipSyncPreviewTranscript;
PreviewLipSync->VisemeAttackSpeed = FMath::Lerp(30.0f, 10.0f, LipSyncPreviewSmoothness);
PreviewLipSync->VisemeReleaseSpeed = FMath::Lerp(16.0f, 4.0f, LipSyncPreviewSmoothness);
PreviewLipSync->SilenceHoldTime = FMath::Lerp(0.02f, 0.09f, LipSyncPreviewSmoothness);
PreviewLipSync->AudioDrivenVisemeCrossfade = FMath::Lerp(0.02f, 0.22f, LipSyncPreviewSmoothness);
PreviewLipSync->bUseFallbackPulseWithoutEnvelope = false;
bLipSyncPreviewAudioActive = PreviewLipSync->PlayAudioDrivenLipSyncWithComponent(PreviewAudioComponent, Sound, StartTime);
PlaybackStateChanged.Broadcast();
return bLipSyncPreviewAudioActive;
}
void FMorphPresetEditorController::StopLipSyncPreviewAudio()
{
bLipSyncPreviewAudioActive = false;
if (ULipSyncComponent* PreviewLipSync = LipSyncPreviewComponent.Get())
{
PreviewLipSync->StopAudioDrivenLipSync();
}
ApplyPresetToPreview();
PlaybackStateChanged.Broadcast();
}
bool FMorphPresetEditorController::IsLipSyncPreviewAudioPlaying() const
{
return bLipSyncPreviewAudioActive && LipSyncPreviewComponent.IsValid() && LipSyncPreviewComponent->IsAudioDrivenLipSyncPlaying();
}
void FMorphPresetEditorController::SetLipSyncPreviewSmoothness(float InSmoothness)
{
LipSyncPreviewSmoothness = FMath::Clamp(InSmoothness, 0.0f, 1.0f);
if (ULipSyncComponent* PreviewLipSync = LipSyncPreviewComponent.Get())
{
PreviewLipSync->VisemeAttackSpeed = FMath::Lerp(30.0f, 10.0f, LipSyncPreviewSmoothness);
PreviewLipSync->VisemeReleaseSpeed = FMath::Lerp(16.0f, 4.0f, LipSyncPreviewSmoothness);
PreviewLipSync->SilenceHoldTime = FMath::Lerp(0.02f, 0.09f, LipSyncPreviewSmoothness);
PreviewLipSync->AudioDrivenVisemeCrossfade = FMath::Lerp(0.02f, 0.22f, LipSyncPreviewSmoothness);
}
PlaybackStateChanged.Broadcast();
}
void FMorphPresetEditorController::SetLipSyncPreviewTranscript(const FText& InTranscript)
{
LipSyncPreviewTranscript = InTranscript;
if (ULipSyncComponent* PreviewLipSync = LipSyncPreviewComponent.Get())
{
PreviewLipSync->AudioDrivenTranscript = LipSyncPreviewTranscript;
PreviewLipSync->AudioDrivenMode = LipSyncPreviewTranscript.IsEmpty()
? ELipSyncAudioDrivenMode::EnvelopeMouthOpen
: ELipSyncAudioDrivenMode::TranscriptVisemes;
}
PlaybackStateChanged.Broadcast();
}
ULipSyncMappingDataAsset* FMorphPresetEditorController::GetLipSyncPreviewMapping() const
{
return LipSyncPreviewMapping.Get();
}
bool FMorphPresetEditorController::SetLipSyncPreviewMapping(ULipSyncMappingDataAsset* InMapping)
{
if (LipSyncPreviewMapping.Get() == InMapping)
{
return false;
}
LipSyncPreviewMapping = InMapping;
if (ULipSyncComponent* PreviewLipSync = LipSyncPreviewComponent.Get())
{
PreviewLipSync->Mapping = ResolveLipSyncPreviewMapping();
}
ApplyPresetToPreview();
PlaybackStateChanged.Broadcast();
return true;
}
void FMorphPresetEditorController::RebuildPreviewScene()
{
StopLipSyncPreviewAudio();
DestroyLipSyncPreviewComponents();
PersonaToolkit.Reset();
PreviewAnimation.Reset();
USkeletalMesh* PreviewMesh = GetPreviewMesh();
if (PreviewMesh == nullptr)
{
PreviewSceneChanged.Broadcast();
return;
}
FPersonaModule& PersonaModule = FModuleManager::LoadModuleChecked<FPersonaModule>("Persona");
FPersonaToolkitArgs ToolkitArgs;
PersonaToolkit = PersonaModule.CreatePersonaToolkit(PreviewMesh, ToolkitArgs);
RebuildPreviewAnimation();
RebuildLipSyncPreviewComponents();
SetCurrentTime(CurrentTime);
PreviewSceneChanged.Broadcast();
}
void FMorphPresetEditorController::RebuildPreviewAnimation()
{
TSharedPtr<IPersonaPreviewScene> PreviewScene = GetPreviewScene();
if (!PreviewScene.IsValid())
{
return;
}
UAnimSequence* AnimSequence = NewObject<UAnimSequence>(GetTransientPackage(), NAME_None, RF_Transient);
PreviewAnimation.Reset();
PreviewStatusText = FText::GetEmpty();
UAnimationAsset* PreviewAsset = ResolvePreviewAnimationAsset();
if (PreviewAsset && !IsPreviewAnimationCompatible(PreviewAsset))
{
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())
{
AnimSequence->SetSkeleton(Skeleton);
}
}
}
PreviewScene->SetPreviewAnimationAsset(PreviewAsset, 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->EnableControllers(true);
PreviewMeshComponent->PreviewInstance->SetSkeletalControlAlpha(1.0f);
PreviewMeshComponent->PreviewInstance->SetPlaying(false);
PreviewMeshComponent->PreviewInstance->SetLooping(false);
PreviewMeshComponent->PreviewInstance->SetPosition(GetPreviewAnimationPosition(), false);
RefreshPreviewAnimationPose();
}
}
}
void FMorphPresetEditorController::RefreshPreviewAnimationPose()
{
TSharedPtr<IPersonaPreviewScene> PreviewScene = GetPreviewScene();
if (!PreviewScene.IsValid())
{
return;
}
UDebugSkelMeshComponent* PreviewMeshComponent = PreviewScene->GetPreviewMeshComponent();
if (!PreviewMeshComponent)
{
return;
}
PreviewMeshComponent->TickAnimation(0.0f, false);
PreviewMeshComponent->RefreshBoneTransforms();
PreviewMeshComponent->MarkRenderStateDirty();
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();
TSharedPtr<IPersonaPreviewScene> PreviewScene = GetPreviewScene();
if (!PresetAsset || !PreviewScene.IsValid())
{
return;
}
UDebugSkelMeshComponent* PreviewMeshComponent = PreviewScene->GetPreviewMeshComponent();
if (!PreviewMeshComponent)
{
return;
}
PreviewMeshComponent->ClearMorphTargets();
if (PreviewMeshComponent->PreviewInstance)
{
PreviewMeshComponent->PreviewInstance->EnableControllers(true);
PreviewMeshComponent->PreviewInstance->SetSkeletalControlAlpha(1.0f);
}
TMap<FName, float> FinalValues;
MorphPresetRuntime::AccumulatePresetMorphValuesAtTime(*PresetAsset, CurrentTime, 1.0f, FinalValues);
for (const TPair<FName, float>& Pair : FinalValues)
{
if (Pair.Value <= 0.001f)
{
continue;
}
PreviewMeshComponent->SetMorphTarget(Pair.Key, Pair.Value, false);
}
RefreshPreviewAnimationPose();
}
void FMorphPresetEditorController::RebuildLipSyncPreviewComponents()
{
TSharedPtr<IPersonaPreviewScene> PreviewScene = GetPreviewScene();
if (!PreviewScene.IsValid())
{
return;
}
UDebugSkelMeshComponent* PreviewMeshComponent = PreviewScene->GetPreviewMeshComponent();
UWorld* PreviewWorld = PreviewScene->GetWorld();
if (!PreviewMeshComponent || !PreviewWorld)
{
return;
}
if (LipSyncPreviewActor.IsValid() && LipSyncPreviewMorphController.IsValid() && LipSyncPreviewComponent.IsValid())
{
LipSyncPreviewMorphController->TargetMeshComponent = PreviewMeshComponent;
LipSyncPreviewComponent->Mapping = ResolveLipSyncPreviewMapping();
return;
}
AActor* PreviewActor = PreviewWorld->SpawnActor<AActor>();
if (!PreviewActor)
{
return;
}
PreviewActor->SetFlags(RF_Transient);
UMorphControllerComponent* MorphController = NewObject<UMorphControllerComponent>(PreviewActor, TEXT("MorphEditorPreviewMorphController"), RF_Transient);
ULipSyncComponent* LipSyncComponent = NewObject<ULipSyncComponent>(PreviewActor, TEXT("MorphEditorPreviewLipSyncComponent"), RF_Transient);
if (!MorphController || !LipSyncComponent)
{
PreviewActor->Destroy();
return;
}
PreviewActor->AddInstanceComponent(MorphController);
PreviewActor->AddInstanceComponent(LipSyncComponent);
MorphController->TargetMeshComponent = PreviewMeshComponent;
MorphController->bAutoUpdate = false;
MorphController->bLogWarnings = false;
MorphController->RegisterComponent();
LipSyncComponent->MorphController = MorphController;
LipSyncComponent->Mapping = ResolveLipSyncPreviewMapping();
LipSyncComponent->AudioDrivenMode = LipSyncPreviewTranscript.IsEmpty()
? ELipSyncAudioDrivenMode::EnvelopeMouthOpen
: ELipSyncAudioDrivenMode::TranscriptVisemes;
LipSyncComponent->AudioDrivenTranscript = LipSyncPreviewTranscript;
LipSyncComponent->bAutoResolveMorphController = false;
LipSyncComponent->VisemeAttackSpeed = FMath::Lerp(30.0f, 10.0f, LipSyncPreviewSmoothness);
LipSyncComponent->VisemeReleaseSpeed = FMath::Lerp(16.0f, 4.0f, LipSyncPreviewSmoothness);
LipSyncComponent->SilenceHoldTime = FMath::Lerp(0.02f, 0.09f, LipSyncPreviewSmoothness);
LipSyncComponent->AudioDrivenVisemeCrossfade = FMath::Lerp(0.02f, 0.22f, LipSyncPreviewSmoothness);
LipSyncComponent->bUseFallbackPulseWithoutEnvelope = false;
LipSyncComponent->RegisterComponent();
LipSyncPreviewActor = PreviewActor;
LipSyncPreviewMorphController = MorphController;
LipSyncPreviewComponent = LipSyncComponent;
}
void FMorphPresetEditorController::DestroyLipSyncPreviewComponents()
{
if (ULipSyncComponent* LipSyncComponent = LipSyncPreviewComponent.Get())
{
LipSyncComponent->StopAudioDrivenLipSync();
LipSyncComponent->DestroyComponent();
}
if (UMorphControllerComponent* MorphController = LipSyncPreviewMorphController.Get())
{
MorphController->DestroyComponent();
}
if (AActor* PreviewActor = LipSyncPreviewActor.Get())
{
PreviewActor->Destroy();
}
LipSyncPreviewComponent.Reset();
LipSyncPreviewMorphController.Reset();
LipSyncPreviewActor.Reset();
}
void FMorphPresetEditorController::TickLipSyncPreviewComponents(float DeltaTime)
{
if (!LipSyncPreviewComponent.IsValid() || !LipSyncPreviewMorphController.IsValid())
{
return;
}
LipSyncPreviewComponent->TickComponent(DeltaTime, LEVELTICK_All, nullptr);
LipSyncPreviewMorphController->UpdateMorphTargetsNow(DeltaTime);
}
ULipSyncMappingDataAsset* FMorphPresetEditorController::ResolveLipSyncPreviewMapping() const
{
if (ULipSyncMappingDataAsset* SelectedMapping = LipSyncPreviewMapping.Get())
{
return SelectedMapping;
}
return DefaultLipSyncPreviewMapping.Get();
}
@@ -0,0 +1,617 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Morph/Editor/MorphPresetEditorWindow.h"
#include "Morph/MorphPresetDataAsset.h"
#include "Widgets/SBoxPanel.h"
#include "Widgets/SWindow.h"
#include "Widgets/Input/SButton.h"
#include "Widgets/Input/SMultiLineEditableTextBox.h"
#include "Widgets/Text/STextBlock.h"
#include "PropertyCustomizationHelpers.h"
#include "Dom/JsonObject.h"
#include "Serialization/JsonSerializer.h"
#include "Serialization/JsonReader.h"
#include "Serialization/JsonWriter.h"
#include "Curves/RichCurve.h"
#define LOCTEXT_NAMESPACE "MorphPresetEditor"
namespace
{
FString CurveInterpToString(ERichCurveInterpMode InterpMode)
{
switch (InterpMode)
{
case RCIM_Constant:
return TEXT("constant");
case RCIM_Cubic:
return TEXT("ease");
case RCIM_Linear:
default:
return TEXT("linear");
}
}
FString CurveInterpToUnrealString(ERichCurveInterpMode InterpMode)
{
switch (InterpMode)
{
case RCIM_Constant:
return TEXT("RCIM_Constant");
case RCIM_Cubic:
return TEXT("RCIM_Cubic");
case RCIM_Linear:
default:
return TEXT("RCIM_Linear");
}
}
ERichCurveInterpMode ParseCurveInterp(const TSharedPtr<FJsonValue>& Value)
{
FString InterpName;
if (!Value.IsValid() || !Value->TryGetString(InterpName))
{
return RCIM_Linear;
}
InterpName = InterpName.ToLower();
if (InterpName == TEXT("constant") || InterpName == TEXT("step"))
{
return RCIM_Constant;
}
if (InterpName == TEXT("ease") || InterpName == TEXT("cubic") || InterpName == TEXT("auto") || InterpName == TEXT("rcim_cubic"))
{
return RCIM_Cubic;
}
return RCIM_Linear;
}
FKeyHandle FindAdjacentKeyHandle(const FRichCurve& Curve, FKeyHandle KeyHandle, bool bFindNext)
{
const float KeyTime = Curve.GetKeyTime(KeyHandle);
FKeyHandle BestHandle;
float BestDistance = TNumericLimits<float>::Max();
for (auto It = Curve.GetKeyHandleIterator(); It; ++It)
{
const FKeyHandle CandidateHandle = *It;
if (CandidateHandle == KeyHandle)
{
continue;
}
const float CandidateTime = Curve.GetKeyTime(CandidateHandle);
const float Distance = bFindNext ? CandidateTime - KeyTime : KeyTime - CandidateTime;
if (Distance > 0.0f && Distance < BestDistance)
{
BestDistance = Distance;
BestHandle = CandidateHandle;
}
}
return BestHandle;
}
void ApplyVisibleEaseTangents(FRichCurve& Curve, FKeyHandle KeyHandle)
{
FKeyHandle SegmentStartHandle = KeyHandle;
FKeyHandle SegmentEndHandle = FindAdjacentKeyHandle(Curve, SegmentStartHandle, true);
if (!SegmentEndHandle.IsValid())
{
SegmentEndHandle = SegmentStartHandle;
SegmentStartHandle = FindAdjacentKeyHandle(Curve, SegmentEndHandle, false);
}
if (!SegmentStartHandle.IsValid() || !SegmentEndHandle.IsValid())
{
return;
}
Curve.SetKeyTangentMode(SegmentStartHandle, RCTM_Break, false);
Curve.SetKeyTangentMode(SegmentEndHandle, RCTM_Break, false);
Curve.GetKey(SegmentStartHandle).LeaveTangent = 0.0f;
Curve.GetKey(SegmentEndHandle).ArriveTangent = 0.0f;
}
void WriteCurveKeys(const FMorphTimelineClip& Clip, const TSharedPtr<FJsonObject>& Obj)
{
TArray<TSharedPtr<FJsonValue>> KeyArray;
if (const FRichCurve* Curve = Clip.WeightCurve.GetRichCurveConst())
{
for (auto It = Curve->GetKeyHandleIterator(); It; ++It)
{
const FKeyHandle KeyHandle = *It;
TSharedPtr<FJsonObject> KeyObj = MakeShared<FJsonObject>();
KeyObj->SetNumberField(TEXT("time"), Curve->GetKeyTime(KeyHandle));
KeyObj->SetNumberField(TEXT("value"), Curve->GetKeyValue(KeyHandle));
KeyObj->SetStringField(TEXT("interp"), CurveInterpToString(Curve->GetKeyInterpMode(KeyHandle)));
KeyObj->SetStringField(TEXT("interpMode"), CurveInterpToUnrealString(Curve->GetKeyInterpMode(KeyHandle)));
KeyObj->SetStringField(TEXT("tangentMode"), TEXT("RCTM_Auto"));
KeyObj->SetStringField(TEXT("tangentWeightMode"), TEXT("RCTWM_WeightedNone"));
KeyObj->SetNumberField(TEXT("arriveTangent"), 0.0);
KeyObj->SetNumberField(TEXT("arriveTangentWeight"), 0.0);
KeyObj->SetNumberField(TEXT("leaveTangent"), 0.0);
KeyObj->SetNumberField(TEXT("leaveTangentWeight"), 0.0);
KeyArray.Add(MakeShared<FJsonValueObject>(KeyObj));
}
}
Obj->SetArrayField(TEXT("curve_keys"), KeyArray);
Obj->SetArrayField(TEXT("curveKeys"), KeyArray);
TSharedPtr<FJsonObject> EditorCurveDataObj = MakeShared<FJsonObject>();
EditorCurveDataObj->SetArrayField(TEXT("keys"), KeyArray);
EditorCurveDataObj->SetNumberField(TEXT("defaultValue"), TNumericLimits<float>::Max());
EditorCurveDataObj->SetStringField(TEXT("preInfinityExtrap"), TEXT("RCCE_Constant"));
EditorCurveDataObj->SetStringField(TEXT("postInfinityExtrap"), TEXT("RCCE_Constant"));
TSharedPtr<FJsonObject> WeightCurveObj = MakeShared<FJsonObject>();
WeightCurveObj->SetObjectField(TEXT("editorCurveData"), EditorCurveDataObj);
WeightCurveObj->SetStringField(TEXT("externalCurve"), TEXT("None"));
Obj->SetObjectField(TEXT("weightCurve"), WeightCurveObj);
}
float ParseCurveNumber(const TSharedPtr<FJsonValue>& Value, float DefaultValue)
{
double Number = 0.0;
return Value.IsValid() && Value->TryGetNumber(Number) ? static_cast<float>(Number) : DefaultValue;
}
void ReadCurveKeys(const TSharedPtr<FJsonObject>& Obj, FMorphTimelineClip& Clip)
{
if (!Obj.IsValid())
{
Clip.EnsureDefaultCurve();
return;
}
const TArray<TSharedPtr<FJsonValue>>* KeyArray = nullptr;
if ((!Obj->TryGetArrayField(TEXT("curve_keys"), KeyArray) || !KeyArray) &&
(!Obj->TryGetArrayField(TEXT("curveKeys"), KeyArray) || !KeyArray))
{
if (const TSharedPtr<FJsonObject>* WeightCurveObj = nullptr; Obj->TryGetObjectField(TEXT("weightCurve"), WeightCurveObj) && WeightCurveObj->IsValid())
{
const TSharedPtr<FJsonObject>* EditorCurveDataObj = nullptr;
if (!(*WeightCurveObj)->TryGetObjectField(TEXT("editorCurveData"), EditorCurveDataObj) || !EditorCurveDataObj || !EditorCurveDataObj->IsValid() ||
!(*EditorCurveDataObj)->TryGetArrayField(TEXT("keys"), KeyArray) || !KeyArray)
{
Clip.EnsureDefaultCurve();
return;
}
}
else
{
Clip.EnsureDefaultCurve();
return;
}
}
FRichCurve* Curve = Clip.WeightCurve.GetRichCurve();
if (!Curve)
{
return;
}
Curve->Reset();
TArray<FKeyHandle> EaseKeyHandles;
for (const TSharedPtr<FJsonValue>& KeyValue : *KeyArray)
{
const TSharedPtr<FJsonObject>* KeyObj = nullptr;
if (!KeyValue.IsValid() || !KeyValue->TryGetObject(KeyObj) || !KeyObj || !KeyObj->IsValid())
{
continue;
}
const float Time = FMath::Clamp(ParseCurveNumber((*KeyObj)->TryGetField(TEXT("time")), 0.0f), 0.0f, FMath::Max(Clip.Duration, 0.001f));
const float Value = FMath::Clamp(ParseCurveNumber((*KeyObj)->TryGetField(TEXT("value")), 0.0f), 0.0f, 1.0f);
const ERichCurveInterpMode InterpMode = ParseCurveInterp((*KeyObj)->TryGetField(TEXT("interp")).IsValid()
? (*KeyObj)->TryGetField(TEXT("interp"))
: (*KeyObj)->TryGetField(TEXT("interpMode")));
const FKeyHandle KeyHandle = Curve->UpdateOrAddKey(Time, Value);
Curve->SetKeyInterpMode(KeyHandle, InterpMode, false);
if (InterpMode == RCIM_Cubic)
{
EaseKeyHandles.Add(KeyHandle);
}
}
for (const FKeyHandle KeyHandle : EaseKeyHandles)
{
ApplyVisibleEaseTangents(*Curve, KeyHandle);
}
Clip.EnsureDefaultCurve();
}
}
void SMorphPresetEditorWindow::Construct(const FArguments& InArgs)
{
if (UMorphPresetDataAsset* PresetArg = InArgs._Preset)
{
SelectedPreset = PresetArg;
bEmbeddedMode = true;
}
ChildSlot
[
BuildContent()
];
if (bEmbeddedMode && SelectedPreset.IsValid())
{
RefreshFromPreset();
}
}
TSharedRef<SWidget> SMorphPresetEditorWindow::BuildContent()
{
TSharedRef<SVerticalBox> Root = SNew(SVerticalBox);
if (!bEmbeddedMode)
{
Root->AddSlot()
.AutoHeight()
.Padding(4.0f)
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.AutoWidth()
.VAlign(VAlign_Center)
.Padding(0, 0, 8, 0)
[
SNew(STextBlock)
.Text(LOCTEXT("PresetLabel", "Morph Preset:"))
]
+ SHorizontalBox::Slot()
.FillWidth(1.0f)
[
SAssignNew(PresetPicker, SObjectPropertyEntryBox)
.AllowedClass(UMorphPresetDataAsset::StaticClass())
.ObjectPath(this, &SMorphPresetEditorWindow::GetSelectedPresetPath)
.OnObjectChanged(this, &SMorphPresetEditorWindow::OnPresetChanged)
.DisplayUseSelected(true)
.DisplayBrowse(true)
.AllowClear(true)
.AllowCreate(false)
]
];
}
Root->AddSlot()
.FillHeight(1.0f)
.Padding(4.0f)
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.AutoHeight()
[
SNew(STextBlock)
.Text(LOCTEXT("JsonLabel", "JSON (структурированный формат):"))
]
+ SVerticalBox::Slot()
.FillHeight(1.0f)
.Padding(0, 4)
[
SAssignNew(JsonTextBox, SMultiLineEditableTextBox)
.HintText(LOCTEXT("JsonHint",
"{\n"
" \"metadata\": { \"display_name\": \"Моргание\", \"description\": \"Полное моргание\" },\n"
" \"settings\": { \"duration\": 0.15, \"loop\": true, \"loop_delay\": 2.5, \"blend_speed\": 18 },\n"
" \"morph_targets\": [\n"
" { \"name\": \"eyeBlinkLeft\", \"start_time\": 0, \"duration\": 0.15, \"weight\": 1.5, \"curve_keys\": [{ \"time\": 0, \"value\": 0, \"interp\": \"ease\" }, { \"time\": 0.15, \"value\": 0, \"interp\": \"ease\" }] },\n"
" { \"name\": \"eyeBlinkRight\", \"start_time\": 0, \"duration\": 0.15, \"weight\": 1.5, \"curve_keys\": [{ \"time\": 0, \"value\": 0, \"interp\": \"ease\" }, { \"time\": 0.15, \"value\": 0, \"interp\": \"ease\" }] }\n"
" ]\n"
"}"))
.Font(FCoreStyle::GetDefaultFontStyle("Mono", 10))
]
];
Root->AddSlot()
.AutoHeight()
.Padding(4.0f)
.HAlign(HAlign_Right)
[
SNew(SHorizontalBox)
+ SHorizontalBox::Slot()
.AutoWidth()
.Padding(0, 0, 8, 0)
[
SNew(SButton)
.Text(LOCTEXT("ExportButton", "Export"))
.OnClicked(this, &SMorphPresetEditorWindow::OnExportClicked)
]
+ SHorizontalBox::Slot()
.AutoWidth()
[
SNew(SButton)
.Text(LOCTEXT("ApplyButton", "Apply"))
.OnClicked(this, &SMorphPresetEditorWindow::OnApplyClicked)
]
];
return Root;
}
void SMorphPresetEditorWindow::RefreshFromPreset()
{
if (!SelectedPreset.IsValid() || !JsonTextBox.IsValid()) return;
TSharedPtr<FJsonObject> RootObj = MakeShared<FJsonObject>();
RootObj->SetStringField(TEXT("displayName"), SelectedPreset->DisplayName.ToString());
RootObj->SetStringField(TEXT("description"), SelectedPreset->Description.ToString());
TArray<TSharedPtr<FJsonValue>> MorphArr;
for (const FMorphTimelineClip& Clip : SelectedPreset->MorphTargets)
{
if (!Clip.IsValid()) continue;
TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
Obj->SetStringField(TEXT("name"), Clip.Name.ToString());
Obj->SetNumberField(TEXT("startTime"), Clip.StartTime);
Obj->SetNumberField(TEXT("duration"), Clip.Duration);
Obj->SetNumberField(TEXT("weight"), Clip.Weight);
WriteCurveKeys(Clip, Obj);
MorphArr.Add(MakeShared<FJsonValueObject>(Obj));
}
RootObj->SetArrayField(TEXT("morphTargets"), MorphArr);
RootObj->SetNumberField(TEXT("duration"), SelectedPreset->Duration);
RootObj->SetBoolField(TEXT("bLoop"), SelectedPreset->bLoop);
RootObj->SetNumberField(TEXT("loopDelay"), SelectedPreset->LoopDelay);
RootObj->SetNumberField(TEXT("blendSpeedOverride"), SelectedPreset->BlendSpeedOverride);
RootObj->SetStringField(TEXT("nativeClass"), TEXT("/Script/CoreUObject.Class'/Script/FunnyGirl.MorphPresetDataAsset'"));
FString OutputString;
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutputString, 0);
FJsonSerializer::Serialize(RootObj.ToSharedRef(), Writer);
JsonTextBox->SetText(FText::FromString(OutputString));
}
FString SMorphPresetEditorWindow::GetSelectedPresetPath() const
{
if (UMorphPresetDataAsset* Preset = SelectedPreset.Get())
{
return Preset->GetPathName();
}
return FString();
}
void SMorphPresetEditorWindow::OnPresetChanged(const FAssetData& AssetData)
{
SelectedPreset = Cast<UMorphPresetDataAsset>(AssetData.GetAsset());
}
FReply SMorphPresetEditorWindow::OnApplyClicked()
{
UMorphPresetDataAsset* Preset = SelectedPreset.Get();
if (!Preset)
{
UE_LOG(LogTemp, Warning, TEXT("Morph Preset Editor: Select a Morph Preset first."));
return FReply::Handled();
}
FString JsonString = JsonTextBox->GetText().ToString();
if (JsonString.IsEmpty())
{
UE_LOG(LogTemp, Warning, TEXT("Morph Preset Editor: JSON is empty."));
return FReply::Handled();
}
// Parse JSON
TSharedPtr<FJsonObject> JsonObject;
TSharedRef<TJsonReader<>> Reader = TJsonReaderFactory<>::Create(JsonString);
if (!FJsonSerializer::Deserialize(Reader, JsonObject) || !JsonObject.IsValid())
{
UE_LOG(LogTemp, Error, TEXT("Morph Preset Editor: Invalid JSON: %s"), *Reader->GetErrorMessage());
return FReply::Handled();
}
// Структурированный парсинг: metadata, settings, morph_targets
// Поддержка обратной совместимости: плоский формат (все поля в корне)
auto ParseFloat = [](const TSharedPtr<FJsonValue>& Val, float Default) -> float
{
double Num = 0.0;
return Val.IsValid() && Val->TryGetNumber(Num) ? static_cast<float>(FMath::Max(0.0, Num)) : Default;
};
auto ParseBool = [](const TSharedPtr<FJsonValue>& Val, bool Default) -> bool
{
bool b = false;
if (Val.IsValid() && Val->TryGetBool(b)) return b;
double Num = 0.0;
if (Val.IsValid() && Val->TryGetNumber(Num)) return Num != 0.0;
return Default;
};
auto ParseInt = [](const TSharedPtr<FJsonValue>& Val, int32 Default) -> int32
{
double Num = 0.0;
return Val.IsValid() && Val->TryGetNumber(Num) ? FMath::Max(0, static_cast<int32>(Num)) : Default;
};
auto ParseString = [](const TSharedPtr<FJsonValue>& Val) -> FString
{
FString S;
return Val.IsValid() && Val->TryGetString(S) ? S : FString();
};
TArray<FMorphTimelineClip> NewMorphTargets;
// metadata
if (const TSharedPtr<FJsonObject>* MetaObj = nullptr; JsonObject->TryGetObjectField(TEXT("metadata"), MetaObj) && MetaObj->IsValid())
{
Preset->DisplayName = FText::FromString(ParseString((*MetaObj)->TryGetField(TEXT("display_name"))));
Preset->Description = FText::FromString(ParseString((*MetaObj)->TryGetField(TEXT("description"))));
}
else
{
FString DisplayNameStr = ParseString(JsonObject->TryGetField(TEXT("displayName")));
if (!DisplayNameStr.IsEmpty()) Preset->DisplayName = FText::FromString(DisplayNameStr);
FString DescStr = ParseString(JsonObject->TryGetField(TEXT("description")));
if (!DescStr.IsEmpty()) Preset->Description = FText::FromString(DescStr);
}
// settings
if (const TSharedPtr<FJsonObject>* SettingsObj = nullptr; JsonObject->TryGetObjectField(TEXT("settings"), SettingsObj) && SettingsObj->IsValid())
{
Preset->Duration = ParseFloat((*SettingsObj)->TryGetField(TEXT("duration")), Preset->Duration);
Preset->bLoop = ParseBool((*SettingsObj)->TryGetField(TEXT("loop")), Preset->bLoop);
Preset->LoopDelay = ParseFloat((*SettingsObj)->TryGetField(TEXT("loop_delay")), Preset->LoopDelay);
Preset->BlendSpeedOverride = ParseFloat((*SettingsObj)->TryGetField(TEXT("blend_speed")), Preset->BlendSpeedOverride);
}
else
{
Preset->Duration = ParseFloat(JsonObject->TryGetField(TEXT("duration")), Preset->Duration);
Preset->bLoop = ParseBool(JsonObject->TryGetField(TEXT("bLoop")), Preset->bLoop);
Preset->LoopDelay = ParseFloat(JsonObject->TryGetField(TEXT("loopDelay")), Preset->LoopDelay);
Preset->BlendSpeedOverride = ParseFloat(JsonObject->TryGetField(TEXT("blendSpeedOverride")), Preset->BlendSpeedOverride);
}
// morph_targets: массив объектов { name, start_time, duration, weight, curve_keys? }
const TArray<TSharedPtr<FJsonValue>>* MorphArr = nullptr;
if ((JsonObject->TryGetArrayField(TEXT("morph_targets"), MorphArr) || JsonObject->TryGetArrayField(TEXT("morphTargets"), MorphArr)) && MorphArr)
{
for (const TSharedPtr<FJsonValue>& Val : *MorphArr)
{
const TSharedPtr<FJsonObject>* Obj = nullptr;
if (Val->TryGetObject(Obj) && Obj->IsValid())
{
FString NameStr = ParseString((*Obj)->TryGetField(TEXT("name")));
if (NameStr.IsEmpty()) continue;
FMorphTimelineClip Clip;
Clip.Name = FName(*NameStr);
Clip.StartTime = ParseFloat((*Obj)->TryGetField(TEXT("start_time")),
ParseFloat((*Obj)->TryGetField(TEXT("startTime")), ParseFloat((*Obj)->TryGetField(TEXT("delay")), 0.0f)));
Clip.Duration = ParseFloat((*Obj)->TryGetField(TEXT("duration")), 0.2f);
if (Clip.Duration < 0.001f) Clip.Duration = 0.2f;
Clip.Weight = ParseFloat((*Obj)->TryGetField(TEXT("weight")), 1.0f);
Clip.Weight = FMath::Clamp(Clip.Weight, 0.0f, 5.0f);
ReadCurveKeys(*Obj, Clip);
NewMorphTargets.Add(Clip);
}
}
}
// Обратная совместимость: старый формат morph_targets как объект { "name": weight }
if (NewMorphTargets.Num() == 0)
{
if (const TSharedPtr<FJsonObject>* MorphObj = nullptr; JsonObject->TryGetObjectField(TEXT("morph_targets"), MorphObj) && MorphObj->IsValid())
{
for (const auto& Pair : (*MorphObj)->Values)
{
double NumValue = 0.0;
if (Pair.Value->TryGetNumber(NumValue))
{
FMorphTimelineClip Clip;
Clip.Name = FName(*Pair.Key);
Clip.StartTime = 0.0f;
Clip.Duration = 0.2f;
Clip.Weight = static_cast<float>(FMath::Clamp(NumValue, 0.0, 5.0));
Clip.EnsureDefaultCurve();
NewMorphTargets.Add(Clip);
}
}
}
// Старый morph_target_settings (если morph_targets был объектом)
if (const TSharedPtr<FJsonObject>* SettingsMap = nullptr; JsonObject->TryGetObjectField(TEXT("morph_target_settings"), SettingsMap) && SettingsMap->IsValid())
{
for (const auto& Pair : (*SettingsMap)->Values)
{
const TSharedPtr<FJsonObject>* TimingObj = nullptr;
if (Pair.Value->TryGetObject(TimingObj) && TimingObj->IsValid())
{
FName MorphName(*Pair.Key);
for (FMorphTimelineClip& Clip : NewMorphTargets)
{
if (Clip.Name == MorphName)
{
Clip.StartTime = ParseFloat((*TimingObj)->TryGetField(TEXT("delay")), 0.0f);
break;
}
}
}
}
}
}
// Плоский формат (поля в корне) — если morph_targets не найден
if (NewMorphTargets.Num() == 0)
{
for (const auto& Pair : JsonObject->Values)
{
const FString& Key = Pair.Key;
const TSharedPtr<FJsonValue>& Value = Pair.Value;
if (Key.Equals(TEXT("metadata"), ESearchCase::IgnoreCase) || Key.Equals(TEXT("settings"), ESearchCase::IgnoreCase) ||
Key.Equals(TEXT("morph_targets"), ESearchCase::IgnoreCase) || Key.Equals(TEXT("morph_target_settings"), ESearchCase::IgnoreCase) ||
Key.Equals(TEXT("duration"), ESearchCase::IgnoreCase) || Key.Equals(TEXT("loop"), ESearchCase::IgnoreCase) ||
Key.Equals(TEXT("loop_delay"), ESearchCase::IgnoreCase) ||
Key.Equals(TEXT("blend_speed"), ESearchCase::IgnoreCase) || Key.Equals(TEXT("display_name"), ESearchCase::IgnoreCase) ||
Key.Equals(TEXT("description"), ESearchCase::IgnoreCase) || Key.Equals(TEXT("morphTargets"), ESearchCase::IgnoreCase) ||
Key.Equals(TEXT("bLoop"), ESearchCase::IgnoreCase) || Key.Equals(TEXT("loopDelay"), ESearchCase::IgnoreCase) ||
Key.Equals(TEXT("blendSpeedOverride"), ESearchCase::IgnoreCase) || Key.Equals(TEXT("displayName"), ESearchCase::IgnoreCase) ||
Key.Equals(TEXT("nativeClass"), ESearchCase::IgnoreCase))
{
continue;
}
double NumValue = 0.0;
if (Value->TryGetNumber(NumValue))
{
FMorphTimelineClip Clip;
Clip.Name = FName(*Key);
Clip.StartTime = 0.0f;
Clip.Duration = 0.2f;
Clip.Weight = static_cast<float>(FMath::Clamp(NumValue, 0.0, 5.0));
Clip.EnsureDefaultCurve();
NewMorphTargets.Add(Clip);
}
}
Preset->Duration = ParseFloat(JsonObject->TryGetField(TEXT("duration")), Preset->Duration);
Preset->bLoop = ParseBool(JsonObject->TryGetField(TEXT("loop")), Preset->bLoop);
Preset->LoopDelay = ParseFloat(JsonObject->TryGetField(TEXT("loop_delay")), Preset->LoopDelay);
Preset->BlendSpeedOverride = ParseFloat(JsonObject->TryGetField(TEXT("blend_speed")), Preset->BlendSpeedOverride);
FString DisplayNameStr = ParseString(JsonObject->TryGetField(TEXT("display_name")));
if (!DisplayNameStr.IsEmpty()) Preset->DisplayName = FText::FromString(DisplayNameStr);
FString DescStr = ParseString(JsonObject->TryGetField(TEXT("description")));
if (!DescStr.IsEmpty()) Preset->Description = FText::FromString(DescStr);
}
Preset->MorphTargets = MoveTemp(NewMorphTargets);
Preset->MarkPackageDirty();
UE_LOG(LogTemp, Log, TEXT("Morph Preset Editor: Applied %d morph targets to %s"), Preset->GetMorphCount(), *Preset->GetName());
return FReply::Handled();
}
FReply SMorphPresetEditorWindow::OnExportClicked()
{
UMorphPresetDataAsset* Preset = SelectedPreset.Get();
if (!Preset)
{
UE_LOG(LogTemp, Warning, TEXT("Morph Preset Editor: Select a Morph Preset first."));
return FReply::Handled();
}
TSharedPtr<FJsonObject> RootObj = MakeShared<FJsonObject>();
RootObj->SetStringField(TEXT("displayName"), Preset->DisplayName.ToString());
RootObj->SetStringField(TEXT("description"), Preset->Description.ToString());
// morph_targets — массив объектов (timeline clips)
TArray<TSharedPtr<FJsonValue>> MorphArr;
for (const FMorphTimelineClip& Clip : Preset->MorphTargets)
{
if (!Clip.IsValid()) continue;
TSharedPtr<FJsonObject> Obj = MakeShared<FJsonObject>();
Obj->SetStringField(TEXT("name"), Clip.Name.ToString());
Obj->SetNumberField(TEXT("startTime"), Clip.StartTime);
Obj->SetNumberField(TEXT("duration"), Clip.Duration);
Obj->SetNumberField(TEXT("weight"), Clip.Weight);
WriteCurveKeys(Clip, Obj);
MorphArr.Add(MakeShared<FJsonValueObject>(Obj));
}
RootObj->SetArrayField(TEXT("morphTargets"), MorphArr);
RootObj->SetNumberField(TEXT("duration"), Preset->Duration);
RootObj->SetBoolField(TEXT("bLoop"), Preset->bLoop);
RootObj->SetNumberField(TEXT("loopDelay"), Preset->LoopDelay);
RootObj->SetNumberField(TEXT("blendSpeedOverride"), Preset->BlendSpeedOverride);
RootObj->SetStringField(TEXT("nativeClass"), TEXT("/Script/CoreUObject.Class'/Script/FunnyGirl.MorphPresetDataAsset'"));
FString OutputString;
TSharedRef<TJsonWriter<>> Writer = TJsonWriterFactory<>::Create(&OutputString, 0);
FJsonSerializer::Serialize(RootObj.ToSharedRef(), Writer);
JsonTextBox->SetText(FText::FromString(OutputString));
return FReply::Handled();
}
#undef LOCTEXT_NAMESPACE
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,479 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#include "Morph/Editor/MorphPresetViewportPanel.h"
#include "AssetRegistry/AssetData.h"
#include "AdvancedPreviewScene.h"
#include "Animation/DebugSkelMeshComponent.h"
#include "EditorViewportClient.h"
#include "Engine/SkeletalMesh.h"
#include "IPersonaPreviewScene.h"
#include "Morph/LipSyncMappingDataAsset.h"
#include "Morph/Editor/MorphPresetEditorController.h"
#include "PropertyCustomizationHelpers.h"
#include "SEditorViewport.h"
#include "Sound/SoundBase.h"
#include "Toolkits/AssetEditorToolkit.h"
#include "Widgets/Input/SButton.h"
#include "Widgets/Input/SEditableTextBox.h"
#include "Widgets/Input/SSlider.h"
#include "Widgets/Layout/SBorder.h"
#include "Widgets/SBoxPanel.h"
#include "Widgets/Text/STextBlock.h"
#define LOCTEXT_NAMESPACE "MorphPresetViewportPanel"
namespace
{
static constexpr float MinFaceCameraDistance = 90.0f;
static constexpr float MaxFaceCameraDistance = 220.0f;
FName FindHeadBoneName(const UDebugSkelMeshComponent* PreviewMeshComponent)
{
if (!PreviewMeshComponent)
{
return NAME_None;
}
static const FName CandidateBoneNames[] =
{
TEXT("head"),
TEXT("Head"),
TEXT("head_jnt"),
TEXT("Head_jnt")
};
for (const FName CandidateBoneName : CandidateBoneNames)
{
if (PreviewMeshComponent->GetBoneIndex(CandidateBoneName) != INDEX_NONE)
{
return CandidateBoneName;
}
}
return NAME_None;
}
FVector ResolveFaceForwardDirection(const FTransform& HeadTransform)
{
const FVector PreferredDirection = FVector::YAxisVector;
const FVector CandidateDirections[] =
{
HeadTransform.GetUnitAxis(EAxis::X),
-HeadTransform.GetUnitAxis(EAxis::X),
HeadTransform.GetUnitAxis(EAxis::Y),
-HeadTransform.GetUnitAxis(EAxis::Y)
};
float BestScore = -FLT_MAX;
FVector BestDirection = PreferredDirection;
for (const FVector& CandidateDirection : CandidateDirections)
{
FVector FlattenedDirection = FVector(CandidateDirection.X, CandidateDirection.Y, 0.0f);
if (!FlattenedDirection.Normalize())
{
continue;
}
const float Score = FVector::DotProduct(FlattenedDirection, PreferredDirection);
if (Score > BestScore)
{
BestScore = Score;
BestDirection = FlattenedDirection;
}
}
return BestDirection;
}
void ResolveDefaultCamera(const TSharedPtr<IPersonaPreviewScene>& PreviewScene, FVector& OutCameraLocation, FVector& OutCameraTarget)
{
OutCameraTarget = FVector(0.0f, 0.0f, 80.0f);
FVector FaceForwardDirection = FVector::YAxisVector;
float CameraDistance = 165.0f;
if (PreviewScene.IsValid())
{
if (UDebugSkelMeshComponent* PreviewMeshComponent = PreviewScene->GetPreviewMeshComponent())
{
const FBoxSphereBounds Bounds = PreviewMeshComponent->Bounds;
CameraDistance = FMath::Clamp(FMath::Max(Bounds.BoxExtent.X, Bounds.BoxExtent.Y) * 1.35f, MinFaceCameraDistance, MaxFaceCameraDistance);
OutCameraTarget = Bounds.Origin + FVector(0.0f, 0.0f, Bounds.BoxExtent.Z * 0.62f);
const FName HeadBoneName = FindHeadBoneName(PreviewMeshComponent);
if (HeadBoneName != NAME_None)
{
const FTransform HeadTransform = PreviewMeshComponent->GetSocketTransform(HeadBoneName, RTS_World);
OutCameraTarget = HeadTransform.GetLocation() + FVector(0.0f, 0.0f, 4.0f);
FaceForwardDirection = ResolveFaceForwardDirection(HeadTransform);
}
}
}
OutCameraLocation = OutCameraTarget + FaceForwardDirection * CameraDistance + FVector(0.0f, 0.0f, 8.0f);
}
class SMorphPresetPreviewViewport : public SEditorViewport
{
public:
SLATE_BEGIN_ARGS(SMorphPresetPreviewViewport) {}
SLATE_ARGUMENT(TSharedPtr<FMorphPresetEditorController>, Controller)
SLATE_END_ARGS()
void Construct(const FArguments& InArgs)
{
Controller = InArgs._Controller;
SEditorViewport::Construct(SEditorViewport::FArguments());
}
protected:
virtual TSharedRef<FEditorViewportClient> MakeEditorViewportClient() override
{
TSharedPtr<IPersonaPreviewScene> PreviewScene = Controller.IsValid() ? Controller->GetPreviewScene() : nullptr;
check(PreviewScene.IsValid());
TSharedRef<FEditorViewportClient> ViewportClient = MakeShared<FEditorViewportClient>(nullptr, static_cast<FPreviewScene*>(PreviewScene.Get()), SharedThis(this));
FVector CameraLocation = FVector::ZeroVector;
FVector CameraTarget = FVector::ZeroVector;
ResolveDefaultCamera(PreviewScene, CameraLocation, CameraTarget);
ViewportClient->SetViewLocation(CameraLocation);
ViewportClient->SetViewRotation((CameraTarget - CameraLocation).Rotation());
ViewportClient->SetLookAtLocation(CameraTarget);
ViewportClient->SetViewModes(VMI_Lit, VMI_Lit);
ViewportClient->SetRealtime(true);
return ViewportClient;
}
virtual TSharedPtr<SWidget> BuildViewportToolbar() override
{
return SNullWidget::NullWidget;
}
private:
TSharedPtr<FMorphPresetEditorController> Controller;
};
}
void SMorphPresetViewportPanel::Construct(const FArguments& InArgs)
{
Controller = InArgs._Controller;
AssetEditorToolkit = InArgs._AssetEditorToolkit;
if (Controller.IsValid())
{
Controller->OnPreviewSceneChanged().AddSP(this, &SMorphPresetViewportPanel::RebuildContent);
Controller->OnPresetChanged().AddSP(this, &SMorphPresetViewportPanel::RebuildContent);
}
ChildSlot
[
SAssignNew(RootBorder, SBorder)
.Padding(8.0f)
];
RebuildContent();
RegisterActiveTimer(0.0f, FWidgetActiveTimerDelegate::CreateSP(this, &SMorphPresetViewportPanel::HandleActiveTimer));
}
SMorphPresetViewportPanel::~SMorphPresetViewportPanel()
{
if (Controller.IsValid())
{
Controller->OnPreviewSceneChanged().RemoveAll(this);
Controller->OnPresetChanged().RemoveAll(this);
}
}
void SMorphPresetViewportPanel::RebuildContent()
{
if (!RootBorder.IsValid())
{
return;
}
if (!Controller.IsValid() || !Controller->GetPreviewScene().IsValid())
{
RootBorder->SetContent(
SNew(SVerticalBox)
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(0.0f, 0.0f, 0.0f, 8.0f)
[
SNew(STextBlock)
.Text(LOCTEXT("PreviewMeshLabel", "Preview Mesh"))
]
+ SVerticalBox::Slot()
.AutoHeight()
[
SNew(SObjectPropertyEntryBox)
.AllowedClass(USkeletalMesh::StaticClass())
.ObjectPath(this, &SMorphPresetViewportPanel::GetSelectedPreviewMeshPath)
.OnObjectChanged(this, &SMorphPresetViewportPanel::OnPreviewMeshChanged)
.DisplayUseSelected(true)
.DisplayBrowse(true)
.AllowClear(true)
]
+ SVerticalBox::Slot()
.FillHeight(1.0f)
.VAlign(VAlign_Center)
.HAlign(HAlign_Center)
.Padding(0.0f, 16.0f, 0.0f, 0.0f)
[
SNew(STextBlock)
.Text(LOCTEXT("NoPreviewMesh", "Assign a skeletal mesh to see the Morph Target preset in the viewport."))
]);
return;
}
RootBorder->SetContent(
SNew(SVerticalBox)
+ 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("LipSyncMappingLabel", "Lip Sync Mapping"))
]
+ SHorizontalBox::Slot()
.FillWidth(1.0f)
.Padding(0.0f, 0.0f, 12.0f, 0.0f)
[
SNew(SObjectPropertyEntryBox)
.AllowedClass(ULipSyncMappingDataAsset::StaticClass())
.ObjectPath(this, &SMorphPresetViewportPanel::GetSelectedLipSyncMappingPath)
.OnObjectChanged(this, &SMorphPresetViewportPanel::OnLipSyncMappingChanged)
.DisplayUseSelected(true)
.DisplayBrowse(true)
.AllowClear(true)
]
+ SHorizontalBox::Slot()
.AutoWidth()
.VAlign(VAlign_Center)
.Padding(0.0f, 0.0f, 8.0f, 0.0f)
[
SNew(STextBlock)
.Text(LOCTEXT("LipSyncAudioLabel", "Lip Sync Audio"))
]
+ SHorizontalBox::Slot()
.FillWidth(1.0f)
.Padding(0.0f, 0.0f, 8.0f, 0.0f)
[
SNew(SObjectPropertyEntryBox)
.AllowedClass(USoundBase::StaticClass())
.ObjectPath(this, &SMorphPresetViewportPanel::GetSelectedLipSyncSoundPath)
.OnObjectChanged(this, &SMorphPresetViewportPanel::OnLipSyncSoundChanged)
.DisplayUseSelected(true)
.DisplayBrowse(true)
.AllowClear(true)
]
+ SHorizontalBox::Slot()
.AutoWidth()
.Padding(0.0f, 0.0f, 4.0f, 0.0f)
[
SNew(SButton)
.Text(this, &SMorphPresetViewportPanel::GetLipSyncPlaybackText)
.OnClicked(this, &SMorphPresetViewportPanel::OnPlayLipSyncClicked)
]
+ SHorizontalBox::Slot()
.AutoWidth()
[
SNew(SButton)
.Text(LOCTEXT("StopLipSync", "Stop"))
.OnClicked(this, &SMorphPresetViewportPanel::OnStopLipSyncClicked)
]
+ SHorizontalBox::Slot()
.AutoWidth()
.VAlign(VAlign_Center)
.Padding(12.0f, 0.0f, 6.0f, 0.0f)
[
SNew(STextBlock)
.Text(LOCTEXT("LipSyncSmoothLabel", "Smooth"))
]
+ SHorizontalBox::Slot()
.AutoWidth()
.VAlign(VAlign_Center)
[
SNew(SSlider)
.MinValue(0.0f)
.MaxValue(1.0f)
.Value(this, &SMorphPresetViewportPanel::GetLipSyncSmoothness)
.OnValueChanged(this, &SMorphPresetViewportPanel::OnLipSyncSmoothnessChanged)
]
]
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(0.0f, 0.0f, 0.0f, 8.0f)
[
SNew(SEditableTextBox)
.HintText(LOCTEXT("LipSyncTranscriptHint", "Paste audio transcript here for text-assisted visemes"))
.Text(this, &SMorphPresetViewportPanel::GetLipSyncTranscript)
.OnTextChanged(this, &SMorphPresetViewportPanel::OnLipSyncTranscriptChanged)
]
+ SVerticalBox::Slot()
.AutoHeight()
.Padding(0.0f, 0.0f, 0.0f, 8.0f)
[
SNew(STextBlock)
.Text(this, &SMorphPresetViewportPanel::GetPreviewStatusText)
.Visibility(this, &SMorphPresetViewportPanel::GetPreviewStatusVisibility)
.ColorAndOpacity(FSlateColor(FLinearColor(1.0f, 0.56f, 0.12f)))
.AutoWrapText(true)
]
+ SVerticalBox::Slot()
.FillHeight(1.0f)
[
SNew(SMorphPresetPreviewViewport)
.Controller(Controller)
]);
}
void SMorphPresetViewportPanel::OnPreviewMeshChanged(const FAssetData& AssetData)
{
if (Controller.IsValid())
{
Controller->SetPreviewMesh(Cast<USkeletalMesh>(AssetData.GetAsset()));
}
}
void SMorphPresetViewportPanel::OnLipSyncSoundChanged(const FAssetData& AssetData)
{
if (Controller.IsValid())
{
Controller->SetLipSyncPreviewSound(Cast<USoundBase>(AssetData.GetAsset()));
}
}
void SMorphPresetViewportPanel::OnLipSyncMappingChanged(const FAssetData& AssetData)
{
if (Controller.IsValid())
{
Controller->SetLipSyncPreviewMapping(Cast<ULipSyncMappingDataAsset>(AssetData.GetAsset()));
}
}
FString SMorphPresetViewportPanel::GetSelectedPreviewMeshPath() const
{
if (Controller.IsValid())
{
if (USkeletalMesh* Mesh = Controller->GetPreviewMesh())
{
return Mesh->GetPathName();
}
}
return FString();
}
FString SMorphPresetViewportPanel::GetSelectedLipSyncSoundPath() const
{
if (Controller.IsValid())
{
if (USoundBase* Sound = Controller->GetLipSyncPreviewSound())
{
return Sound->GetPathName();
}
}
return FString();
}
FString SMorphPresetViewportPanel::GetSelectedLipSyncMappingPath() const
{
if (Controller.IsValid())
{
if (ULipSyncMappingDataAsset* Mapping = Controller->GetLipSyncPreviewMapping())
{
return Mapping->GetPathName();
}
}
return FString();
}
FReply SMorphPresetViewportPanel::OnPlayLipSyncClicked()
{
if (Controller.IsValid())
{
if (Controller->IsLipSyncPreviewAudioPlaying())
{
Controller->StopLipSyncPreviewAudio();
}
else
{
Controller->PlayLipSyncPreviewAudio();
}
}
return FReply::Handled();
}
FReply SMorphPresetViewportPanel::OnStopLipSyncClicked()
{
if (Controller.IsValid())
{
Controller->StopLipSyncPreviewAudio();
}
return FReply::Handled();
}
FText SMorphPresetViewportPanel::GetLipSyncPlaybackText() const
{
return Controller.IsValid() && Controller->IsLipSyncPreviewAudioPlaying()
? LOCTEXT("PauseLipSync", "Pause Lip Sync")
: LOCTEXT("PlayLipSync", "Play Lip Sync");
}
float SMorphPresetViewportPanel::GetLipSyncSmoothness() const
{
return Controller.IsValid() ? Controller->GetLipSyncPreviewSmoothness() : 0.55f;
}
void SMorphPresetViewportPanel::OnLipSyncSmoothnessChanged(float NewValue)
{
if (Controller.IsValid())
{
Controller->SetLipSyncPreviewSmoothness(NewValue);
}
}
FText SMorphPresetViewportPanel::GetLipSyncTranscript() const
{
return Controller.IsValid() ? Controller->GetLipSyncPreviewTranscript() : FText::GetEmpty();
}
void SMorphPresetViewportPanel::OnLipSyncTranscriptChanged(const FText& NewText)
{
if (Controller.IsValid())
{
Controller->SetLipSyncPreviewTranscript(NewText);
}
}
FText SMorphPresetViewportPanel::GetPreviewStatusText() const
{
return Controller.IsValid() ? Controller->GetPreviewStatusText() : FText::GetEmpty();
}
EVisibility SMorphPresetViewportPanel::GetPreviewStatusVisibility() const
{
return !GetPreviewStatusText().IsEmpty() ? EVisibility::Visible : EVisibility::Collapsed;
}
EActiveTimerReturnType SMorphPresetViewportPanel::HandleActiveTimer(double, float InDeltaTime)
{
if (Controller.IsValid())
{
Controller->Tick(InDeltaTime);
}
return EActiveTimerReturnType::Continue;
}
#undef LOCTEXT_NAMESPACE
@@ -0,0 +1,62 @@
// Copyright Epic Games, Inc. All Rights Reserved.
// Vendored for FunnyGirlEditor (adapted from UE Persona private SAnimEditorBase).
#pragma once
#include "CoreMinimal.h"
#include "UObject/GCObject.h"
#include "IPersonaPreviewScene.h"
#include "Widgets/SCompoundWidget.h"
#include "PersonaDelegates.h"
class UAnimationAsset;
class UAnimPreviewInstance;
class FUNNYGIRLEDITOR_API SAnimEditorBase : public SCompoundWidget, public FGCObject
{
public:
SLATE_BEGIN_ARGS(SAnimEditorBase)
: _DisplayAnimTimeline(true)
, _DisplayAnimScrubBar(false)
, _DisplayAnimScrubBarEditing(false)
{}
SLATE_EVENT(FOnObjectsSelected, OnObjectsSelected)
SLATE_ARGUMENT(bool, DisplayAnimTimeline)
SLATE_ARGUMENT(bool, DisplayAnimScrubBar)
SLATE_ARGUMENT(bool, DisplayAnimScrubBarEditing)
SLATE_END_ARGS()
void Construct(const FArguments& InArgs, const TSharedPtr<IPersonaPreviewScene>& InPreviewScene);
float GetViewMinInput() const { return ViewMinInput; }
float GetViewMaxInput() const { return ViewMaxInput; }
float GetMinInput() const { return 0.0f; }
float GetMaxInput() const { return GetSequenceLength(); }
void SetInputViewRange(float InViewMinInput, float InViewMaxInput);
float GetScrubValue() const;
void OnSelectionChanged(const TArray<UObject*>& SelectedItems);
// FGCObject
virtual void AddReferencedObjects(FReferenceCollector& Collector) override {}
virtual FString GetReferencerName() const override { return TEXT("FunnyGirlVendoredSAnimEditorBase"); }
virtual TSharedRef<SWidget> CreateDocumentAnchor();
protected:
virtual TSharedRef<SWidget> CreateAnimTimelineWidget() = 0;
virtual TSharedRef<SWidget> ConstructAnimScrubPanel(bool bDisplayAnimScrubBarEditing);
virtual UAnimationAsset* GetEditorObject() const = 0;
float GetSequenceLength() const;
TSharedRef<IPersonaPreviewScene> GetPreviewScene() const { return PreviewScenePtr.Pin().ToSharedRef(); }
protected:
TWeakPtr<IPersonaPreviewScene> PreviewScenePtr;
TSharedPtr<SVerticalBox> EditorPanels;
TSharedPtr<SVerticalBox> NonScrollEditorPanels;
FOnObjectsSelected OnObjectsSelectedDelegate;
float ViewMinInput = 0.0f;
float ViewMaxInput = 0.0f;
};
@@ -0,0 +1,19 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Factories/Factory.h"
#include "LipSyncMappingAssetFactory.generated.h"
UCLASS()
class FUNNYGIRLEDITOR_API ULipSyncMappingAssetFactory : public UFactory
{
GENERATED_BODY()
public:
ULipSyncMappingAssetFactory();
virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
virtual FString GetDefaultNewAssetName() const override;
};
@@ -0,0 +1,16 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "AssetTypeActions_Base.h"
class FLipSyncMappingAssetTypeActions : public FAssetTypeActions_Base
{
public:
virtual FText GetName() const override;
virtual FColor GetTypeColor() const override;
virtual UClass* GetSupportedClass() const override;
virtual uint32 GetCategories() override;
virtual FText GetAssetDescription(const FAssetData& AssetData) const override;
};
@@ -0,0 +1,49 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Toolkits/AssetEditorToolkit.h"
class IDetailsView;
class SDockTab;
class UMorphPresetDataAsset;
class FMorphPresetEditorController;
class SMorphPresetTimelinePanel;
class SMorphPresetViewportPanel;
class FUNNYGIRLEDITOR_API FMorphPresetAssetEditor : public FAssetEditorToolkit
{
public:
static const FName EditorName;
static const FName ViewportTabId;
static const FName TimelineTabId;
static const FName DetailsTabId;
void InitMorphPresetAssetEditor(const EToolkitMode::Type Mode, const TSharedPtr<class IToolkitHost>& InitToolkitHost, UMorphPresetDataAsset* InAsset);
virtual void RegisterTabSpawners(const TSharedRef<FTabManager>& InTabManager) override;
virtual void UnregisterTabSpawners(const TSharedRef<FTabManager>& InTabManager) override;
virtual FName GetToolkitFName() const override;
virtual FText GetBaseToolkitName() const override;
virtual FString GetWorldCentricTabPrefix() const override;
virtual FLinearColor GetWorldCentricTabColorScale() const override;
UMorphPresetDataAsset* GetPreset() const { return Preset.Get(); }
private:
TSharedRef<SDockTab> SpawnTab_Viewport(const FSpawnTabArgs& Args);
TSharedRef<SDockTab> SpawnTab_Timeline(const FSpawnTabArgs& Args);
TSharedRef<SDockTab> SpawnTab_Details(const FSpawnTabArgs& Args);
void HandleDetailsChanged(const struct FPropertyChangedEvent& PropertyChangedEvent);
void HandlePresetChanged();
private:
TWeakObjectPtr<UMorphPresetDataAsset> Preset;
TSharedPtr<FMorphPresetEditorController> EditorController;
TSharedPtr<SMorphPresetViewportPanel> ViewportWidget;
TSharedPtr<SMorphPresetTimelinePanel> TimelineWidget;
TSharedPtr<IDetailsView> DetailsView;
};
@@ -0,0 +1,19 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Factories/Factory.h"
#include "MorphPresetAssetFactory.generated.h"
UCLASS()
class FUNNYGIRLEDITOR_API UMorphPresetAssetFactory : public UFactory
{
GENERATED_BODY()
public:
UMorphPresetAssetFactory();
virtual UObject* FactoryCreateNew(UClass* InClass, UObject* InParent, FName InName, EObjectFlags Flags, UObject* Context, FFeedbackContext* Warn) override;
virtual FString GetDefaultNewAssetName() const override;
};
@@ -0,0 +1,17 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "AssetTypeActions_Base.h"
class FMorphPresetAssetTypeActions : public FAssetTypeActions_Base
{
public:
virtual FText GetName() const override;
virtual FColor GetTypeColor() const override;
virtual UClass* GetSupportedClass() const override;
virtual uint32 GetCategories() override;
virtual FText GetAssetDescription(const FAssetData& AssetData) const override;
virtual void OpenAssetEditor(const TArray<UObject*>& InObjects, TSharedPtr<IToolkitHost> EditWithinLevelEditor = TSharedPtr<IToolkitHost>()) override;
};
@@ -0,0 +1,37 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "IDetailCustomization.h"
class IDetailLayoutBuilder;
class IPropertyUtilities;
class SMultiLineEditableTextBox;
class UMorphPresetDataAsset;
class FUNNYGIRLEDITOR_API FMorphPresetDetailsCustomization : public IDetailCustomization
{
public:
static TSharedRef<IDetailCustomization> MakeInstance();
virtual void CustomizeDetails(IDetailLayoutBuilder& DetailBuilder) override;
private:
void RefreshJsonFromPreset();
void ApplyJsonToPreset(const FString& JsonString);
FText GetStatusText() const;
FSlateColor GetStatusColor() const;
FReply OnApplyJsonClicked();
FReply OnResetJsonClicked();
void HandleJsonTextChanged(const FText& NewText);
private:
TWeakObjectPtr<UMorphPresetDataAsset> Preset;
TSharedPtr<SMultiLineEditableTextBox> JsonTextBox;
TSharedPtr<IPropertyUtilities> PropertyUtilities;
FString CachedJsonText;
FText StatusText;
bool bIsUpdatingJsonText = false;
bool bLastApplySucceeded = true;
};
@@ -0,0 +1,99 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
class FUICommandList;
class AActor;
class IPersonaPreviewScene;
class IPersonaToolkit;
class UAnimationAsset;
class UAnimSequence;
class ULipSyncComponent;
class ULipSyncMappingDataAsset;
class UMorphControllerComponent;
class UMorphPresetDataAsset;
class USoundBase;
class USkeletalMesh;
class FUNNYGIRLEDITOR_API FMorphPresetEditorController : public TSharedFromThis<FMorphPresetEditorController>
{
public:
explicit FMorphPresetEditorController(UMorphPresetDataAsset* InPreset);
~FMorphPresetEditorController();
void Initialize();
UMorphPresetDataAsset* GetPreset() const;
USkeletalMesh* GetPreviewMesh() const;
bool SetPreviewMesh(USkeletalMesh* InPreviewMesh);
TSharedPtr<IPersonaToolkit> GetPersonaToolkit() const { return PersonaToolkit; }
TSharedPtr<IPersonaPreviewScene> GetPreviewScene() const;
TSharedRef<FUICommandList> GetCommandList() const { return CommandList.ToSharedRef(); }
float GetTimelineLength() const;
float GetCurrentTime() const;
FText GetPreviewStatusText() const { return PreviewStatusText; }
void SetCurrentTime(float InTime);
bool TryGetHeadLocation(FVector& OutHeadLocation) const;
bool ResolvePreviewBoneName(FName InBoneName, FName& OutBoneName) const;
void NotifyPresetEdited(bool bBroadcastPresetChanged = true);
bool IsPlaying() const { return bIsPlaying; }
void SetPlaying(bool bInPlaying);
void TogglePlaying();
void Tick(float DeltaTime);
USoundBase* GetLipSyncPreviewSound() const;
bool SetLipSyncPreviewSound(USoundBase* InSound);
ULipSyncMappingDataAsset* GetLipSyncPreviewMapping() const;
bool SetLipSyncPreviewMapping(ULipSyncMappingDataAsset* InMapping);
float GetLipSyncPreviewSmoothness() const { return LipSyncPreviewSmoothness; }
void SetLipSyncPreviewSmoothness(float InSmoothness);
FText GetLipSyncPreviewTranscript() const { return LipSyncPreviewTranscript; }
void SetLipSyncPreviewTranscript(const FText& InTranscript);
bool PlayLipSyncPreviewAudio(float StartTime = 0.0f);
void StopLipSyncPreviewAudio();
bool IsLipSyncPreviewAudioPlaying() const;
DECLARE_MULTICAST_DELEGATE(FOnChanged);
FOnChanged& OnPreviewSceneChanged() { return PreviewSceneChanged; }
FOnChanged& OnPlaybackStateChanged() { return PlaybackStateChanged; }
FOnChanged& OnPresetChanged() { return PresetChanged; }
private:
void RebuildPreviewScene();
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);
ULipSyncMappingDataAsset* ResolveLipSyncPreviewMapping() const;
private:
TWeakObjectPtr<UMorphPresetDataAsset> Preset;
TSharedPtr<IPersonaToolkit> PersonaToolkit;
TSharedPtr<FUICommandList> CommandList;
TStrongObjectPtr<UAnimSequence> PreviewAnimation;
TStrongObjectPtr<ULipSyncMappingDataAsset> DefaultLipSyncPreviewMapping;
TWeakObjectPtr<ULipSyncMappingDataAsset> LipSyncPreviewMapping;
TWeakObjectPtr<USoundBase> LipSyncPreviewSound;
TWeakObjectPtr<AActor> LipSyncPreviewActor;
TWeakObjectPtr<UMorphControllerComponent> LipSyncPreviewMorphController;
TWeakObjectPtr<ULipSyncComponent> LipSyncPreviewComponent;
bool bIsPlaying = false;
bool bLipSyncPreviewAudioActive = false;
float CurrentTime = 0.0f;
float LipSyncPreviewSmoothness = 0.55f;
FText LipSyncPreviewTranscript;
FText PreviewStatusText;
FOnChanged PreviewSceneChanged;
FOnChanged PlaybackStateChanged;
FOnChanged PresetChanged;
};
@@ -0,0 +1,40 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Widgets/SCompoundWidget.h"
class UMorphPresetDataAsset;
/**
* Editor Window для импорта морф таргетов из JSON в Morph Preset.
* Режим Embedded: передай Preset — пикер скрыт, редактор привязан к ассету.
* Режим Standalone: Preset = nullptr — показывается пикер (Tools -> Morph Preset Editor).
*/
class FUNNYGIRLEDITOR_API SMorphPresetEditorWindow : public SCompoundWidget
{
public:
SLATE_BEGIN_ARGS(SMorphPresetEditorWindow) {}
SLATE_ARGUMENT(UMorphPresetDataAsset*, Preset)
SLATE_END_ARGS()
void Construct(const FArguments& InArgs);
/** Только в embedded режиме (Preset задан): обновить JSON из ассета. */
void RefreshFromPreset();
private:
TSharedRef<SWidget> BuildContent();
/** true = внутри окна ассета (preset передан), false = отдельное окно с пикером */
bool bEmbeddedMode = false;
FReply OnApplyClicked();
FReply OnExportClicked();
void OnPresetChanged(const FAssetData& AssetData);
FString GetSelectedPresetPath() const;
TSharedPtr<class SObjectPropertyEntryBox> PresetPicker;
TSharedPtr<class SMultiLineEditableTextBox> JsonTextBox;
TWeakObjectPtr<UMorphPresetDataAsset> SelectedPreset;
};
@@ -0,0 +1,81 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "ITransportControl.h"
#include "Input/Events.h"
#include "Input/Reply.h"
#include "Widgets/SCompoundWidget.h"
class FMorphPresetEditorController;
template<typename ItemType> class SListView;
class FUNNYGIRLEDITOR_API SMorphPresetTimelinePanel : public SCompoundWidget
{
public:
SLATE_BEGIN_ARGS(SMorphPresetTimelinePanel) {}
SLATE_ARGUMENT(TSharedPtr<FMorphPresetEditorController>, Controller)
SLATE_END_ARGS()
void Construct(const FArguments& InArgs);
virtual ~SMorphPresetTimelinePanel() override;
void Refresh();
virtual bool SupportsKeyboardFocus() const override { return true; }
virtual FReply OnKeyDown(const FGeometry& MyGeometry, const FKeyEvent& InKeyEvent) override;
bool HasSelectedClips() const;
int32 GetSelectedClipCount() const;
bool IsClipSelected(int32 ClipIndex) const;
void ToggleClipSelection(int32 ClipIndex);
void SelectOnlyClip(int32 ClipIndex);
void ClearClipSelection();
void SelectAllClips();
void BeginMoveSelectedClips(int32 PrimaryClipIndex);
void ApplyMoveSelectedClips(float DeltaTime);
void EndMoveSelectedClips();
void DeleteSelectedClips();
TSharedRef<SWidget> BuildClipInspectorWidget(int32 ClipIndex);
void RegisterTimelineScrollBox(TSharedPtr<class SScrollBox> InTimelineScrollBox);
private:
void RefreshAvailableMorphs();
FReply OnPlayPauseClicked();
FReply OnStopClicked();
FReply OnAddMorphClicked();
FReply OnDeleteSelectedClicked();
EPlaybackMode::Type GetPlaybackMode() const;
void OnLoopCheckStateChanged(ECheckBoxState NewState);
ECheckBoxState GetLoopCheckState() const;
void HandleTimeChanged(float InNewValue);
TSharedRef<SWidget> BuildTimelineWidget();
TSharedRef<SWidget> BuildMorphLibraryWidget();
TSharedRef<class ITableRow> GenerateMorphRow(TSharedPtr<FString> Item, const TSharedRef<class STableViewBase>& OwnerTable) const;
void HandleMorphSelectionChanged(TSharedPtr<FString> Item, ESelectInfo::Type SelectInfo);
void HandleMorphDoubleClicked(TSharedPtr<FString> Item);
bool CanAddSelectedMorph() const;
struct FMorphTimelineClip* GetMutableClipByIndex(int32 ClipIndex) const;
const struct FMorphTimelineClip* GetClipByIndex(int32 ClipIndex) const;
int32 GetPrimarySelectedClipIndex() const;
void NotifyClipChanged(bool bRefreshTimeline = false);
void HandleClipWeightChanged(float NewValue);
void HandleClipStartTimeChanged(float NewValue);
void HandleClipDurationChanged(float NewValue);
EActiveTimerReturnType HandleActiveTimer(double InCurrentTime, float InDeltaTime);
FText GetPlayPauseText() const;
float GetCurrentTime() const;
TOptional<float> GetTimelineMaxTime() const;
void HandleControllerChanged();
private:
TSharedPtr<FMorphPresetEditorController> Controller;
TSharedPtr<class SBox> TimelineContainer;
TSharedPtr<class SScrollBox> TimelineScrollBox;
TArray<TSharedPtr<FString>> AvailableMorphItems;
TSharedPtr<FString> SelectedMorphItem;
TSharedPtr<SListView<TSharedPtr<FString>>> MorphListView;
TSet<int32> SelectedClipIndices;
TMap<int32, float> DragInitialClipStartTimes;
int32 DragPrimaryClipIndex = INDEX_NONE;
float TimelineScrollOffset = 0.0f;
};
@@ -0,0 +1,47 @@
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "CoreMinimal.h"
#include "Widgets/SCompoundWidget.h"
class FMorphPresetEditorController;
class ULipSyncMappingDataAsset;
class USoundBase;
class USkeletalMesh;
class FUNNYGIRLEDITOR_API SMorphPresetViewportPanel : public SCompoundWidget
{
public:
SLATE_BEGIN_ARGS(SMorphPresetViewportPanel) {}
SLATE_ARGUMENT(TSharedPtr<FMorphPresetEditorController>, Controller)
SLATE_ARGUMENT(TSharedPtr<class FAssetEditorToolkit>, AssetEditorToolkit)
SLATE_END_ARGS()
void Construct(const FArguments& InArgs);
virtual ~SMorphPresetViewportPanel() override;
private:
void RebuildContent();
void OnPreviewMeshChanged(const struct FAssetData& AssetData);
void OnLipSyncSoundChanged(const struct FAssetData& AssetData);
void OnLipSyncMappingChanged(const struct FAssetData& AssetData);
FString GetSelectedPreviewMeshPath() const;
FString GetSelectedLipSyncSoundPath() const;
FString GetSelectedLipSyncMappingPath() const;
FReply OnPlayLipSyncClicked();
FReply OnStopLipSyncClicked();
FText GetLipSyncPlaybackText() const;
float GetLipSyncSmoothness() const;
void OnLipSyncSmoothnessChanged(float NewValue);
FText GetLipSyncTranscript() const;
void OnLipSyncTranscriptChanged(const FText& NewText);
FText GetPreviewStatusText() const;
EVisibility GetPreviewStatusVisibility() const;
EActiveTimerReturnType HandleActiveTimer(double InCurrentTime, float InDeltaTime);
private:
TSharedPtr<FMorphPresetEditorController> Controller;
TWeakPtr<class FAssetEditorToolkit> AssetEditorToolkit;
TSharedPtr<class SBorder> RootBorder;
};