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:
26
Source/OptimizerEditor/Public/OptimizerEditorModule.h
Normal file
26
Source/OptimizerEditor/Public/OptimizerEditorModule.h
Normal 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;
|
||||
};
|
||||
76
Source/OptimizerEditor/Public/OptimizerSubsystem.h
Normal file
76
Source/OptimizerEditor/Public/OptimizerSubsystem.h
Normal 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;
|
||||
};
|
||||
157
Source/OptimizerEditor/Public/OptimizerTypes.h
Normal file
157
Source/OptimizerEditor/Public/OptimizerTypes.h
Normal 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;
|
||||
};
|
||||
Reference in New Issue
Block a user