Editor plugin that detects geometrically-identical sibling StaticMeshes across a level, rebases each placement onto one canonical mesh with a corrected transform (W' = D * W, verified by exact vertex matching), and can collapse groups into HISM. Native Slate tool panel + BlueprintCallable UOptimizerSubsystem. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
212 lines
6.3 KiB
C++
212 lines
6.3 KiB
C++
// Copyright IHY.
|
|
#include "SOptimizerPanel.h"
|
|
|
|
#include "OptimizerSubsystem.h"
|
|
|
|
#include "Editor.h"
|
|
#include "Widgets/SBoxPanel.h"
|
|
#include "Widgets/Layout/SBorder.h"
|
|
#include "Widgets/Layout/SScrollBox.h"
|
|
#include "Widgets/Input/SButton.h"
|
|
#include "Widgets/Input/SCheckBox.h"
|
|
#include "Widgets/Text/STextBlock.h"
|
|
#include "Styling/AppStyle.h"
|
|
#include "Styling/CoreStyle.h"
|
|
|
|
#define LOCTEXT_NAMESPACE "OptimizerPanel"
|
|
|
|
void SOptimizerPanel::Construct(const FArguments& InArgs)
|
|
{
|
|
const FSlateFontInfo TitleFont = FCoreStyle::GetDefaultFontStyle("Bold", 15);
|
|
const FSlateFontInfo MonoFont = FCoreStyle::GetDefaultFontStyle("Mono", 9);
|
|
|
|
ChildSlot
|
|
[
|
|
SNew(SBorder)
|
|
.BorderImage(FAppStyle::GetBrush("ToolPanel.GroupBorder"))
|
|
.Padding(FMargin(14.f))
|
|
[
|
|
SNew(SVerticalBox)
|
|
|
|
// Title
|
|
+ SVerticalBox::Slot().AutoHeight().Padding(0, 0, 0, 2)
|
|
[
|
|
SNew(STextBlock)
|
|
.Text(LOCTEXT("Title", "Mesh Optimizer"))
|
|
.Font(TitleFont)
|
|
]
|
|
|
|
// Subtitle
|
|
+ SVerticalBox::Slot().AutoHeight().Padding(0, 0, 0, 12)
|
|
[
|
|
SNew(STextBlock)
|
|
.Text(LOCTEXT("Subtitle", "Find sibling StaticMeshes, unify onto one canonical mesh, then build HISM."))
|
|
.ColorAndOpacity(FSlateColor::UseSubduedForeground())
|
|
.AutoWrapText(true)
|
|
]
|
|
|
|
// Detection toggles
|
|
+ SVerticalBox::Slot().AutoHeight().Padding(0, 0, 0, 10)
|
|
[
|
|
SNew(SHorizontalBox)
|
|
+ SHorizontalBox::Slot().AutoWidth()[ MakeLabeledCheck(ChkMirror, LOCTEXT("Mirror", "Mirrored"), false) ]
|
|
+ SHorizontalBox::Slot().AutoWidth()[ MakeLabeledCheck(ChkScale, LOCTEXT("Scale", "Scaled"), false) ]
|
|
+ SHorizontalBox::Slot().AutoWidth()[ MakeLabeledCheck(ChkMergeMat, LOCTEXT("MergeMat", "Merge materials"), false) ]
|
|
]
|
|
|
|
// Action buttons
|
|
+ SVerticalBox::Slot().AutoHeight().Padding(0, 0, 0, 8)
|
|
[
|
|
SNew(SHorizontalBox)
|
|
+ SHorizontalBox::Slot().AutoWidth().Padding(0, 0, 8, 0)
|
|
[ MakeButton(LOCTEXT("Scan", "Scan"), FOnClicked::CreateSP(this, &SOptimizerPanel::OnScan)) ]
|
|
+ SHorizontalBox::Slot().AutoWidth().Padding(0, 0, 8, 0)
|
|
[ MakeButton(LOCTEXT("Unify", "Unify"), FOnClicked::CreateSP(this, &SOptimizerPanel::OnUnify)) ]
|
|
+ SHorizontalBox::Slot().AutoWidth()
|
|
[ MakeButton(LOCTEXT("HISM", "Build HISM"), FOnClicked::CreateSP(this, &SOptimizerPanel::OnBuildHISM)) ]
|
|
]
|
|
|
|
// Destroy-originals toggle
|
|
+ SVerticalBox::Slot().AutoHeight().Padding(0, 0, 0, 12)
|
|
[
|
|
MakeLabeledCheck(ChkDestroy, LOCTEXT("Destroy", "Destroy originals when building HISM"), true)
|
|
]
|
|
|
|
// Results area
|
|
+ SVerticalBox::Slot().FillHeight(1.f)
|
|
[
|
|
SNew(SBorder)
|
|
.BorderImage(FAppStyle::GetBrush("ToolPanel.DarkGroupBorder"))
|
|
.Padding(FMargin(8.f))
|
|
[
|
|
SNew(SScrollBox)
|
|
+ SScrollBox::Slot()
|
|
[
|
|
SAssignNew(ResultsBox, STextBlock)
|
|
.Text(LOCTEXT("Idle", "Press Scan to analyze the level."))
|
|
.Font(MonoFont)
|
|
.AutoWrapText(true)
|
|
]
|
|
]
|
|
]
|
|
]
|
|
];
|
|
}
|
|
|
|
TSharedRef<SWidget> SOptimizerPanel::MakeLabeledCheck(TSharedPtr<SCheckBox>& Out, const FText& Label, bool bDefault)
|
|
{
|
|
return SNew(SHorizontalBox)
|
|
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center)
|
|
[
|
|
SAssignNew(Out, SCheckBox)
|
|
.IsChecked(bDefault ? ECheckBoxState::Checked : ECheckBoxState::Unchecked)
|
|
]
|
|
+ SHorizontalBox::Slot().AutoWidth().VAlign(VAlign_Center).Padding(5, 0, 16, 0)
|
|
[
|
|
SNew(STextBlock).Text(Label)
|
|
];
|
|
}
|
|
|
|
TSharedRef<SWidget> SOptimizerPanel::MakeButton(const FText& Label, FOnClicked OnClicked)
|
|
{
|
|
return SNew(SButton)
|
|
.Text(Label)
|
|
.OnClicked(OnClicked)
|
|
.ContentPadding(FMargin(18.f, 6.f))
|
|
.HAlign(HAlign_Center);
|
|
}
|
|
|
|
bool SOptimizerPanel::IsChecked(const TSharedPtr<SCheckBox>& Chk) const
|
|
{
|
|
return Chk.IsValid() && Chk->IsChecked();
|
|
}
|
|
|
|
void SOptimizerPanel::SetReport(const FString& Text)
|
|
{
|
|
if (ResultsBox.IsValid())
|
|
{
|
|
ResultsBox->SetText(FText::FromString(Text));
|
|
}
|
|
}
|
|
|
|
FReply SOptimizerPanel::OnScan()
|
|
{
|
|
UOptimizerSubsystem* SS = GEditor ? GEditor->GetEditorSubsystem<UOptimizerSubsystem>() : nullptr;
|
|
if (!SS)
|
|
{
|
|
SetReport(TEXT("Subsystem unavailable."));
|
|
return FReply::Handled();
|
|
}
|
|
|
|
FOptimizerScanSettings Settings;
|
|
Settings.bAllowMirrored = IsChecked(ChkMirror);
|
|
Settings.bAllowUniformScale = IsChecked(ChkScale);
|
|
Settings.bMergeAcrossMaterials = IsChecked(ChkMergeMat);
|
|
|
|
const FOptimizerScanResult R = SS->ScanLevel(Settings);
|
|
|
|
FString Report = R.Summary + TEXT("\n");
|
|
Report += FString::Printf(TEXT("actors=%d components=%d unique meshes=%d\n\n"),
|
|
R.ActorsScanned, R.ComponentsScanned, R.UniqueMeshes);
|
|
|
|
if (R.Groups.Num() == 0)
|
|
{
|
|
Report += TEXT("No sibling groups found.");
|
|
}
|
|
for (const FOptimizerGroupView& G : R.Groups)
|
|
{
|
|
FString Flags;
|
|
if (G.bHasMirrored) { Flags += TEXT(" [mirror]"); }
|
|
if (G.bHasScaled) { Flags += TEXT(" [scale]"); }
|
|
if (G.bMaterialMismatch) { Flags += TEXT(" [mat!=]"); }
|
|
if (G.ShearRejectedCount > 0) { Flags += FString::Printf(TEXT(" [shear x%d]"), G.ShearRejectedCount); }
|
|
|
|
Report += FString::Printf(TEXT("- %s (members=%d, placements=%d, fix=%d, maxDev=%.4f cm)%s\n %s\n"),
|
|
*G.CanonicalMeshName, G.MemberMeshNames.Num(), G.InstanceCount, G.TransformFixCount,
|
|
G.MaxDeviation, *Flags, *FString::Join(G.MemberMeshNames, TEXT(", ")));
|
|
}
|
|
|
|
SetReport(Report);
|
|
return FReply::Handled();
|
|
}
|
|
|
|
FReply SOptimizerPanel::OnUnify()
|
|
{
|
|
UOptimizerSubsystem* SS = GEditor ? GEditor->GetEditorSubsystem<UOptimizerSubsystem>() : nullptr;
|
|
if (!SS)
|
|
{
|
|
SetReport(TEXT("Subsystem unavailable."));
|
|
return FReply::Handled();
|
|
}
|
|
if (!SS->HasPlan())
|
|
{
|
|
SetReport(TEXT("Run Scan first."));
|
|
return FReply::Handled();
|
|
}
|
|
const int32 N = SS->ApplyUnify();
|
|
SetReport(FString::Printf(TEXT("Unify done: %d placement(s) reassigned to canonical mesh + corrected transform.\nUndo with Ctrl+Z. Re-scan before Build HISM."), N));
|
|
return FReply::Handled();
|
|
}
|
|
|
|
FReply SOptimizerPanel::OnBuildHISM()
|
|
{
|
|
UOptimizerSubsystem* SS = GEditor ? GEditor->GetEditorSubsystem<UOptimizerSubsystem>() : nullptr;
|
|
if (!SS)
|
|
{
|
|
SetReport(TEXT("Subsystem unavailable."));
|
|
return FReply::Handled();
|
|
}
|
|
if (!SS->HasPlan())
|
|
{
|
|
SetReport(TEXT("Run Scan first."));
|
|
return FReply::Handled();
|
|
}
|
|
const bool bDestroy = IsChecked(ChkDestroy);
|
|
const int32 N = SS->BuildHISM(bDestroy);
|
|
SetReport(FString::Printf(TEXT("Build HISM done: %d HISM actor(s) created%s.\nUndo with Ctrl+Z."),
|
|
N, bDestroy ? TEXT(" (originals destroyed)") : TEXT("")));
|
|
return FReply::Handled();
|
|
}
|
|
|
|
#undef LOCTEXT_NAMESPACE
|