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

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.