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:
153
README.md
Normal file
153
README.md
Normal 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.
|
||||
Reference in New Issue
Block a user