Files
mesh-project-duplicate-remover/Source/OptimizerEditor/Private/OptimizerMatcher.cpp
Bonchellon a95b299680 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>
2026-07-01 18:26:45 +03:00

348 lines
9.8 KiB
C++

// 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;
}
}