You've already forked UnrealEngineUWP
mirror of
https://github.com/izzy2lost/UnrealEngineUWP.git
synced 2026-03-26 18:15:20 -07:00
#jira UE-151297 #rb rinat.abdrashitov #preflight 627d25564a05ef0394cc0592 [CL 20165579 by Jimmy Andrews in ue5-main branch]
1459 lines
48 KiB
C++
1459 lines
48 KiB
C++
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
// Port of geometry3Sharp MeshBoolean
|
|
|
|
#include "Operations/MeshBoolean.h"
|
|
#include "Operations/MeshMeshCut.h"
|
|
#include "Selections/MeshConnectedComponents.h"
|
|
|
|
#include "Async/ParallelFor.h"
|
|
#include "DynamicMesh/MeshTransforms.h"
|
|
#include "DynamicMesh/MeshNormals.h"
|
|
#include "Spatial/SparseDynamicOctree3.h"
|
|
|
|
#include "Algo/RemoveIf.h"
|
|
|
|
#include "DynamicMesh/DynamicMeshAABBTree3.h"
|
|
|
|
using namespace UE::Geometry;
|
|
|
|
// 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]->GetBounds(true), Transforms[0]);
|
|
FAxisAlignedBox3d MeshB_AABB(CutMesh[1]->GetBounds(true), 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++)
|
|
{
|
|
FTransformSRT3d 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 = FTransformSRT3d(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;
|
|
}
|
|
|
|
bool bOpOnSingleMesh = Operation == EBooleanOp::TrimInside || Operation == EBooleanOp::TrimOutside || Operation == EBooleanOp::NewGroupInside || Operation == EBooleanOp::NewGroupOutside;
|
|
|
|
// cut the meshes
|
|
FMeshMeshCut Cut(CutMesh[0], CutMesh[1]);
|
|
Cut.bTrackInsertedVertices = bCollapseDegenerateEdgesOnCut; // to collect candidates to collapse
|
|
Cut.bMutuallyCut = !bOpOnSingleMesh;
|
|
Cut.SnapTolerance = SnapTolerance;
|
|
Cut.Cut(Intersections);
|
|
|
|
if (Cancelled())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
int NumMeshesToProcess = bOpOnSingleMesh ? 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 (DistanceSquared(CutMesh[MeshIdx]->GetVertex(VID[0]), 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 (DistanceSquared(A,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];
|
|
// This array is used to double-check the assumption that we will delete the other surface when we keep a coplanar tri
|
|
// Note we only need it for mesh 0 (i.e., the mesh we try to keep triangles from when we preserve coplanar surfaces)
|
|
TArray<int32> DeleteIfOtherKept;
|
|
if (NumMeshesToProcess > 1)
|
|
{
|
|
DeleteIfOtherKept.Init(-1, CutMesh[0]->MaxTriangleID());
|
|
}
|
|
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 && Operation != EBooleanOp::TrimInside && Operation != EBooleanOp::NewGroupInside);
|
|
bool bRemoveInside = 1; // whether to remove the inside triangles (e.g. for union) or the outside ones (e.g. for intersection)
|
|
if (Operation == EBooleanOp::NewGroupOutside || Operation == EBooleanOp::TrimOutside || Operation == EBooleanOp::Intersect || (Operation == EBooleanOp::Difference && MeshIdx == 1))
|
|
{
|
|
bRemoveInside = 0;
|
|
}
|
|
FMeshNormals OtherNormals(OtherSpatial.GetMesh());
|
|
OtherNormals.ComputeTriangleNormals();
|
|
const double OnPlaneTolerance = SnapTolerance;
|
|
IMeshSpatial::FQueryOptions NonDegenCoplanarCandidateFilter(OnPlaneTolerance,
|
|
[&OtherNormals](int TID) -> bool // filter degenerate triangles from matching
|
|
{
|
|
// By convention, the normal for degenerate triangles is the zero vector
|
|
return !OtherNormals[TID].IsZero();
|
|
});
|
|
ParallelFor(MaxTriID, [&](int TID)
|
|
{
|
|
if (!ProcessMesh.IsTriangle(TID))
|
|
{
|
|
return;
|
|
}
|
|
|
|
FTriangle3d Tri;
|
|
ProcessMesh.GetTriVertices(TID, Tri.V[0], Tri.V[1], Tri.V[2]);
|
|
FVector3d Centroid = Tri.Centroid();
|
|
|
|
// first check for the coplanar case
|
|
{
|
|
double DSq;
|
|
int OtherTID = OtherSpatial.FindNearestTriangle(Centroid, DSq, NonDegenCoplanarCandidateFilter);
|
|
if (OtherTID > -1) // only consider it coplanar if there is a matching tri
|
|
{
|
|
|
|
FVector3d OtherNormal = OtherNormals[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)
|
|
|
|
bool bAllTrisOnOtherMesh = true;
|
|
for (int Idx = 0; Idx < 3; Idx++)
|
|
{
|
|
// use a slightly more forgiving tolerance to account for the likelihood that these vertices were mesh-cut right to the boundary of the coplanar region and have some additional error
|
|
if (OtherSpatial.FindNearestTriangle(Tri.V[Idx], DSq, OnPlaneTolerance * 2) == FDynamicMesh3::InvalidID)
|
|
{
|
|
bAllTrisOnOtherMesh = false;
|
|
break;
|
|
}
|
|
}
|
|
if (bAllTrisOnOtherMesh)
|
|
{
|
|
// for coplanar tris favor the first mesh; just delete from the other mesh
|
|
// for fully degenerate tris, favor deletion also
|
|
// (Note: For degenerate tris we have no orientation info, so we are choosing between
|
|
// potentially leaving 'cracks' in solid regions or 'spikes' in empty regions)
|
|
if (MeshIdx != 0 || Normal.IsZero())
|
|
{
|
|
KeepTri[MeshIdx][TID] = false;
|
|
return;
|
|
}
|
|
else // for the first mesh, & with a valid normal, logic depends on orientation of matching tri
|
|
{
|
|
bool bKeep = DotNormals > 0 == bCoplanarKeepSameDir;
|
|
KeepTri[MeshIdx][TID] = bKeep;
|
|
if (NumMeshesToProcess > 1 && bKeep)
|
|
{
|
|
// If we kept this tri, remember the coplanar pair we expect to be deleted, in case
|
|
// it isn't deleted (e.g. because it wasn't coplanar); to then delete this one instead.
|
|
// This can help clean up sliver triangles near a cut boundary that look locally coplanar
|
|
DeleteIfOtherKept[TID] = OtherTID;
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// didn't already return a coplanar result; use the winding number
|
|
double WindingNum = Winding.FastWindingNumber(Centroid);
|
|
KeepTri[MeshIdx][TID] = (WindingNum > WindingThreshold) != bRemoveInside;
|
|
});
|
|
}
|
|
|
|
// Don't keep coplanar tris if the matched, second-mesh tri that we expected to delete was actually kept
|
|
if (NumMeshesToProcess > 1)
|
|
{
|
|
for (int TID : CutMesh[0]->TriangleIndicesItr())
|
|
{
|
|
int32 DeleteIfOtherKeptTID = DeleteIfOtherKept[TID];
|
|
if (DeleteIfOtherKeptTID > -1 && KeepTri[1][DeleteIfOtherKeptTID])
|
|
{
|
|
KeepTri[0][TID] = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
for (int MeshIdx = 0; MeshIdx < NumMeshesToProcess; MeshIdx++)
|
|
{
|
|
FDynamicMesh3& ProcessMesh = *CutMesh[MeshIdx];
|
|
for (int EID : ProcessMesh.EdgeIndicesItr())
|
|
{
|
|
FDynamicMesh3::FEdge Edge = ProcessMesh.GetEdge(EID);
|
|
if (Edge.Tri.B == IndexConstants::InvalidID || KeepTri[MeshIdx][Edge.Tri.A] == KeepTri[MeshIdx][Edge.Tri.B])
|
|
{
|
|
continue;
|
|
}
|
|
|
|
CutBoundaryEdges[MeshIdx].Add(EID);
|
|
PossUnmatchedBdryVerts[MeshIdx].Add(Edge.Vert.A);
|
|
PossUnmatchedBdryVerts[MeshIdx].Add(Edge.Vert.B);
|
|
}
|
|
}
|
|
// now go ahead and delete from both meshes
|
|
bool bRegroupInsteadOfDelete = Operation == EBooleanOp::NewGroupInside || Operation == EBooleanOp::NewGroupOutside;
|
|
int NewGroupID = -1;
|
|
TArray<int> NewGroupTris;
|
|
if (bRegroupInsteadOfDelete)
|
|
{
|
|
ensure(NumMeshesToProcess == 1);
|
|
NewGroupID = CutMesh[0]->AllocateTriangleGroup();
|
|
}
|
|
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])
|
|
{
|
|
if (bRegroupInsteadOfDelete)
|
|
{
|
|
ProcessMesh.SetTriangleGroup(TID, NewGroupID);
|
|
NewGroupTris.Add(TID);
|
|
}
|
|
else
|
|
{
|
|
ProcessMesh.RemoveTriangle(TID, true, false);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (bRegroupInsteadOfDelete)
|
|
{
|
|
// the new triangle group could include disconnected components; best to give them separate triangle groups
|
|
FMeshConnectedComponents Components(CutMesh[0]);
|
|
Components.FindConnectedTriangles(NewGroupTris);
|
|
for (int ComponentIdx = 1; ComponentIdx < Components.Num(); ComponentIdx++)
|
|
{
|
|
int SplitGroupID = CutMesh[0]->AllocateTriangleGroup();
|
|
for (int TID : Components.GetComponent(ComponentIdx).Indices)
|
|
{
|
|
CutMesh[0]->SetTriangleGroup(TID, SplitGroupID);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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)
|
|
{
|
|
TMap<int, int> FoundMatchesMaps[2]; // mappings of matched vertex IDs from mesh 1->0 and mesh 0->1
|
|
double SnapToleranceSq = SnapTolerance * SnapTolerance;
|
|
|
|
// 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];
|
|
|
|
TPointHashGrid3d<int> OtherMeshPointHash(OtherMesh.GetBounds(true).MaxDim() / 64, -1);
|
|
for (int BoundaryVID : PossUnmatchedBdryVerts[OtherMeshIdx])
|
|
{
|
|
OtherMeshPointHash.InsertPointUnsafe(BoundaryVID, OtherMesh.GetVertex(BoundaryVID));
|
|
}
|
|
|
|
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 = FoundMatchesMaps[MeshIdx];
|
|
|
|
for (int BoundaryVID : PossUnmatchedBdryVerts[MeshIdx])
|
|
{
|
|
if (MeshIdx == 1 && FoundMatchesMaps[0].Contains(BoundaryVID))
|
|
{
|
|
continue; // was already snapped to a vertex
|
|
}
|
|
|
|
FVector3d Pos = CutMesh[MeshIdx]->GetVertex(BoundaryVID);
|
|
TPair<int, double> VIDDist = OtherMeshPointHash.FindNearestInRadius(Pos, SnapTolerance, [&Pos, &OtherMesh](int VID)
|
|
{
|
|
return DistanceSquared(Pos, 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 = DistanceSquared(CutMesh[MeshIdx]->GetVertex(*Match), 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 (DistanceSquared(EdgePts[0], Pos) > SnapToleranceSq&& DistanceSquared(EdgePts[1], 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 (bSimplifyAlongNewEdges)
|
|
{
|
|
SimplifyAlongNewEdges(NumMeshesToProcess, CutMesh, CutBoundaryEdges, AllVIDMatches);
|
|
}
|
|
|
|
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 (bPopulateSecondMeshGroupMap)
|
|
{
|
|
SecondMeshGroupMap = IndexMaps.GetGroupMap();
|
|
}
|
|
|
|
if (bWeldSharedEdges)
|
|
{
|
|
bool bWeldSuccess = MergeEdges(IndexMaps, CutMesh, CutBoundaryEdges, AllVIDMatches);
|
|
bSuccess = bSuccess && bWeldSuccess;
|
|
}
|
|
else
|
|
{
|
|
CreatedBoundaryEdges = CutBoundaryEdges[0];
|
|
for (int OldMeshEID : CutBoundaryEdges[1])
|
|
{
|
|
if (!CutMesh[1]->IsEdge(OldMeshEID))
|
|
{
|
|
ensure(false);
|
|
continue;
|
|
}
|
|
FIndex2i OtherEV = CutMesh[1]->GetEdgeV(OldMeshEID);
|
|
int MappedEID = Result->FindEdge(IndexMaps.GetNewVertex(OtherEV.A), IndexMaps.GetNewVertex(OtherEV.B));
|
|
checkSlow(Result->IsBoundaryEdge(MappedEID));
|
|
CreatedBoundaryEdges.Add(MappedEID);
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
CreatedBoundaryEdges = CutBoundaryEdges[0];
|
|
}
|
|
|
|
if (bTrackAllNewEdges)
|
|
{
|
|
for (int32 eid : CreatedBoundaryEdges)
|
|
{
|
|
AllNewEdges.Add(eid);
|
|
}
|
|
}
|
|
|
|
if (bPutResultInInputSpace)
|
|
{
|
|
MeshTransforms::ApplyTransform(*Result, ResultTransform);
|
|
ResultTransform = FTransformSRT3d::Identity();
|
|
}
|
|
|
|
return bSuccess;
|
|
}
|
|
|
|
|
|
|
|
bool FMeshBoolean::IsFlat(const FDynamicMesh3& Mesh, int VID, double DotTolerance, FVector3d& OutFirstNormal)
|
|
{
|
|
bool bHasFirst = false;
|
|
bool bIsFlat = true;
|
|
Mesh.EnumerateVertexTriangles(VID, [&Mesh, DotTolerance, &OutFirstNormal, &bHasFirst, &bIsFlat](int32 TID)
|
|
{
|
|
if (!bIsFlat)
|
|
{
|
|
return;
|
|
}
|
|
FVector3d Normal = Mesh.GetTriNormal(TID);
|
|
if (!bHasFirst)
|
|
{
|
|
OutFirstNormal = Normal;
|
|
bHasFirst = true;
|
|
}
|
|
else
|
|
{
|
|
bIsFlat = bIsFlat && Normal.Dot(OutFirstNormal) >= DotTolerance;
|
|
}
|
|
});
|
|
return bIsFlat;
|
|
}
|
|
|
|
/**
|
|
* Test if a given edge collapse would cause a triangle flip or other unacceptable decrease in mesh quality
|
|
*/
|
|
bool FMeshBoolean::CollapseWouldHurtTriangleQuality(
|
|
const FDynamicMesh3& Mesh, const FVector3d& ExpectNormal,
|
|
int32 RemoveV, const FVector3d& RemoveVPos, int32 KeepV, const FVector3d& KeepVPos,
|
|
double TryToImproveTriQualityThreshold
|
|
)
|
|
{
|
|
double WorstQualityNewTriangle = FMathd::MaxReal;
|
|
|
|
bool bIsHurt = false;
|
|
Mesh.EnumerateVertexTriangles(RemoveV,
|
|
[&Mesh, &bIsHurt, &KeepVPos, RemoveV, KeepV, &ExpectNormal,
|
|
TryToImproveTriQualityThreshold, &WorstQualityNewTriangle](int32 TID)
|
|
{
|
|
if (bIsHurt)
|
|
{
|
|
return;
|
|
}
|
|
FIndex3i Tri = Mesh.GetTriangle(TID);
|
|
FVector3d Verts[3];
|
|
for (int Idx = 0; Idx < 3; Idx++)
|
|
{
|
|
int VID = Tri[Idx];
|
|
if (VID == KeepV)
|
|
{
|
|
// this tri has both RemoveV and KeepV, so it'll be removed and we don't need to consider it
|
|
return;
|
|
}
|
|
else if (VID == RemoveV)
|
|
{
|
|
// anything at RemoveV is reconnected to KeepV's position
|
|
Verts[Idx] = KeepVPos;
|
|
}
|
|
else
|
|
{
|
|
// it's not on the collapsed edge so it stays still
|
|
Verts[Idx] = Mesh.GetVertex(Tri[Idx]);
|
|
}
|
|
}
|
|
FVector3d Edge1(Verts[1] - Verts[0]);
|
|
FVector3d Edge2(Verts[2] - Verts[0]);
|
|
FVector3d VCross(Edge2.Cross(Edge1));
|
|
|
|
// TODO: does this tolerance make a difference? if not, set to zero and remove the Normalize(VCross)
|
|
double EdgeFlipTolerance = 1.e-5;
|
|
double Area2 = Normalize(VCross);
|
|
if (TryToImproveTriQualityThreshold > 0)
|
|
{
|
|
FVector3d Edge3(Verts[2] - Verts[1]);
|
|
double MaxLenSq = FMathd::Max3(Edge1.SquaredLength(), Edge2.SquaredLength(), Edge3.SquaredLength());
|
|
double Quality = Area2 / (MaxLenSq + FMathd::ZeroTolerance);
|
|
WorstQualityNewTriangle = FMathd::Min(Quality, WorstQualityNewTriangle);
|
|
}
|
|
|
|
bIsHurt = VCross.Dot(ExpectNormal) <= EdgeFlipTolerance;
|
|
}
|
|
);
|
|
|
|
// note tri quality was computed as 2*Area / MaxEdgeLenSquared
|
|
// so need to multiply it by 2/sqrt(3) to get actual aspect ratio
|
|
if (!bIsHurt && WorstQualityNewTriangle * 2 * FMathd::InvSqrt3 < TryToImproveTriQualityThreshold)
|
|
{
|
|
// we found a bad tri; tentatively switch to rejecting this edge collapse
|
|
bIsHurt = true;
|
|
// but if there was an even worse tri in the original neighborhood, accept the collapse after all
|
|
Mesh.EnumerateVertexTriangles(RemoveV, [&Mesh, &bIsHurt, WorstQualityNewTriangle](int TID)
|
|
{
|
|
if (!bIsHurt) // early out if we already found an originally-worse tri
|
|
{
|
|
return;
|
|
}
|
|
FVector3d A, B, C;
|
|
Mesh.GetTriVertices(TID, A, B, C);
|
|
FVector3d E1 = B - A, E2 = C - A, E3 = C - B;
|
|
double Area2 = E1.Cross(E2).Length();
|
|
double MaxLenSq = FMathd::Max3(E1.SquaredLength(), E2.SquaredLength(), E3.SquaredLength());
|
|
double Quality = Area2 / (MaxLenSq + FMathd::ZeroTolerance);
|
|
if (Quality < WorstQualityNewTriangle)
|
|
{
|
|
bIsHurt = false;
|
|
}
|
|
}
|
|
);
|
|
}
|
|
return bIsHurt;
|
|
}
|
|
|
|
|
|
/**
|
|
* Test if a given edge collapse would change the mesh shape or UVs unacceptably
|
|
*/
|
|
bool FMeshBoolean::CollapseWouldChangeShapeOrUVs(
|
|
const FDynamicMesh3& Mesh, const TSet<int>& CutBoundaryEdgeSet, double DotTolerance, int SourceEID,
|
|
int32 RemoveV, const FVector3d& RemoveVPos, int32 KeepV, const FVector3d& KeepVPos, const FVector3d& EdgeDir,
|
|
bool bPreserveTriangleGroups, bool bPreserveUVsForMesh, bool bPreserveVertexUVs, bool bPreserveOverlayUVs,
|
|
float UVEqualThresholdSq, bool bPreserveVertexNormals, float NormalEqualCosThreshold)
|
|
{
|
|
// Search the edges connected to the vertex to find one in the boundary set that points in the opposite direction
|
|
// If we don't find that edge, or if there are other boundary/seam edges attached, we can't remove this vertex
|
|
// We also can't remove the vertex if doing so would distort the UVs
|
|
bool bHasBadEdge = false;
|
|
|
|
int OpposedEdge = -1;
|
|
int SourceGroupID = Mesh.GetTriangleGroup(Mesh.GetEdgeT(SourceEID).A);
|
|
Mesh.EnumerateVertexEdges(RemoveV,
|
|
[&](int32 VertEID)
|
|
{
|
|
if (bHasBadEdge || VertEID == SourceEID)
|
|
{
|
|
return;
|
|
}
|
|
|
|
FDynamicMesh3::FEdge Edge = Mesh.GetEdge(VertEID);
|
|
if (bPreserveTriangleGroups && Mesh.HasTriangleGroups())
|
|
{
|
|
if (SourceGroupID != Mesh.GetTriangleGroup(Edge.Tri.A) ||
|
|
(Edge.Tri.B != FDynamicMesh3::InvalidID && SourceGroupID != Mesh.GetTriangleGroup(Edge.Tri.B)))
|
|
{
|
|
// RemoveV is on a group boundary, so the edge collapse would change the shape of the groups
|
|
bHasBadEdge = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
// it's a known boundary edge; check if it's the opposite-facing one we need
|
|
if (CutBoundaryEdgeSet.Contains(VertEID))
|
|
{
|
|
if (OpposedEdge != -1)
|
|
{
|
|
bHasBadEdge = true;
|
|
return;
|
|
}
|
|
FIndex2i OtherEdgeV = Edge.Vert;
|
|
int OtherV = IndexUtil::FindEdgeOtherVertex(OtherEdgeV, RemoveV);
|
|
FVector3d OtherVPos = Mesh.GetVertex(OtherV);
|
|
FVector3d OtherEdgeDir = OtherVPos - RemoveVPos;
|
|
if (Normalize(OtherEdgeDir) == 0)
|
|
{
|
|
// collapsing degenerate edges above should prevent this
|
|
bHasBadEdge = true;
|
|
return; // break instead of continue to skip the whole edge
|
|
}
|
|
if (OtherEdgeDir.Dot(EdgeDir) <= -DotTolerance)
|
|
{
|
|
OpposedEdge = VertEID;
|
|
}
|
|
else
|
|
{
|
|
bHasBadEdge = true;
|
|
return;
|
|
}
|
|
|
|
// test that UVs are not too distorted through the collapse
|
|
if (!bPreserveUVsForMesh)
|
|
{
|
|
return;
|
|
}
|
|
float LerpT = (RemoveVPos - OtherVPos).Dot(OtherEdgeDir) / (KeepVPos - OtherVPos).Dot(OtherEdgeDir);
|
|
if (bPreserveVertexUVs && Mesh.HasVertexUVs())
|
|
{
|
|
FVector2f OtherUV = Mesh.GetVertexUV(OtherV);
|
|
FVector2f RemoveUV = Mesh.GetVertexUV(RemoveV);
|
|
FVector2f KeepUV = Mesh.GetVertexUV(KeepV);
|
|
if ( DistanceSquared( Lerp(OtherUV, KeepUV, LerpT), RemoveUV) > UVEqualThresholdSq)
|
|
{
|
|
bHasBadEdge = true;
|
|
return;
|
|
}
|
|
}
|
|
if (bPreserveVertexNormals && Mesh.HasVertexNormals())
|
|
{
|
|
FVector3f OtherN = Mesh.GetVertexNormal(OtherV);
|
|
FVector3f RemoveN = Mesh.GetVertexNormal(RemoveV);
|
|
FVector3f KeepN = Mesh.GetVertexNormal(KeepV);
|
|
if (Normalized(Lerp(OtherN, KeepN, LerpT)).Dot(Normalized(RemoveN)) < NormalEqualCosThreshold)
|
|
{
|
|
bHasBadEdge = true;
|
|
return;
|
|
}
|
|
}
|
|
if (bPreserveOverlayUVs && Mesh.HasAttributes())
|
|
{
|
|
int NumLayers = Mesh.Attributes()->NumUVLayers();
|
|
FIndex2i SourceEdgeTris = Mesh.GetEdgeT(SourceEID);
|
|
FIndex2i OppEdgeTris = Edge.Tri;
|
|
|
|
// special handling of seam edge when the edges aren't boundary edges
|
|
// -- if they're seams, we'd need to check both sides of the seams for a UV match
|
|
// but this is complicated and should be quite rare, so we just don't collapse these
|
|
if (SourceEdgeTris.B != -1 || OppEdgeTris.B != -1)
|
|
{
|
|
if (Mesh.Attributes()->IsSeamEdge(SourceEID) ||
|
|
Mesh.Attributes()->IsSeamEdge(VertEID))
|
|
{
|
|
bHasBadEdge = true;
|
|
return;
|
|
}
|
|
}
|
|
|
|
FIndex3i SourceBaseTri = Mesh.GetTriangle(SourceEdgeTris.A);
|
|
FIndex3i OppBaseTri = Mesh.GetTriangle(OppEdgeTris.A);
|
|
int KeepSourceIdx = IndexUtil::FindTriIndex(KeepV, SourceBaseTri);
|
|
int RemoveSourceIdx = IndexUtil::FindTriIndex(RemoveV, SourceBaseTri);
|
|
int OtherOppIdx = IndexUtil::FindTriIndex(OtherV, OppBaseTri);
|
|
if (!ensure(KeepSourceIdx != -1 && RemoveSourceIdx != -1 && OtherOppIdx != -1))
|
|
{
|
|
bHasBadEdge = true;
|
|
return;
|
|
}
|
|
|
|
// get the UVs per overlay off the triangle(s) attached the two edges
|
|
for (int UVLayerIdx = 0; UVLayerIdx < NumLayers; UVLayerIdx++)
|
|
{
|
|
const FDynamicMeshUVOverlay* UVs = Mesh.Attributes()->GetUVLayer(UVLayerIdx);
|
|
if (UVs->ElementCount() < 3)
|
|
{
|
|
// overlay is not actually in use; skip it
|
|
continue;
|
|
}
|
|
FIndex3i SourceT = UVs->GetTriangle(SourceEdgeTris.A);
|
|
FIndex3i OppT = UVs->GetTriangle(OppEdgeTris.A);
|
|
int KeepE = SourceT[KeepSourceIdx];
|
|
int RemoveE = SourceT[RemoveSourceIdx];
|
|
int OtherE = OppT[OtherOppIdx];
|
|
if (KeepE == -1 || RemoveE == -1 || OtherE == -1)
|
|
{
|
|
// overlay is not set on relevant triangles; skip it
|
|
continue;
|
|
}
|
|
FVector2f OtherUV = UVs->GetElement(OtherE);
|
|
FVector2f RemoveUV = UVs->GetElement(RemoveE);
|
|
FVector2f KeepUV = UVs->GetElement(KeepE);
|
|
if ( DistanceSquared( Lerp(OtherUV, KeepUV, LerpT), RemoveUV) > UVEqualThresholdSq)
|
|
{
|
|
bHasBadEdge = true;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
else // it wasn't in the boundary edge set; check if it's one that would prevent us from safely removing the vertex
|
|
{
|
|
if (Mesh.IsBoundaryEdge(VertEID) || (Mesh.HasAttributes() && Mesh.Attributes()->IsSeamEdge(VertEID)))
|
|
{
|
|
bHasBadEdge = true;
|
|
}
|
|
}
|
|
});
|
|
|
|
return bHasBadEdge;
|
|
}
|
|
|
|
|
|
void FMeshBoolean::SimplifyAlongNewEdges(int NumMeshesToProcess, FDynamicMesh3* CutMesh[2], TArray<int> CutBoundaryEdges[2], TMap<int, int>& AllVIDMatches)
|
|
{
|
|
double DotTolerance = FMathd::Cos(SimplificationAngleTolerance * FMathd::DegToRad);
|
|
|
|
TSet<int> CutBoundaryEdgeSets[2]; // set versions of CutBoundaryEdges, for faster membership tests
|
|
for (int MeshIdx = 0; MeshIdx < NumMeshesToProcess; MeshIdx++)
|
|
{
|
|
CutBoundaryEdgeSets[MeshIdx].Append(CutBoundaryEdges[MeshIdx]);
|
|
}
|
|
|
|
int NumCollapses = 0, CollapseIters = 0;
|
|
int MaxCollapseIters = 1; // TODO: is there a case where we need more iterations? Perhaps if we add some triangle quality criteria?
|
|
while (CollapseIters < MaxCollapseIters)
|
|
{
|
|
int LastNumCollapses = NumCollapses;
|
|
for (int EID : CutBoundaryEdges[0])
|
|
{
|
|
// this can happen if a collapse removes another cut boundary edge
|
|
// (which can happen e.g. if you have a degenerate (colinear) tri flat on the cut boundary)
|
|
if (!CutMesh[0]->IsEdge(EID))
|
|
{
|
|
continue;
|
|
}
|
|
// don't allow collapses if we somehow get down to our last triangle on either mesh
|
|
if (CutMesh[0]->TriangleCount() <= 1 || (NumMeshesToProcess == 2 && CutMesh[1]->TriangleCount() <= 1))
|
|
{
|
|
break;
|
|
}
|
|
|
|
FDynamicMesh3::FEdge Edge = CutMesh[0]->GetEdge(EID);
|
|
int Matches[2]{ -1, -1 };
|
|
bool bHasMatches = NumMeshesToProcess == 2;
|
|
if (bHasMatches)
|
|
{
|
|
for (int MatchIdx = 0; MatchIdx < 2; MatchIdx++)
|
|
{
|
|
int* Match = AllVIDMatches.Find(Edge.Vert[MatchIdx]);
|
|
if (Match)
|
|
{
|
|
Matches[MatchIdx] = *Match;
|
|
}
|
|
else
|
|
{
|
|
bHasMatches = false;
|
|
// TODO: if we switch to allow collapse on unmatched edges, we shouldn't break here
|
|
// b/c we may be partially matched, and need to track which is matched.
|
|
break;
|
|
}
|
|
}
|
|
if (!bHasMatches)
|
|
{
|
|
continue; // edge wasn't matched up on the other mesh; can't collapse it?
|
|
// TODO: consider supporting collapses in this case?
|
|
}
|
|
}
|
|
// if we have matched vertices, we also need a matched edge to collapse
|
|
int OtherEID = -1;
|
|
if (bHasMatches)
|
|
{
|
|
OtherEID = CutMesh[1]->FindEdge(Matches[0], Matches[1]);
|
|
if (OtherEID == -1)
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
// track whether the neighborhood of the vertex is flat (and likewise its matched pair's neighborhood, if present)
|
|
bool Flat[2]{ false, false };
|
|
// normals for each flat vertex, and each "side" (mesh 0 and mesh 1, if mesh 1 is present)
|
|
FVector3d FlatNormals[2][2]{ {FVector3d::Zero(), FVector3d::Zero()}, {FVector3d::Zero(), FVector3d::Zero()} };
|
|
int NumFlat = 0;
|
|
for (int VIdx = 0; VIdx < 2; VIdx++)
|
|
{
|
|
if (IsFlat(*CutMesh[0], Edge.Vert[VIdx], DotTolerance, FlatNormals[VIdx][0]))
|
|
{
|
|
Flat[VIdx] = (Matches[VIdx] == -1) || IsFlat(*CutMesh[1], Matches[VIdx], DotTolerance, FlatNormals[VIdx][1]);
|
|
}
|
|
|
|
if (Flat[VIdx])
|
|
{
|
|
NumFlat++;
|
|
}
|
|
}
|
|
|
|
if (NumFlat == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// see if we can collapse to remove either vertex
|
|
for (int RemoveVIdx = 0; RemoveVIdx < 2; RemoveVIdx++)
|
|
{
|
|
if (!Flat[RemoveVIdx])
|
|
{
|
|
continue;
|
|
}
|
|
int KeepVIdx = 1 - RemoveVIdx;
|
|
FVector3d RemoveVPos = CutMesh[0]->GetVertex(Edge.Vert[RemoveVIdx]);
|
|
FVector3d KeepVPos = CutMesh[0]->GetVertex(Edge.Vert[KeepVIdx]);
|
|
FVector3d EdgeDir = KeepVPos - RemoveVPos;
|
|
if (Normalize(EdgeDir) == 0) // 0 is returned as a special case when the edge was too short to normalize
|
|
{
|
|
// collapsing degenerate edges above should prevent this
|
|
ensure(!bCollapseDegenerateEdgesOnCut);
|
|
// Just skip these edges, because in practice we generally have bCollapseDegenerateEdgesOnCut enabled
|
|
break; // break instead of continue to skip the whole edge
|
|
}
|
|
|
|
bool bHasBadEdge = false; // will be set if either mesh can't collapse the edge
|
|
for (int MeshIdx = 0; !bHasBadEdge && MeshIdx < NumMeshesToProcess; MeshIdx++)
|
|
{
|
|
int RemoveV = MeshIdx == 0 ? Edge.Vert[RemoveVIdx] : Matches[RemoveVIdx];
|
|
int KeepV = MeshIdx == 0 ? Edge.Vert[KeepVIdx] : Matches[KeepVIdx];
|
|
int SourceEID = MeshIdx == 0 ? EID : OtherEID;
|
|
|
|
bHasBadEdge = bHasBadEdge || CollapseWouldHurtTriangleQuality(*CutMesh[MeshIdx],
|
|
FlatNormals[RemoveVIdx][MeshIdx], RemoveV, RemoveVPos, KeepV, KeepVPos, TryToImproveTriQualityThreshold);
|
|
|
|
bHasBadEdge = bHasBadEdge || CollapseWouldChangeShapeOrUVs(
|
|
*CutMesh[MeshIdx], CutBoundaryEdgeSets[MeshIdx], DotTolerance,
|
|
SourceEID, RemoveV, RemoveVPos, KeepV, KeepVPos, EdgeDir, bPreserveTriangleGroups,
|
|
PreserveUVsOnlyForMesh == -1 || MeshIdx == PreserveUVsOnlyForMesh,
|
|
bPreserveVertexUVs, bPreserveOverlayUVs, UVDistortTolerance * UVDistortTolerance,
|
|
bPreserveVertexNormals, FMathf::Cos(NormalDistortTolerance * FMathf::DegToRad));
|
|
};
|
|
|
|
if (bHasBadEdge)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// do some pre-collapse sanity checks on the matched edge (if present) to see if it will fail to collapse
|
|
bool bAttemptCollapse = true;
|
|
if (bHasMatches)
|
|
{
|
|
int OtherRemoveV = Matches[RemoveVIdx];
|
|
int OtherKeepV = Matches[KeepVIdx];
|
|
|
|
int a = OtherRemoveV, b = OtherKeepV;
|
|
int eab = CutMesh[1]->FindEdge(OtherRemoveV, OtherKeepV);
|
|
|
|
const FDynamicMesh3::FEdge EdgeAB = CutMesh[1]->GetEdge(eab);
|
|
int t0 = EdgeAB.Tri[0];
|
|
if (t0 == FDynamicMesh3::InvalidID)
|
|
{
|
|
bAttemptCollapse = false;
|
|
}
|
|
else
|
|
{
|
|
FIndex3i T0tv = CutMesh[1]->GetTriangle(t0);
|
|
int c = IndexUtil::FindTriOtherVtx(a, b, T0tv);
|
|
checkSlow(EdgeAB.Tri[1] == FDynamicMesh3::InvalidID);
|
|
// We cannot collapse if edge lists of a and b share vertices other
|
|
// than c and d (because then we will make a triangle [x b b].
|
|
// Brute-force search logic adapted from FDynamicMesh3::CollapseEdge implementation.
|
|
// (simplified because we know this is a boundary edge)
|
|
CutMesh[1]->EnumerateVertexVertices(a, [&](int VID)
|
|
{
|
|
if (!bAttemptCollapse || VID == c || VID == b)
|
|
{
|
|
return;
|
|
}
|
|
CutMesh[1]->EnumerateVertexVertices(b, [&](int VID2)
|
|
{
|
|
bAttemptCollapse &= (VID != VID2);
|
|
});
|
|
});
|
|
}
|
|
}
|
|
if (!bAttemptCollapse)
|
|
{
|
|
break; // don't try starting from other vertex if the match edge couldn't be collapsed
|
|
}
|
|
|
|
FDynamicMesh3::FEdgeCollapseInfo CollapseInfo;
|
|
int RemoveV = Edge.Vert[RemoveVIdx];
|
|
int KeepV = Edge.Vert[KeepVIdx];
|
|
// Detect the case of a triangle with two boundary edges, where collapsing
|
|
// the target boundary edge would keep the non-boundary edge.
|
|
// This collapse will remove the triangle, so we add the
|
|
// (formerly) non-boundary edge as our new boundary edge.
|
|
auto WouldRemoveTwoBoundaryEdges = [](const FDynamicMesh3& Mesh, int EID, int RemoveV)
|
|
{
|
|
checkSlow(Mesh.IsEdge(EID));
|
|
int OppV = Mesh.GetEdgeOpposingV(EID).A;
|
|
int NextOnTri = Mesh.FindEdge(RemoveV, OppV);
|
|
return Mesh.IsBoundaryEdge(NextOnTri);
|
|
};
|
|
bool bWouldRemoveNext = WouldRemoveTwoBoundaryEdges(*CutMesh[0], EID, RemoveV);
|
|
EMeshResult CollapseResult = CutMesh[0]->CollapseEdge(KeepV, RemoveV, 0, CollapseInfo);
|
|
if (CollapseResult == EMeshResult::Ok)
|
|
{
|
|
if (bWouldRemoveNext && ensure(CutMesh[0]->IsBoundaryEdge(CollapseInfo.KeptEdges.A)))
|
|
{
|
|
CutBoundaryEdgeSets[0].Add(CollapseInfo.KeptEdges.A);
|
|
}
|
|
|
|
if (bHasMatches)
|
|
{
|
|
int OtherRemoveV = Matches[RemoveVIdx];
|
|
int OtherKeepV = Matches[KeepVIdx];
|
|
bool bOtherWouldRemoveNext = WouldRemoveTwoBoundaryEdges(*CutMesh[1], OtherEID, OtherRemoveV);
|
|
FDynamicMesh3::FEdgeCollapseInfo OtherCollapseInfo;
|
|
EMeshResult OtherCollapseResult = CutMesh[1]->CollapseEdge(OtherKeepV, OtherRemoveV, 0, OtherCollapseInfo);
|
|
if (OtherCollapseResult != EMeshResult::Ok)
|
|
{
|
|
// if we get here, we've somehow managed to collapse the first edge but failed on the second (matched) edge
|
|
// which will leave a crack in the result unless we can somehow undo the first collapse, which would require a bunch of extra work
|
|
// but the only case where I could see this happen is if the second edge is on an isolated triangle, which means there is a hole anyway
|
|
// or if the mesh topology is somehow invalid
|
|
ensureMsgf(OtherCollapseResult == EMeshResult::Failed_CollapseTriangle, TEXT("Collapse failed with result: %d"), (int)OtherCollapseResult);
|
|
}
|
|
else
|
|
{
|
|
if (bOtherWouldRemoveNext && ensure(CutMesh[1]->IsBoundaryEdge(OtherCollapseInfo.KeptEdges.A)))
|
|
{
|
|
CutBoundaryEdgeSets[1].Add(OtherCollapseInfo.KeptEdges.A);
|
|
}
|
|
AllVIDMatches.Remove(RemoveV);
|
|
CutBoundaryEdgeSets[1].Remove(OtherCollapseInfo.CollapsedEdge);
|
|
CutBoundaryEdgeSets[1].Remove(OtherCollapseInfo.RemovedEdges[0]);
|
|
if (OtherCollapseInfo.RemovedEdges[1] != -1)
|
|
{
|
|
CutBoundaryEdgeSets[1].Remove(OtherCollapseInfo.RemovedEdges[1]);
|
|
}
|
|
}
|
|
}
|
|
|
|
NumCollapses++;
|
|
CutBoundaryEdgeSets[0].Remove(CollapseInfo.CollapsedEdge);
|
|
CutBoundaryEdgeSets[0].Remove(CollapseInfo.RemovedEdges[0]);
|
|
if (CollapseInfo.RemovedEdges[1] != -1)
|
|
{
|
|
CutBoundaryEdgeSets[0].Remove(CollapseInfo.RemovedEdges[1]);
|
|
}
|
|
}
|
|
break; // if we got through to trying to collapse the edge, don't try to collapse from the other vertex.
|
|
}
|
|
}
|
|
|
|
CutBoundaryEdges[0] = CutBoundaryEdgeSets[0].Array();
|
|
CutBoundaryEdges[1] = CutBoundaryEdgeSets[1].Array();
|
|
|
|
if (NumCollapses == LastNumCollapses)
|
|
{
|
|
break;
|
|
}
|
|
|
|
CollapseIters++;
|
|
}
|
|
}
|
|
|
|
|
|
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])
|
|
{
|
|
if (!ensure(CutMesh[1]->IsEdge(OldMeshEID)))
|
|
{
|
|
continue;
|
|
}
|
|
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);
|
|
}
|
|
else
|
|
{
|
|
if (bTrackAllNewEdges)
|
|
{
|
|
AllNewEdges.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 = true;
|
|
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 (DistanceSquared(OA, A) < SnapToleranceSq && DistanceSquared(OB, B) < SnapToleranceSq)
|
|
{
|
|
FDynamicMesh3::FMergeEdgesInfo MergeInfo;
|
|
EMeshResult EdgeMergeResult = Result->MergeEdges(EID, OtherEID, MergeInfo);
|
|
if (EdgeMergeResult == EMeshResult::Ok)
|
|
{
|
|
UnmatchedEdges.RemoveAtSwap(UnmatchedIdx, 1, false);
|
|
if (bTrackAllNewEdges)
|
|
{
|
|
AllNewEdges.Add(EID);
|
|
}
|
|
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;
|
|
}
|