From f19bb1e750cc392411c311a6c6e36a7a9a81571a Mon Sep 17 00:00:00 2001 From: Eragon-Brisingr <450614754@qq.com> Date: Tue, 30 Jun 2026 18:02:07 +0800 Subject: [PATCH] Opt Editor create instance workflow --- .../ShaderLabMaterialInstanceFactory.cpp | 322 +++++++++++++++--- .../UShaderLabEditor.Build.cs | 1 + 2 files changed, 267 insertions(+), 56 deletions(-) diff --git a/Source/UShaderLabEditor/Private/ShaderLabMaterialInstanceFactory.cpp b/Source/UShaderLabEditor/Private/ShaderLabMaterialInstanceFactory.cpp index 2af81d3..28a5b88 100644 --- a/Source/UShaderLabEditor/Private/ShaderLabMaterialInstanceFactory.cpp +++ b/Source/UShaderLabEditor/Private/ShaderLabMaterialInstanceFactory.cpp @@ -6,98 +6,308 @@ #include "AssetThumbnail.h" #include "AssetTypeCategories.h" #include "Editor.h" -#include "Framework/Application/SlateApplication.h" #include "Materials/Material.h" #include "Misc/App.h" +#include "ShaderCompiler.h" #include "ShaderLabMaterialInstanceConstant.h" #include "ShaderLabMaterialRegistry.h" +#include "ShaderLabModel.h" #include "Widgets/Input/SButton.h" +#include "Widgets/Input/SSearchBox.h" +#include "Widgets/Layout/SBorder.h" #include "Widgets/Layout/SBox.h" -#include "Widgets/Layout/SScrollBox.h" -#include "Widgets/Layout/SUniformWrapPanel.h" #include "Widgets/SBoxPanel.h" #include "Widgets/SWindow.h" #include "Widgets/Text/STextBlock.h" +#include "Widgets/Views/SListView.h" +#include "Widgets/Views/STableRow.h" #define LOCTEXT_NAMESPACE "ShaderLabMaterialInstanceFactory" namespace { - /** Modal picker of the discovered ShaderLab base materials, with preview thumbnails. */ - UMaterial* PickShaderLabBase() - { - FShaderLabMaterialRegistry& Registry = FShaderLabMaterialRegistry::Get(); + constexpr int32 GThumbnailSize = 64; - // Collect the registered in-memory base materials (sorted by name for a stable layout). - TArray Bases; - TArray Names; - Registry.GetRegisteredModels().GenerateKeyArray(Names); - Names.Sort([](const FName& A, const FName& B) { return A.Compare(B) < 0; }); - for (const FName& Name : Names) + /** One row in the base-material picker: the in-memory base + display strings + a lazily-made thumbnail. */ + struct FShaderLabBaseEntry + { + UMaterial* Material = nullptr; + FString Name; + /** "Surface / Opaque" etc., from the parsed model settings (shown as the row subtitle). */ + FString DomainBlend; + /** Live thumbnail, created on first display (on-demand) and reused across row recycling. */ + TSharedPtr Thumbnail; + }; + using FBaseEntryPtr = TSharedPtr; + + FString DomainToString(EShaderLabDomain Domain) + { + switch (Domain) { - if (UMaterial* Base = Registry.FindMaterial(Name.ToString())) + case EShaderLabDomain::Surface: return TEXT("Surface"); + case EShaderLabDomain::PostProcess: return TEXT("PostProcess"); + case EShaderLabDomain::UI: return TEXT("UI"); + case EShaderLabDomain::Decal: return TEXT("Decal"); + } + checkNoEntry(); + return FString(); + } + + FString BlendToString(EShaderLabBlendMode Blend) + { + switch (Blend) + { + case EShaderLabBlendMode::Opaque: return TEXT("Opaque"); + case EShaderLabBlendMode::Masked: return TEXT("Masked"); + case EShaderLabBlendMode::Translucent: return TEXT("Translucent"); + case EShaderLabBlendMode::Additive: return TEXT("Additive"); + case EShaderLabBlendMode::Modulate: return TEXT("Modulate"); + } + checkNoEntry(); + return FString(); + } + + /** + * Modal picker over the discovered ShaderLab base materials, modelled on the engine's class-picker + * dialog: a search box above a flat, single-selection list with a live preview thumbnail per row. + * + * Thumbnails render asynchronously and on-demand: each base is a shader-map-less /Script shell, so + * MaterialInstanceThumbnailRenderer renders it via a child instance whose shader map compiles in the + * background; SListView only realizes visible rows, so only those compile/render (scroll-to-compile). + * + * The catch: FSlateApplication::AddModalWindow's loop ticks Slate but NOT the editor engine, so the + * FAssetThumbnailPool (a FTickableEditorObject) and shader-compile finalization never advance on + * their own here — a stock live thumbnail would freeze on its first black frame. We therefore drive + * both from a Slate active timer (which DOES fire inside the modal loop): see PumpThumbnails. + */ + class SShaderLabBasePicker : public SCompoundWidget + { + public: + SLATE_BEGIN_ARGS(SShaderLabBasePicker) {} + SLATE_ARGUMENT(TSharedPtr, ParentWindow) + SLATE_END_ARGS() + + void Construct(const FArguments& InArgs) + { + ParentWindow = InArgs._ParentWindow; + check(ParentWindow.IsValid()); + + // Local pool: we tick it ourselves (the editor won't, inside the modal loop). + ThumbnailPool = MakeShared(/*InNumInPool*/ 32); + + BuildEntries(); + FilteredEntries = AllEntries; + + ChildSlot + [ + SNew(SVerticalBox) + + // Search box. + + SVerticalBox::Slot().AutoHeight().Padding(8.f, 8.f, 8.f, 4.f) + [ + SNew(SSearchBox) + .HintText(LOCTEXT("SearchHint", "Search base materials...")) + .OnTextChanged(this, &SShaderLabBasePicker::OnSearchTextChanged) + ] + + // Scrollable list of bases. + + SVerticalBox::Slot().FillHeight(1.f).Padding(8.f, 4.f) + [ + SNew(SBorder) + .BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder")) + .Padding(2.f) + [ + SAssignNew(ListView, SListView) + .ListItemsSource(&FilteredEntries) + .SelectionMode(ESelectionMode::Single) + .OnGenerateRow(this, &SShaderLabBasePicker::OnGenerateRow) + .OnMouseButtonDoubleClick(this, &SShaderLabBasePicker::OnItemDoubleClicked) + .OnSelectionChanged(this, &SShaderLabBasePicker::OnSelectionChanged) + ] + ] + + // Select / Cancel. + + SVerticalBox::Slot().AutoHeight().HAlign(HAlign_Right).Padding(8.f) + [ + SNew(SHorizontalBox) + + SHorizontalBox::Slot().AutoWidth().Padding(0, 0, 4, 0) + [ + SNew(SButton) + .Text(LOCTEXT("Select", "Select")) + .IsEnabled(this, &SShaderLabBasePicker::IsSelectEnabled) + .OnClicked(this, &SShaderLabBasePicker::OnSelectClicked) + ] + + SHorizontalBox::Slot().AutoWidth() + [ + SNew(SButton) + .Text(LOCTEXT("Cancel", "Cancel")) + .OnClicked(this, &SShaderLabBasePicker::OnCancelClicked) + ] + ] + ]; + + // Drive shader-compile finalization + thumbnail rendering every frame for the modal's life. + RegisterActiveTimer(0.f, FWidgetActiveTimerDelegate::CreateSP(this, &SShaderLabBasePicker::PumpThumbnails)); + } + + /** The chosen base material, or nullptr if the user cancelled / closed the window. */ + UMaterial* GetChosen() const { return Chosen; } + + private: + void BuildEntries() + { + FShaderLabMaterialRegistry& Registry = FShaderLabMaterialRegistry::Get(); + const TMap& Models = Registry.GetRegisteredModels(); + + // Sort by name for a stable, flat layout. + TArray Names; + Models.GenerateKeyArray(Names); + Names.Sort([](const FName& A, const FName& B) { return A.Compare(B) < 0; }); + + for (const FName& Name : Names) { - Bases.Add(Base); + UMaterial* Base = Registry.FindMaterial(Name.ToString()); + check(Base); // Registered models always have a created material (RegisterFromModel contract). + const FShaderLabModel& Model = Models.FindChecked(Name); + + FBaseEntryPtr Entry = MakeShared(); + Entry->Material = Base; + Entry->Name = Base->GetName(); + Entry->DomainBlend = FString::Printf(TEXT("%s / %s"), + *DomainToString(Model.Settings.Domain), *BlendToString(Model.Settings.BlendMode)); + AllEntries.Add(Entry); } } - if (Bases.IsEmpty()) + + /** Finalize background shader compiles and render any queued/realtime thumbnails (see class note). */ + EActiveTimerReturnType PumpThumbnails(double, float DeltaTime) { - return nullptr; + if (GShaderCompilingManager) + { + GShaderCompilingManager->ProcessAsyncResults(/*bLimitExecutionTime*/ true, /*bBlockOnGlobalShaderCompletion*/ false); + } + check(ThumbnailPool.IsValid()); + ThumbnailPool->Tick(DeltaTime); + return EActiveTimerReturnType::Continue; } - UMaterial* Chosen = nullptr; - TSharedRef Window = SNew(SWindow) - .Title(LOCTEXT("PickBaseTitle", "Choose ShaderLab base material")) - .ClientSize(FVector2D(560, 520)) - .SupportsMaximize(false) - .SupportsMinimize(false); - - TSharedRef ThumbnailPool = MakeShared(64); - TSharedRef Grid = SNew(SUniformWrapPanel).SlotPadding(FMargin(6.f)); - - for (UMaterial* Base : Bases) + TSharedRef OnGenerateRow(FBaseEntryPtr Entry, const TSharedRef& OwnerTable) { - // Hold the thumbnail alive for the window's lifetime by capturing it in the button lambda. - TSharedRef Thumbnail = MakeShared(FAssetData(Base), 96, 96, ThumbnailPool); - Grid->AddSlot() + check(Entry.IsValid()); + + // On-demand: SListView only generates visible rows, so a base's thumbnail (and thus its + // background shader compile) only starts once the user actually scrolls it into view. + if (!Entry->Thumbnail.IsValid()) + { + Entry->Thumbnail = MakeShared(FAssetData(Entry->Material), GThumbnailSize, GThumbnailSize, ThumbnailPool); + Entry->Thumbnail->SetRealTime(true); // keep refreshing past the first (still-compiling) frame + } + + return SNew(STableRow, OwnerTable) [ - SNew(SButton) - .HAlign(HAlign_Center) - .VAlign(VAlign_Center) - .ToolTipText(FText::FromString(Base->GetName())) - .OnClicked_Lambda([&Chosen, Base, Window, Thumbnail]() - { - Chosen = Base; - Window->RequestDestroyWindow(); - return FReply::Handled(); - }) - .Content() + SNew(SHorizontalBox) + + SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(4.f) + [ + SNew(SBox).WidthOverride(GThumbnailSize).HeightOverride(GThumbnailSize) + [ + Entry->Thumbnail->MakeThumbnailWidget() + ] + ] + + SHorizontalBox::Slot().FillWidth(1.f).VAlign(VAlign_Center).Padding(8.f, 0.f) [ SNew(SVerticalBox) - + SVerticalBox::Slot().AutoHeight().HAlign(HAlign_Center) + + SVerticalBox::Slot().AutoHeight() [ - SNew(SBox).WidthOverride(96.f).HeightOverride(96.f) - [ - Thumbnail->MakeThumbnailWidget() - ] + SNew(STextBlock) + .Text(FText::FromString(Entry->Name)) + .Font(FAppStyle::GetFontStyle("PropertyWindow.BoldFont")) ] - + SVerticalBox::Slot().AutoHeight().HAlign(HAlign_Center).Padding(0, 4, 0, 0) + + SVerticalBox::Slot().AutoHeight() [ - SNew(STextBlock).Text(FText::FromString(Base->GetName())) + SNew(STextBlock) + .Text(FText::FromString(Entry->DomainBlend)) + .ColorAndOpacity(FSlateColor::UseSubduedForeground()) ] ] ]; } - Window->SetContent( - SNew(SScrollBox) - + SScrollBox::Slot().Padding(8.f) - [ - Grid - ]); + void OnSearchTextChanged(const FText& Text) + { + const FString Filter = Text.ToString(); + FilteredEntries.Reset(); + for (const FBaseEntryPtr& Entry : AllEntries) + { + if (Filter.IsEmpty() || Entry->Name.Contains(Filter)) + { + FilteredEntries.Add(Entry); + } + } + check(ListView.IsValid()); + ListView->RequestListRefresh(); + } + + void OnSelectionChanged(FBaseEntryPtr Entry, ESelectInfo::Type) { Selected = Entry; } + + void OnItemDoubleClicked(FBaseEntryPtr Entry) + { + check(Entry.IsValid()); + Selected = Entry; + Confirm(); + } + + bool IsSelectEnabled() const { return Selected.IsValid(); } + + FReply OnSelectClicked() + { + check(Selected.IsValid()); // Button is disabled without a selection. + Confirm(); + return FReply::Handled(); + } + + FReply OnCancelClicked() + { + ParentWindow.Pin()->RequestDestroyWindow(); + return FReply::Handled(); + } + + void Confirm() + { + check(Selected.IsValid()); + Chosen = Selected->Material; + ParentWindow.Pin()->RequestDestroyWindow(); + } + + TWeakPtr ParentWindow; + TSharedPtr ThumbnailPool; + TSharedPtr> ListView; + TArray AllEntries; + TArray FilteredEntries; + FBaseEntryPtr Selected; + UMaterial* Chosen = nullptr; + }; + + /** Pop the modal base-material picker; returns the chosen base or nullptr (cancelled / none registered). */ + UMaterial* PickShaderLabBase() + { + // No registered bases -> nothing to pick (the factory aborts creation). + if (FShaderLabMaterialRegistry::Get().GetRegisteredModels().IsEmpty()) + { + return nullptr; + } + + TSharedRef Window = SNew(SWindow) + .Title(LOCTEXT("PickBaseTitle", "Choose ShaderLab base material")) + .ClientSize(FVector2D(480, 560)) + .SupportsMaximize(false) + .SupportsMinimize(false); + + // Hold the picker alive past window destruction so we can read the choice after the modal loop. + TSharedRef Picker = SNew(SShaderLabBasePicker).ParentWindow(Window); + Window->SetContent(Picker); GEditor->EditorAddModalWindow(Window); - return Chosen; + return Picker->GetChosen(); } } diff --git a/Source/UShaderLabEditor/UShaderLabEditor.Build.cs b/Source/UShaderLabEditor/UShaderLabEditor.Build.cs index 1a18727..f7c4457 100644 --- a/Source/UShaderLabEditor/UShaderLabEditor.Build.cs +++ b/Source/UShaderLabEditor/UShaderLabEditor.Build.cs @@ -27,6 +27,7 @@ public class UShaderLabEditor : ModuleRules "MaterialEditor", "Slate", "SlateCore", + "InputCore", "Projects", "DirectoryWatcher", "ContentBrowserData",