You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
- Use octree to speed up closest-edge finding - Center and rescale data before computing intersections (and put back to original space after operation is complete) - Add option to skip edge welding, matching MeshBoolean - Minor comment and variable name cleanup #rb david.hill #rnx [CL 14824234 by Jimmy Andrews in ue5-main branch]
798 lines
25 KiB
C++
798 lines
25 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
// Port of geometry3Sharp MeshBoolean
|
|
|
|
#include "Operations/MeshBoolean.h"
|
|
#include "Operations/MeshMeshCut.h"
|
|
|
|
#include "Async/ParallelFor.h"
|
|
#include "MeshTransforms.h"
|
|
#include "Spatial/SparseDynamicOctree3.h"
|
|
|
|
#include "Algo/RemoveIf.h"
|
|
|
|
#include "DynamicMeshAABBTree3.h"
|
|
|
|
|
|
|
|
// TODO: Commented out below is a custom thick triangle intersection.
|
|
// It's much better at finding all the near-tolerance collision edges.
|
|
// But it ends up creating harder problems downstream in terms of
|
|
// tiny edges, almost-exactly-at-tolerance coplanar faces, etc.
|
|
// Consider re-enabling it in combination with more robust downstream processing!
|
|
|
|
//// helper for ThickTriTriIntersection, using the plane of Tri0 as reference
|
|
//// (factored out so we can try using both planes as reference, and make the result less dependent on triangle ordering)
|
|
//bool ThickTriTriHelper(const FTriangle3d& Tri0, const FTriangle3d& Tri1, const FPlane3d& Plane0,
|
|
// const FVector3d& IntersectionDir, const FVector3d& dist1, const FIndex3i& sign1,
|
|
// int pos1, int neg1, int zero1,
|
|
// FIntrTriangle3Triangle3d& Intr, double Tolerance)
|
|
//{
|
|
// int SegmentsFound = 0;
|
|
// int PtsFound[2]{ 0,0 }; // points found on the positive and negative sides
|
|
// FVector3d CrossingPts[4]; // space for triangle-plane intersection segments; with negative-side endpoints first
|
|
// int PtsSide[4]; // -1, 1 or 0
|
|
//
|
|
// // offset tolerance -- used to accept intersections off the plane, when we'd otherwise miss "near intersections"
|
|
// double ToleranceOffset = Tolerance * .99; // scale down to be extra sure not to create un-snappable geometry
|
|
// // only accept 'offset plane' points if not doing so would miss a much-larger-than-tolerance edge
|
|
// double AcceptOffsetPointsThresholdSq = 1e-2 * 1e-2;
|
|
//
|
|
// double InPlaneTolerance = FMathd::ZeroTolerance;
|
|
//
|
|
// // consider all crossings
|
|
// for (int i = 0, lasti = 2; i < 3; lasti = i++)
|
|
// {
|
|
// if (sign1[lasti] == sign1[i])
|
|
// {
|
|
// continue;
|
|
// }
|
|
// //
|
|
// if (sign1[lasti] == 0 || sign1[i] == 0)
|
|
// {
|
|
// int nzi = lasti, zi = i;
|
|
// if (sign1[lasti] == 0)
|
|
// {
|
|
// nzi = i;
|
|
// zi = lasti;
|
|
// }
|
|
// int SideIdx = (sign1[nzi] + 1) / 2;
|
|
// int PtIdx = SideIdx * 2 + PtsFound[SideIdx];
|
|
// int Side = sign1[nzi];
|
|
//
|
|
// double ParamOnTolPlane = (dist1[nzi] - sign1[nzi] * ToleranceOffset) / (dist1[nzi] - dist1[zi]);
|
|
// FVector3d IntrPt;
|
|
// if (ParamOnTolPlane < 1)
|
|
// {
|
|
// IntrPt = Tri1.V[nzi] + (Tri1.V[zi] - Tri1.V[nzi]) * ParamOnTolPlane;
|
|
// if (IntrPt.DistanceSquared(Tri1.V[zi]) < AcceptOffsetPointsThresholdSq)
|
|
// {
|
|
// Side = 0;
|
|
// IntrPt = Tri1.V[zi];
|
|
// }
|
|
// }
|
|
// else
|
|
// {
|
|
// IntrPt = Tri1.V[zi];
|
|
// }
|
|
//
|
|
// // record crossing pt
|
|
// PtsSide[PtIdx] = Side;
|
|
// CrossingPts[PtIdx] = IntrPt;
|
|
// PtsFound[SideIdx]++;
|
|
// }
|
|
// else
|
|
// {
|
|
// double OffsetParamDiff = sign1[i] * ToleranceOffset / (dist1[i] - dist1[lasti]);
|
|
// FVector3d Edge = Tri1.V[lasti] - Tri1.V[i];
|
|
// double OffsetDSq = Edge.SquaredLength() * OffsetParamDiff * OffsetParamDiff;
|
|
// if (OffsetDSq < AcceptOffsetPointsThresholdSq)
|
|
// {
|
|
// FVector3d IntrPt = Tri1.V[i] + Edge * dist1[i] / (dist1[i] - dist1[lasti]);
|
|
// for (int SideIdx = 0; SideIdx < 2; SideIdx++)
|
|
// {
|
|
// int PtIdx = SideIdx * 2 + PtsFound[SideIdx];
|
|
// PtsSide[PtIdx] = 0;
|
|
// CrossingPts[PtIdx] = IntrPt;
|
|
// PtsFound[SideIdx]++;
|
|
// }
|
|
// }
|
|
// else
|
|
// {
|
|
// for (int Sign = -1; Sign <= 1; Sign += 2)
|
|
// {
|
|
// double ParamOnPlane = (dist1[i] - Sign * ToleranceOffset) / (dist1[i] - dist1[lasti]);
|
|
// FVector3d IntrPt = Tri1.V[i] + (Tri1.V[lasti] - Tri1.V[i]) * ParamOnPlane;
|
|
// int SideIdx = (Sign + 1) / 2;
|
|
// int PtIdx = SideIdx * 2 + PtsFound[SideIdx];
|
|
// PtsSide[PtIdx] = Sign;
|
|
// CrossingPts[PtIdx] = IntrPt;
|
|
// PtsFound[SideIdx]++;
|
|
// }
|
|
// }
|
|
// }
|
|
// }
|
|
//
|
|
// bool bMadeZeroEdge = false;
|
|
// int AddedPts = 0;
|
|
// for (int SideIdx = 0; SideIdx < 2; SideIdx++)
|
|
// {
|
|
// if (PtsFound[SideIdx] == 2)
|
|
// {
|
|
// int PtIdx0 = SideIdx * 2;
|
|
// if (PtsSide[PtIdx0] == 0 && PtsSide[PtIdx0 + 1] == 0)
|
|
// {
|
|
// if (bMadeZeroEdge)
|
|
// {
|
|
// continue;
|
|
// }
|
|
// bMadeZeroEdge = true;
|
|
// }
|
|
// FVector3d OutA, OutB;
|
|
// int IntrQ = FIntrTriangle3Triangle3d::IntersectTriangleWithCoplanarSegment(Plane0, Tri0, CrossingPts[PtIdx0], CrossingPts[PtIdx0 + 1], OutA, OutB, InPlaneTolerance);
|
|
//
|
|
// if (IntrQ == 2)
|
|
// {
|
|
// Intr.Points[AddedPts++] = OutA;
|
|
// Intr.Points[AddedPts++] = OutB;
|
|
// }
|
|
// }
|
|
// }
|
|
// Intr.Quantity = AddedPts;
|
|
// if (AddedPts == 4)
|
|
// {
|
|
// Intr.Result = EIntersectionResult::Intersects;
|
|
// Intr.Type = EIntersectionType::MultiSegment;
|
|
// }
|
|
// else if (AddedPts == 2)
|
|
// {
|
|
// Intr.Result = EIntersectionResult::Intersects;
|
|
// Intr.Type = EIntersectionType::Segment;
|
|
// }
|
|
// else
|
|
// {
|
|
// Intr.Result = EIntersectionResult::NoIntersection;
|
|
// Intr.Type = EIntersectionType::Empty;
|
|
// return false;
|
|
// }
|
|
//
|
|
// return true;
|
|
//}
|
|
//
|
|
//bool ThickTriTriIntersection(FIntrTriangle3Triangle3d& Intr, double Tolerance)
|
|
//{
|
|
// // intersection tolerance is applied one dimension at a time, so we scale down by 1/sqrt(2)
|
|
// // to ensure approximations remain within snapping distance
|
|
// Intr.SetTolerance(Tolerance);
|
|
// const FTriangle3d& Triangle0 = Intr.GetTriangle0();
|
|
// const FTriangle3d& Triangle1 = Intr.GetTriangle1();
|
|
// FPlane3d Plane0(Triangle0.V[0], Triangle0.V[1], Triangle0.V[2]);
|
|
//
|
|
// // Compute the signed distances of Triangle1 vertices to Plane0. Use an epsilon-thick plane test.
|
|
// int pos1, neg1, zero1;
|
|
// FVector3d dist1;
|
|
// FIndex3i sign1;
|
|
// FIntrTriangle3Triangle3d::TrianglePlaneRelations(Triangle1, Plane0, dist1, sign1, pos1, neg1, zero1, Tolerance);
|
|
// if (pos1 == 3 || neg1 == 3)
|
|
// {
|
|
// // ignore triangles that are more than tolerance-separated
|
|
// Intr.SetResultNone();
|
|
// return false;
|
|
// }
|
|
//
|
|
// FPlane3d Plane1(Triangle1.V[0], Triangle1.V[1], Triangle1.V[2]);
|
|
// FVector3d IntersectionDir = Plane0.Normal.Cross(Plane1.Normal);
|
|
//
|
|
// FVector3d SegA, SegB;
|
|
// bool bFound = false;
|
|
// bFound = zero1 < 3 && ThickTriTriHelper(Triangle0, Triangle1, Plane0, IntersectionDir, dist1, sign1, pos1, neg1, zero1, Intr, Tolerance);
|
|
// if (!bFound || Intr.Quantity == 1)
|
|
// {
|
|
// int pos0, neg0, zero0;
|
|
// FVector3d dist0;
|
|
// FIndex3i sign0;
|
|
// FIntrTriangle3Triangle3d::TrianglePlaneRelations(Triangle0, Plane1, dist0, sign0, pos0, neg0, zero0, Tolerance);
|
|
// bFound = zero1 < 3 && ThickTriTriHelper(Triangle1, Triangle0, Plane1, IntersectionDir, dist0, sign0, pos0, neg0, zero0, Intr, Tolerance);
|
|
// }
|
|
// if (!bFound) // make sure the Intr values are set in the coplanar case
|
|
// {
|
|
// Intr.SetResultNone();
|
|
// }
|
|
//
|
|
// return bFound;
|
|
//}
|
|
|
|
bool FMeshBoolean::Compute()
|
|
{
|
|
// copy meshes
|
|
FDynamicMesh3 CutMeshB(*Meshes[1]);
|
|
if (Result != Meshes[0])
|
|
{
|
|
*Result = *Meshes[0];
|
|
}
|
|
FDynamicMesh3* CutMesh[2]{ Result, &CutMeshB }; // just an alias to keep things organized
|
|
|
|
// transform the copies to a shared space (centered at the origin and scaled to a unit cube)
|
|
FAxisAlignedBox3d CombinedAABB(CutMesh[0]->GetCachedBounds(), Transforms[0]);
|
|
FAxisAlignedBox3d MeshB_AABB(CutMesh[1]->GetCachedBounds(), Transforms[1]);
|
|
CombinedAABB.Contain(MeshB_AABB);
|
|
double ScaleFactor = 1.0 / FMath::Clamp(CombinedAABB.MaxDim(), 0.01, 1000000.0);
|
|
for (int MeshIdx = 0; MeshIdx < 2; MeshIdx++)
|
|
{
|
|
FTransform3d CenteredTransform = Transforms[MeshIdx];
|
|
CenteredTransform.SetTranslation(ScaleFactor*(CenteredTransform.GetTranslation() - CombinedAABB.Center()));
|
|
CenteredTransform.SetScale(ScaleFactor*CenteredTransform.GetScale());
|
|
MeshTransforms::ApplyTransform(*CutMesh[MeshIdx], CenteredTransform);
|
|
if (CenteredTransform.GetDeterminant() < 0)
|
|
{
|
|
CutMesh[MeshIdx]->ReverseOrientation(false);
|
|
}
|
|
}
|
|
ResultTransform = FTransform3d(CombinedAABB.Center());
|
|
ResultTransform.SetScale(FVector3d::One() * (1.0 / ScaleFactor));
|
|
|
|
if (Cancelled())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// build spatial data and use it to find intersections
|
|
FDynamicMeshAABBTree3 Spatial[2]{ CutMesh[0], CutMesh[1] };
|
|
Spatial[0].SetTolerance(SnapTolerance);
|
|
Spatial[1].SetTolerance(SnapTolerance);
|
|
MeshIntersection::FIntersectionsQueryResult Intersections
|
|
= Spatial[0].FindAllIntersections(Spatial[1], nullptr, IMeshSpatial::FQueryOptions(), IMeshSpatial::FQueryOptions(),
|
|
[this](FIntrTriangle3Triangle3d& Intr)
|
|
{
|
|
Intr.SetTolerance(SnapTolerance);
|
|
return Intr.Find();
|
|
|
|
// TODO: if we revisit "thick" tri tri collisions, this is where we'd call:
|
|
// ThickTriTriIntersection(Intr, SnapTolerance);
|
|
});
|
|
|
|
if (Cancelled())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// cut the meshes
|
|
FMeshMeshCut Cut(CutMesh[0], CutMesh[1]);
|
|
Cut.bTrackInsertedVertices = bCollapseDegenerateEdgesOnCut; // to collect candidates to collapse
|
|
Cut.bMutuallyCut = Operation != EBooleanOp::Trim;
|
|
Cut.SnapTolerance = SnapTolerance;
|
|
Cut.Cut(Intersections);
|
|
|
|
if (Cancelled())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
int NumMeshesToProcess = Operation == EBooleanOp::Trim ? 1 : 2;
|
|
|
|
// collapse tiny edges along cut boundary
|
|
if (bCollapseDegenerateEdgesOnCut)
|
|
{
|
|
double DegenerateEdgeTolSq = DegenerateEdgeTolFactor * DegenerateEdgeTolFactor * SnapTolerance * SnapTolerance;
|
|
for (int MeshIdx = 0; MeshIdx < NumMeshesToProcess; MeshIdx++)
|
|
{
|
|
// convert vertex chains to edge IDs to simplify logic of finding remaining candidate edges after collapses
|
|
TArray<int> EIDs;
|
|
for (int ChainIdx = 0; ChainIdx < Cut.VertexChains[MeshIdx].Num();)
|
|
{
|
|
int ChainLen = Cut.VertexChains[MeshIdx][ChainIdx];
|
|
int ChainEnd = ChainIdx + 1 + ChainLen;
|
|
for (int ChainSubIdx = ChainIdx + 1; ChainSubIdx + 1 < ChainEnd; ChainSubIdx++)
|
|
{
|
|
int VID[2]{ Cut.VertexChains[MeshIdx][ChainSubIdx], Cut.VertexChains[MeshIdx][ChainSubIdx + 1] };
|
|
if (CutMesh[MeshIdx]->GetVertex(VID[0]).DistanceSquared(CutMesh[MeshIdx]->GetVertex(VID[1])) < DegenerateEdgeTolSq)
|
|
{
|
|
EIDs.Add(CutMesh[MeshIdx]->FindEdge(VID[0], VID[1]));
|
|
}
|
|
}
|
|
ChainIdx = ChainEnd;
|
|
}
|
|
TSet<int> AllEIDs(EIDs);
|
|
for (int Idx = 0; Idx < EIDs.Num(); Idx++)
|
|
{
|
|
int EID = EIDs[Idx];
|
|
if (!CutMesh[MeshIdx]->IsEdge(EID))
|
|
{
|
|
continue;
|
|
}
|
|
FVector3d A, B;
|
|
CutMesh[MeshIdx]->GetEdgeV(EID, A, B);
|
|
if (A.DistanceSquared(B) > DegenerateEdgeTolSq)
|
|
{
|
|
continue;
|
|
}
|
|
FIndex2i EV = CutMesh[MeshIdx]->GetEdgeV(EID);
|
|
|
|
// if the vertex we'd remove is on a seam, try removing the other one instead
|
|
if (CutMesh[MeshIdx]->HasAttributes() && CutMesh[MeshIdx]->Attributes()->IsSeamVertex(EV.B, false))
|
|
{
|
|
Swap(EV.A, EV.B);
|
|
// if they were both on seams, then collapse should not happen? (& would break OnCollapseEdge assumptions in overlay)
|
|
if (CutMesh[MeshIdx]->HasAttributes() && CutMesh[MeshIdx]->Attributes()->IsSeamVertex(EV.B, false))
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
FDynamicMesh3::FEdgeCollapseInfo CollapseInfo;
|
|
EMeshResult CollapseResult = CutMesh[MeshIdx]->CollapseEdge(EV.A, EV.B, .5, CollapseInfo);
|
|
if (CollapseResult == EMeshResult::Ok)
|
|
{
|
|
for (int i = 0; i < 2; i++)
|
|
{
|
|
if (AllEIDs.Contains(CollapseInfo.RemovedEdges[i]))
|
|
{
|
|
int ToAdd = CollapseInfo.KeptEdges[i];
|
|
bool bWasPresent;
|
|
AllEIDs.Add(ToAdd, &bWasPresent);
|
|
if (!bWasPresent)
|
|
{
|
|
EIDs.Add(ToAdd);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (Cancelled())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// edges that will become new boundary edges after the boolean op removes triangles on each mesh
|
|
TArray<int> CutBoundaryEdges[2];
|
|
// Vertices on the cut boundary that *may* not have a corresonding vertex on the other mesh
|
|
TSet<int> PossUnmatchedBdryVerts[2];
|
|
|
|
// delete geometry according to boolean rules, tracking the boundary edges
|
|
{ // (just for scope)
|
|
// first decide what triangles to delete for both meshes (*before* deleting anything so winding doesn't get messed up!)
|
|
TArray<bool> KeepTri[2];
|
|
for (int MeshIdx = 0; MeshIdx < NumMeshesToProcess; MeshIdx++)
|
|
{
|
|
TFastWindingTree<FDynamicMesh3> Winding(&Spatial[1 - MeshIdx]);
|
|
FDynamicMeshAABBTree3& OtherSpatial = Spatial[1 - MeshIdx];
|
|
FDynamicMesh3& ProcessMesh = *CutMesh[MeshIdx];
|
|
int MaxTriID = ProcessMesh.MaxTriangleID();
|
|
KeepTri[MeshIdx].SetNumUninitialized(MaxTriID);
|
|
bool bCoplanarKeepSameDir = Operation != EBooleanOp::Difference;
|
|
bool bRemoveInside = 1; // whether to remove the inside triangles (e.g. for union) or the outside ones (e.g. for intersection)
|
|
if (Operation == EBooleanOp::Trim || Operation == EBooleanOp::Intersect || (Operation == EBooleanOp::Difference && MeshIdx == 1))
|
|
{
|
|
bRemoveInside = 0;
|
|
}
|
|
ParallelFor(MaxTriID, [this, &KeepTri, &MeshIdx, &Winding, &OtherSpatial, &ProcessMesh, bCoplanarKeepSameDir, bRemoveInside](int TID)
|
|
{
|
|
if (!ProcessMesh.IsTriangle(TID))
|
|
{
|
|
return;
|
|
}
|
|
|
|
FVector3d Centroid = ProcessMesh.GetTriCentroid(TID);
|
|
|
|
// first check for the coplanar case
|
|
{
|
|
double DSq;
|
|
double OnPlaneTolerance = SnapTolerance;
|
|
int OtherTID = OtherSpatial.FindNearestTriangle(Centroid, DSq, OnPlaneTolerance);
|
|
if (OtherTID > -1) // only consider it coplanar if there is a matching tri
|
|
{
|
|
|
|
FVector3d OtherNormal = OtherSpatial.GetMesh()->GetTriNormal(OtherTID);
|
|
FVector3d Normal = ProcessMesh.GetTriNormal(TID);
|
|
double DotNormals = OtherNormal.Dot(Normal);
|
|
|
|
//if (FMath::Abs(DotNormals) > .9) // TODO: do we actually want to check for a normal match? coplanar vertex check below is more robust?
|
|
{
|
|
// To be extra sure it's a coplanar match, check the vertices are *also* on the other mesh (w/in SnapTolerance)
|
|
FTriangle3d Tri;
|
|
ProcessMesh.GetTriVertices(TID, Tri.V[0], Tri.V[1], Tri.V[2]);
|
|
bool bAllTrisOnOtherMesh = true;
|
|
for (int Idx = 0; Idx < 3; Idx++)
|
|
{
|
|
if (OtherSpatial.FindNearestTriangle(Tri.V[Idx], DSq, OnPlaneTolerance) == FDynamicMesh3::InvalidID)
|
|
{
|
|
bAllTrisOnOtherMesh = false;
|
|
break;
|
|
}
|
|
}
|
|
if (bAllTrisOnOtherMesh)
|
|
{
|
|
if (MeshIdx != 0) // for coplanar tris favor the first mesh; just delete from the other mesh
|
|
{
|
|
KeepTri[MeshIdx][TID] = false;
|
|
return;
|
|
}
|
|
else // for the first mesh, logic depends on orientation of matching tri
|
|
{
|
|
KeepTri[MeshIdx][TID] = DotNormals > 0 == bCoplanarKeepSameDir;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// didn't already return a coplanar result; use the winding number
|
|
double WindingNum = Winding.FastWindingNumber(Centroid);
|
|
KeepTri[MeshIdx][TID] = (WindingNum > WindingThreshold) != bRemoveInside;
|
|
});
|
|
for (int EID : ProcessMesh.EdgeIndicesItr())
|
|
{
|
|
FIndex2i TriPair = ProcessMesh.GetEdgeT(EID);
|
|
if (TriPair.B == IndexConstants::InvalidID || KeepTri[MeshIdx][TriPair.A] == KeepTri[MeshIdx][TriPair.B])
|
|
{
|
|
continue;
|
|
}
|
|
|
|
CutBoundaryEdges[MeshIdx].Add(EID);
|
|
FIndex2i VertPair = ProcessMesh.GetEdgeV(EID);
|
|
PossUnmatchedBdryVerts[MeshIdx].Add(VertPair.A);
|
|
PossUnmatchedBdryVerts[MeshIdx].Add(VertPair.B);
|
|
}
|
|
}
|
|
// now go ahead and delete from both meshes
|
|
for (int MeshIdx = 0; MeshIdx < NumMeshesToProcess; MeshIdx++)
|
|
{
|
|
FDynamicMesh3& ProcessMesh = *CutMesh[MeshIdx];
|
|
|
|
for (int TID = 0; TID < KeepTri[MeshIdx].Num(); TID++)
|
|
{
|
|
if (ProcessMesh.IsTriangle(TID) && !KeepTri[MeshIdx][TID])
|
|
{
|
|
ProcessMesh.RemoveTriangle(TID, true, false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (Cancelled())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
// correspond vertices across both meshes (in cases where both meshes were processed)
|
|
TMap<int, int> AllVIDMatches; // mapping of matched vertex IDs from cutmesh 0 to cutmesh 1
|
|
if (NumMeshesToProcess == 2)
|
|
{
|
|
double SnapToleranceSq = SnapTolerance * SnapTolerance;
|
|
|
|
// Hash boundary verts for faster search
|
|
TArray<TPointHashGrid3d<int>> PointHashes;
|
|
for (int MeshIdx = 0; MeshIdx < 2; MeshIdx++)
|
|
{
|
|
PointHashes.Emplace(CutMesh[MeshIdx]->GetCachedBounds().MaxDim() / 64, -1);
|
|
for (int BoundaryVID : PossUnmatchedBdryVerts[MeshIdx])
|
|
{
|
|
PointHashes[MeshIdx].InsertPointUnsafe(BoundaryVID, CutMesh[MeshIdx]->GetVertex(BoundaryVID));
|
|
}
|
|
}
|
|
|
|
// ensure segments that are now on boundaries have 1:1 vertex correspondence across meshes
|
|
for (int MeshIdx = 0; MeshIdx < 2; MeshIdx++)
|
|
{
|
|
int OtherMeshIdx = 1 - MeshIdx;
|
|
FDynamicMesh3& OtherMesh = *CutMesh[OtherMeshIdx];
|
|
|
|
|
|
FSparseDynamicOctree3 EdgeOctree;
|
|
EdgeOctree.RootDimension = .25;
|
|
EdgeOctree.SetMaxTreeDepth(7);
|
|
auto EdgeBounds = [&OtherMesh](int EID)
|
|
{
|
|
FDynamicMesh3::FEdge Edge = OtherMesh.GetEdge(EID);
|
|
FVector3d A = OtherMesh.GetVertex(Edge.Vert.A);
|
|
FVector3d B = OtherMesh.GetVertex(Edge.Vert.B);
|
|
if (A.X > B.X)
|
|
{
|
|
Swap(A.X, B.X);
|
|
}
|
|
if (A.Y > B.Y)
|
|
{
|
|
Swap(A.Y, B.Y);
|
|
}
|
|
if (A.Z > B.Z)
|
|
{
|
|
Swap(A.Z, B.Z);
|
|
}
|
|
return FAxisAlignedBox3d(A, B);
|
|
};
|
|
auto AddEdge = [&EdgeOctree, &OtherMesh, EdgeBounds](int EID)
|
|
{
|
|
EdgeOctree.InsertObject(EID, EdgeBounds(EID));
|
|
};
|
|
auto UpdateEdge = [&EdgeOctree, &OtherMesh, EdgeBounds](int EID)
|
|
{
|
|
EdgeOctree.ReinsertObject(EID, EdgeBounds(EID));
|
|
};
|
|
for (int EID : CutBoundaryEdges[OtherMeshIdx])
|
|
{
|
|
AddEdge(EID);
|
|
}
|
|
TArray<int> EdgesInRange;
|
|
|
|
|
|
// mapping from OtherMesh VIDs to ProcessMesh VIDs
|
|
// used to ensure we only keep the best match, in cases where multiple boundary vertices map to a given vertex on the other mesh boundary
|
|
TMap<int, int> FoundMatches;
|
|
|
|
for (int BoundaryVID : PossUnmatchedBdryVerts[MeshIdx])
|
|
{
|
|
FVector3d Pos = CutMesh[MeshIdx]->GetVertex(BoundaryVID);
|
|
TPair<int, double> VIDDist = PointHashes[OtherMeshIdx].FindNearestInRadius(Pos, SnapTolerance, [&Pos, &OtherMesh](int VID)
|
|
{
|
|
return Pos.DistanceSquared(OtherMesh.GetVertex(VID));
|
|
});
|
|
int NearestVID = VIDDist.Key; // ID of nearest vertex on other mesh
|
|
double DSq = VIDDist.Value; // square distance to that vertex
|
|
|
|
if (NearestVID != FDynamicMesh3::InvalidID)
|
|
{
|
|
|
|
int* Match = FoundMatches.Find(NearestVID);
|
|
if (Match)
|
|
{
|
|
double OldDSq = CutMesh[MeshIdx]->GetVertex(*Match).DistanceSquared(OtherMesh.GetVertex(NearestVID));
|
|
if (DSq < OldDSq) // new vertex is a better match than the old one
|
|
{
|
|
int OldVID = *Match; // copy old VID out of match before updating the TMap
|
|
FoundMatches.Add(NearestVID, BoundaryVID); // new VID is recorded as best match
|
|
|
|
// old VID is swapped in as the one to consider as unmatched
|
|
// it will now be matched below
|
|
BoundaryVID = OldVID;
|
|
Pos = CutMesh[MeshIdx]->GetVertex(BoundaryVID);
|
|
DSq = OldDSq;
|
|
}
|
|
NearestVID = FDynamicMesh3::InvalidID; // one of these vertices will be unmatched
|
|
}
|
|
else
|
|
{
|
|
FoundMatches.Add(NearestVID, BoundaryVID);
|
|
}
|
|
}
|
|
|
|
// if we didn't find a valid match, try to split the nearest edge to create a match
|
|
if (NearestVID == FDynamicMesh3::InvalidID)
|
|
{
|
|
// vertex had no match -- try to split edge to match it
|
|
FAxisAlignedBox3d QueryBox(Pos, SnapTolerance);
|
|
EdgesInRange.Reset();
|
|
EdgeOctree.RangeQuery(QueryBox, EdgesInRange);
|
|
|
|
int OtherEID = FindNearestEdge(OtherMesh, EdgesInRange, Pos);
|
|
if (OtherEID != FDynamicMesh3::InvalidID)
|
|
{
|
|
FVector3d EdgePts[2];
|
|
OtherMesh.GetEdgeV(OtherEID, EdgePts[0], EdgePts[1]);
|
|
// only accept the match if it's not going to create a degenerate edge -- TODO: filter already-matched edges from the FindNearestEdge query!
|
|
if (EdgePts[0].DistanceSquared(Pos) > SnapToleranceSq&& EdgePts[1].DistanceSquared(Pos) > SnapToleranceSq)
|
|
{
|
|
FSegment3d Seg(EdgePts[0], EdgePts[1]);
|
|
double Along = Seg.ProjectUnitRange(Pos);
|
|
FDynamicMesh3::FEdgeSplitInfo SplitInfo;
|
|
if (ensure(EMeshResult::Ok == OtherMesh.SplitEdge(OtherEID, SplitInfo, Along)))
|
|
{
|
|
FoundMatches.Add(SplitInfo.NewVertex, BoundaryVID);
|
|
OtherMesh.SetVertex(SplitInfo.NewVertex, Pos);
|
|
CutBoundaryEdges[OtherMeshIdx].Add(SplitInfo.NewEdges.A);
|
|
UpdateEdge(OtherEID);
|
|
AddEdge(SplitInfo.NewEdges.A);
|
|
// Note: Do not update PossUnmatchedBdryVerts with the new vertex, because it is already matched by construction
|
|
// Likewise do not update the pointhash -- we don't want it to find vertices that were already perfectly matched
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// actually snap the positions together for final matches
|
|
for (TPair<int, int>& Match : FoundMatches)
|
|
{
|
|
CutMesh[MeshIdx]->SetVertex(Match.Value, OtherMesh.GetVertex(Match.Key));
|
|
|
|
// Copy match to AllVIDMatches; note this is always mapping from CutMesh 0 to 1
|
|
int VIDs[2]{ Match.Key, Match.Value }; // just so we can access by index
|
|
AllVIDMatches.Add(VIDs[1 - MeshIdx], VIDs[MeshIdx]);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (Operation == EBooleanOp::Difference)
|
|
{
|
|
// TODO: implement a way to flip all the triangles in the mesh without building this AllTID array
|
|
TArray<int> AllTID;
|
|
for (int TID : CutMesh[1]->TriangleIndicesItr())
|
|
{
|
|
AllTID.Add(TID);
|
|
}
|
|
FDynamicMeshEditor FlipEditor(CutMesh[1]);
|
|
FlipEditor.ReverseTriangleOrientations(AllTID, true);
|
|
}
|
|
|
|
if (Cancelled())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool bSuccess = true;
|
|
|
|
if (NumMeshesToProcess > 1)
|
|
{
|
|
FDynamicMeshEditor Editor(Result);
|
|
FMeshIndexMappings IndexMaps;
|
|
Editor.AppendMesh(CutMesh[1], IndexMaps);
|
|
|
|
if (bWeldSharedEdges)
|
|
{
|
|
bool bWeldSuccess = MergeEdges(IndexMaps, CutMesh, CutBoundaryEdges, AllVIDMatches);
|
|
bSuccess = bSuccess && bWeldSuccess;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
CreatedBoundaryEdges = CutBoundaryEdges[0];
|
|
}
|
|
|
|
if (bPutResultInInputSpace)
|
|
{
|
|
MeshTransforms::ApplyTransform(*Result, ResultTransform);
|
|
ResultTransform = FTransform3d::Identity();
|
|
}
|
|
|
|
return bSuccess;
|
|
}
|
|
|
|
|
|
bool FMeshBoolean::MergeEdges(const FMeshIndexMappings& IndexMaps, FDynamicMesh3* CutMesh[2], const TArray<int> CutBoundaryEdges[2], const TMap<int, int>& AllVIDMatches)
|
|
{
|
|
// translate the edge IDs from CutMesh[1] over to Result mesh edge IDs
|
|
TArray<int> OtherMeshEdges;
|
|
for (int OldMeshEID : CutBoundaryEdges[1])
|
|
{
|
|
FIndex2i OtherEV = CutMesh[1]->GetEdgeV(OldMeshEID);
|
|
int MappedEID = Result->FindEdge(IndexMaps.GetNewVertex(OtherEV.A), IndexMaps.GetNewVertex(OtherEV.B));
|
|
if (ensure(Result->IsBoundaryEdge(MappedEID)))
|
|
{
|
|
OtherMeshEdges.Add(MappedEID);
|
|
}
|
|
}
|
|
|
|
// find "easy" match candidates using the already-made vertex correspondence
|
|
TArray<FIndex2i> CandidateMatches;
|
|
TArray<int> UnmatchedEdges;
|
|
for (int EID : CutBoundaryEdges[0])
|
|
{
|
|
if (!ensure(Result->IsBoundaryEdge(EID)))
|
|
{
|
|
continue;
|
|
}
|
|
FIndex2i VIDs = Result->GetEdgeV(EID);
|
|
const int* OtherA = AllVIDMatches.Find(VIDs.A);
|
|
const int* OtherB = AllVIDMatches.Find(VIDs.B);
|
|
bool bAddedCandidate = false;
|
|
if (OtherA && OtherB)
|
|
{
|
|
int MapOtherA = IndexMaps.GetNewVertex(*OtherA);
|
|
int MapOtherB = IndexMaps.GetNewVertex(*OtherB);
|
|
int OtherEID = Result->FindEdge(MapOtherA, MapOtherB);
|
|
if (OtherEID != FDynamicMesh3::InvalidID)
|
|
{
|
|
CandidateMatches.Add(FIndex2i(EID, OtherEID));
|
|
bAddedCandidate = true;
|
|
}
|
|
}
|
|
if (!bAddedCandidate)
|
|
{
|
|
UnmatchedEdges.Add(EID);
|
|
}
|
|
}
|
|
|
|
// merge the easy matches
|
|
for (FIndex2i Candidate : CandidateMatches)
|
|
{
|
|
if (!Result->IsEdge(Candidate.A) || !Result->IsBoundaryEdge(Candidate.A))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
FDynamicMesh3::FMergeEdgesInfo MergeInfo;
|
|
EMeshResult EdgeMergeResult = Result->MergeEdges(Candidate.A, Candidate.B, MergeInfo);
|
|
if (EdgeMergeResult != EMeshResult::Ok)
|
|
{
|
|
UnmatchedEdges.Add(Candidate.A);
|
|
}
|
|
}
|
|
|
|
// filter matched edges from the edge array for the other mesh
|
|
OtherMeshEdges.SetNum(Algo::RemoveIf(OtherMeshEdges, [this](int EID)
|
|
{
|
|
return !Result->IsEdge(EID) || !Result->IsBoundaryEdge(EID);
|
|
}));
|
|
|
|
// see if we can match anything else
|
|
bool bAllMatched = CutBoundaryEdges[0].Num() == CutBoundaryEdges[1].Num();
|
|
if (UnmatchedEdges.Num() > 0)
|
|
{
|
|
// greedily match within snap tolerance
|
|
double SnapToleranceSq = SnapTolerance * SnapTolerance;
|
|
for (int OtherEID : OtherMeshEdges)
|
|
{
|
|
if (!Result->IsEdge(OtherEID) || !Result->IsBoundaryEdge(OtherEID))
|
|
{
|
|
continue;
|
|
}
|
|
FVector3d OA, OB;
|
|
Result->GetEdgeV(OtherEID, OA, OB);
|
|
for (int UnmatchedIdx = 0; UnmatchedIdx < UnmatchedEdges.Num(); UnmatchedIdx++)
|
|
{
|
|
int EID = UnmatchedEdges[UnmatchedIdx];
|
|
if (!Result->IsEdge(EID) || !Result->IsBoundaryEdge(EID))
|
|
{
|
|
UnmatchedEdges.RemoveAtSwap(UnmatchedIdx, 1, false);
|
|
UnmatchedIdx--;
|
|
continue;
|
|
}
|
|
FVector3d A, B;
|
|
Result->GetEdgeV(EID, A, B);
|
|
if (OA.DistanceSquared(A) < SnapToleranceSq && OB.DistanceSquared(B) < SnapToleranceSq)
|
|
{
|
|
FDynamicMesh3::FMergeEdgesInfo MergeInfo;
|
|
EMeshResult EdgeMergeResult = Result->MergeEdges(EID, OtherEID, MergeInfo);
|
|
if (EdgeMergeResult == EMeshResult::Ok)
|
|
{
|
|
UnmatchedEdges.RemoveAtSwap(UnmatchedIdx, 1, false);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// store the failure cases from the first mesh's array
|
|
for (int EID : UnmatchedEdges)
|
|
{
|
|
if (Result->IsEdge(EID) && Result->IsBoundaryEdge(EID))
|
|
{
|
|
CreatedBoundaryEdges.Add(EID);
|
|
bAllMatched = false;
|
|
}
|
|
}
|
|
}
|
|
// store the failure cases from the second mesh's array
|
|
for (int OtherEID : OtherMeshEdges)
|
|
{
|
|
if (Result->IsEdge(OtherEID) && Result->IsBoundaryEdge(OtherEID))
|
|
{
|
|
CreatedBoundaryEdges.Add(OtherEID);
|
|
bAllMatched = false;
|
|
}
|
|
}
|
|
return bAllMatched;
|
|
}
|
|
|
|
|
|
int FMeshBoolean::FindNearestEdge(const FDynamicMesh3& OnMesh, const TArray<int>& EIDs, FVector3d Pos)
|
|
{
|
|
int NearEID = FDynamicMesh3::InvalidID;
|
|
double NearSqr = SnapTolerance * SnapTolerance;
|
|
FVector3d EdgePts[2];
|
|
for (int EID : EIDs) {
|
|
OnMesh.GetEdgeV(EID, EdgePts[0], EdgePts[1]);
|
|
|
|
FSegment3d Seg(EdgePts[0], EdgePts[1]);
|
|
double DSqr = Seg.DistanceSquared(Pos);
|
|
if (DSqr < NearSqr)
|
|
{
|
|
NearEID = EID;
|
|
NearSqr = DSqr;
|
|
}
|
|
}
|
|
return NearEID;
|
|
}
|