// Copyright Epic Games, Inc. All Rights Reserved. #include "MeshRegionBoundaryLoops.h" #include "Algo/ForEach.h" #include "DynamicMesh/DynamicMeshAttributeSet.h" #include "MeshBoundaryLoops.h" // has a set of internal static functions we re-use #include "VectorUtil.h" #include "Util/SparseIndexCollectionTypes.h" using namespace UE::Geometry; FMeshRegionBoundaryLoops::FMeshRegionBoundaryLoops(const FDynamicMesh3* MeshIn, const TArray& RegionTris, bool bAutoCompute) { SetMesh(MeshIn, RegionTris); if (bAutoCompute) { Compute(); } } void FMeshRegionBoundaryLoops::SetMesh(const FDynamicMesh3* MeshIn, const TArray& RegionTris) { this->Mesh = MeshIn; // make flag set for included triangles Triangles.InitAuto(Mesh->MaxTriangleID(), RegionTris.Num()); for (int i = 0; i < RegionTris.Num(); ++i) { Triangles.Add(RegionTris[i]); } // make flag set for included edges // NOTE: this currently processes non-boundary-edges twice. Could // avoid w/ another IndexFlagSet, but the check is inexpensive... Edges.InitAuto(Mesh->MaxEdgeID(), RegionTris.Num()); for (int i = 0; i < RegionTris.Num(); ++i) { int tid = RegionTris[i]; FIndex3i te = Mesh->GetTriEdges(tid); for (int j = 0; j < 3; ++j) { int eid = te[j]; if (!Edges.Contains(eid)) { FIndex2i et = Mesh->GetEdgeT(eid); if (et.B == IndexConstants::InvalidID || Triangles[et.A] != Triangles[et.B]) { edges_roi.Add(eid); Edges.Add(eid); } } } } } int FMeshRegionBoundaryLoops::GetMaxVerticesLoopIndex() const { int j = 0; for (int i = 1; i < Loops.Num(); ++i) { if (Loops[i].Vertices.Num() > Loops[j].Vertices.Num()) { j = i; } } return j; } bool FMeshRegionBoundaryLoops::Compute() { bFailed = false; // reset // This algorithm assumes that triangles are oriented consistently, // so closed boundary-loop can be followed by walking edges in-order Loops.SetNum(0); // Temporary memory used to indicate when we have "used" an edge. FIndexFlagSet used_edge; used_edge.InitAuto(Mesh->MaxEdgeID(), edges_roi.Num()); // current loop is stored here, cleared after each loop extracted TArray loop_edges; TArray loop_verts; TArray bowties; // Temp buffer for reading back all boundary edges of a vertex. // probably always small but : pathological cases it could be large... TArray all_e; all_e.SetNum(16); // process all edges of mesh for (int eid : edges_roi) { if (used_edge[eid] == true) { continue; } if (IsEdgeOnBoundary(eid) == false) { continue; } // ok this is start of a boundary chain int eStart = eid; used_edge.Add(eStart); loop_edges.Add(eStart); int eCur = eid; int eFirstVert = -1; // the first vertex on eCur, in terms of our walking order // follow the chain : order of oriented edges bool bClosed = false; while (!bClosed) { // [TODO] can do this more efficiently? int tid_in = IndexConstants::InvalidID, tid_out = IndexConstants::InvalidID; IsEdgeOnBoundary(eCur, tid_in, tid_out); int cure_a, cure_b; if (eFirstVert == -1) { FIndex2i ev = GetOrientedEdgeVerts(eCur, tid_in); cure_a = ev.A; cure_b = ev.B; } else { // once we've walked on at least one edge, no longer need to rely on triangle orientation to know which way we were walking FIndex2i edgev = Mesh->GetEdgeV(eCur); checkSlow(edgev.Contains(eFirstVert)); cure_a = eFirstVert; cure_b = edgev.A == cure_a ? edgev.B : edgev.A; } loop_verts.Add(cure_a); int e0 = -1, e1 = 1; int bdry_nbrs = GetVertexBoundaryEdges(cure_b, e0, e1); if (bdry_nbrs < 2) { // found broken neighbourhood at vertex cure_b -- unrecoverable failure (unclosed loop) bFailed = true; return false; } int eNext = -1; if (bdry_nbrs > 2) { // found "bowtie" vertex...things just got complicated! if (cure_b == loop_verts[0]) { // The "end" of the current edge is the same as the start vertex. // This means we can close the loop here. Might as well! eNext = -2; // sentinel value used below } else { // try to find an unused outgoing edge that is oriented properly. // This could create sub-loops, we will handle those later if (bdry_nbrs >= all_e.Num()) all_e.SetNum(bdry_nbrs); int num_be = GetAllVertexBoundaryEdges(cure_b, all_e); check(num_be == bdry_nbrs); // Try to pick the best "turn left" vertex. eNext = FindLeftTurnEdge(eCur, cure_b, all_e, num_be, used_edge); if (eNext == -1) { // Cannot find valid outgoing edge at bowtie vertex cure_b -- unrecoverable failure bFailed = true; return false; } } if (bowties.Contains(cure_b) == false) { bowties.Add(cure_b); } } else { check(e0 == eCur || e1 == eCur); eNext = (e0 == eCur) ? e1 : e0; } if (eNext == -2) { // found a bowtie vert that is the same as start-of-loop, so we // are just closing it off explicitly bClosed = true; } else if (eNext == eStart) { // found edge at start of loop, so loop is done. bClosed = true; } else { // push onto accumulated list check(used_edge[eNext] == false); loop_edges.Add(eNext); eCur = eNext; used_edge.Add(eCur); } eFirstVert = cure_b; } // if we saw a bowtie vertex, we might need to break up this loop, // so call ExtractSubloops if (bowties.Num() > 0) { TArray subloops; bool bExtractedLoops = TryExtractSubloops(loop_verts, loop_edges, bowties, subloops); if (!bExtractedLoops) { // skip adding subloops and mark as failure (but go on computing the rest of the boundary loops) bFailed = true; } else { for (int i = 0; i < subloops.Num(); ++i) { Loops.Add(subloops[i]); } } } else { // clean simple loop, convert to FEdgeLoop instance FEdgeLoop loop(Mesh); loop.Vertices = loop_verts; loop.Edges = loop_edges; Loops.Add(loop); } // reset these lists loop_edges.SetNum(0); loop_verts.SetNum(0); bowties.SetNum(0); } return !bFailed; } // returns true for both internal and mesh boundary edges // tid_in and tid_out are triangles 'in' and 'out' of set, respectively bool FMeshRegionBoundaryLoops::IsEdgeOnBoundary(int eid, int& tid_in, int& tid_out) const { if (Edges.Contains(eid) == false) { return false; } tid_in = tid_out = IndexConstants::InvalidID; FIndex2i et = Mesh->GetEdgeT(eid); if (et.B == IndexConstants::InvalidID) // boundary edge! { tid_in = et.A; tid_out = et.B; return true; } bool in0 = Triangles[et.A]; bool in1 = Triangles[et.B]; if (in0 != in1) { tid_in = (in0) ? et.A : et.B; tid_out = (in0) ? et.B : et.A; return true; } return false; } // return same indices as GetEdgeV, but oriented based on attached triangle FIndex2i FMeshRegionBoundaryLoops::GetOrientedEdgeVerts(int eID, int tid_in) { FIndex2i edgev = Mesh->GetEdgeV(eID); int a = edgev.A, b = edgev.B; FIndex3i tri = Mesh->GetTriangle(tid_in); int ai = IndexUtil::FindEdgeIndexInTri(a, b, tri); return FIndex2i(tri[ai], tri[(ai + 1) % 3]); } int FMeshRegionBoundaryLoops::GetVertexBoundaryEdges(int vID, int& e0, int& e1) { int count = 0; for (int eid : Mesh->VtxEdgesItr(vID)) { if (IsEdgeOnBoundary(eid)) { if (count == 0) { e0 = eid; } else if (count == 1) { e1 = eid; } count++; } } return count; } int FMeshRegionBoundaryLoops::GetAllVertexBoundaryEdges(int vID, TArray& e) { int count = 0; for (int eid : Mesh->VtxEdgesItr(vID)) { if (IsEdgeOnBoundary(eid)) { e[count++] = eid; } } return count; } FVector3d FMeshRegionBoundaryLoops::GetVertexNormal(int vid) { FVector3d n = FVector3d::Zero(); for (int ti : Mesh->VtxTrianglesItr(vid)) { n += Mesh->GetTriNormal(ti); } Normalize(n); return n; } // // [TODO] for internal vertices, there is no ambiguity : which is the left-turn edge, // we should be using 'closest' left-neighbour edge. // // ok, bdry_edges[0...bdry_edges_count] contains the boundary edges coming out of bowtie_v. // We want to pick the best one to continue the loop that came : to bowtie_v on incoming_e. // If the loops are all sane, then we will get the smallest loops by "turning left" at bowtie_v. // So, we compute the tangent plane at bowtie_v, and then the signed angle for each // viable edge : this plane. int FMeshRegionBoundaryLoops::FindLeftTurnEdge(int incoming_e, int bowtie_v, TArray& bdry_edges, int bdry_edges_count, const FIndexFlagSet& used_edges) { // compute normal and edge [a,bowtie] FVector3d n = GetVertexNormal(bowtie_v); //int other_v = Mesh->edge_other_v(incoming_e, bowtie_v); FIndex2i ev = Mesh->GetEdgeV(incoming_e); int other_v = (ev.A == bowtie_v) ? ev.B : ev.A; FVector3d ab = Mesh->GetVertex(bowtie_v) - Mesh->GetVertex(other_v); // our winner int best_e = -1; double best_angle = TNumericLimits::Max(); for (int i = 0; i < bdry_edges_count; ++i) { int bdry_eid = bdry_edges[i]; if (used_edges[bdry_eid] == true) continue; // this edge is already used // [TODO] can do this more efficiently? int tid_in = IndexConstants::InvalidID, tid_out = IndexConstants::InvalidID; IsEdgeOnBoundary(bdry_eid, tid_in, tid_out); FIndex2i bdry_ev = GetOrientedEdgeVerts(bdry_eid, tid_in); //FIndex2i bdry_ev = Mesh.GetOrientedBoundaryEdgeV(bdry_eid); if (bdry_ev.A != bowtie_v) { continue; // have to be able to chain to end of current edge, orientation-wise } // compute projected angle FVector3d bc = Mesh->GetVertex(bdry_ev.B) - Mesh->GetVertex(bowtie_v); double fAngleS = -VectorUtil::PlaneAngleSignedD(ab, bc, n); // turn left! if (best_angle == TNumericLimits::Max() || fAngleS < best_angle) { best_angle = fAngleS; best_e = bdry_eid; } } return best_e; } // This is called when LoopV contains one or more "bowtie" vertices. // These vertices *might* be duplicated in LoopV (but not necessarily) // If they are, we have to break LoopV into subloops that don't contain duplicates. // // The list Bowties contains all the possible duplicates // (all v in Bowties occur in LoopV at least once) // // Currently LoopE is not used. // // Note: the approach used here, which doesn't fail, should also be used in FMeshBoundaryLoops. However we're // going to do additional tests there with hole filling to make sure that not failing has an overall positive // impact on hole filling, since we would rather fail there if the resulting subloops are more likely to be // bad for hole filling than good. bool FMeshRegionBoundaryLoops::TryExtractSubloops(TArray& LoopV, const TArray& LoopE, const TArray& Bowties, TArray& SubloopsOut) { // We keep track of the last place we've seen a bowtie in our list. As soon as we see a bowtie a second // time, we extract that subloop. As long as we do this immediately, the intervening vids will not be // duplicates even if they are bowties. TMap BowtieVidToLoopIndex; TSet BowtieVids(Bowties); for (int LoopIndex = 0; LoopIndex < LoopV.Num(); ++LoopIndex) { int32 Vid = LoopV[LoopIndex]; int32* SeenBowtieIndex = BowtieVidToLoopIndex.Find(Vid); if (SeenBowtieIndex) { // Check that the place we saw this wasn't in an extracted subloop (in which case the vert is set to -1 in LoopV) if (LoopV[*SeenBowtieIndex] >= 0) { // Now that we've seen the bowtie twice, extract the subloop TArray SubloopVertices; FMeshBoundaryLoops::ExtractSpan(LoopV, *SeenBowtieIndex, LoopIndex, true, SubloopVertices); FEdgeLoop& NewLoop = SubloopsOut[SubloopsOut.Emplace()]; NewLoop.InitializeFromVertices(Mesh, SubloopVertices, false); NewLoop.SetBowtieVertices(Bowties); } // Update the last place we saw the bowtie, in case a subsequent loop goes through here. BowtieVidToLoopIndex[Vid] = LoopIndex; } else if (BowtieVids.Contains(Vid)) { BowtieVidToLoopIndex.Add(Vid, LoopIndex); } } // Should have one loop left. TArray RemainingLoopVids; for (int32 Vid : LoopV) { if (Vid >= 0) { RemainingLoopVids.Add(Vid); } } if (ensure(RemainingLoopVids.Num() > 2)) { FEdgeLoop& NewLoop = SubloopsOut[SubloopsOut.Emplace()]; NewLoop.InitializeFromVertices(Mesh, RemainingLoopVids, false); NewLoop.SetBowtieVertices(Bowties); } else { return false; } return true; } template bool FMeshRegionBoundaryLoops::GetLoopOverlayMap(const FEdgeLoop& LoopIn, const TDynamicMeshOverlay& Overlay, VidOverlayMap& LoopVidsToOverlayElementsOut) { for (int32 i = 0; i < LoopIn.Vertices.Num(); ++i) { int32 Vid = LoopIn.Vertices[i]; // Get the inner triangle associated with the edges going forward from this vertex int32 TidInside, TidOutside; IsEdgeOnBoundary(LoopIn.Edges[i], TidInside, TidOutside); check(TidInside != IndexConstants::InvalidID); // Find the overlay element associated with the vertex FIndex3i TriangleVerts = Mesh->GetTriangle(TidInside); int32 VidTriIndex = TriangleVerts.IndexOf(Vid); check(VidTriIndex >= 0); FIndex3i TriangleElements = Overlay.GetTriangle(TidInside); int32 UVElementID = TriangleElements[VidTriIndex]; if (!Overlay.IsElement(UVElementID)) { return false; } ElementType Element; Overlay.GetElement(UVElementID, Element); LoopVidsToOverlayElementsOut.Add(Vid, ElementIDAndValue(UVElementID, Element)); } return true; } template void FMeshRegionBoundaryLoops::UpdateLoopOverlayMapValidity( VidOverlayMap& LoopVidsToOverlayElements, const TDynamicMeshOverlay& Overlay) { // Go through all the overlay element ids's and see if they are still an element // in the overlay. If not, make that id an invalid ID. Algo::ForEachIf(LoopVidsToOverlayElements, [&Overlay](const auto& Entry) { return !Overlay.IsElement(Entry.Value.Key); }, [](auto& Entry) { Entry.Value.Key = IndexConstants::InvalidID; }); } // Right now we use our templated functions just for UV layers. If we need other overlay layers, // we'll need to add instantiations here. template GEOMETRYCORE_API bool FMeshRegionBoundaryLoops::GetLoopOverlayMap( const FEdgeLoop& LoopIn, const TDynamicMeshOverlay& Overlay, VidOverlayMap& LoopVidsToOverlayElementsOut); template GEOMETRYCORE_API void FMeshRegionBoundaryLoops::UpdateLoopOverlayMapValidity( VidOverlayMap& LoopVidsToOverlayElements, const TDynamicMeshOverlay& Overlay); bool FMeshRegionBoundaryLoops::GetTriangleSetBoundaryLoop(const FDynamicMesh3& Mesh, const TArray& Tris, FEdgeLoop& Loop) { // todo: special-case single triangle // collect list of border edges TArray Edges; for (int32 tid : Tris) { FIndex3i TriEdges = Mesh.GetTriEdges(tid); for (int32 j = 0; j < 3; ++j) { FIndex2i EdgeT = Mesh.GetEdgeT(TriEdges[j]); int32 OtherT = (EdgeT.A == tid) ? EdgeT.B : EdgeT.A; if (OtherT == FDynamicMesh3::InvalidID || Tris.Contains(OtherT) == false) { Edges.AddUnique(TriEdges[j]); } } } if (Edges.Num() == 0) { return false; } Loop.Mesh = &Mesh; // Start at first edge and walk around loop, adding one vertex and edge each time. // Abort if we encounter any nonmanifold configuration int32 NumEdges = Edges.Num(); int32 StartEdge = Edges[0]; FIndex2i StartEdgeT = Mesh.GetEdgeT(StartEdge); int32 InTri = Tris.Contains(StartEdgeT.A) ? StartEdgeT.A : StartEdgeT.B; FIndex2i StartEdgeV = Mesh.GetEdgeV(StartEdge); IndexUtil::OrientTriEdge(StartEdgeV.A, StartEdgeV.B, Mesh.GetTriangle(InTri)); Loop.Vertices.Reset(); Loop.Vertices.Add(StartEdgeV.A); Loop.Vertices.Add(StartEdgeV.B); int32 CurEndVert = Loop.Vertices.Last(); int32 PrevEdge = StartEdge; Loop.Edges.Reset(); Loop.Edges.Add(StartEdge); int32 NumEdgesUsed = 1; bool bContinue = true; do { bContinue = false; for (int32 eid : Mesh.VtxEdgesItr(CurEndVert)) { if (eid != PrevEdge && Edges.Contains(eid) && Loop.Edges.Contains(eid) == false) { FIndex2i EdgeV = Mesh.GetEdgeV(eid); int32 NextV = (EdgeV.A == CurEndVert) ? EdgeV.B : EdgeV.A; if (NextV == Loop.Vertices[0]) // closed loop { Loop.Edges.Add(eid); NumEdgesUsed++; bContinue = false; break; } else { if (Loop.Vertices.Contains(NextV)) { return false; // hit a middle vertex, we have nonmanifold set of edges, abort } Loop.Edges.Add(eid); PrevEdge = eid; Loop.Vertices.Add(NextV); NumEdgesUsed++; CurEndVert = NextV; bContinue = true; break; } } } } while (bContinue); if (NumEdgesUsed != Edges.Num()) // closed loop but we still have edges? must have nonmanifold configuration, abort. { return false; } return true; }