Files
UnrealEngine/Engine/Source/Editor/Persona/Private/PersonaPreviewSceneSkelMeshInstanceController.cpp
2025-05-18 13:04:45 +08:00

432 lines
14 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "PersonaPreviewSceneSkelMeshInstanceController.h"
#include "AnimationEditorPreviewScene.h"
#include "AnimPreviewInstance.h"
#include "DetailWidgetRow.h"
#include "Editor.h"
#include "EngineUtils.h"
#include "PersonaPreviewSceneDescription.h"
#include "IPersonaToolkit.h"
#include "Selection.h"
#include "Widgets/Layout/SGridPanel.h"
#define LOCTEXT_NAMESPACE "UPersonaPreviewSceneSkelMeshInstanceController"
AActor* FSkeletalMeshDebugInstance::GetActor() const
{
return SkeletalMeshComponent.IsValid() ? SkeletalMeshComponent->GetOwner() : nullptr;
}
void UPersonaPreviewSceneSkelMeshInstanceController::InitializeView(
UPersonaPreviewSceneDescription* SceneDescription,
IPersonaPreviewScene* PreviewScene) const
{
constexpr bool bShowReferencePose = true;
constexpr bool bResetTransforms = true;
PreviewScene->ShowReferencePose(bShowReferencePose, bResetTransforms);
}
void UPersonaPreviewSceneSkelMeshInstanceController::UninitializeView(
UPersonaPreviewSceneDescription* SceneDescription,
IPersonaPreviewScene* PreviewScene) const
{
if (UDebugSkelMeshComponent* PreviewMeshComponent = PreviewScene->GetPreviewMeshComponent())
{
PreviewMeshComponent->bTrackAttachedInstanceLOD = false;
if (UAnimPreviewInstance* PreviewAnimInstance = PreviewMeshComponent->PreviewInstance)
{
PreviewAnimInstance->SetDebugSkeletalMeshComponent(nullptr);
}
}
PreviewScene->ShowDefaultMode();
}
IDetailPropertyRow* UPersonaPreviewSceneSkelMeshInstanceController::AddPreviewControllerPropertyToDetails(
const TSharedRef<IPersonaToolkit>& PersonaToolkit,
IDetailLayoutBuilder& DetailBuilder,
IDetailCategoryBuilder& Category,
const FProperty* Property,
const EPropertyLocation::Type PropertyLocation)
{
TSharedPtr<IPersonaPreviewScene> PreviewScenePtr = PersonaToolkit->GetPreviewScene();
const USkeletalMesh* SkeletalMesh = PersonaToolkit->GetPreviewMeshComponent()->GetSkeletalMeshAsset();
if (!SkeletalMesh)
{
return nullptr;
}
if (Property->GetName() != GET_MEMBER_NAME_CHECKED(UPersonaPreviewSceneSkelMeshInstanceController, ActivePreviewInstance))
{
return nullptr;
}
// create custom widget to select preview instance
const TArray<UObject*> ListOfPreviewController{ this };
const FName PropertyName = GET_MEMBER_NAME_CHECKED(UPersonaPreviewSceneSkelMeshInstanceController, ActivePreviewInstance);
IDetailPropertyRow* NewRow = Category.AddExternalObjectProperty(ListOfPreviewController, PropertyName, EPropertyLocation::Common);
NewRow->CustomWidget()
.NameContent()
[
NewRow->GetPropertyHandle()->CreatePropertyNameWidget()
]
.ValueContent()
.MaxDesiredWidth(250.0f)
.MinDesiredWidth(250.0f)
[
SNew(SSkeletalMeshDebugSelectionWidget).PreviewScene(PreviewScenePtr)
];
return NewRow;
}
void SSkeletalMeshDebugSelectionWidget::Construct(const FArguments& InArgs)
{
PreviewScenePtr = InArgs._PreviewScene;
ChildSlot
[
SNew(SHorizontalBox)
+SHorizontalBox::Slot()
.AutoWidth()
.Padding(FMargin(2, 0, 0, 0))
.HAlign(HAlign_Left)
.VAlign(VAlign_Center)
[
SAssignNew(InstanceComboBox, SComboBox<TSharedPtr<FSkeletalMeshDebugInstance>>)
.OptionsSource(&AllMeshInstances)
.OnGenerateWidget(this, &SSkeletalMeshDebugSelectionWidget::OnGenerateComboBoxItemWidget)
.OnSelectionChanged(this, &SSkeletalMeshDebugSelectionWidget::OnSelectionChanged)
[
SNew(STextBlock)
.Text_Lambda([this]()
{
return ActiveInstance.IsValid() ? ActiveInstance->DisplayName : LOCTEXT("PreviewDisabledText", "Preview Disabled");
})
.ColorAndOpacity_Lambda([this]()
{
const bool bIsPreviewingInstance = ActiveInstance.IsValid() && ActiveInstance->SkeletalMeshComponent.IsValid();
return bIsPreviewingInstance ? FLinearColor::Green : FSlateColor::UseForeground();
})
]
]
];
Refresh();
FWorldDelegates::OnPostWorldInitialization.AddSPLambda(this, [this](UWorld* World, const UWorld::InitializationValues IVS) { Refresh(); });
FWorldDelegates::OnWorldCleanup.AddSPLambda(this, [this](UWorld* World, bool bSessionEnded, bool bCleanupResources) { Refresh(); } );
FWorldDelegates::OnPostDuplicate.AddSPLambda(this, [this](UWorld* World, bool bDuplicateForPIE, FWorldDelegates::FReplacementMap& ReplacementMap, TArray<UObject*>& ObjectsToFixReferences) { Refresh(); });
FWorldDelegates::OnPostWorldRename.AddSPLambda(this, [this](UWorld* World) { Refresh(); } );
FWorldDelegates::OnPIEReady.AddSPLambda(this, [this](UGameInstance*) { Refresh(); });
FEditorDelegates::PostPIEStarted.AddSPLambda(this, [this](bool bSimulating) { Refresh(); });
FEditorDelegates::PausePIE.AddSPLambda(this, [this](bool bSimulating) { Refresh(); });
FEditorDelegates::ResumePIE.AddSPLambda(this, [this](bool bSimulating) { Refresh(); });
FEditorDelegates::SingleStepPIE.AddSPLambda(this, [this](bool bSimulating) { Refresh(); });
FEditorDelegates::EndPIE.AddSPLambda(this, [this](bool bSimulating) { Refresh(); });
FEditorDelegates::CancelPIE.AddSPLambda(this, [this]() { Refresh(); });
FEditorDelegates::OnNewActorsPlaced.AddSPLambda(this, [this](UObject*, const TArray<AActor*>&) { Refresh(); });
FEditorDelegates::OnDeleteActorsBegin.AddSPLambda(this, [this]() { Refresh(); });
FEditorDelegates::OnSwitchBeginPIEAndSIE.AddSPLambda(this, [this](bool bSimulating) { Refresh(); });
USelection::SelectObjectEvent.AddSPLambda(this, [this](UObject* NewSelection) { Refresh(); });
USelection::SelectionChangedEvent.AddSPLambda(this, [this](UObject* NewSelection) { Refresh(); });
USelection::SelectNoneEvent.AddSPLambda(this, [this]() { Refresh(); });
}
void SSkeletalMeshDebugSelectionWidget::Refresh()
{
// empty list of instances to refresh
AllMeshInstances.Reset();
// create a default/empty item used to disable preview
TSharedPtr<FSkeletalMeshDebugInstance> EmptyItem = MakeShared<FSkeletalMeshDebugInstance>();
EmptyItem->DisplayName = LOCTEXT("DefaultItemText", "None");
AllMeshInstances.Emplace(EmptyItem);
const IPersonaPreviewScene* PreviewScene = PreviewScenePtr.Get();
if (!PreviewScene)
{
return;
}
const USkeletalMesh* PreviewMesh = PreviewScene->GetPreviewMesh();
if (!PreviewMesh)
{
return;
}
const UDebugSkelMeshComponent* PreviewComponent = PreviewScene->GetPreviewMeshComponent();
if (!PreviewComponent)
{
return;
}
// get all debug worlds
TArray<TWeakObjectPtr<UWorld>> AllDebugWorlds = []()
{
TArray<TWeakObjectPtr<UWorld>> AllDebugWorlds;
for (TObjectIterator<UWorld> It; It; ++It)
{
UWorld* World = *It;
// include only PIE and worlds that own the persistent level (i.e. non-streaming levels).
const bool bIsValidDebugWorld = (World != nullptr)
&& (World->WorldType == EWorldType::PIE || World->WorldType == EWorldType::Editor || World->WorldType == EWorldType::EditorPreview)
&& World->PersistentLevel != nullptr
&& World->PersistentLevel->OwningWorld == World;
if (!bIsValidDebugWorld)
{
continue;
}
AllDebugWorlds.Add(World);
}
return MoveTemp(AllDebugWorlds);
}();
// spin through all available worlds and find all skeletal mesh components using the target skeletal mesh
for (int32 WorldIndex=0; WorldIndex<AllDebugWorlds.Num(); ++WorldIndex)
{
UWorld* World = AllDebugWorlds[WorldIndex].Get();
if (!World)
{
// double-check because we have had crashes in TActorIterator below on null worlds
continue;
}
for (TActorIterator<AActor> ActorItr(World); ActorItr; ++ActorItr)
{
const AActor* Actor = *ActorItr;
if (!Actor)
{
continue;
}
constexpr bool bIncludeChildActors = true;
Actor->ForEachComponent<USkeletalMeshComponent>(bIncludeChildActors,
[this, PreviewMesh, PreviewComponent](USkeletalMeshComponent* Component)
{
if (!Component)
{
return;
}
const bool bUsingPreviewMesh = Component->GetSkeletalMeshAsset() == PreviewMesh;
const bool bIsNotPreviewComponent = Component != PreviewComponent;
if (bUsingPreviewMesh && bIsNotPreviewComponent)
{
TSharedPtr<FSkeletalMeshDebugInstance> NewInstance = MakeShared<FSkeletalMeshDebugInstance>();
NewInstance->SkeletalMeshComponent = Component;
AActor* Actor = NewInstance->GetActor();
NewInstance->DisplayName = IsValid(Actor) ? FText::FromString(Actor->GetActorNameOrLabel()) : FText();
AllMeshInstances.Emplace(NewInstance);
}
});
}
}
// update bIsSelected for each debug instance based on if the actor is selected in the level editor
{
// default to NOT selected
for (TSharedPtr<FSkeletalMeshDebugInstance>& MeshInstance : AllMeshInstances)
{
if (MeshInstance.IsValid())
{
MeshInstance->bIsSelected = false;
}
}
// get the selected actors in the editor.
const USelection* ActiveDebugActors = GEditor->GetSelectedActors();
if (ActiveDebugActors == nullptr)
{
return;
}
// processed in reverse order, as we want the last selected item to be the one we pick.
// there can only be one actor selected to preview, while many can be selected in the editor itself.
for (int32 Index = ActiveDebugActors->Num() - 1; Index >= 0; --Index)
{
const AActor* Actor = Cast<AActor>(ActiveDebugActors->GetSelectedObject(Index));
if (Actor == nullptr)
{
continue;
}
// is this an actor with a preview mesh?
const TSharedPtr<FSkeletalMeshDebugInstance>* SelectedMeshInstance = AllMeshInstances.FindByPredicate
([Actor](const TSharedPtr<FSkeletalMeshDebugInstance>& MeshInstance)
{
if (!MeshInstance.IsValid())
{
return false;
}
return MeshInstance->GetActor() == Actor;
});
// found a selected preview mesh?
if (SelectedMeshInstance && SelectedMeshInstance->IsValid())
{
// mark it as selected and break out (only one/last selection considered for debug preview)
SelectedMeshInstance->Get()->bIsSelected = true;
break;
}
}
} // END update selection state
// restore active running instance if there is one AND it's still in the list of available instances
TSharedPtr<FSkeletalMeshDebugInstance> ItemToActivate = EmptyItem; // default to empty item
for (const TSharedPtr<FSkeletalMeshDebugInstance>& Instance : AllMeshInstances)
{
// search for actor with same name as was previously selected
if (Instance->DisplayName.EqualTo(NameOfLastSelectedInstance, ETextComparisonLevel::Default))
{
ItemToActivate = Instance;
break;
}
if (PreviewComponent->PreviewInstance && PreviewComponent->PreviewInstance->GetDebugSkeletalMeshComponent() == Instance->SkeletalMeshComponent)
{
ItemToActivate = Instance;
break;
}
}
// assign selection to combobox
InstanceComboBox.Get()->SetSelectedItem(ItemToActivate);
}
TSharedRef<SWidget> SSkeletalMeshDebugSelectionWidget::OnGenerateComboBoxItemWidget(TSharedPtr<FSkeletalMeshDebugInstance> Item)
{
// If we have the first item in the actor list, generate a special widget.
const TWeakObjectPtr<USkeletalMeshComponent> Component = Item.IsValid() ? Item->SkeletalMeshComponent : nullptr;
const AActor* Actor = Component.IsValid() ? Component->GetOwner() : nullptr;
FText ActorName = Actor ? FText::FromString(Actor->GetActorNameOrLabel()) : LOCTEXT("DestroyedActorText", "<Destroyed>");
const UWorld* World = Actor ? Actor->GetWorld() : nullptr;
FText WorldName = World ? FText::FromString(GetDebugStringForWorld(World)) : LOCTEXT("DestroyedWorldText", "<Destroyed>");
constexpr FLinearColor ActiveColor = FLinearColor(0.0f, 1.0f, 0.0f);
constexpr FLinearColor EditorSelectedMarkColor = FLinearColor(1.0f, 0.5f, 0.0f);
// special item to disable debugging.
if (Actor == nullptr)
{
ActorName = LOCTEXT("DebugDisabledActorText", "None");
WorldName = LOCTEXT("DebugDisabledWorldText", "Editor Preview World");
}
const TSharedPtr<SWidget> ItemWidget =
SNew(SGridPanel)
+SGridPanel::Slot(0, 0)
.Padding(2.0f)
.HAlign(EHorizontalAlignment::HAlign_Right)
[
SNew(STextBlock)
.Text(LOCTEXT("ActorName", "Actor:"))
.ColorAndOpacity(FSlateColor::UseSubduedForeground())
]
+SGridPanel::Slot(1, 0)
.Padding(2.0f)
.HAlign(EHorizontalAlignment::HAlign_Left)
[
SNew(STextBlock)
.Text(ActorName)
.Font_Lambda([Item]()
{
if (Item.IsValid() && Item->bIsSelected)
{
return FAppStyle::GetFontStyle(TEXT("NormalFontBold"));
}
return FAppStyle::GetFontStyle(TEXT("NormalFont"));
})
.ColorAndOpacity_Lambda([this, Item, ActiveColor]()
{
const AActor* ItemActor = Item.IsValid() ? Item->GetActor() : nullptr;
return (ActiveInstance.IsValid() && ActiveInstance->GetActor() == ItemActor) ? ActiveColor : FSlateColor::UseForeground();
})
]
+SGridPanel::Slot(2, 0)
.Padding(2.0f)
.HAlign(EHorizontalAlignment::HAlign_Left)
[
SNew(STextBlock)
.Text_Lambda([Item]()
{
if (Item.IsValid() && Item->bIsSelected)
{
return LOCTEXT("SelectedText", "(Selected)");
}
return FText();
})
.TextStyle(FAppStyle::Get(), "RichTextBlock.Bold")
.ColorAndOpacity(EditorSelectedMarkColor)
]
+SGridPanel::Slot(0, 1)
.Padding(2.0f)
.HAlign(EHorizontalAlignment::HAlign_Right)
[
SNew(STextBlock)
.Text(LOCTEXT("WorldName", "World:"))
.ColorAndOpacity(FSlateColor::UseSubduedForeground())
]
+SGridPanel::Slot(1, 1)
.Padding(2.0f)
.HAlign(EHorizontalAlignment::HAlign_Left)
[
SNew(STextBlock)
.Text(WorldName)
.ColorAndOpacity_Lambda([this, Item, ActiveColor]()
{
const TObjectPtr<AActor> ItemActor = Item.IsValid() ? Item->GetActor() : nullptr;
return (ActiveInstance.IsValid() && ActiveInstance->GetActor() == ItemActor) ? ActiveColor : FSlateColor::UseForeground();
})
];
return ItemWidget.ToSharedRef();
}
void SSkeletalMeshDebugSelectionWidget::OnSelectionChanged(TSharedPtr<FSkeletalMeshDebugInstance> Item, ESelectInfo::Type SelectInfo)
{
ActiveInstance = Item;
if (SelectInfo != ESelectInfo::Type::Direct)
{
NameOfLastSelectedInstance = ActiveInstance->DisplayName;
}
const IPersonaPreviewScene* PreviewScene = PreviewScenePtr.Get();
if (!PreviewScene)
{
return;
}
UDebugSkelMeshComponent* PreviewMeshComponent = PreviewScene->GetPreviewMeshComponent();
if (!PreviewMeshComponent)
{
return;
}
UAnimPreviewInstance* PreviewAnimInstance = PreviewMeshComponent->PreviewInstance;
if (!PreviewAnimInstance)
{
return;
}
// reset to show no preview
if (!Item->SkeletalMeshComponent.IsValid())
{
PreviewAnimInstance->SetDebugSkeletalMeshComponent(nullptr);
PreviewMeshComponent->bTrackAttachedInstanceLOD = false;
return;
}
// assign the preview mesh to the debug skel mesh component
PreviewAnimInstance->SetDebugSkeletalMeshComponent(Item->SkeletalMeshComponent.Get());
PreviewMeshComponent->bTrackAttachedInstanceLOD = true;
}
#undef LOCTEXT_NAMESPACE