Files
UnrealEngine/Engine/Source/Runtime/GeometryCore/Private/MeshRegionBoundaryLoops.cpp
2025-05-18 13:04:45 +08:00

624 lines
17 KiB
C++

// 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<int>& RegionTris, bool bAutoCompute)
{
SetMesh(MeshIn, RegionTris);
if (bAutoCompute)
{
Compute();
}
}
void FMeshRegionBoundaryLoops::SetMesh(const FDynamicMesh3* MeshIn, const TArray<int>& 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<int> loop_edges;
TArray<int> loop_verts;
TArray<int> bowties;
// Temp buffer for reading back all boundary edges of a vertex.
// probably always small but : pathological cases it could be large...
TArray<int> 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<FEdgeLoop> 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<int>& 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<int>& 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<double>::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<double>::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<int>& LoopV, const TArray<int>& LoopE, const TArray<int>& Bowties, TArray<FEdgeLoop>& 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<int32, int32> BowtieVidToLoopIndex;
TSet<int32> 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<int32> 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<int32> 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<typename StorageType, int ElementSize, typename ElementType>
bool FMeshRegionBoundaryLoops::GetLoopOverlayMap(const FEdgeLoop& LoopIn,
const TDynamicMeshOverlay<StorageType, ElementSize>& Overlay,
VidOverlayMap<ElementType>& 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<ElementType>(UVElementID, Element));
}
return true;
}
template<typename StorageType, int ElementSize, typename ElementType>
void FMeshRegionBoundaryLoops::UpdateLoopOverlayMapValidity(
VidOverlayMap<ElementType>& LoopVidsToOverlayElements,
const TDynamicMeshOverlay<StorageType, ElementSize>& 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<float, 2, FVector2f>(
const FEdgeLoop& LoopIn, const TDynamicMeshOverlay<float, 2>& Overlay,
VidOverlayMap<FVector2f>& LoopVidsToOverlayElementsOut);
template GEOMETRYCORE_API void FMeshRegionBoundaryLoops::UpdateLoopOverlayMapValidity<float, 2, FVector2f>(
VidOverlayMap<FVector2f>& LoopVidsToOverlayElements, const TDynamicMeshOverlay<float, 2>& Overlay);
bool FMeshRegionBoundaryLoops::GetTriangleSetBoundaryLoop(const FDynamicMesh3& Mesh, const TArray<int32>& Tris, FEdgeLoop& Loop)
{
// todo: special-case single triangle
// collect list of border edges
TArray<int32> 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;
}