Mesh Optimizer: sibling StaticMesh duplicate remover (UE 5.7)

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>
This commit is contained in:
2026-07-01 18:26:45 +03:00
commit a95b299680
17 changed files with 2172 additions and 0 deletions

11
.gitignore vendored Normal file
View File

@ -0,0 +1,11 @@
# Unreal Engine build artifacts
Binaries/
Intermediate/
DerivedDataCache/
Saved/
# IDE / OS
.vs/
*.pdb
*.VC.db
.DS_Store

21
Optimizer.uplugin Normal file
View File

@ -0,0 +1,21 @@
{
"FileVersion": 3,
"Version": 1,
"VersionName": "0.1.0",
"FriendlyName": "Mesh Optimizer",
"Description": "Editor tool: detect geometrically-identical sibling StaticMeshes across level actors, reassign every user to one canonical mesh with a pivot/rotation-corrected transform, so the set can be converted to HISM.",
"Category": "Editor",
"CreatedBy": "IHY",
"EnabledByDefault": true,
"CanContainContent": false,
"IsBetaVersion": true,
"IsExperimentalVersion": false,
"Installed": false,
"Modules": [
{
"Name": "OptimizerEditor",
"Type": "Editor",
"LoadingPhase": "Default"
}
]
}

153
README.md Normal file
View File

@ -0,0 +1,153 @@
# Mesh Optimizer — Sibling Duplicate Remover
Editor-only Unreal Engine plugin that finds **geometrically identical StaticMesh assets** used across a
level, rebases every placement onto a single **canonical** mesh with a corrected transform, and
optionally collapses the group into a **Hierarchical Instanced Static Mesh (HISM)**.
> UE 5.7 · Editor module `OptimizerEditor` · native Slate tool panel.
---
## The problem it solves
Bought kits and **Kitbash3D**-style packs import the *same* geometry as many separate assets:
`SM_Fence_01`, `SM_Fence_02`, … are literal twins — identical vertices, sometimes with a different
**baked pivot or rotation**. That defeats instancing: the engine sees N different meshes, so it can't
batch them, and you can't make one HISM out of them.
Mesh Optimizer detects those "sibling" meshes, picks one to keep, and re-points every actor that used a
sibling to the survivor — **fixing each actor's transform** so nothing moves on screen — leaving a set
that is ready to become a single HISM.
---
## What it does
1. **Scan** (dry run — mutates nothing): walk the level's StaticMesh placements, fingerprint every
unique mesh, group the geometric siblings, and report each group with a confidence figure
(`maxDeviation`).
2. **Unify**: reassign every sibling placement to its group's canonical mesh and apply the corrected
world transform. Undoable (`Ctrl+Z`).
3. **Build HISM**: create one `HierarchicalInstancedStaticMeshComponent` per group from the corrected
transforms, optionally destroying the originals. Undoable.
Every mutation runs inside a single editor transaction.
---
## How it works
```
extract geometry (LOD0 FMeshDescription)
→ fingerprint (welded vertex/tri counts, surface area, |volume|,
covariance eigenvalues, radial histogram — all rotation-invariant)
→ bucket by (welded vertex count, triangle count)
→ recover the rigid delta between candidates
(identity fast-path, then the 24/48 signed-permutation axis rotations)
→ verify by order-independent nearest-neighbour vertex match (spatial hash)
→ union-find into groups, pick a canonical
→ plan the corrected transform per placement
```
**The corrective transform.** With `D` = the recovered *canonical-local → sibling-local* delta and
`W` = the sibling placement's current world transform, the canonical mesh must be placed at
```
W' = D * W (UE FTransform multiply order; matrix W'_mat = D_mat * W_mat)
```
built via `FMatrix` and reconstructed into an `FTransform`. Because a non-uniform actor scale combined
with a rotational `D` can produce **shear** (which `FTransform` cannot represent), each result is
checked at the bounding-box corners and any placement that would shear is **skipped and reported**
rather than silently corrupted.
Every candidate match is confirmed by an **exact vertex-set verification** before it is ever applied, so
a bad transform is rejected, not shipped.
---
## Installation
1. Copy this folder into your project's `Plugins/` directory (so it lives at `Plugins/Optimizer/`).
2. Add it to your `.uproject` (editor-only):
```json
{ "Name": "Optimizer", "Enabled": true, "TargetAllowList": [ "Editor" ] }
```
3. Regenerate project files and build the editor target (a new module needs a full build — Live Coding
cannot register a brand-new module).
Requires **Unreal Engine 5.7** and an editor build (`<Project>Editor Win64 Development`).
---
## Usage
Open the panel from **Tools → Optimizer → Mesh Optimizer**.
| Control | Effect |
|---|---|
| **Scan** | Dry-run analysis of the current level; fills the report. Changes nothing. |
| **Unify** | Reassign sibling placements to the canonical mesh + corrected transform. |
| **Build HISM** | Collapse each group into one HISM actor. |
| ☐ Mirrored | Also treat negative-scale (mirrored) variants as siblings. |
| ☐ Scaled | Also treat uniformly-scaled copies as siblings. |
| ☐ Merge materials | Group siblings even if their material sets differ (the canonical's materials win). |
| ☐ Destroy originals | When building HISM, delete the original actors/components. |
Recommended flow: **Scan → review the report** (check `maxDev` and the flags) **→ Unify** (or **Build
HISM**). Everything is undoable with `Ctrl+Z`.
---
## Detection modes
- **Exact + rigid (default):** identical geometry up to a rigid transform (rotation and/or a baked
pivot offset). Covers almost all Kitbash cases.
- **+ Mirrored:** negative-scale reflections (kept as a separate canonical group, since HISM winding
differs).
- **+ Scaled:** uniform-scale copies (the instance carries the scale).
Advanced tolerances (weld epsilon, accept tolerance, scalar relative tolerance, degenerate-eigenvalue
epsilon) are exposed on the scan settings for tuning against specific content.
---
## Safety & limitations
- **World Partition:** only *loaded* cells are visible to the scan — coverage is partial and the report
says so.
- **Shear:** non-uniform actor scale combined with a rotational delta is unrepresentable by
`FTransform`; those placements are skipped and counted, never corrupted.
- **ISM/HISM sources:** existing instanced components are out of scope in this version (only plain
`StaticMeshActor`s / components are processed).
- **Materials:** by default siblings with different material sets are kept in separate groups so each
HISM keeps one material set.
---
## Architecture
```
Source/OptimizerEditor/
OptimizerEditor.Build.cs
Public/
OptimizerEditorModule.h module + Tools menu / nomad tab
OptimizerSubsystem.h UEditorSubsystem: ScanLevel / ApplyUnify / BuildHISM
OptimizerTypes.h settings + result structs/enums
Private/
OptimizerEditorModule.cpp
OptimizerGeometry.{h,cpp} geometry extraction + fingerprint + 3x3 Jacobi eigensolver
OptimizerMatcher.{h,cpp} bucketing, transform recovery, spatial-hash verification
OptimizerReconciler.{h,cpp} W' = D * W + shear detection
OptimizerSubsystem.cpp enumeration, planning, transacted mutation, HISM build
SOptimizerPanel.{h,cpp} native Slate tool panel
```
The heavy logic is UI-agnostic C++ in `UOptimizerSubsystem` (`BlueprintCallable`); the Slate panel is a
thin driver.
---
## License
Internal tool. © IHY / ExbyteLabs.

View File

@ -0,0 +1,39 @@
// Copyright IHY.
using UnrealBuildTool;
public class OptimizerEditor : ModuleRules
{
public OptimizerEditor(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[]
{
"Core",
"CoreUObject",
"Engine", // UStaticMesh, UStaticMeshComponent, AActor, FTransform
});
PrivateDependencyModuleNames.AddRange(new string[]
{
// --- editor core ---
"UnrealEd", // GEditor, FScopedTransaction, UEditorActorSubsystem, editor world context
"EditorSubsystem", // UEditorSubsystem (base of UOptimizerSubsystem)
"Slate", // notifications / future Slate bits
"SlateCore",
"InputCore",
"ToolMenus", // UToolMenus — Tools menu entry
"Projects", // IPluginManager
"UMG", // UWidgetBlueprint chain pulled in by the EUW headers
"UMGEditor", // WidgetBlueprint.h include path (transitive via EditorUtilityWidgetBlueprint.h)
"Blutility", // UEditorUtilitySubsystem / UEditorUtilityWidgetBlueprint (open the EUW panel)
// --- mesh geometry & asset discovery (load-bearing for fingerprinting) ---
"MeshDescription", // FMeshDescription — LOD0 source vertex/poly data
"StaticMeshDescription", // FStaticMeshAttributes, UStaticMesh::GetMeshDescription
"AssetRegistry", // enumerate UStaticMesh assets when needed
"AssetTools", // asset metadata helpers
});
}
}

View File

@ -0,0 +1,79 @@
// Copyright IHY.
#include "OptimizerEditorModule.h"
#include "Modules/ModuleManager.h"
#include "ToolMenus.h"
#include "ToolMenuSection.h"
#include "Framework/Commands/UIAction.h"
#include "Framework/Docking/TabManager.h"
#include "Framework/Application/SlateApplication.h"
#include "Widgets/Docking/SDockTab.h"
#include "Styling/AppStyle.h"
#include "SOptimizerPanel.h"
#define LOCTEXT_NAMESPACE "OptimizerEditor"
namespace { const FName OptimizerTabId(TEXT("MeshOptimizer")); }
IMPLEMENT_MODULE(FOptimizerEditorModule, OptimizerEditor)
void FOptimizerEditorModule::StartupModule()
{
UToolMenus::RegisterStartupCallback(
FSimpleMulticastDelegate::FDelegate::CreateRaw(this, &FOptimizerEditorModule::RegisterMenus));
}
void FOptimizerEditorModule::ShutdownModule()
{
if (bTabRegistered && FSlateApplication::IsInitialized())
{
FGlobalTabmanager::Get()->UnregisterNomadTabSpawner(OptimizerTabId);
bTabRegistered = false;
}
if (UToolMenus::IsToolMenuUIEnabled())
{
UToolMenus::UnRegisterStartupCallback(this);
UToolMenus::UnregisterOwner(this);
}
}
void FOptimizerEditorModule::RegisterMenus()
{
FToolMenuOwnerScoped OwnerScoped(this);
if (!bTabRegistered && FSlateApplication::IsInitialized())
{
FGlobalTabmanager::Get()->RegisterNomadTabSpawner(
OptimizerTabId,
FOnSpawnTab::CreateRaw(this, &FOptimizerEditorModule::SpawnPanelTab))
.SetDisplayName(LOCTEXT("OptimizerTabTitle", "Mesh Optimizer"))
.SetTooltipText(LOCTEXT("OptimizerTabTip", "Find sibling meshes and rebase them onto one canonical mesh (HISM-ready)."))
.SetMenuType(ETabSpawnerMenuType::Hidden);
bTabRegistered = true;
}
if (UToolMenu* ToolsMenu = UToolMenus::Get()->ExtendMenu("LevelEditor.MainMenu.Tools"))
{
FToolMenuSection& Section = ToolsMenu->FindOrAddSection("Optimizer", LOCTEXT("OptimizerSection", "Optimizer"));
Section.AddMenuEntry(
"OpenMeshOptimizer",
LOCTEXT("OpenOptimizer", "Mesh Optimizer"),
LOCTEXT("OpenOptimizerTip", "Find sibling meshes and rebase them onto one canonical mesh (HISM-ready)."),
FSlateIcon(FAppStyle::GetAppStyleSetName(), "Icons.Settings"),
FUIAction(FExecuteAction::CreateLambda([]()
{
FGlobalTabmanager::Get()->TryInvokeTab(OptimizerTabId);
})));
}
}
TSharedRef<SDockTab> FOptimizerEditorModule::SpawnPanelTab(const FSpawnTabArgs& Args)
{
return SNew(SDockTab)
.TabRole(ETabRole::NomadTab)
[
SNew(SOptimizerPanel)
];
}
#undef LOCTEXT_NAMESPACE

View File

@ -0,0 +1,292 @@
// Copyright IHY.
#include "OptimizerGeometry.h"
#include "Engine/StaticMesh.h"
#include "MeshDescription.h"
#include "StaticMeshAttributes.h"
#include "StaticMeshResources.h"
#include "Rendering/PositionVertexBuffer.h"
#include "Materials/MaterialInterface.h"
namespace
{
// Read LOD0 source geometry into compact arrays. Returns false if no source model.
bool ReadFromMeshDescription(const UStaticMesh* Mesh, TArray<FVector>& OutRaw, TArray<FIntVector>& OutTris)
{
if (!Mesh->IsMeshDescriptionValid(0))
{
return false;
}
const FMeshDescription* MD = Mesh->GetMeshDescription(0);
if (!MD || MD->Vertices().Num() == 0)
{
return false;
}
FStaticMeshConstAttributes Attributes(*MD);
TVertexAttributesConstRef<FVector3f> Positions = Attributes.GetVertexPositions();
OutRaw.Reset(MD->Vertices().Num());
TMap<FVertexID, int32> Compact;
Compact.Reserve(MD->Vertices().Num());
for (const FVertexID VertexID : MD->Vertices().GetElementIDs())
{
Compact.Add(VertexID, OutRaw.Num());
OutRaw.Add(FVector(Positions[VertexID]));
}
OutTris.Reset(MD->Triangles().Num());
for (const FTriangleID TriangleID : MD->Triangles().GetElementIDs())
{
TArrayView<const FVertexID> Tri = MD->GetTriangleVertices(TriangleID);
if (Tri.Num() == 3)
{
OutTris.Add(FIntVector(Compact[Tri[0]], Compact[Tri[1]], Compact[Tri[2]]));
}
}
return OutRaw.Num() > 0;
}
// Fallback: built render-data LOD0 (welded/optimized). Tag the result as render-derived.
bool ReadFromRenderData(const UStaticMesh* Mesh, TArray<FVector>& OutRaw, TArray<FIntVector>& OutTris)
{
const FStaticMeshRenderData* RD = Mesh->GetRenderData();
if (!RD || RD->LODResources.Num() == 0)
{
return false;
}
const FStaticMeshLODResources& LOD = RD->LODResources[0];
const FPositionVertexBuffer& Pos = LOD.VertexBuffers.PositionVertexBuffer;
const uint32 NumVerts = Pos.GetNumVertices();
if (NumVerts == 0)
{
return false;
}
OutRaw.Reset(NumVerts);
for (uint32 i = 0; i < NumVerts; ++i)
{
OutRaw.Add(FVector(Pos.VertexPosition(i)));
}
TArray<uint32> Indices;
LOD.IndexBuffer.GetCopy(Indices);
OutTris.Reset(Indices.Num() / 3);
for (int32 i = 0; i + 2 < Indices.Num(); i += 3)
{
OutTris.Add(FIntVector((int32)Indices[i], (int32)Indices[i + 1], (int32)Indices[i + 2]));
}
return true;
}
}
namespace OptimizerGeometry
{
void SymmetricEigen3x3(const double In[3][3], double OutValues[3], double OutVecs[3][3])
{
double a[3][3];
double v[3][3] = { {1,0,0}, {0,1,0}, {0,0,1} };
for (int32 i = 0; i < 3; ++i)
{
for (int32 j = 0; j < 3; ++j)
{
a[i][j] = In[i][j];
}
}
// Cyclic Jacobi rotations on the three off-diagonal entries.
for (int32 Sweep = 0; Sweep < 64; ++Sweep)
{
const double Off = FMath::Abs(a[0][1]) + FMath::Abs(a[0][2]) + FMath::Abs(a[1][2]);
if (Off < 1e-18)
{
break;
}
static const int32 P[3] = { 0, 0, 1 };
static const int32 Q[3] = { 1, 2, 2 };
for (int32 k = 0; k < 3; ++k)
{
const int32 p = P[k];
const int32 q = Q[k];
if (FMath::Abs(a[p][q]) < 1e-300)
{
continue;
}
const double Theta = (a[q][q] - a[p][p]) / (2.0 * a[p][q]);
double t = (Theta >= 0.0 ? 1.0 : -1.0) / (FMath::Abs(Theta) + FMath::Sqrt(Theta * Theta + 1.0));
const double c = 1.0 / FMath::Sqrt(t * t + 1.0);
const double s = t * c;
// Rotate a: a = Jᵀ a J
const double app = a[p][p];
const double aqq = a[q][q];
const double apq = a[p][q];
a[p][p] = c * c * app - 2.0 * s * c * apq + s * s * aqq;
a[q][q] = s * s * app + 2.0 * s * c * apq + c * c * aqq;
a[p][q] = 0.0;
a[q][p] = 0.0;
const int32 r = 3 - p - q; // the third index
const double arp = a[r][p];
const double arq = a[r][q];
a[r][p] = c * arp - s * arq;
a[p][r] = a[r][p];
a[r][q] = s * arp + c * arq;
a[q][r] = a[r][q];
// Accumulate eigenvectors: v = v J
for (int32 i = 0; i < 3; ++i)
{
const double vip = v[i][p];
const double viq = v[i][q];
v[i][p] = c * vip - s * viq;
v[i][q] = s * vip + c * viq;
}
}
}
int32 Order[3] = { 0, 1, 2 };
const double Diag[3] = { a[0][0], a[1][1], a[2][2] };
// Sort indices by eigenvalue DESC.
if (Diag[Order[0]] < Diag[Order[1]]) { Swap(Order[0], Order[1]); }
if (Diag[Order[0]] < Diag[Order[2]]) { Swap(Order[0], Order[2]); }
if (Diag[Order[1]] < Diag[Order[2]]) { Swap(Order[1], Order[2]); }
for (int32 i = 0; i < 3; ++i)
{
OutValues[i] = Diag[Order[i]];
OutVecs[0][i] = v[0][Order[i]];
OutVecs[1][i] = v[1][Order[i]];
OutVecs[2][i] = v[2][Order[i]];
}
}
bool ExtractGeom(const UStaticMesh* Mesh, float WeldEps, bool bWantPositions, FOptMeshGeom& Out)
{
Out = FOptMeshGeom();
if (!Mesh)
{
return false;
}
TArray<FVector> Raw;
TArray<FIntVector> Tris;
if (ReadFromMeshDescription(Mesh, Raw, Tris))
{
Out.bRenderDerived = false;
}
else if (ReadFromRenderData(Mesh, Raw, Tris))
{
Out.bRenderDerived = true;
}
else
{
return false;
}
Out.RawVertexCount = Raw.Num();
Out.TriangleCount = Tris.Num();
// Surface area + signed volume from the raw triangle soup (welding doesn't change these).
double Area = 0.0;
double Vol6 = 0.0;
for (const FIntVector& T : Tris)
{
const FVector& A = Raw[T.X];
const FVector& B = Raw[T.Y];
const FVector& C = Raw[T.Z];
Area += 0.5 * FVector::CrossProduct(B - A, C - A).Size();
Vol6 += FVector::DotProduct(A, FVector::CrossProduct(B, C));
}
Out.SurfaceArea = Area;
Out.Volume = FMath::Abs(Vol6) / 6.0;
// Weld positions onto a grid so seam-split duplicates collapse to one unique position.
const double InvEps = (WeldEps > UE_KINDA_SMALL_NUMBER) ? (1.0 / (double)WeldEps) : 1.0;
TMap<FIntVector, int32> Grid;
Grid.Reserve(Raw.Num());
TArray<FVector> Welded;
Welded.Reserve(Raw.Num());
for (const FVector& P : Raw)
{
const FIntVector Key(
FMath::RoundToInt(P.X * InvEps),
FMath::RoundToInt(P.Y * InvEps),
FMath::RoundToInt(P.Z * InvEps));
if (!Grid.Contains(Key))
{
Grid.Add(Key, Welded.Num());
Welded.Add(P);
}
}
Out.WeldedVertexCount = Welded.Num();
if (Welded.Num() == 0)
{
return false;
}
// Centroid (mean of welded positions) + bounds.
FVector Sum(0.0);
FBox Box(ForceInit);
for (const FVector& P : Welded)
{
Sum += P;
Box += P;
}
Out.Centroid = Sum / (double)Welded.Num();
Out.LocalBounds = Box;
// Covariance of centered welded cloud -> eigenvalues (rotation invariant).
double Cov[3][3] = { {0,0,0}, {0,0,0}, {0,0,0} };
double MaxR = 0.0;
for (const FVector& P : Welded)
{
const FVector d = P - Out.Centroid;
Cov[0][0] += d.X * d.X; Cov[0][1] += d.X * d.Y; Cov[0][2] += d.X * d.Z;
Cov[1][1] += d.Y * d.Y; Cov[1][2] += d.Y * d.Z;
Cov[2][2] += d.Z * d.Z;
MaxR = FMath::Max(MaxR, d.Size());
}
const double InvN = 1.0 / (double)Welded.Num();
Cov[0][0] *= InvN; Cov[0][1] *= InvN; Cov[0][2] *= InvN;
Cov[1][1] *= InvN; Cov[1][2] *= InvN; Cov[2][2] *= InvN;
Cov[1][0] = Cov[0][1]; Cov[2][0] = Cov[0][2]; Cov[2][1] = Cov[1][2];
double EVecs[3][3];
SymmetricEigen3x3(Cov, Out.EigenValues, EVecs);
// Radial histogram of |v - centroid|, normalized by the max radius then to sum 1.
if (MaxR > UE_KINDA_SMALL_NUMBER)
{
const double InvMax = (double)FOptMeshGeom::NumRadialBins / MaxR;
for (const FVector& P : Welded)
{
const double r = (P - Out.Centroid).Size();
int32 Bin = (int32)FMath::FloorToDouble(r * InvMax);
Bin = FMath::Clamp(Bin, 0, FOptMeshGeom::NumRadialBins - 1);
Out.RadialHistogram[Bin] += 1.0;
}
for (int32 i = 0; i < FOptMeshGeom::NumRadialBins; ++i)
{
Out.RadialHistogram[i] *= InvN;
}
}
// Material / section signature (kept separate from geometry grouping).
uint32 H = 0;
const TArray<FStaticMaterial>& Mats = Mesh->GetStaticMaterials();
for (const FStaticMaterial& M : Mats)
{
const FString N = M.MaterialInterface ? M.MaterialInterface->GetPathName() : TEXT("None");
H = HashCombine(H, GetTypeHash(N));
}
Out.MaterialHash = H;
Out.SectionCount = Mats.Num();
if (bWantPositions)
{
Out.Positions = MoveTemp(Welded);
}
Out.bValid = true;
return true;
}
}

View File

@ -0,0 +1,53 @@
// Copyright IHY.
#pragma once
#include "CoreMinimal.h"
class UStaticMesh;
/**
* Rotation/translation-invariant geometry fingerprint of a single UStaticMesh, plus the
* (lazily loaded) welded vertex cloud used for exact pairwise verification.
*
* Source is always LOD0 FMeshDescription (the imported authoring geometry) so the fingerprint
* is stable across LOD reduction / Nanite / build settings. Render data is a tagged fallback.
*/
struct FOptMeshGeom
{
static constexpr int32 NumRadialBins = 16;
bool bValid = false;
bool bRenderDerived = false; // true if extracted from render data (compare only against other render-derived)
int32 RawVertexCount = 0;
int32 WeldedVertexCount = 0; // unique positions after welding -> the hard fingerprint gate
int32 TriangleCount = 0;
double SurfaceArea = 0.0; // local space, rigid-invariant, scales as s^2
double Volume = 0.0; // |signed tetrahedron sum|, scales as s^3
FVector Centroid = FVector::ZeroVector; // mean of welded positions (local origin for cov/hist)
double EigenValues[3] = { 0.0, 0.0, 0.0 }; // covariance eigenvalues, sorted DESC (rotation-invariant)
double RadialHistogram[NumRadialBins] = { 0.0 }; // normalized hist of |v - centroid|
FBox LocalBounds = FBox(ForceInit);
// Heavy payload, populated only when this mesh enters pairwise recovery (bWantPositions).
TArray<FVector> Positions; // welded unique positions, local space
// Material / section signature (NOT part of geometry grouping; used to partition for HISM).
uint32 MaterialHash = 0;
int32 SectionCount = 0;
};
namespace OptimizerGeometry
{
/**
* Extract the fingerprint (and optionally the welded position cloud) for a static mesh.
* @param WeldEps position weld grid in cm.
* @param bWantPositions also fill Out.Positions (the expensive part) for verification.
* @return false if no usable geometry could be read.
*/
bool ExtractGeom(const UStaticMesh* Mesh, float WeldEps, bool bWantPositions, FOptMeshGeom& Out);
/** Symmetric 3x3 Jacobi eigensolver. Eigenvalues returned sorted DESC; eigenvectors as columns of OutVecs. */
void SymmetricEigen3x3(const double M[3][3], double OutValues[3], double OutVecs[3][3]);
}

View File

@ -0,0 +1,347 @@
// Copyright IHY.
#include "OptimizerMatcher.h"
namespace
{
// --- small helpers ---------------------------------------------------------
bool RelClose(double A, double B, double RelTol)
{
const double Scale = FMath::Max3(FMath::Abs(A), FMath::Abs(B), 1.0);
return FMath::Abs(A - B) <= RelTol * Scale;
}
struct FRot3
{
double M[3][3] = { {1,0,0},{0,1,0},{0,0,1} };
bool bImproper = false;
};
FVector ApplyRot(const double R[3][3], const FVector& V)
{
return FVector(
R[0][0] * V.X + R[0][1] * V.Y + R[0][2] * V.Z,
R[1][0] * V.X + R[1][1] * V.Y + R[1][2] * V.Z,
R[2][0] * V.X + R[2][1] * V.Y + R[2][2] * V.Z);
}
// All 48 signed permutation rotations split into proper (det+1) and improper (det-1).
const TArray<FRot3>& AxisRotations()
{
static TArray<FRot3> Cached;
if (Cached.Num() == 0)
{
static const int32 Perm[6][3] = { {0,1,2},{0,2,1},{1,0,2},{1,2,0},{2,0,1},{2,1,0} };
static const int32 Parity[6] = { 1, -1, -1, 1, 1, -1 };
for (int32 p = 0; p < 6; ++p)
{
for (int32 sgn = 0; sgn < 8; ++sgn)
{
const int32 e0 = (sgn & 1) ? -1 : 1;
const int32 e1 = (sgn & 2) ? -1 : 1;
const int32 e2 = (sgn & 4) ? -1 : 1;
const int32 Det = Parity[p] * e0 * e1 * e2;
FRot3 R;
FMemory::Memzero(R.M, sizeof(R.M));
R.M[0][Perm[p][0]] = e0;
R.M[1][Perm[p][1]] = e1;
R.M[2][Perm[p][2]] = e2;
R.bImproper = (Det < 0);
Cached.Add(R);
}
}
}
return Cached;
}
// Uniform spatial hash over a point cloud for order-independent nearest-neighbour queries.
struct FPointGrid
{
double InvCell = 1.0;
const TArray<FVector>* Pts = nullptr;
TMap<FIntVector, TArray<int32>> Cells;
void Build(const TArray<FVector>& InPts, double Cell)
{
Pts = &InPts;
InvCell = 1.0 / FMath::Max(Cell, 1e-6);
Cells.Reserve(InPts.Num());
for (int32 i = 0; i < InPts.Num(); ++i)
{
Cells.FindOrAdd(KeyOf(InPts[i])).Add(i);
}
}
FIntVector KeyOf(const FVector& P) const
{
return FIntVector(
FMath::FloorToInt(P.X * InvCell),
FMath::FloorToInt(P.Y * InvCell),
FMath::FloorToInt(P.Z * InvCell));
}
// Nearest point within MaxDist. Returns index or INDEX_NONE; OutDist set when found.
int32 Nearest(const FVector& Q, double MaxDist, double& OutDist) const
{
const FIntVector Base = KeyOf(Q);
double Best = MaxDist * MaxDist;
int32 BestIdx = INDEX_NONE;
// Search enough neighbour cells to cover MaxDist (cell size = 1/InvCell), not just +/-1,
// so reported deviations for over-tolerance vertices aren't truncated.
const int32 Rad = FMath::Max(1, FMath::CeilToInt(MaxDist * InvCell));
for (int32 dz = -Rad; dz <= Rad; ++dz)
for (int32 dy = -Rad; dy <= Rad; ++dy)
for (int32 dx = -Rad; dx <= Rad; ++dx)
{
const TArray<int32>* Bucket = Cells.Find(Base + FIntVector(dx, dy, dz));
if (!Bucket)
{
continue;
}
for (int32 Idx : *Bucket)
{
const double D2 = FVector::DistSquared((*Pts)[Idx], Q);
if (D2 < Best)
{
Best = D2;
BestIdx = Idx;
}
}
}
if (BestIdx != INDEX_NONE)
{
OutDist = FMath::Sqrt(Best);
}
return BestIdx;
}
};
// Transform canonical point p -> member space via q = s*R*(p - cA) + cB.
FVector Map(const double R[3][3], double s, const FVector& cA, const FVector& cB, const FVector& P)
{
return s * ApplyRot(R, P - cA) + cB;
}
// Cheap score over a subsample: sum of NN distances, with a fixed penalty for misses (lower is better).
double ScoreCandidate(const TArray<FVector>& A, const FPointGrid& BGrid,
const double R[3][3], double s, const FVector& cA, const FVector& cB,
double Tol, int32 Stride)
{
const double MissPenalty = Tol * 1000.0;
double Sum = 0.0;
for (int32 i = 0; i < A.Num(); i += Stride)
{
const FVector Q = Map(R, s, cA, cB, A[i]);
double Dist;
if (BGrid.Nearest(Q, Tol, Dist) != INDEX_NONE)
{
Sum += Dist;
}
else
{
Sum += MissPenalty;
}
}
return Sum;
}
// Full verification of a winning candidate: per-vertex max/rms deviation + reverse coverage.
void VerifyCandidate(const TArray<FVector>& A, const TArray<FVector>& B, const FPointGrid& BGrid,
const double R[3][3], double s, const FVector& cA, const FVector& cB,
double Tol, double& OutMaxDev, double& OutRms, bool& OutCoverageOK)
{
double MaxDev = 0.0;
double SumSq = 0.0;
int32 Count = 0;
TBitArray<> Hit(false, B.Num());
bool bAllWithin = true;
for (const FVector& P : A)
{
const FVector Q = Map(R, s, cA, cB, P);
double Dist;
const int32 Idx = BGrid.Nearest(Q, Tol * 4.0, Dist); // widen search so over-tol matches still report
if (Idx == INDEX_NONE || Dist > Tol)
{
bAllWithin = false;
MaxDev = FMath::Max(MaxDev, Idx == INDEX_NONE ? Tol * 4.0 : Dist);
}
else
{
Hit[Idx] = true;
MaxDev = FMath::Max(MaxDev, Dist);
}
SumSq += (Idx == INDEX_NONE) ? (Tol * 4.0) * (Tol * 4.0) : Dist * Dist;
++Count;
}
OutMaxDev = MaxDev;
OutRms = (Count > 0) ? FMath::Sqrt(SumSq / Count) : 0.0;
OutCoverageOK = bAllWithin && (Hit.CountSetBits() >= B.Num());
}
}
namespace OptimizerMatcher
{
bool FingerprintCompatible(const FOptMeshGeom& A, const FOptMeshGeom& B, const FOptimizerScanSettings& S)
{
if (!A.bValid || !B.bValid)
{
return false;
}
if (A.bRenderDerived != B.bRenderDerived)
{
return false; // never cross-compare source vs render-derived geometry
}
// Hard gates: welded vertex + triangle counts are exactly rotation/mirror invariant.
if (A.WeldedVertexCount != B.WeldedVertexCount || A.TriangleCount != B.TriangleCount)
{
return false;
}
if (!S.bMergeAcrossMaterials && A.MaterialHash != B.MaterialHash)
{
return false;
}
const double RelTol = FMath::Max((double)S.ScalarRelTolerance, 1e-6);
if (S.bAllowUniformScale)
{
// Require area/volume/eigenvalue ratios to agree on a single scale factor s.
if (A.SurfaceArea <= 0 || B.SurfaceArea <= 0)
{
return false;
}
const double sArea = FMath::Sqrt(B.SurfaceArea / A.SurfaceArea);
const double sVol = (A.Volume > 0 && B.Volume > 0) ? FMath::Pow(B.Volume / A.Volume, 1.0 / 3.0) : sArea;
const double sEig = (A.EigenValues[0] > 0 && B.EigenValues[0] > 0) ? FMath::Sqrt(B.EigenValues[0] / A.EigenValues[0]) : sArea;
if (!RelClose(sArea, sVol, 0.02) || !RelClose(sArea, sEig, 0.02))
{
return false;
}
}
else
{
if (!RelClose(A.SurfaceArea, B.SurfaceArea, RelTol))
{
return false;
}
if (A.Volume > 0 && B.Volume > 0 && !RelClose(A.Volume, B.Volume, RelTol))
{
return false;
}
for (int32 i = 0; i < 3; ++i)
{
if (!RelClose(A.EigenValues[i], B.EigenValues[i], FMath::Max(RelTol, 1e-3)))
{
return false;
}
}
}
// Radial histograms are normalized per-mesh (scale tolerant); compare by L1.
double L1 = 0.0;
for (int32 i = 0; i < FOptMeshGeom::NumRadialBins; ++i)
{
L1 += FMath::Abs(A.RadialHistogram[i] - B.RadialHistogram[i]);
}
return L1 <= 0.02;
}
bool RecoverDelta(const FOptMeshGeom& A, const FOptMeshGeom& B, const FOptimizerScanSettings& S, FOptDelta& Out)
{
Out = FOptDelta();
if (A.Positions.Num() == 0 || B.Positions.Num() == 0)
{
return false;
}
const double Tol = FMath::Max((double)S.AcceptTolerance, 1e-5);
FPointGrid BGrid;
BGrid.Build(B.Positions, Tol);
// Scoring stride keeps the 24/48 candidate scan cheap on dense meshes.
const int32 Stride = FMath::Max(1, A.Positions.Num() / 1500);
auto BuildMatrix = [&](const double R[3][3], double s, const FVector& cA, const FVector& cB) -> FMatrix
{
// Linear part L = s*R (column form); UE row-vector matrix is L^T with translation row tt.
const FVector tt = cB - s * ApplyRot(R, cA);
FMatrix Mtx = FMatrix::Identity;
for (int32 i = 0; i < 3; ++i)
{
for (int32 j = 0; j < 3; ++j)
{
Mtx.M[i][j] = s * R[j][i];
}
}
Mtx.M[3][0] = tt.X; Mtx.M[3][1] = tt.Y; Mtx.M[3][2] = tt.Z;
return Mtx;
};
// --- Exact-only mode: just test true identity (same orientation AND pivot) ---
if (S.Precision == EOptimizerMatchPrecision::ExactOnly)
{
static const double I3[3][3] = { {1,0,0},{0,1,0},{0,0,1} };
double MaxDev, Rms; bool Cov;
VerifyCandidate(A.Positions, B.Positions, BGrid, I3, 1.0, FVector::ZeroVector, FVector::ZeroVector, Tol, MaxDev, Rms, Cov);
if (Cov)
{
Out.bValid = true;
Out.bIdentity = true;
Out.MaxDev = MaxDev;
Out.Rms = Rms;
Out.CanonToMember = FMatrix::Identity;
return true;
}
return false;
}
// --- Rigid mode: identity-with-pivot fast path, then brute-force axis rotations ---
const double sEst = (S.bAllowUniformScale && A.SurfaceArea > 0)
? FMath::Sqrt(B.SurfaceArea / A.SurfaceArea) : 1.0;
struct FBest { double Score = TNumericLimits<double>::Max(); FRot3 R; double s = 1.0; };
FBest Best;
// Candidate set.
TArray<const FRot3*> Candidates;
static const FRot3 Identity; // R = I, proper
Candidates.Add(&Identity);
for (const FRot3& R : AxisRotations())
{
if (R.bImproper && !S.bAllowMirrored)
{
continue;
}
Candidates.Add(&R);
}
for (const FRot3* R : Candidates)
{
const double Score = ScoreCandidate(A.Positions, BGrid, R->M, sEst, A.Centroid, B.Centroid, Tol, Stride);
if (Score < Best.Score)
{
Best.Score = Score;
Best.R = *R;
Best.s = sEst;
}
}
// Full verify the winner.
double MaxDev, Rms; bool Cov;
VerifyCandidate(A.Positions, B.Positions, BGrid, Best.R.M, Best.s, A.Centroid, B.Centroid, Tol, MaxDev, Rms, Cov);
if (!Cov)
{
return false;
}
Out.bValid = true;
Out.bMirrored = Best.R.bImproper;
Out.bScaled = !FMath::IsNearlyEqual(Best.s, 1.0, 1e-4);
Out.Scale = Best.s;
Out.MaxDev = MaxDev;
Out.Rms = Rms;
Out.CanonToMember = BuildMatrix(Best.R.M, Best.s, A.Centroid, B.Centroid);
Out.bIdentity = !Out.bMirrored && !Out.bScaled && Out.CanonToMember.Equals(FMatrix::Identity, 1e-4);
return true;
}
}

View File

@ -0,0 +1,35 @@
// Copyright IHY.
#pragma once
#include "CoreMinimal.h"
#include "OptimizerTypes.h"
#include "OptimizerGeometry.h"
/**
* Recovered rigid (optionally mirrored/scaled) delta that maps CANONICAL-local geometry onto
* MEMBER(sibling)-local geometry. Stored as a UE row-vector matrix: v_member = v_canon * CanonToMember.
* This is exactly the D in the corrective formula W' = D * W.
*/
struct FOptDelta
{
bool bValid = false;
bool bIdentity = false; // delta is (within tolerance) identity -> swap mesh, keep transform
bool bMirrored = false; // recovered rotation is improper (det < 0) -> negative-scale instance
bool bScaled = false; // uniform scale != 1 folded in
double Scale = 1.0;
double MaxDev = 0.0; // worst per-vertex deviation under the delta, cm
double Rms = 0.0;
FMatrix CanonToMember = FMatrix::Identity;
};
namespace OptimizerMatcher
{
/** Cheap bucket test: are these two fingerprints compatible enough to attempt recovery? */
bool FingerprintCompatible(const FOptMeshGeom& A, const FOptMeshGeom& B, const FOptimizerScanSettings& Settings);
/**
* Recover the delta mapping canonical-local (A) onto member-local (B). Both A.Positions and
* B.Positions must be populated. Returns false if no candidate passes verification.
*/
bool RecoverDelta(const FOptMeshGeom& A, const FOptMeshGeom& B, const FOptimizerScanSettings& Settings, FOptDelta& Out);
}

View File

@ -0,0 +1,40 @@
// Copyright IHY.
#include "OptimizerReconciler.h"
namespace OptimizerReconciler
{
bool ComputeCorrectedWorld(
const FMatrix& CanonToMember,
const FTransform& W,
const FBox& CanonLocalBounds,
double ShearTol,
FTransform& OutWprime,
double& OutMaxCornerDev)
{
// Exact corrected world matrix: apply D first (canon-local -> sibling-local), then W.
const FMatrix Exact = CanonToMember * W.ToMatrixWithScale();
OutWprime.SetFromMatrix(Exact);
// Compare the FTransform reconstruction against the exact matrix at the canonical bbox corners.
// Any gap means the exact matrix had shear that FTransform discarded -> not representable.
const FVector Mn = CanonLocalBounds.Min;
const FVector Mx = CanonLocalBounds.Max;
const FVector Corners[8] =
{
FVector(Mn.X, Mn.Y, Mn.Z), FVector(Mx.X, Mn.Y, Mn.Z),
FVector(Mn.X, Mx.Y, Mn.Z), FVector(Mx.X, Mx.Y, Mn.Z),
FVector(Mn.X, Mn.Y, Mx.Z), FVector(Mx.X, Mn.Y, Mx.Z),
FVector(Mn.X, Mx.Y, Mx.Z), FVector(Mx.X, Mx.Y, Mx.Z)
};
double MaxDev = 0.0;
for (const FVector& C : Corners)
{
const FVector ExactPt = Exact.TransformPosition(C);
const FVector ViaTransform = OutWprime.TransformPosition(C);
MaxDev = FMath::Max(MaxDev, FVector::Dist(ExactPt, ViaTransform));
}
OutMaxCornerDev = MaxDev;
return MaxDev <= ShearTol;
}
}

View File

@ -0,0 +1,27 @@
// Copyright IHY.
#pragma once
#include "CoreMinimal.h"
namespace OptimizerReconciler
{
/**
* Compute the corrected world transform for the canonical mesh so it lands exactly where the
* sibling currently sits. With D = CanonToMember (row-vector matrix) and W = sibling world
* transform: W'_matrix = D * W_matrix (the verified formula).
*
* FTransform cannot represent shear; if the actor's non-uniform scale combined with D's rotation
* produces shear, the reconstructed FTransform deviates from the exact matrix at the bbox corners.
* We measure that deviation and reject (return false) when it exceeds ShearTol — the caller then
* skips and reports the placement rather than silently corrupting it.
*
* @return false if the result shears (unrepresentable) — OutWprime is then not safe to apply.
*/
bool ComputeCorrectedWorld(
const FMatrix& CanonToMember,
const FTransform& W,
const FBox& CanonLocalBounds,
double ShearTol,
FTransform& OutWprime,
double& OutMaxCornerDev);
}

View File

@ -0,0 +1,565 @@
// Copyright IHY.
#include "OptimizerSubsystem.h"
#include "OptimizerGeometry.h"
#include "OptimizerMatcher.h"
#include "OptimizerReconciler.h"
#include "Editor.h"
#include "Subsystems/EditorActorSubsystem.h"
#include "Engine/StaticMesh.h"
#include "Engine/StaticMeshActor.h"
#include "Engine/World.h"
#include "GameFramework/Actor.h"
#include "Components/StaticMeshComponent.h"
#include "Components/InstancedStaticMeshComponent.h"
#include "Components/HierarchicalInstancedStaticMeshComponent.h"
#include "ScopedTransaction.h"
DEFINE_LOG_CATEGORY_STATIC(LogOptimizer, Log, All);
namespace
{
UStaticMesh* ChooseCanonical(
const TArray<UStaticMesh*>& Members,
const TMap<UStaticMesh*, FOptMeshGeom>& Geoms,
const TMap<UStaticMesh*, int32>& Counts,
EOptimizerCanonicalPolicy Policy)
{
UStaticMesh* Best = Members.Num() > 0 ? Members[0] : nullptr;
if (!Best)
{
return nullptr;
}
switch (Policy)
{
case EOptimizerCanonicalPolicy::LowestVertexCount:
for (UStaticMesh* M : Members)
{
const FOptMeshGeom* G = Geoms.Find(M);
const FOptMeshGeom* BG = Geoms.Find(Best);
if (G && BG && G->WeldedVertexCount < BG->WeldedVertexCount)
{
Best = M;
}
}
break;
case EOptimizerCanonicalPolicy::FirstAlphabetical:
for (UStaticMesh* M : Members)
{
if (M->GetName() < Best->GetName())
{
Best = M;
}
}
break;
case EOptimizerCanonicalPolicy::MostInstances:
default:
for (UStaticMesh* M : Members)
{
const int32 C = Counts.FindRef(M);
const int32 BC = Counts.FindRef(Best);
if (C > BC)
{
Best = M;
}
}
break;
}
return Best;
}
}
FOptimizerScanResult UOptimizerSubsystem::ScanLevel(const FOptimizerScanSettings& Settings)
{
LastSettings = Settings;
Groups.Reset();
FOptimizerScanResult Result;
UWorld* World = GEditor ? GEditor->GetEditorWorldContext().World() : nullptr;
if (!World)
{
Result.Summary = TEXT("No editor world.");
return Result;
}
Result.bWorldPartitionCoverageWarning = World->IsPartitionedWorld();
// --- 1. Gather non-instanced static-mesh placements ---
TArray<FPlacement> Placements;
UEditorActorSubsystem* AS = GEditor->GetEditorSubsystem<UEditorActorSubsystem>();
TArray<AActor*> Actors;
if (AS)
{
Actors = (Settings.Scope == EOptimizerScanScope::SelectedActors)
? AS->GetSelectedLevelActors()
: AS->GetAllLevelActors();
}
Result.ActorsScanned = Actors.Num();
for (AActor* Actor : Actors)
{
if (!Actor)
{
continue;
}
TArray<UStaticMeshComponent*> SMCs;
Actor->GetComponents<UStaticMeshComponent>(SMCs);
for (UStaticMeshComponent* SMC : SMCs)
{
// ISM/HISM sources are out of v1 scope (avoids per-instance removal complexity).
if (SMC->IsA<UInstancedStaticMeshComponent>())
{
continue;
}
UStaticMesh* M = SMC->GetStaticMesh();
if (!M)
{
continue;
}
FPlacement P;
P.Component = SMC;
P.Mesh = M;
P.World = SMC->GetComponentTransform();
Placements.Add(MoveTemp(P));
}
}
Result.ComponentsScanned = Placements.Num();
// --- 2. Fingerprint every unique mesh ---
TSet<UStaticMesh*> UniqueSet;
for (const FPlacement& P : Placements)
{
if (P.Mesh.IsValid())
{
UniqueSet.Add(P.Mesh.Get());
}
}
TArray<UStaticMesh*> Unique = UniqueSet.Array();
Result.UniqueMeshes = Unique.Num();
TMap<UStaticMesh*, FOptMeshGeom> Geoms;
Geoms.Reserve(Unique.Num());
for (UStaticMesh* M : Unique)
{
FOptMeshGeom G;
if (OptimizerGeometry::ExtractGeom(M, Settings.WeldEpsilon, /*bWantPositions*/false, G))
{
Geoms.Add(M, MoveTemp(G));
}
}
auto EnsurePositions = [&](UStaticMesh* M)
{
FOptMeshGeom* G = Geoms.Find(M);
if (G && G->Positions.Num() == 0)
{
OptimizerGeometry::ExtractGeom(M, Settings.WeldEpsilon, /*bWantPositions*/true, *G);
}
};
TMap<UStaticMesh*, int32> MeshInstanceCount;
for (const FPlacement& P : Placements)
{
if (P.Mesh.IsValid())
{
MeshInstanceCount.FindOrAdd(P.Mesh.Get())++;
}
}
// --- 3. Bucket by (welded vertex count, triangle count) ---
TMap<TPair<int32, int32>, TArray<UStaticMesh*>> Buckets;
for (UStaticMesh* M : Unique)
{
const FOptMeshGeom* G = Geoms.Find(M);
if (G && G->bValid)
{
Buckets.FindOrAdd(TPair<int32, int32>(G->WeldedVertexCount, G->TriangleCount)).Add(M);
}
}
const double ShearTol = FMath::Max((double)Settings.AcceptTolerance, 0.01);
// --- 4. Recover + group within each bucket ---
for (auto& BucketPair : Buckets)
{
TArray<UStaticMesh*>& Bucket = BucketPair.Value;
if (Bucket.Num() < 2)
{
continue;
}
for (UStaticMesh* M : Bucket)
{
EnsurePositions(M);
}
const int32 K = Bucket.Num();
TArray<int32> Parent;
Parent.SetNum(K);
for (int32 i = 0; i < K; ++i)
{
Parent[i] = i;
}
auto Find = [&Parent](int32 x)
{
while (Parent[x] != x)
{
Parent[x] = Parent[Parent[x]];
x = Parent[x];
}
return x;
};
for (int32 i = 0; i < K; ++i)
{
for (int32 j = i + 1; j < K; ++j)
{
const FOptMeshGeom& Gi = Geoms[Bucket[i]];
const FOptMeshGeom& Gj = Geoms[Bucket[j]];
if (!OptimizerMatcher::FingerprintCompatible(Gi, Gj, Settings))
{
continue;
}
FOptDelta D;
if (OptimizerMatcher::RecoverDelta(Gi, Gj, Settings, D))
{
Parent[Find(i)] = Find(j);
}
}
}
TMap<int32, TArray<UStaticMesh*>> Components;
for (int32 i = 0; i < K; ++i)
{
Components.FindOrAdd(Find(i)).Add(Bucket[i]);
}
for (auto& CompPair : Components)
{
TArray<UStaticMesh*>& Members = CompPair.Value;
if (Members.Num() < 2)
{
continue;
}
UStaticMesh* Canon = ChooseCanonical(Members, Geoms, MeshInstanceCount, Settings.CanonicalPolicy);
if (!Canon)
{
continue;
}
EnsurePositions(Canon);
const FOptMeshGeom& CanonGeom = Geoms[Canon];
// Recover canon->member delta for each member (direct re-verify, no transitivity trust).
TMap<UStaticMesh*, FOptDelta> Deltas;
FGroup Group;
Group.Canonical = Canon;
for (UStaticMesh* Mem : Members)
{
if (Geoms[Mem].MaterialHash != CanonGeom.MaterialHash)
{
Group.bMaterialMismatch = true;
}
if (Mem == Canon)
{
FOptDelta Id;
Id.bValid = true;
Id.bIdentity = true;
Id.CanonToMember = FMatrix::Identity;
Deltas.Add(Mem, Id);
continue;
}
EnsurePositions(Mem);
FOptDelta D;
if (OptimizerMatcher::RecoverDelta(CanonGeom, Geoms[Mem], Settings, D))
{
Group.bHasMirrored |= D.bMirrored;
Group.bHasScaled |= D.bScaled;
Group.MaxDeviation = FMath::Max(Group.MaxDeviation, (float)D.MaxDev);
Deltas.Add(Mem, D);
}
}
if (Deltas.Num() < 2)
{
continue;
}
for (const auto& DPair : Deltas)
{
Group.Members.Add(DPair.Key);
}
// Plan each placement whose mesh is in this group.
for (const FPlacement& P : Placements)
{
UStaticMesh* PM = P.Mesh.Get();
const FOptDelta* D = PM ? Deltas.Find(PM) : nullptr;
if (!D)
{
continue;
}
FPlacement Plan = P;
if (D->bIdentity)
{
Plan.PlannedWorld = P.World;
Plan.bNeedsTransformFix = false;
}
else
{
FTransform Wp;
double CornerDev = 0.0;
if (OptimizerReconciler::ComputeCorrectedWorld(D->CanonToMember, P.World, CanonGeom.LocalBounds, ShearTol, Wp, CornerDev))
{
Plan.PlannedWorld = Wp;
Plan.bNeedsTransformFix = true;
}
else
{
Plan.bShearRejected = true;
Plan.PlannedWorld = P.World;
Group.ShearRejected++;
}
}
Group.Placements.Add(MoveTemp(Plan));
}
if (Group.Placements.Num() > 0)
{
Groups.Add(MoveTemp(Group));
}
}
}
// --- 5. Build the UI view ---
FOptimizerScanResult View = BuildResultView();
View.ActorsScanned = Result.ActorsScanned;
View.ComponentsScanned = Result.ComponentsScanned;
View.UniqueMeshes = Result.UniqueMeshes;
View.bWorldPartitionCoverageWarning = Result.bWorldPartitionCoverageWarning;
int32 TotalDupMeshes = 0;
for (const FOptimizerGroupView& GV : View.Groups)
{
TotalDupMeshes += FMath::Max(0, GV.MemberMeshNames.Num() - 1);
}
View.Summary = FString::Printf(
TEXT("%d sibling group(s): %d duplicate mesh asset(s) across %d placement(s)%s%s"),
View.Groups.Num(), TotalDupMeshes, View.InstancesCollapsible,
View.bWorldPartitionCoverageWarning ? TEXT(" | World Partition: only loaded cells scanned") : TEXT(""),
View.Groups.ContainsByPredicate([](const FOptimizerGroupView& G) { return G.ShearRejectedCount > 0; })
? TEXT(" | some placements skipped (shear)") : TEXT(""));
UE_LOG(LogOptimizer, Log, TEXT("Optimizer scan: %s"), *View.Summary);
return View;
}
FOptimizerScanResult UOptimizerSubsystem::BuildResultView() const
{
FOptimizerScanResult View;
int32 Collapsible = 0;
for (int32 gi = 0; gi < Groups.Num(); ++gi)
{
const FGroup& G = Groups[gi];
FOptimizerGroupView V;
V.GroupId = gi;
if (UStaticMesh* Canon = G.Canonical.Get())
{
V.CanonicalMeshName = Canon->GetName();
V.CanonicalMeshPath = Canon->GetPathName();
}
for (const TWeakObjectPtr<UStaticMesh>& M : G.Members)
{
if (UStaticMesh* SM = M.Get())
{
V.MemberMeshNames.Add(SM->GetName());
}
}
V.InstanceCount = G.Placements.Num();
for (const FPlacement& P : G.Placements)
{
if (P.bNeedsTransformFix)
{
V.TransformFixCount++;
}
}
V.MaxDeviation = G.MaxDeviation;
V.bHasMirrored = G.bHasMirrored;
V.bHasScaled = G.bHasScaled;
V.bMaterialMismatch = G.bMaterialMismatch;
V.ShearRejectedCount = G.ShearRejected;
Collapsible += G.Placements.Num();
View.Groups.Add(MoveTemp(V));
}
View.InstancesCollapsible = Collapsible;
return View;
}
int32 UOptimizerSubsystem::ApplyUnify()
{
if (Groups.Num() == 0)
{
return 0;
}
const FScopedTransaction Transaction(NSLOCTEXT("Optimizer", "Unify", "Optimizer: unify sibling meshes"));
int32 Changed = 0;
for (FGroup& G : Groups)
{
UStaticMesh* Canon = G.Canonical.Get();
if (!Canon)
{
continue;
}
for (FPlacement& P : G.Placements)
{
if (P.bShearRejected)
{
continue;
}
UStaticMeshComponent* C = P.Component.Get();
if (!C)
{
continue;
}
if (P.Mesh.Get() == Canon && !P.bNeedsTransformFix)
{
continue; // already canonical, nothing to fix
}
C->Modify();
if (AActor* Owner = C->GetOwner())
{
Owner->Modify();
}
C->SetStaticMesh(Canon);
C->SetWorldTransform(P.PlannedWorld, false, nullptr, ETeleportType::TeleportPhysics);
C->MarkRenderStateDirty();
if (AActor* Owner = C->GetOwner())
{
Owner->MarkPackageDirty();
}
++Changed;
}
}
if (GEditor)
{
GEditor->RedrawLevelEditingViewports();
}
UE_LOG(LogOptimizer, Log, TEXT("Optimizer unify: %d placement(s) reassigned."), Changed);
return Changed;
}
int32 UOptimizerSubsystem::BuildHISM(bool bDestroyOriginals)
{
if (Groups.Num() == 0 || !GEditor)
{
return 0;
}
UWorld* World = GEditor->GetEditorWorldContext().World();
if (!World)
{
return 0;
}
UEditorActorSubsystem* AS = GEditor->GetEditorSubsystem<UEditorActorSubsystem>();
const FScopedTransaction Transaction(NSLOCTEXT("Optimizer", "HISM", "Optimizer: build HISM"));
int32 Built = 0;
TSet<AActor*> Destroyed;
for (FGroup& G : Groups)
{
UStaticMesh* Canon = G.Canonical.Get();
if (!Canon)
{
continue;
}
TArray<FTransform> Instances;
for (const FPlacement& P : G.Placements)
{
if (!P.bShearRejected)
{
Instances.Add(P.PlannedWorld);
}
}
if (Instances.Num() == 0)
{
continue;
}
FActorSpawnParameters Sp;
Sp.ObjectFlags |= RF_Transactional;
AActor* Holder = World->SpawnActor<AActor>(AActor::StaticClass(), FTransform::Identity, Sp);
if (!Holder)
{
continue;
}
UHierarchicalInstancedStaticMeshComponent* HISM =
NewObject<UHierarchicalInstancedStaticMeshComponent>(Holder, NAME_None, RF_Transactional);
HISM->SetStaticMesh(Canon);
HISM->SetMobility(EComponentMobility::Static);
Holder->SetRootComponent(HISM);
Holder->AddInstanceComponent(HISM);
HISM->RegisterComponent();
for (const FTransform& T : Instances)
{
HISM->AddInstance(T, /*bWorldSpace*/true);
}
Holder->SetActorLabel(FString::Printf(TEXT("HISM_%s"), *Canon->GetName()));
++Built;
if (bDestroyOriginals)
{
for (FPlacement& P : G.Placements)
{
if (P.bShearRejected)
{
continue;
}
UStaticMeshComponent* C = P.Component.Get();
if (!C)
{
continue;
}
AActor* Owner = C->GetOwner();
if (Owner && Owner->IsA<AStaticMeshActor>())
{
if (!Destroyed.Contains(Owner))
{
Owner->Modify();
if (AS)
{
AS->DestroyActor(Owner);
}
else
{
World->DestroyActor(Owner);
}
Destroyed.Add(Owner);
}
}
else
{
C->Modify();
C->DestroyComponent();
}
}
}
}
// Plan is now stale (originals consumed); require a re-scan before further ops.
if (bDestroyOriginals)
{
Groups.Reset();
}
if (GEditor)
{
GEditor->RedrawLevelEditingViewports();
}
UE_LOG(LogOptimizer, Log, TEXT("Optimizer HISM: %d actor(s) built."), Built);
return Built;
}
void UOptimizerSubsystem::ClearPlan()
{
Groups.Reset();
}
bool UOptimizerSubsystem::HasPlan() const
{
return Groups.Num() > 0;
}

View File

@ -0,0 +1,211 @@
// 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

View File

@ -0,0 +1,40 @@
// Copyright IHY.
#pragma once
#include "CoreMinimal.h"
#include "Widgets/SCompoundWidget.h"
class STextBlock;
class SCheckBox;
class SWidget;
/**
* Native Slate panel for the Mesh Optimizer (hosted in a nomad tab). Drives UOptimizerSubsystem:
* Scan (dry-run report) / Unify (reassign siblings) / Build HISM (collapse). Uses editor styling
* so it reads as a native tool rather than raw UMG.
*/
class SOptimizerPanel : public SCompoundWidget
{
public:
SLATE_BEGIN_ARGS(SOptimizerPanel) {}
SLATE_END_ARGS()
void Construct(const FArguments& InArgs);
private:
TSharedRef<SWidget> MakeLabeledCheck(TSharedPtr<SCheckBox>& Out, const FText& Label, bool bDefault);
TSharedRef<SWidget> MakeButton(const FText& Label, FOnClicked OnClicked);
FReply OnScan();
FReply OnUnify();
FReply OnBuildHISM();
void SetReport(const FString& Text);
bool IsChecked(const TSharedPtr<SCheckBox>& Chk) const;
TSharedPtr<SCheckBox> ChkMirror;
TSharedPtr<SCheckBox> ChkScale;
TSharedPtr<SCheckBox> ChkMergeMat;
TSharedPtr<SCheckBox> ChkDestroy;
TSharedPtr<STextBlock> ResultsBox;
};

View File

@ -0,0 +1,26 @@
// Copyright IHY.
#pragma once
#include "CoreMinimal.h"
#include "Modules/ModuleInterface.h"
/**
* Editor-only module for the Mesh Optimizer.
*
* Registers a Tools-menu entry + nomad tab hosting the native Slate panel (SOptimizerPanel),
* which drives UOptimizerSubsystem: scan level StaticMesh usage, group geometrically-identical
* "sibling" meshes, rebase every user onto one canonical mesh with a pivot/rotation-corrected
* transform, and optionally collapse the group into a HierarchicalInstancedStaticMesh.
*/
class FOptimizerEditorModule : public IModuleInterface
{
public:
virtual void StartupModule() override;
virtual void ShutdownModule() override;
private:
void RegisterMenus(); // deferred via UToolMenus startup callback
TSharedRef<class SDockTab> SpawnPanelTab(const class FSpawnTabArgs& Args);
bool bTabRegistered = false;
};

View File

@ -0,0 +1,76 @@
// Copyright IHY.
#pragma once
#include "CoreMinimal.h"
#include "EditorSubsystem.h"
#include "OptimizerTypes.h"
#include "OptimizerSubsystem.generated.h"
class UStaticMesh;
class UStaticMeshComponent;
/**
* Editor subsystem driving the Mesh Optimizer pipeline. The Editor Utility Widget calls these:
* 1. ScanLevel -> dry-run plan (sibling groups + per-placement corrected transforms)
* 2. ApplyUnify -> reassign every sibling placement to its canonical mesh with the fixed transform
* 3. BuildHISM -> collapse each group into one HierarchicalInstancedStaticMesh
* Steps 2 and 3 each run inside a single undoable transaction. BuildHISM is independent of ApplyUnify
* (it uses the canonical mesh + planned transforms directly).
*/
UCLASS()
class OPTIMIZEREDITOR_API UOptimizerSubsystem : public UEditorSubsystem
{
GENERATED_BODY()
public:
/** Dry run: scan the level, group sibling meshes, plan corrected transforms. Mutates nothing. */
UFUNCTION(BlueprintCallable, Category = "Optimizer")
FOptimizerScanResult ScanLevel(const FOptimizerScanSettings& Settings);
/** Reassign all non-instanced sibling placements to their canonical mesh + corrected transform. Returns count changed. */
UFUNCTION(BlueprintCallable, Category = "Optimizer")
int32 ApplyUnify();
/** Build one HISM actor per group from the planned transforms. Returns number of HISM actors created. */
UFUNCTION(BlueprintCallable, Category = "Optimizer")
int32 BuildHISM(bool bDestroyOriginals = true);
/** Discard the current plan. */
UFUNCTION(BlueprintCallable, Category = "Optimizer")
void ClearPlan();
UFUNCTION(BlueprintCallable, Category = "Optimizer")
bool HasPlan() const;
private:
/** One placement of a mesh in the level (a component, or one instance inside an ISM/HISM). */
struct FPlacement
{
TWeakObjectPtr<UStaticMeshComponent> Component;
int32 InstanceIndex = INDEX_NONE; // >= 0 when this is an instance inside an ISM/HISM
TWeakObjectPtr<UStaticMesh> Mesh;
FTransform World = FTransform::Identity;
FTransform PlannedWorld = FTransform::Identity;
bool bNeedsTransformFix = false;
bool bShearRejected = false;
bool bInstanced = false; // inside an ISM/HISM -> ApplyUnify can't swap it (HISM-only)
};
/** A sibling group: distinct meshes that share geometry, collapsed onto one canonical. */
struct FGroup
{
TArray<TWeakObjectPtr<UStaticMesh>> Members;
TWeakObjectPtr<UStaticMesh> Canonical;
TArray<FPlacement> Placements;
float MaxDeviation = 0.0f;
bool bHasMirrored = false;
bool bHasScaled = false;
bool bMaterialMismatch = false;
int32 ShearRejected = 0;
};
TArray<FGroup> Groups;
FOptimizerScanSettings LastSettings;
FOptimizerScanResult BuildResultView() const;
};

View File

@ -0,0 +1,157 @@
// Copyright IHY.
#pragma once
#include "CoreMinimal.h"
#include "OptimizerTypes.generated.h"
/**
* Shared data contract between the C++ optimizer core and the Editor Utility Widget UI.
* BlueprintType structs/enums here are what the EUW reads/writes; the heavy internal
* algorithm structures (point clouds, recovered deltas) live in the matcher/reconciler.
*/
/** Which placements to scan. v1 = level StaticMeshActors / standalone components. */
UENUM(BlueprintType)
enum class EOptimizerScanScope : uint8
{
SelectedActors UMETA(DisplayName = "Selected Actors"),
WholeLevel UMETA(DisplayName = "Whole Level (loaded)")
};
/** How aggressively to treat two meshes as the same geometry. */
UENUM(BlueprintType)
enum class EOptimizerMatchPrecision : uint8
{
// Identical geometry AND identical orientation/pivot (recovered delta D == Identity).
ExactOnly UMETA(DisplayName = "Exact duplicates only"),
// Identical geometry up to a rigid transform (rotation and/or baked pivot offset).
RigidTransforms UMETA(DisplayName = "Exact + rotated/pivot (rigid)")
};
/** How to pick the surviving canonical mesh within a sibling group. */
UENUM(BlueprintType)
enum class EOptimizerCanonicalPolicy : uint8
{
MostInstances UMETA(DisplayName = "Most-used mesh"),
LowestVertexCount UMETA(DisplayName = "Lowest vertex count"),
FirstAlphabetical UMETA(DisplayName = "First alphabetical")
};
/** User-facing scan configuration. */
USTRUCT(BlueprintType)
struct FOptimizerScanSettings
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Scope")
EOptimizerScanScope Scope = EOptimizerScanScope::WholeLevel;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Detection")
EOptimizerMatchPrecision Precision = EOptimizerMatchPrecision::RigidTransforms;
/** Treat negative-scale (mirrored) variants as siblings. They become their own canonical/HISM. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Detection")
bool bAllowMirrored = false;
/** Treat uniformly-scaled copies as siblings (instance carries the scale). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Detection")
bool bAllowUniformScale = false;
/** Merge geometry siblings even when their material sets differ (canonical's materials win — risky). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Detection")
bool bMergeAcrossMaterials = false;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Canonical")
EOptimizerCanonicalPolicy CanonicalPolicy = EOptimizerCanonicalPolicy::MostInstances;
/** Position weld grid, cm. Snaps near-coincident verts so seam-splitting doesn't change counts. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Advanced|Tolerances", AdvancedDisplay, meta = (ClampMin = "0.0"))
float WeldEpsilon = 0.01f;
/** Max per-vertex world deviation to ACCEPT a match, cm. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Advanced|Tolerances", AdvancedDisplay, meta = (ClampMin = "0.0"))
float AcceptTolerance = 0.01f;
/** Relative tolerance for scalar fingerprint features (area / volume / eigenvalues). */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Advanced|Tolerances", AdvancedDisplay, meta = (ClampMin = "0.0"))
float ScalarRelTolerance = 0.001f;
/** Eigenvalue-gap fraction below which PCA orientation is considered degenerate -> brute-24 fallback. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Advanced|Tolerances", AdvancedDisplay, meta = (ClampMin = "0.0"))
float DegenerateEigenEpsilon = 0.05f;
};
/** One sibling group, summarised for the UI list. */
USTRUCT(BlueprintType)
struct FOptimizerGroupView
{
GENERATED_BODY()
UPROPERTY(BlueprintReadOnly, Category = "Optimizer")
int32 GroupId = -1;
UPROPERTY(BlueprintReadOnly, Category = "Optimizer")
FString CanonicalMeshName;
UPROPERTY(BlueprintReadOnly, Category = "Optimizer")
FString CanonicalMeshPath;
/** Distinct sibling assets in the group (includes the canonical). */
UPROPERTY(BlueprintReadOnly, Category = "Optimizer")
TArray<FString> MemberMeshNames;
/** Total placements (actors/components) that will collapse onto the canonical. */
UPROPERTY(BlueprintReadOnly, Category = "Optimizer")
int32 InstanceCount = 0;
/** Placements that need a corrected transform (W' != W). */
UPROPERTY(BlueprintReadOnly, Category = "Optimizer")
int32 TransformFixCount = 0;
/** Worst per-vertex verification deviation across the group, cm. */
UPROPERTY(BlueprintReadOnly, Category = "Optimizer")
float MaxDeviation = 0.0f;
UPROPERTY(BlueprintReadOnly, Category = "Optimizer")
bool bHasMirrored = false;
UPROPERTY(BlueprintReadOnly, Category = "Optimizer")
bool bHasScaled = false;
UPROPERTY(BlueprintReadOnly, Category = "Optimizer")
bool bMaterialMismatch = false;
/** Placements skipped because the corrective transform would shear (non-uniform scale + rotation). */
UPROPERTY(BlueprintReadOnly, Category = "Optimizer")
int32 ShearRejectedCount = 0;
};
/** Result of a dry-run scan: the plan the user reviews before applying. */
USTRUCT(BlueprintType)
struct FOptimizerScanResult
{
GENERATED_BODY()
UPROPERTY(BlueprintReadOnly, Category = "Optimizer")
TArray<FOptimizerGroupView> Groups;
UPROPERTY(BlueprintReadOnly, Category = "Optimizer")
int32 ActorsScanned = 0;
UPROPERTY(BlueprintReadOnly, Category = "Optimizer")
int32 ComponentsScanned = 0;
UPROPERTY(BlueprintReadOnly, Category = "Optimizer")
int32 UniqueMeshes = 0;
/** Placements removable by collapsing siblings (sum over groups of InstanceCount - 1 distinct survivors). */
UPROPERTY(BlueprintReadOnly, Category = "Optimizer")
int32 InstancesCollapsible = 0;
/** True if the world is partitioned and some cells were unloaded -> coverage is partial. */
UPROPERTY(BlueprintReadOnly, Category = "Optimizer")
bool bWorldPartitionCoverageWarning = false;
UPROPERTY(BlueprintReadOnly, Category = "Optimizer")
FString Summary;
};