852 lines
30 KiB
C++
852 lines
30 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
#include "Parameterization/MeshPlanarSymmetry.h"
|
|
#include "Async/ParallelFor.h"
|
|
|
|
#include "CompGeom/ConvexHull3.h" // for TExtremePoints3
|
|
#include "BoxTypes.h"
|
|
#include "MeshCurvature.h"
|
|
#include "DynamicMesh/MeshNormals.h"
|
|
#include "Spatial/PointHashGrid3.h"
|
|
#include "Spatial/PointHashGrid2.h"
|
|
#include "VertexConnectedComponents.h"
|
|
|
|
|
|
using namespace UE::Geometry;
|
|
|
|
void FMeshPlanarSymmetry::ComputeMeshInfo(const FDynamicMesh3* Mesh, const FAxisAlignedBox3d& Bounds, TArray<FVector3d>& InvariantFeaturesOut, FVector3d& MeshCentroidOut)
|
|
{
|
|
// Compute centroid with an online sum to avoid messing up accuracy if the number of vertices is very large
|
|
MeshCentroidOut = FVector::ZeroVector;
|
|
int32 Idx = 0;
|
|
FVector3d Center = Bounds.Center();
|
|
for (FVector3d V : Mesh->VerticesItr())
|
|
{
|
|
MeshCentroidOut += (V - Center - MeshCentroidOut) / double(++Idx);
|
|
}
|
|
MeshCentroidOut += Center;
|
|
|
|
// To group vertices by symmetry-pair candidates, we map the vertices to a feature space of:
|
|
// (Vertex Angle Sum / 2pi, cos(Angle between normal and direction from centroid), Distance from Vertex Centroid / Half Max Bounds Dim)
|
|
// Note these values are scaled so that a single distance tolerance can be used to compare features.
|
|
FMeshNormals MeshNormals(Mesh);
|
|
MeshNormals.ComputeVertexNormals(false, true); // Note: only weighting by angle, b/c it is less sensitive to tesselation than area
|
|
InvariantFeaturesOut.SetNumUninitialized(Mesh->MaxVertexID());
|
|
double InvHalfMaxDim = 2.0 / Bounds.MaxDim();
|
|
ParallelFor(Mesh->MaxVertexID(), [&](int32 VID)
|
|
{
|
|
if (!Mesh->IsVertex(VID))
|
|
{
|
|
return;
|
|
}
|
|
|
|
double AngleSum = 0;
|
|
for (int TID : Mesh->VtxTrianglesItr(VID))
|
|
{
|
|
FIndex3i Triangle = Mesh->GetTriangle(TID);
|
|
int IndexInTriangle = IndexUtil::FindTriIndex(VID, Triangle);
|
|
AngleSum += Mesh->GetTriInternalAngleR(TID, IndexInTriangle);
|
|
}
|
|
FVector3d VertexNormal = MeshNormals[VID];
|
|
FVector3d Pos = Mesh->GetVertex(VID);
|
|
double CentroidDist = FVector3d::Distance(MeshCentroidOut, Pos);
|
|
FVector3d FromCentroid = Pos - MeshCentroidOut;
|
|
if (CentroidDist > FMathd::ZeroTolerance)
|
|
{
|
|
FromCentroid /= CentroidDist;
|
|
}
|
|
double NormalAngleCos = FromCentroid.Dot(VertexNormal);
|
|
// Note: Downscale the angle and normal features because they tend to have more error / tesselation sensitivity
|
|
InvariantFeaturesOut[VID] = FVector3d(AngleSum * FMathd::InvTwoPi * .01, NormalAngleCos * .001, CentroidDist * InvHalfMaxDim);
|
|
});
|
|
|
|
}
|
|
|
|
bool FMeshPlanarSymmetry::Validate(const FDynamicMesh3* Mesh)
|
|
{
|
|
if (!Mesh)
|
|
{
|
|
return false;
|
|
}
|
|
if (Mesh->VertexCount() == 0)
|
|
{
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void FMeshPlanarSymmetry::SetErrorScale(const FAxisAlignedBox3d& Bounds)
|
|
{
|
|
ErrorScale = FMath::Max(1, Bounds.MaxDim()); // Error tolerances scale up with larger meshes
|
|
}
|
|
|
|
bool FMeshPlanarSymmetry::FindPlaneAndInitialize(FDynamicMesh3* Mesh, const FAxisAlignedBox3d& Bounds, FFrame3d& SymmetryFrameOut, TArrayView<const FVector3d> PreferredNormals)
|
|
{
|
|
this->TargetMesh = Mesh;
|
|
if (!Validate(Mesh))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
SetErrorScale(Bounds);
|
|
double OnPlaneTolerance = OnPlaneToleranceFactor * ErrorScale;
|
|
double MatchVertexTolerance = MatchVertexToleranceFactor * ErrorScale;
|
|
double VertexHashCellSize = VertexHashCellSizeFactor * ErrorScale;
|
|
|
|
// If this mesh is a tiny mesh, then all points are on the symmetry plane ...
|
|
double BoundsMaxDim = Bounds.MaxDim();
|
|
if (BoundsMaxDim < OnPlaneTolerance)
|
|
{
|
|
if (bCanIgnoreDegenerateSymmetries)
|
|
{
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
// Just use a default/preferred plane
|
|
FVector3d UseNormal = PreferredNormals.Num() ? PreferredNormals[0] : FVector3d::UnitX();
|
|
SymmetryFrameOut = FFrame3d(Bounds.Center(), UseNormal);
|
|
return Initialize(Mesh, Bounds, SymmetryFrameOut);
|
|
}
|
|
}
|
|
|
|
// Compute vertex hash and features
|
|
FVector3d MeshCentroid;
|
|
TArray<FVector3d> Features;
|
|
ComputeMeshInfo(Mesh, Bounds, Features, MeshCentroid);
|
|
TPointHashGrid3d<int32> VertexHash(VertexHashCellSize, INDEX_NONE);
|
|
VertexHash.Reserve(Mesh->VertexCount());
|
|
for (int32 VID : Mesh->VertexIndicesItr())
|
|
{
|
|
VertexHash.InsertPointUnsafe(VID, Mesh->GetVertex(VID));
|
|
}
|
|
|
|
// Find groups of vertices that could by symmetric pairs, based on similar distance from the MeshCentroid
|
|
FVertexConnectedComponents PairCandidateComponents(Mesh->MaxVertexID());
|
|
|
|
// To quickly match in dist-to-centroid space, we hash the points by their distances
|
|
// In the worst case, all the vertices may have the same distance (a sphere) so we create a hash entry covering a 'range'
|
|
// and when we overlap an existing range, we just expand that range rather than create a new one
|
|
struct FRange
|
|
{
|
|
FInterval1d Range;
|
|
int32 ID;
|
|
};
|
|
// Because ranges cannot overlap within a bucket, there can only be a fixed number of ranges per bucket
|
|
// A single-point range covers an interval of size 2*Tolerance, so a 3*Tolerance-wide bucket
|
|
// should only have 3 separate ranges before any new range entered in the bucket will instead expand an existing range.
|
|
// (3 because ranges could partially touch the bucket on each side.)
|
|
// For safety, we allocate to allow up to 4 ...
|
|
constexpr int MaxRangesPerBucket = 4;
|
|
using FRangesBucket = TArray<FRange, TFixedAllocator<MaxRangesPerBucket>>;
|
|
TMap<int32, FRangesBucket> Ranges;
|
|
double RangeCellWidth = MatchVertexTolerance * (MaxRangesPerBucket - 1);
|
|
double ToCellIdx = 1.0 / RangeCellWidth;
|
|
for (int32 VID : Mesh->VertexIndicesItr())
|
|
{
|
|
double CentroidDistance = FVector::Distance(Mesh->GetVertex(VID), MeshCentroid);
|
|
FInterval1d Range(CentroidDistance - MatchVertexTolerance, CentroidDistance + MatchVertexTolerance);
|
|
for (int32 CellIdx = FMath::FloorToInt32(Range.Min * ToCellIdx); CellIdx <= FMath::FloorToInt32(Range.Max * ToCellIdx); ++CellIdx)
|
|
{
|
|
FRangesBucket& Bucket = Ranges.FindOrAdd(CellIdx);
|
|
bool bPlacedInExisting = false;
|
|
for (FRange& Existing : Bucket)
|
|
{
|
|
if (Existing.Range.Overlaps(Range))
|
|
{
|
|
bPlacedInExisting = true;
|
|
PairCandidateComponents.ConnectVertices(VID, Existing.ID);
|
|
Existing.Range.Contain(Range);
|
|
}
|
|
}
|
|
if (!bPlacedInExisting)
|
|
{
|
|
check(Bucket.Num() < MaxRangesPerBucket);
|
|
Bucket.Add(FRange{Range, VID});
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get an ordered array of feature components and compute their centroids
|
|
TArray<int32> PairCandidateComponentsArray = PairCandidateComponents.MakeContiguousComponentsArray(Mesh->MaxVertexID());
|
|
TArray<FVector3d> AllCentroids;
|
|
AllCentroids.Add(MeshCentroid);
|
|
FVector3d BoundsCenter = Bounds.Center();
|
|
PairCandidateComponents.EnumerateContiguousComponentsFromArray(PairCandidateComponentsArray, [&](int32 ComponentID, TArrayView<const int32> VertexIDs) -> bool
|
|
{
|
|
if (!Mesh->IsVertex(ComponentID))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// Compute centroid with an online sum to avoid messing up accuracy if there is a very large component
|
|
FVector3d Centroid(0, 0, 0);
|
|
int32 Idx = 0;
|
|
for (int32 VID : VertexIDs)
|
|
{
|
|
const FVector3d CenteredPos = Mesh->GetVertex(VID) - BoundsCenter;
|
|
Centroid += (CenteredPos - Centroid) / double(++Idx);
|
|
}
|
|
Centroid += BoundsCenter;
|
|
AllCentroids.Add(Centroid);
|
|
return true;
|
|
});
|
|
|
|
// Test a plane through MeshCentroid with the given Normal for symmetry. While we're at it, accumulate an improved normal estimate from the matches.
|
|
// Returns false at the first failure to find a match, so often very fast to reject an incorrect plane
|
|
auto TestPlane = [Mesh, MeshCentroid, &VertexHash, this, OnPlaneTolerance, MatchVertexTolerance](FVector3d Normal, FVector3d& RefinedNormalOut) -> bool
|
|
{
|
|
FVector3d SumOfPtToMirrorDeltas(0, 0, 0);
|
|
bool bAllOnPlane = true;
|
|
RefinedNormalOut = Normal;
|
|
for (int32 VID : Mesh->VertexIndicesItr())
|
|
{
|
|
FVector3d Position = Mesh->GetVertex(VID);
|
|
double SignedPlaneDist = Normal.Dot(Position - MeshCentroid);
|
|
if (SignedPlaneDist < OnPlaneTolerance)
|
|
{
|
|
continue;
|
|
}
|
|
bAllOnPlane = false;
|
|
FVector3d MirrorPosition = Position - 2 * Normal * SignedPlaneDist;
|
|
double NearestDistSqr = 0;
|
|
TPair<int32, double> Nearest = VertexHash.FindNearestInRadius(MirrorPosition, MatchVertexTolerance, [MirrorPosition, &Mesh](const int32& FoundVID)
|
|
{
|
|
return FVector3d::DistSquared(MirrorPosition, Mesh->GetVertex(FoundVID));
|
|
});
|
|
int32 NearestVID = Nearest.Key;
|
|
if (NearestVID == INDEX_NONE)
|
|
{
|
|
return false;
|
|
}
|
|
FVector3d FoundMirrorPosition = Mesh->GetVertex(NearestVID);
|
|
|
|
FVector3d Delta = (Position - FoundMirrorPosition);
|
|
SumOfPtToMirrorDeltas += Delta;
|
|
}
|
|
if (!bAllOnPlane && SumOfPtToMirrorDeltas.Normalize())
|
|
{
|
|
RefinedNormalOut = SumOfPtToMirrorDeltas;
|
|
}
|
|
return true;
|
|
};
|
|
|
|
// Check whether the centroids already span a plane; if so, we already have the mirror plane candidate
|
|
// Note that the ExtremePoints algorithm internally scales its tolerance by the scale of the input,
|
|
// so we shouldn't use our ErrorScale-scaled tolerances with it.
|
|
constexpr double ExtremePointsTolerance = UE_DOUBLE_KINDA_SMALL_NUMBER;
|
|
FExtremePoints3d CentroidsExtremePoints(AllCentroids.Num(), [&AllCentroids](int32 Idx) {return AllCentroids[Idx];}, [](int32) {return true;}, ExtremePointsTolerance);
|
|
if (CentroidsExtremePoints.Dimension > 1)
|
|
{
|
|
FVector3d Normal = CentroidsExtremePoints.Basis[2];
|
|
if (CentroidsExtremePoints.Dimension == 3)
|
|
{
|
|
// The centroids spanned 3 dimensions -> no possible symmetry plane
|
|
return false;
|
|
}
|
|
// The plane the centroids spanned must be the symmetry plane
|
|
SymmetryFrameOut = FFrame3d(MeshCentroid, Normal);
|
|
return AssignMatches(Mesh, VertexHash, Features, SymmetryFrameOut);
|
|
}
|
|
|
|
// Track whether we at least have a line that the symmetry plane must include
|
|
// Note: Often this will be the line of intersection of two (or more) symmetry planes, but
|
|
// that is not guaranteed due to the possibility that a symmetry cluster can merge multiple
|
|
// near-in-feature-space groups of points. (And AddPointOnPlane() can later set this vector
|
|
// in a way that definitely won't correspond to a symmetry plane intersection line.)
|
|
bool bHasVectorOnPlane = false;
|
|
FVector3d VectorOnPlane(0, 0, 0);
|
|
if (CentroidsExtremePoints.Dimension == 1)
|
|
{
|
|
bHasVectorOnPlane = true;
|
|
VectorOnPlane = CentroidsExtremePoints.Basis[0];
|
|
}
|
|
|
|
// If the caller passed in preferred plane normals, try those first
|
|
for (FVector3d PreferNormal : PreferredNormals)
|
|
{
|
|
// If we know a vector must be on the plane, the normal must be perpendicular to that
|
|
if (!bHasVectorOnPlane || FMathd::Abs(PreferNormal.Dot(VectorOnPlane)) < UE_DOUBLE_KINDA_SMALL_NUMBER)
|
|
{
|
|
FVector3d RefinedNormal;
|
|
if (TestPlane(PreferNormal, RefinedNormal))
|
|
{
|
|
// Note: Ignore RefinedNormal, since we already have an exact value preferred by the caller
|
|
SymmetryFrameOut = FFrame3d(MeshCentroid, PreferNormal);
|
|
return AssignMatches(Mesh, VertexHash, Features, SymmetryFrameOut);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Test for the degenerate case of a completely collinear mesh (should be fast, usually, since we break out at the first off-axis point)
|
|
if (bHasVectorOnPlane)
|
|
{
|
|
bool bAllOnAxis = true;
|
|
for (int32 VID : Mesh->VertexIndicesItr())
|
|
{
|
|
FVector3d FromCentroid = Mesh->GetVertex(VID) - MeshCentroid;
|
|
FVector3d OnAxis = FromCentroid.ProjectOnToNormal(VectorOnPlane);
|
|
if (FVector3d::DistSquared(OnAxis, FromCentroid) > OnPlaneTolerance)
|
|
{
|
|
bAllOnAxis = false;
|
|
break;
|
|
}
|
|
}
|
|
if (bAllOnAxis)
|
|
{
|
|
if (bCanIgnoreDegenerateSymmetries)
|
|
{
|
|
return false;
|
|
}
|
|
else
|
|
{
|
|
FVector3d Normal = VectorUtil::MakePerpVector(VectorOnPlane);
|
|
SymmetryFrameOut = FFrame3d(MeshCentroid, Normal);
|
|
return AssignMatches(Mesh, VertexHash, Features, SymmetryFrameOut);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Get a list of clusters sorted by size, so we can start w/ the smallest cluster when finding symmetry
|
|
struct FClusterInfo
|
|
{
|
|
int32 StartIdx = -1, Size = 0;
|
|
};
|
|
TArray<FClusterInfo> SortedClusters;
|
|
for (int32 ContigStart = 0, NextStart = -1; ContigStart < PairCandidateComponentsArray.Num(); ContigStart = NextStart)
|
|
{
|
|
int32 ComponentID = PairCandidateComponents.GetComponent(PairCandidateComponentsArray[ContigStart]);
|
|
int32 ComponentSize = PairCandidateComponents.GetComponentSize(ComponentID);
|
|
NextStart = ContigStart + ComponentSize;
|
|
SortedClusters.Add(FClusterInfo{ ContigStart, ComponentSize });
|
|
}
|
|
SortedClusters.Sort([&](const FClusterInfo& A, const FClusterInfo& B) -> bool
|
|
{
|
|
return A.Size < B.Size;
|
|
});
|
|
|
|
// Helper to update our symmetry axis and plane estimate, when we have a new point that must by on the plane.
|
|
// Returns true if a plane normal has been successfully estimated using the point, false if we still don't have a plane estimate.
|
|
// (Note: We want to use a tolerance similar to ExtremePoints, but internally ExtremePoints scales by magnitude, so we scale by BoundsMaxDim)
|
|
const double AddPointDistanceToleranceSq = ExtremePointsTolerance * ExtremePointsTolerance * BoundsMaxDim * BoundsMaxDim;
|
|
auto AddPointOnPlane = [&bHasVectorOnPlane, &VectorOnPlane, MeshCentroid, AddPointDistanceToleranceSq](FVector3d Point, FVector3d& NormalOut) -> bool
|
|
{
|
|
FVector3d ToPoint = Point - MeshCentroid;
|
|
double ToPointLenSq = ToPoint.SquaredLength();
|
|
if (ToPointLenSq < AddPointDistanceToleranceSq)
|
|
{
|
|
return false;
|
|
}
|
|
if (bHasVectorOnPlane)
|
|
{
|
|
double DistSqToAxis = FVector3d::DistSquared(ToPoint.ProjectOnToNormal(VectorOnPlane), ToPoint);
|
|
if (DistSqToAxis > AddPointDistanceToleranceSq)
|
|
{
|
|
NormalOut = ToPoint.Cross(VectorOnPlane);
|
|
return NormalOut.Normalize();
|
|
}
|
|
}
|
|
else
|
|
{
|
|
VectorOnPlane = ToPoint;
|
|
bHasVectorOnPlane = ToPoint.Normalize();
|
|
}
|
|
return false;
|
|
};
|
|
|
|
// Process the clusters of possible symmetry pairs from smallest to largest
|
|
for (const FClusterInfo& Cluster : SortedClusters)
|
|
{
|
|
int32 FirstVID = PairCandidateComponentsArray[Cluster.StartIdx];
|
|
if (!Mesh->IsVertex(FirstVID))
|
|
{
|
|
continue;
|
|
}
|
|
// clusters of size 1 are their own centroid, and we already processed the centroids,
|
|
// so we can't get any new information from them
|
|
if (Cluster.Size == 1)
|
|
{
|
|
continue;
|
|
}
|
|
// Use ExtremePoints to find reference points to look for mirror symmetry; if there are any points in the cluster
|
|
// that are not on the mirror symmetry plane, at least one of them should be in ExtremePoints
|
|
FExtremePoints3d ExtremePoints(Cluster.Size, [&](int32 WithinClusterIdx) -> FVector3d
|
|
{
|
|
int32 VID = PairCandidateComponentsArray[Cluster.StartIdx + WithinClusterIdx];
|
|
return Mesh->GetVertex(VID);
|
|
}, [](int32 Idx) { return true; }, ExtremePointsTolerance);
|
|
// helper to avoid repeating evaluation of extreme points
|
|
auto IsPointExtreme = [ExtremePoints](int32 WithinClusterIdx, int32 NumExtremeToConsider)
|
|
{
|
|
for (int32 ExtremeIdx = 0; ExtremeIdx < NumExtremeToConsider; ++ExtremeIdx)
|
|
{
|
|
if (WithinClusterIdx == ExtremePoints.Extreme[ExtremeIdx])
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
for (int32 ExtremePtIdx = 0; ExtremePtIdx < ExtremePoints.Dimension + 1; ++ExtremePtIdx)
|
|
{
|
|
int32 RefVID = PairCandidateComponentsArray[Cluster.StartIdx + ExtremePoints.Extreme[ExtremePtIdx]];
|
|
FVector3d RefPos = Mesh->GetVertex(RefVID);
|
|
FVector3d RefFeature = Features[RefVID];
|
|
// Try pairing the extreme point with every other vertex in the cluster, and checking if that creates a mirror plane
|
|
// Note this sounds O(ClusterSize*MeshSize) but in practice most bad matches can be rejected by just testing a few points,
|
|
// so it's often closer to just ~ ClusterSize work (or ClusterSize+MeshSize if it finds a symmetry plane)
|
|
for (int32 WithinClusterIdx = 0; WithinClusterIdx < Cluster.Size; ++WithinClusterIdx)
|
|
{
|
|
// Skip the point if it's the current or a previously-considered extreme point
|
|
if (IsPointExtreme(WithinClusterIdx, ExtremePtIdx + 1))
|
|
{
|
|
continue;
|
|
}
|
|
int32 VID = PairCandidateComponentsArray[Cluster.StartIdx + WithinClusterIdx];
|
|
checkSlow(VID != RefVID);
|
|
FVector3d Pos = Mesh->GetVertex(VID);
|
|
FVector3d Normal = Pos - RefPos;
|
|
// Reject the plane if we can't compute its normal because the points are too close
|
|
if (!Normal.Normalize())
|
|
{
|
|
continue;
|
|
}
|
|
// If we know a vector on the plane is known, sample points should project to the same position w.r.t. that
|
|
// (i.e., plane normal should be perpendicular to the vector on the plane)
|
|
if (bHasVectorOnPlane && FMath::IsNearlyEqual(VectorOnPlane.Dot(Pos), VectorOnPlane.Dot(RefPos), MatchVertexTolerance))
|
|
{
|
|
continue;
|
|
}
|
|
// Reject the plane if the points do not have matching feature vectors
|
|
if (FVector3d::DistSquared(RefFeature, Features[VID]) > MatchFeaturesTolerance * MatchFeaturesTolerance)
|
|
{
|
|
FVector3d Diff = Features[VID] - RefFeature;
|
|
continue;
|
|
}
|
|
FVector3d RefinedNormal;
|
|
if (TestPlane(Normal, RefinedNormal))
|
|
{
|
|
SymmetryFrameOut = FFrame3d(MeshCentroid, RefinedNormal);
|
|
return AssignMatches(Mesh, VertexHash, Features, SymmetryFrameOut);
|
|
}
|
|
}
|
|
|
|
// Failed to find a symmetry plane from the matches in this cluster, so if the mesh is symmetric the point must
|
|
// be on the symmetry plane. Update what we know about the symmetry plane w/ this point, and early-out if it has given us the plane
|
|
FVector3d Normal;
|
|
if (AddPointOnPlane(Mesh->GetVertex(FirstVID), Normal))
|
|
{
|
|
SymmetryFrameOut = FFrame3d(MeshCentroid, Normal);
|
|
return AssignMatches(Mesh, VertexHash, Features, SymmetryFrameOut);
|
|
}
|
|
}
|
|
// Failed to find a symmetry plane within the whole cluster; all of its point must be on the plane
|
|
for (int32 WithinClusterIdx = 0; WithinClusterIdx < Cluster.Size; ++WithinClusterIdx)
|
|
{
|
|
if (IsPointExtreme(WithinClusterIdx, ExtremePoints.Dimension + 1))
|
|
{
|
|
continue; // already added the extreme points in the above loop
|
|
}
|
|
int32 VID = PairCandidateComponentsArray[Cluster.StartIdx + WithinClusterIdx];
|
|
FVector3d Normal;
|
|
if (AddPointOnPlane(Mesh->GetVertex(FirstVID), Normal))
|
|
{
|
|
SymmetryFrameOut = FFrame3d(MeshCentroid, Normal);
|
|
return AssignMatches(Mesh, VertexHash, Features, SymmetryFrameOut);
|
|
}
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool FMeshPlanarSymmetry::AssignMatches(const FDynamicMesh3* Mesh, const TPointHashGrid3d<int32>& VertexHash, const TArray<FVector3d>& InvariantFeatures, FFrame3d SymmetryFrameIn)
|
|
{
|
|
this->SymmetryFrame = SymmetryFrameIn;
|
|
this->CachedSymmetryAxis = SymmetryFrameIn.Z();
|
|
Vertices.Init(FSymmetryVertex(), TargetMesh->MaxVertexID());
|
|
|
|
TArray<int32> PositiveVerts;
|
|
PositiveVerts.Reserve(Vertices.Num());
|
|
|
|
for (int32 VID : Mesh->VertexIndicesItr())
|
|
{
|
|
FVector3d Position = Mesh->GetVertex(VID);
|
|
Vertices[VID].PlaneSignedDistance = (Position - SymmetryFrame.Origin).Dot(CachedSymmetryAxis);
|
|
if (Vertices[VID].PlaneSignedDistance > FMathd::ZeroTolerance)
|
|
{
|
|
PositiveVerts.Add(VID);
|
|
Vertices[VID].bIsSourceVertex = true;
|
|
}
|
|
else
|
|
{
|
|
Vertices[VID].bIsSourceVertex = false;
|
|
}
|
|
}
|
|
|
|
// Whether to reattempt matches w/out feature-based filtering, if a match-failure would break symmetry overall
|
|
constexpr bool bReattemptMatchesOnFailure = true;
|
|
|
|
double OnPlaneTolerance = OnPlaneToleranceFactor * ErrorScale;
|
|
double MatchVertexTolerance = MatchVertexToleranceFactor * ErrorScale;
|
|
|
|
// Compute unique matching vertices on mirror side (negative side of plane) for positive-side vertices
|
|
ParallelFor(PositiveVerts.Num(), [&](int32 k)
|
|
{
|
|
int32 VertexID = PositiveVerts[k];
|
|
|
|
|
|
FVector3d Position = Mesh->GetVertex(VertexID);
|
|
FVector3d MirrorPosition = GetMirroredPosition(Position);
|
|
|
|
// find vertex that is closest to mirrored position
|
|
FVector3d Feature = InvariantFeatures[VertexID];
|
|
double MatchFeaturesToleranceSqr = MatchFeaturesTolerance * MatchFeaturesTolerance;
|
|
TPair<int32, double> NearestInfo = VertexHash.FindNearestInRadius(MirrorPosition, MatchVertexTolerance, [MirrorPosition, Mesh](const int32& OtherVID)
|
|
{
|
|
return FVector3d::DistSquared(MirrorPosition, Mesh->GetVertex(OtherVID));
|
|
}
|
|
, [Feature, VertexID, this, &InvariantFeatures, MatchFeaturesToleranceSqr](const int32& OtherVID) -> bool // Ignore filter fn returns true for vertices we want to skip
|
|
{
|
|
return OtherVID == VertexID // don't match to self
|
|
|| Vertices[OtherVID].bIsSourceVertex // don't match to other positive vertices
|
|
|| FVector3d::DistSquared(InvariantFeatures[OtherVID], Feature) > MatchFeaturesToleranceSqr; // don't match if the features don't match
|
|
}
|
|
);
|
|
int32 NearestVID = NearestInfo.Key;
|
|
if (NearestVID != INDEX_NONE)
|
|
{
|
|
Vertices[NearestVID].PairedVertex = VertexID;
|
|
Vertices[VertexID].PairedVertex = NearestVID;
|
|
}
|
|
// If a failed match would break symmetry overall, we can retry without feature-based filtering
|
|
else if (bReattemptMatchesOnFailure && Vertices[VertexID].PlaneSignedDistance >= OnPlaneTolerance)
|
|
{
|
|
TPair<int32, double> FeaturelessNearestInfo = VertexHash.FindNearestInRadius(MirrorPosition, MatchVertexTolerance, [MirrorPosition, Mesh](const int32& OtherVID)
|
|
{
|
|
return FVector3d::DistSquared(MirrorPosition, Mesh->GetVertex(OtherVID));
|
|
}
|
|
, [Feature, VertexID, this, &InvariantFeatures, MatchFeaturesToleranceSqr](const int32& OtherVID) -> bool // Ignore filter fn returns true for vertices we want to skip
|
|
{
|
|
return OtherVID == VertexID // don't match to self
|
|
|| Vertices[OtherVID].bIsSourceVertex; // don't match to other positive vertices
|
|
}
|
|
);
|
|
NearestVID = FeaturelessNearestInfo.Key;
|
|
if (NearestVID != INDEX_NONE)
|
|
{
|
|
Vertices[NearestVID].PairedVertex = VertexID;
|
|
Vertices[VertexID].PairedVertex = NearestVID;
|
|
}
|
|
}
|
|
});
|
|
|
|
|
|
// If a vertex has no match and is within the symmetry-plane tolerance band, consider it a plane vertex.
|
|
// Otherwise it is a failed match
|
|
int32 NumFailedMatches = 0;
|
|
for (int32 VertexID : Mesh->VertexIndicesItr())
|
|
{
|
|
if (Vertices[VertexID].PairedVertex < 0)
|
|
{
|
|
if (FMathd::Abs(Vertices[VertexID].PlaneSignedDistance) < OnPlaneTolerance)
|
|
{
|
|
Vertices[VertexID].bIsSourceVertex = false;
|
|
Vertices[VertexID].bOnPlane = true;
|
|
}
|
|
else
|
|
{
|
|
NumFailedMatches++;
|
|
}
|
|
}
|
|
}
|
|
|
|
return (NumFailedMatches == 0);
|
|
}
|
|
|
|
|
|
bool FMeshPlanarSymmetry::Initialize(FDynamicMesh3* Mesh, FDynamicMeshAABBTree3* Spatial, FFrame3d SymmetryFrameIn)
|
|
{
|
|
return Initialize(Mesh, Spatial->GetBoundingBox(), SymmetryFrameIn);
|
|
}
|
|
|
|
bool FMeshPlanarSymmetry::Initialize(FDynamicMesh3* Mesh, const FAxisAlignedBox3d& Bounds, FFrame3d SymmetryFrameIn)
|
|
{
|
|
this->TargetMesh = Mesh;
|
|
if (!Validate(Mesh))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
SetErrorScale(Bounds);
|
|
|
|
// Compute vertex hash and features
|
|
FVector3d MeshCentroid;
|
|
TArray<FVector3d> Features;
|
|
ComputeMeshInfo(Mesh, Bounds, Features, MeshCentroid);
|
|
|
|
// Quick reject test: MeshCentroid must be on desired symmetry plane
|
|
double CentroidPlaneSignedDist = (MeshCentroid - SymmetryFrameIn.Origin).Dot(SymmetryFrameIn.Z());
|
|
if (FMath::Abs(CentroidPlaneSignedDist) > OnPlaneToleranceFactor * ErrorScale)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// Hash the vertices.
|
|
// TODO: For this case where we only test one plane, we could just hash the vertices w/ plane signed dist <= FMathd::ZeroTolerance ...
|
|
// (but that would be better done inside AssignMatches, where it's clearer exactly how the hash will be used, so code would need some restructuring)
|
|
TPointHashGrid3d<int32> VertexHash(VertexHashCellSizeFactor * ErrorScale, INDEX_NONE);
|
|
VertexHash.Reserve(Mesh->VertexCount());
|
|
for (int32 VID : Mesh->VertexIndicesItr())
|
|
{
|
|
VertexHash.InsertPointUnsafe(VID, Mesh->GetVertex(VID));
|
|
}
|
|
|
|
return AssignMatches(Mesh, VertexHash, Features, SymmetryFrameIn);
|
|
}
|
|
|
|
|
|
|
|
FFrame3d FMeshPlanarSymmetry::GetPositiveSideFrame(FFrame3d FromFrame) const
|
|
{
|
|
FVector3d DeltaVec = (FromFrame.Origin - SymmetryFrame.Origin);
|
|
double SignedDistance = DeltaVec.Dot(CachedSymmetryAxis);
|
|
if (SignedDistance < 0)
|
|
{
|
|
FromFrame.Origin = GetMirroredPosition(FromFrame.Origin);
|
|
FromFrame.Rotation = GetMirroredOrientation(FromFrame.Rotation);
|
|
}
|
|
return FromFrame;
|
|
}
|
|
|
|
|
|
|
|
FVector3d FMeshPlanarSymmetry::GetMirroredPosition(const FVector3d& Position) const
|
|
{
|
|
FVector3d DeltaVec = (Position - SymmetryFrame.Origin);
|
|
double SignedDistance = DeltaVec.Dot(CachedSymmetryAxis);
|
|
FVector3d MirrorPosition = Position - (2 * SignedDistance * CachedSymmetryAxis);
|
|
return MirrorPosition;
|
|
}
|
|
|
|
FVector3d FMeshPlanarSymmetry::GetMirroredAxis(const FVector3d& Axis) const
|
|
{
|
|
double SignedDistance = Axis.Dot(CachedSymmetryAxis);
|
|
FVector3d MirrorAxis = Axis - (2 * SignedDistance * CachedSymmetryAxis);
|
|
return MirrorAxis;
|
|
}
|
|
|
|
FQuaterniond FMeshPlanarSymmetry::GetMirroredOrientation(const FQuaterniond& Orientation) const
|
|
{
|
|
FVector3d Axis(Orientation.X, Orientation.Y, Orientation.Z);
|
|
Axis = GetMirroredAxis(Axis);
|
|
return FQuaterniond(Axis.X, Axis.Y, Axis.Z, -Orientation.W);
|
|
}
|
|
|
|
|
|
void FMeshPlanarSymmetry::GetMirrorVertexROI(const TArray<int>& VertexROI, TArray<int>& MirrorVertexROI, bool bForceSameSizeWithGaps) const
|
|
{
|
|
MirrorVertexROI.Reset();
|
|
if (bForceSameSizeWithGaps)
|
|
{
|
|
MirrorVertexROI.Reserve(VertexROI.Num());
|
|
}
|
|
|
|
for (int32 VertexID : VertexROI)
|
|
{
|
|
const FSymmetryVertex& Vertex = Vertices[VertexID];
|
|
if (Vertex.bIsSourceVertex)
|
|
{
|
|
int32 MirrorVID = Vertex.PairedVertex;
|
|
MirrorVertexROI.Add(MirrorVID);
|
|
}
|
|
else if (Vertex.bIsSourceVertex == false && Vertex.bOnPlane == false && Vertex.PairedVertex >= 0)
|
|
{
|
|
// The ROI already contains the mirror-vertex, we will keep it so that it can be updated later
|
|
MirrorVertexROI.Add(VertexID);
|
|
}
|
|
else if (bForceSameSizeWithGaps)
|
|
{
|
|
MirrorVertexROI.Add(-1);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
void FMeshPlanarSymmetry::ApplySymmetryPlaneConstraints(const TArray<int>& VertexIndices, TArray<FVector3d>& VertexPositions) const
|
|
{
|
|
int32 N = VertexIndices.Num();
|
|
check(N == VertexPositions.Num());
|
|
for (int32 k = 0; k < N; ++k)
|
|
{
|
|
int32 VertexID = VertexIndices[k];
|
|
const FSymmetryVertex& Vertex = Vertices[VertexID];
|
|
if (Vertex.bOnPlane)
|
|
{
|
|
VertexPositions[k] = SymmetryFrame.ToPlane(VertexPositions[k]);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
namespace MeshPlanarSymmetryLocals
|
|
{
|
|
template<bool bSkipNonSourceVertices>
|
|
inline bool ValidateVertexSymmetry(const FDynamicMesh3& TestMesh, int32 Idx,
|
|
const bool bOnPlane, const bool bIsSourceVertex, const int32 PairedVertex,
|
|
const FVector3d& Origin, const FVector3d& Normal, double OnPlaneTolerance, double MatchVertexToleranceSq)
|
|
{
|
|
FVector3d Pos = TestMesh.GetVertex(Idx);
|
|
if (bOnPlane)
|
|
{
|
|
const double SignedDistance = (Pos - Origin).Dot(Normal);
|
|
if (FMath::Abs(SignedDistance) > OnPlaneTolerance)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if constexpr (bSkipNonSourceVertices)
|
|
{
|
|
if (!bIsSourceVertex)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
const int32 PairedVertexIdx = PairedVertex;
|
|
if (!TestMesh.IsVertex(PairedVertexIdx))
|
|
{
|
|
return false;
|
|
}
|
|
FVector3d PairedPos = TestMesh.GetVertex(PairedVertexIdx);
|
|
const double SignedDistance = (Pos - Origin).Dot(Normal);
|
|
FVector3d MirrorPos = Pos - (2 * SignedDistance * Normal);
|
|
if (FVector3d::DistSquared(MirrorPos, PairedPos) > MatchVertexToleranceSq)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
bool FMeshPlanarSymmetry::ValidateSymmetry(const FDynamicMesh3& TestMesh) const
|
|
{
|
|
if (TestMesh.MaxVertexID() != Vertices.Num())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
const double OnPlaneTolerance = OnPlaneToleranceFactor * ErrorScale;
|
|
const double MatchVertexToleranceSq = (MatchVertexToleranceFactor * ErrorScale) * (MatchVertexToleranceFactor * ErrorScale);
|
|
|
|
for (int32 Idx = 0; Idx < Vertices.Num(); ++Idx)
|
|
{
|
|
if (!TestMesh.IsVertex(Idx))
|
|
{
|
|
continue;
|
|
}
|
|
const FSymmetryVertex& SV = Vertices[Idx];
|
|
if (!MeshPlanarSymmetryLocals::ValidateVertexSymmetry<true>(TestMesh, Idx, SV.bOnPlane, SV.bIsSourceVertex, SV.PairedVertex,
|
|
SymmetryFrame.Origin, CachedSymmetryAxis, OnPlaneTolerance, MatchVertexToleranceSq))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
bool FMeshPlanarSymmetry::ValidateSymmetry(const FDynamicMesh3& TestMesh, TConstArrayView<int32> VertexROI) const
|
|
{
|
|
const double OnPlaneTolerance = OnPlaneToleranceFactor * ErrorScale;
|
|
const double MatchVertexToleranceSq = (MatchVertexToleranceFactor * ErrorScale) * (MatchVertexToleranceFactor * ErrorScale);
|
|
|
|
for (int32 Idx : VertexROI)
|
|
{
|
|
// All ROI vertices must be valid
|
|
if (!TestMesh.IsVertex(Idx) || !Vertices.IsValidIndex(Idx))
|
|
{
|
|
return false;
|
|
}
|
|
const FSymmetryVertex& SV = Vertices[Idx];
|
|
if (!MeshPlanarSymmetryLocals::ValidateVertexSymmetry<false>(TestMesh, Idx, SV.bOnPlane, SV.bIsSourceVertex, SV.PairedVertex,
|
|
SymmetryFrame.Origin, CachedSymmetryAxis, OnPlaneTolerance, MatchVertexToleranceSq))
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
void FMeshPlanarSymmetry::ComputeSymmetryConstrainedPositions(
|
|
const TArray<int>& SourceVertexROI,
|
|
const TArray<int>& MirrorVertexROI,
|
|
const TArray<FVector3d>& SourceVertexPositions,
|
|
TArray<FVector3d>& MirrorVertexPositionsOut) const
|
|
{
|
|
int32 NumV = SourceVertexROI.Num();
|
|
check(MirrorVertexROI.Num() == NumV);
|
|
check(SourceVertexPositions.Num() == NumV);
|
|
|
|
MirrorVertexPositionsOut.SetNum(NumV, EAllowShrinking::No);
|
|
|
|
for (int32 k = 0; k < NumV; ++k)
|
|
{
|
|
int32 MirrorVertexID = MirrorVertexROI[k];
|
|
if (MirrorVertexID >= 0)
|
|
{
|
|
int32 SourceVertexID = SourceVertexROI[k];
|
|
if (SourceVertexID == MirrorVertexID)
|
|
{
|
|
SourceVertexID = Vertices[MirrorVertexID].PairedVertex;
|
|
MirrorVertexPositionsOut[k] = GetMirroredPosition(TargetMesh->GetVertex(SourceVertexID));
|
|
}
|
|
else
|
|
{
|
|
check(Vertices[SourceVertexID].PairedVertex == MirrorVertexID);
|
|
MirrorVertexPositionsOut[k] = GetMirroredPosition(SourceVertexPositions[k]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void FMeshPlanarSymmetry::FullSymmetryUpdate()
|
|
{
|
|
for (int32 vid : TargetMesh->VertexIndicesItr())
|
|
{
|
|
if (Vertices[vid].bIsSourceVertex)
|
|
{
|
|
UpdateSourceVertex(vid);
|
|
}
|
|
else if (Vertices[vid].bOnPlane)
|
|
{
|
|
UpdatePlaneVertex(vid);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
void FMeshPlanarSymmetry::UpdateSourceVertex(int32 VertexID)
|
|
{
|
|
FVector3d CurPosition = TargetMesh->GetVertex(VertexID);
|
|
int32 MirrorVID = Vertices[VertexID].PairedVertex;
|
|
check(TargetMesh->IsVertex(MirrorVID));
|
|
FVector3d MirrorPosition = GetMirroredPosition(CurPosition);
|
|
TargetMesh->SetVertex(MirrorVID, MirrorPosition);
|
|
}
|
|
|
|
void FMeshPlanarSymmetry::UpdatePlaneVertex(int32 VertexID)
|
|
{
|
|
FVector3d CurPosition = TargetMesh->GetVertex(VertexID);
|
|
FVector3d PlanePos = SymmetryFrame.ToPlane(CurPosition);
|
|
TargetMesh->SetVertex(VertexID, PlanePos);
|
|
}
|
|
|
|
|