// Copyright Epic Games, Inc. All Rights Reserved. #include "GroupTopology.h" #include "Algo/MinElement.h" #include "MeshRegionBoundaryLoops.h" using namespace UE::Geometry; bool FGroupTopology::FGroupEdge::IsConnectedToVertices(const TSet& Vertices) const { for (int VertexID : Span.Vertices) { if (Vertices.Contains(VertexID)) { return true; } } return false; } FGroupTopology::FGroupTopology(const FDynamicMesh3* MeshIn, bool bAutoBuild) { this->Mesh = MeshIn; this->GroupLayer = nullptr; if (bAutoBuild) { RebuildTopology(); } } FGroupTopology::FGroupTopology(const FDynamicMesh3* MeshIn, const FDynamicMeshPolygroupAttribute* GroupLayerIn, bool bAutoBuild) { this->Mesh = MeshIn; this->GroupLayer = GroupLayerIn; if (bAutoBuild) { RebuildTopology(); } } bool FGroupTopology::RebuildTopology() { Groups.Reset(); Edges.Reset(); Corners.Reset(); CurrentExtraCornerVids.Reset(); int32 MaxGroupID = 0; for (int32 tid : Mesh->TriangleIndicesItr()) { MaxGroupID = FMath::Max(GetGroupID(tid), MaxGroupID); } MaxGroupID++; // initialize groups map first to avoid resizes GroupIDToGroupIndexMap.Reset(); GroupIDToGroupIndexMap.Init(-1, MaxGroupID); TArray GroupFaceCounts; GroupFaceCounts.Init(0, MaxGroupID); for (int tid : Mesh->TriangleIndicesItr()) { int GroupID = GetGroupID(tid); if (GroupIDToGroupIndexMap[GroupID] == -1) { FGroup NewGroup; NewGroup.GroupID = GroupID; GroupIDToGroupIndexMap[GroupID] = Groups.Add(NewGroup); } GroupFaceCounts[GroupID]++; } for (FGroup& Group : Groups) { Group.Triangles.Reserve(GroupFaceCounts[Group.GroupID]); } // sort faces into groups for (int tid : Mesh->TriangleIndicesItr()) { int GroupID = GetGroupID(tid); Groups[GroupIDToGroupIndexMap[GroupID]].Triangles.Add(tid); } VertexIDToCornerIDMap.Reset(); TMap GroupEdgeMinEidToGroupEdgeID; TBitArray<> VertCheckedForCorner; VertCheckedForCorner.Init(false, Mesh->MaxVertexID()); // construct boundary loops for (FGroup& Group : Groups) { bool bOK = GenerateBoundaryAndGroupEdges(Group, GroupEdgeMinEidToGroupEdgeID, VertCheckedForCorner); if (!bOK) { return false; } // collect up .NeighbourGroupIDs and set .bIsOnBoundary for (FGroupBoundary& Boundary : Group.Boundaries) { Boundary.bIsOnBoundary = false; for (int EdgeIndex : Boundary.GroupEdges) { FGroupEdge& Edge = Edges[EdgeIndex]; int OtherGroupID = (Edge.Groups.A == Group.GroupID) ? Edge.Groups.B : Edge.Groups.A; if (OtherGroupID != FDynamicMesh3::InvalidID) { Boundary.NeighbourGroupIDs.AddUnique(OtherGroupID); } else { Boundary.bIsOnBoundary = true; } } } // make all-neighbour-groups list at group level for (FGroupBoundary& Boundary : Group.Boundaries) { for (int NbrGroupID : Boundary.NeighbourGroupIDs) { Group.NeighbourGroupIDs.AddUnique(NbrGroupID); } } } return true; } void FGroupTopology::RetargetOnClonedMesh(const FDynamicMesh3* NewMesh) { Mesh = NewMesh; for (FGroupEdge& Edge : Edges) { Edge.Span.Mesh = NewMesh; } } bool FGroupTopology::IsCornerVertex(int VertexID) const { return ShouldVertBeCorner(VertexID); } bool FGroupTopology::ShouldVertBeCorner(int VertexID, bool* bIsAnExtraCornerOut) const { if (bIsAnExtraCornerOut) { *bIsAnExtraCornerOut = false; } // If the vertex has at least three attached groupedge edges (ie three edges that separate two groups // or are boundary edges), then it has to be a corner. Otherwise, we let the user supply a function that // determines whether the vert should be a corner anyway, passing in the group edges that do pass through the // vertex. // only ends up being used if there are less than 3, so that we can pass in the // relevant edges to the user function. FIndex2i AttachedGroupEdgeEids(IndexConstants::InvalidID, IndexConstants::InvalidID); for (int32 Eid : Mesh->VtxEdgesItr(VertexID)) { FIndex2i EdgeTris = Mesh->GetEdgeT(Eid); if (EdgeTris.B == IndexConstants::InvalidID || GetGroupID(EdgeTris.A) != GetGroupID(EdgeTris.B)) { // This is a group edge. if (AttachedGroupEdgeEids[0] == IndexConstants::InvalidID) { AttachedGroupEdgeEids[0] = Eid; } else if (AttachedGroupEdgeEids[1] == IndexConstants::InvalidID) { AttachedGroupEdgeEids[1] = Eid; } else { // This is the third such edge. Has to be corner. return true; } }//end if part of group edge }//end looking through attached edges. // If we got to here, then the vert isn't a corner just based on group topology, but can be made // into a corner anyway by user function (for instance, at sharp corners, or at user-defined points). if (ShouldAddExtraCornerAtVert && ShouldAddExtraCornerAtVert(*this, VertexID, AttachedGroupEdgeEids)) { if (bIsAnExtraCornerOut) { *bIsAnExtraCornerOut = true; } return true; } return false; } int FGroupTopology::GetCornerVertexID(int CornerID) const { check(CornerID >= 0 && CornerID < Corners.Num()); return Corners[CornerID].VertexID; } int32 FGroupTopology::GetCornerIDFromVertexID(int32 VertexID) const { check(Mesh->IsVertex(VertexID)); const int32* Found = VertexIDToCornerIDMap.Find(VertexID); return (Found == nullptr) ? IndexConstants::InvalidID : *Found; } const FGroupTopology::FGroup* FGroupTopology::FindGroupByID(int GroupID) const { if (GroupID < 0 || GroupID >= GroupIDToGroupIndexMap.Num() || GroupIDToGroupIndexMap[GroupID] == -1) { return nullptr; } return &Groups[GroupIDToGroupIndexMap[GroupID]]; } const TArray& FGroupTopology::GetGroupTriangles(int GroupID) const { const FGroup* Found = FindGroupByID(GroupID); ensure(Found != nullptr); return (Found != nullptr) ? Found->Triangles : EmptyArray; } const TArray& FGroupTopology::GetGroupNbrGroups(int GroupID) const { const FGroup* Found = FindGroupByID(GroupID); ensure(Found != nullptr); return (Found != nullptr) ? Found->NeighbourGroupIDs : EmptyArray; } bool FGroupTopology::IsGroupEdge(FMeshTriEdgeID TriEdgeID, bool bIncludeMeshBoundary) const { int32 EdgeID = Mesh->GetTriEdge((int32)TriEdgeID.TriangleID, (int32)TriEdgeID.TriEdgeIndex); if (EdgeID != IndexConstants::InvalidID) { FIndex2i EdgeT = Mesh->GetEdgeT(EdgeID); if (EdgeT.B == IndexConstants::InvalidID) { return bIncludeMeshBoundary; } int32 GroupA = GetGroupID(EdgeT.A); int32 GroupB = GetGroupID(EdgeT.B); return GroupA != GroupB; } return false; } int FGroupTopology::FindGroupEdgeID(int MeshEdgeID) const { int GroupID = GetGroupID(Mesh->GetEdgeT(MeshEdgeID).A); const FGroup* Group = FindGroupByID(GroupID); ensure(Group != nullptr); if (Group != nullptr) { for (const FGroupBoundary& Boundary : Group->Boundaries) { for (int EdgeID : Boundary.GroupEdges) { const FGroupEdge& Edge = Edges[EdgeID]; if (Edge.Span.Edges.Contains(MeshEdgeID)) { return EdgeID; } } } } return -1; } int FGroupTopology::FindGroupEdgeID(FMeshTriEdgeID TriEdgeID) const { int32 EdgeID = Mesh->GetTriEdge((int32)TriEdgeID.TriangleID, (int32)TriEdgeID.TriEdgeIndex); return (EdgeID != IndexConstants::InvalidID) ? FindGroupEdgeID(EdgeID) : -1; } const TArray& FGroupTopology::GetGroupEdgeVertices(int GroupEdgeID) const { check(GroupEdgeID >= 0 && GroupEdgeID < Edges.Num()); return Edges[GroupEdgeID].Span.Vertices; } const TArray& FGroupTopology::GetGroupEdgeEdges(int GroupEdgeID) const { check(GroupEdgeID >= 0 && GroupEdgeID < Edges.Num()); return Edges[GroupEdgeID].Span.Edges; } bool FGroupTopology::IsSimpleGroupEdge(int32 GroupEdgeID) const { check(GroupEdgeID >= 0 && GroupEdgeID < Edges.Num()); return Edges[GroupEdgeID].Span.Edges.Num() == 1; } void FGroupTopology::FindEdgeNbrGroups(int GroupEdgeID, TArray& GroupsOut) const { check(GroupEdgeID >= 0 && GroupEdgeID < Edges.Num()); const TArray & Vertices = GetGroupEdgeVertices(GroupEdgeID); FindVertexNbrGroups(Vertices[0], GroupsOut); FindVertexNbrGroups(Vertices[Vertices.Num() - 1], GroupsOut); } void FGroupTopology::FindEdgeNbrGroups(const TArray& GroupEdgeIDs, TArray& GroupsOut) const { for (int GroupEdgeID : GroupEdgeIDs) { FindEdgeNbrGroups(GroupEdgeID, GroupsOut); } } void FGroupTopology::FindEdgeNbrEdges(int GroupEdgeID, TArray& EdgesOut) const { check(GroupEdgeID >= 0 && GroupEdgeID < Edges.Num()); const FGroupEdge& Edge = Edges[GroupEdgeID]; if ( Edge.EndpointCorners.A != IndexConstants::InvalidID ) { FindCornerNbrEdges(Edge.EndpointCorners.A, EdgesOut); } if ( Edge.EndpointCorners.B != IndexConstants::InvalidID ) { FindCornerNbrEdges(Edge.EndpointCorners.B, EdgesOut); } } bool FGroupTopology::IsBoundaryEdge(int32 GroupEdgeID) const { return Mesh->IsBoundaryEdge(Edges[GroupEdgeID].Span.Edges[0]); } bool FGroupTopology::IsIsolatedLoop(int GroupEdgeID) const { const FGroupEdge& Edge = Edges[GroupEdgeID]; return Edge.EndpointCorners[0] == IndexConstants::InvalidID; } double FGroupTopology::GetEdgeArcLength(int32 GroupEdgeID, TArray* PerVertexLengthsOut) const { check(GroupEdgeID >= 0 && GroupEdgeID < Edges.Num()); const TArray& Vertices = GetGroupEdgeVertices(GroupEdgeID); int32 NumV = Vertices.Num(); if (PerVertexLengthsOut != nullptr) { PerVertexLengthsOut->SetNum(NumV); (*PerVertexLengthsOut)[0] = 0.0; } double AccumLength = 0; for (int32 k = 1; k < NumV; ++k) { AccumLength += Distance(Mesh->GetVertex(Vertices[k]), Mesh->GetVertex(Vertices[k-1])); if (PerVertexLengthsOut != nullptr) { (*PerVertexLengthsOut)[k] = AccumLength; } } return AccumLength; } FVector3d FGroupTopology::GetEdgeAveragePosition(int32 GroupEdgeID) const { const TArray& Vertices = GetGroupEdgeVertices(GroupEdgeID); double WtSum = 0; FVector3d Sum = FVector3d::ZeroVector; FVector LastV = Mesh->GetVertex(Vertices[0]); for (int32 Idx = 0; Idx + 1 < Vertices.Num(); ++Idx) { FVector3d A = LastV; FVector3d B = Mesh->GetVertex(Vertices[Idx + 1]); // weight edge centers by edge lengths double Wt = FVector3d::Distance(A, B); Sum += Wt * .5 * (A + B); WtSum += Wt; LastV = B; } if (WtSum < FMathd::Epsilon) // if edges were all ~ zero length, just use one of the vertices { return LastV; } return Sum / WtSum; } FVector3d FGroupTopology::GetEdgeMidpoint(int32 GroupEdgeID, double* ArcLengthOut, TArray* PerVertexLengthsOut) const { check(GroupEdgeID >= 0 && GroupEdgeID < Edges.Num()); const TArray& Vertices = GetGroupEdgeVertices(GroupEdgeID); int32 NumV = Vertices.Num(); // trivial case if (NumV == 2) { FVector3d A(Mesh->GetVertex(Vertices[0])), B(Mesh->GetVertex(Vertices[1])); if (ArcLengthOut) { *ArcLengthOut = Distance(A, B); } if (PerVertexLengthsOut) { (*PerVertexLengthsOut).SetNum(2); (*PerVertexLengthsOut)[0] = 0; (*PerVertexLengthsOut)[1] = Distance(A, B); } return (A + B) * 0.5; } // if we want lengths anyway we can avoid second loop if (PerVertexLengthsOut) { double Len = GetEdgeArcLength(GroupEdgeID, PerVertexLengthsOut); if (ArcLengthOut) { *ArcLengthOut = Len; } Len /= 2; int32 k = 0; while ((*PerVertexLengthsOut)[k] < Len) { k++; } int32 kprev = k - 1; double a = (*PerVertexLengthsOut)[k-1], b = (*PerVertexLengthsOut)[k]; double t = (Len - a) / (b - a); FVector3d A(Mesh->GetVertex(Vertices[k-1])), B(Mesh->GetVertex(Vertices[k])); return Lerp(A, B, t); } // compute arclen and then walk forward until we get halfway double Len = GetEdgeArcLength(GroupEdgeID); if (ArcLengthOut) { *ArcLengthOut = Len; } Len /= 2; double AccumLength = 0; for (int32 k = 1; k < NumV; ++k) { double NewLen = AccumLength + Distance(Mesh->GetVertex(Vertices[k]), Mesh->GetVertex(Vertices[k-1])); if ( NewLen > Len ) { double t = (Len - AccumLength) / (NewLen - AccumLength); FVector3d A(Mesh->GetVertex(Vertices[k - 1])), B(Mesh->GetVertex(Vertices[k])); return Lerp(A, B, t); } AccumLength = NewLen; } // somehow failed? return (Mesh->GetVertex(Vertices[0]) + Mesh->GetVertex(Vertices[NumV-1])) * 0.5; } void FGroupTopology::FindCornerNbrGroups(int CornerID, TArray& GroupsOut) const { check(CornerID >= 0 && CornerID < Corners.Num()); for (int GroupID : Corners[CornerID].NeighbourGroupIDs) { GroupsOut.AddUnique(GroupID); } } void FGroupTopology::FindCornerNbrGroups(const TArray& CornerIDs, TArray& GroupsOut) const { for (int cid : CornerIDs) { FindCornerNbrGroups(cid, GroupsOut); } } void FGroupTopology::FindCornerNbrEdges(int CornerID, TArray& EdgesOut) const { ForCornerNbrEdges(CornerID, [&EdgesOut](int32 EdgeID) { EdgesOut.Add(EdgeID); return true; }); } void UE::Geometry::FGroupTopology::ForCornerNbrEdges(int CornerID, TFunctionRefReturnTrueToContinue) const { check(CornerID >= 0 && CornerID < Corners.Num()); TSet ProcessedEdges; for (int GroupID : Corners[CornerID].NeighbourGroupIDs) { const FGroup* Group = FindGroupByID(GroupID); for (const FGroupBoundary& Boundary : Group->Boundaries) { for (int32 EdgeID : Boundary.GroupEdges) { const FGroupEdge& Edge = Edges[EdgeID]; if (Edge.EndpointCorners.A != CornerID && Edge.EndpointCorners.B != CornerID) { continue; } bool bAlreadyProcessed = false; ProcessedEdges.Add(EdgeID, &bAlreadyProcessed); if (bAlreadyProcessed) { continue; } if (!ReturnTrueToContinue(EdgeID)) { return; } } } } } void FGroupTopology::FindCornerNbrCorners(int CornerID, TArray& CornersOut) const { check(CornerID >= 0 && CornerID < Corners.Num()); for (int GroupID : Corners[CornerID].NeighbourGroupIDs) { const FGroup* Group = FindGroupByID(GroupID); for (const FGroupBoundary& Boundary : Group->Boundaries) { for (int32 EdgeID : Boundary.GroupEdges) { const FGroupEdge& Edge = Edges[EdgeID]; if (Edge.EndpointCorners.A == CornerID || Edge.EndpointCorners.B == CornerID) { int32 OtherCornerID = Edge.EndpointCorners.OtherElement(CornerID); CornersOut.AddUnique(OtherCornerID); } } } } } void FGroupTopology::FindVertexNbrGroups(int VertexID, TArray& GroupsOut) const { for (int tid : Mesh->VtxTrianglesItr(VertexID)) { int GroupID = GetGroupID(tid); GroupsOut.AddUnique(GroupID); } } void FGroupTopology::FindVertexNbrGroups(const TArray& VertexIDs, TArray& GroupsOut) const { for (int vid : VertexIDs) { for (int tid : Mesh->VtxTrianglesItr(vid)) { int GroupID = GetGroupID(tid); GroupsOut.AddUnique(GroupID); } } } void FGroupTopology::CollectGroupVertices(int GroupID, TSet& Vertices) const { const FGroup* Found = FindGroupByID(GroupID); ensure(Found != nullptr); if (Found != nullptr) { for (int TriID : Found->Triangles) { FIndex3i TriVerts = Mesh->GetTriangle(TriID); Vertices.Add(TriVerts.A); Vertices.Add(TriVerts.B); Vertices.Add(TriVerts.C); } } } void FGroupTopology::CollectGroupBoundaryVertices(int GroupID, TSet& Vertices) const { const FGroup* Group = FindGroupByID(GroupID); ensure(Group != nullptr); if (Group != nullptr) { for (const FGroupBoundary& Boundary : Group->Boundaries) { for (int EdgeIndex : Boundary.GroupEdges) { const FGroupEdge& Edge = Edges[EdgeIndex]; for (int vid : Edge.Span.Vertices) { Vertices.Add(vid); } } } } } bool FGroupTopology::GenerateBoundaryAndGroupEdges(FGroup& Group, TMap& GroupEdgeMinEidToGroupEdgeID, TBitArray<>& VertCheckedForCorner) { FMeshRegionBoundaryLoops BdryLoops(Mesh, Group.Triangles, true); if (BdryLoops.bFailed) { // Unrecoverable error when trying to find the group boundary loops return false; } int NumLoops = BdryLoops.Loops.Num(); // Function we use to see if a vertex is a corner, creating a corner object if so auto CheckForCornerAndCreateIfNeeded = [this, &GroupEdgeMinEidToGroupEdgeID, &VertCheckedForCorner](int32 Vid) { // See if we have a cached result if (VertCheckedForCorner[Vid]) { return VertexIDToCornerIDMap.Contains(Vid); } else { VertCheckedForCorner[Vid] = true; bool bIsExtraCorner = false; if (ShouldVertBeCorner(Vid, &bIsExtraCorner)) { // New corner vertex found. Create a corner for this vertex. int32 CornerID = Corners.Emplace(); FCorner& Corner = Corners[CornerID]; Corner.VertexID = Vid; GetAllVertexGroups(Vid, Corner.NeighbourGroupIDs); VertexIDToCornerIDMap.Add(Vid, CornerID); if (bIsExtraCorner) { CurrentExtraCornerVids.Add(Vid); } return true; } else { return false; } } }; // Go through the boundary, find the corners, and use the intervening edges to create // group edges. Group.Boundaries.SetNum(NumLoops); for ( int li = 0; li < NumLoops; ++li ) { FEdgeLoop& Loop = BdryLoops.Loops[li]; FGroupBoundary& Boundary = Group.Boundaries[li]; // find indices of corners of group polygon TArray CornerIndices; int NumV = Loop.Vertices.Num(); for (int i = 0; i < NumV; ++i) { if (CheckForCornerAndCreateIfNeeded(Loop.Vertices[i])) { CornerIndices.Add(i); } } // if we had no indices then this is like the cap of a cylinder, just one single long edge if ( CornerIndices.Num() == 0 ) { int32 MinEid = *Algo::MinElement(Loop.Edges); int32* ExistingGroupEdgeID = GroupEdgeMinEidToGroupEdgeID.Find(MinEid); int EdgeIndex = ExistingGroupEdgeID ? *ExistingGroupEdgeID : IndexConstants::InvalidID; if (EdgeIndex == IndexConstants::InvalidID) { FGroupEdge Edge = { MakeEdgeGroupsPair(Loop.Edges[0]) }; Edge.Span = FEdgeSpan(Mesh); Edge.Span.InitializeFromEdges(Loop.Edges); Edge.EndpointCorners = FIndex2i::Invalid(); EdgeIndex = Edges.Add(Edge); GroupEdgeMinEidToGroupEdgeID.Add(MinEid, EdgeIndex); } Boundary.GroupEdges.Add(EdgeIndex); continue; } // duplicate first corner vertex so that we can just loop back around to it w/ modulo count int NumSpans = CornerIndices.Num(); int FirstIdx = CornerIndices[0]; CornerIndices.Add(FirstIdx); // add each span for (int k = 0; k < NumSpans; ++k) { int32 StartIndex = CornerIndices[k]; int32 EndIndex = CornerIndices[k + 1]; // note: StartIndex == EndIndex on a closed loop, ie NumSpans == 1 // This calculation of number of edges is correct as long as we're not starting and stopping in the same place. int32 NumSpanEdges = (EndIndex + NumV - StartIndex) % NumV; // If StartIndex == EndIndex, then NumSpanEdges should actually be NumV, not 0, because we are on a loop with // a single corner (either a bowtie or user-generated). if (NumSpanEdges == 0) { ensure(NumSpans == 1); NumSpanEdges = NumV; } // Find the min eid in this subsection so that we can see if we already have a group edge for it. int32 MinEid = Loop.Edges[StartIndex]; for (int32 i = 1; i < NumSpanEdges; ++i) { int32 Index = (StartIndex + i) % NumV; MinEid = FMath::Min(MinEid, Loop.Edges[Index]); } int32* ExistingGroupEdgeID = GroupEdgeMinEidToGroupEdgeID.Find(MinEid); int EdgeIndex = ExistingGroupEdgeID ? *ExistingGroupEdgeID : IndexConstants::InvalidID; if (EdgeIndex != IndexConstants::InvalidID) { FGroupEdge& Existing = Edges[EdgeIndex]; Boundary.GroupEdges.Add(EdgeIndex); continue; } FGroupEdge Edge = { MakeEdgeGroupsPair(Loop.Edges[StartIndex]) }; TArray SpanVertices; int32 NumSpanVertices = NumSpanEdges + 1; for (int32 i = 0; i < NumSpanVertices; ++i) { int32 Index = (StartIndex + i) % NumV; SpanVertices.Add(Loop.Vertices[Index]); } Edge.Span = FEdgeSpan(Mesh); Edge.Span.InitializeFromVertices(SpanVertices); Edge.EndpointCorners = FIndex2i(GetCornerIDFromVertexID(SpanVertices[0]), GetCornerIDFromVertexID(SpanVertices.Last())); check(Edge.EndpointCorners.A != IndexConstants::InvalidID && Edge.EndpointCorners.B != IndexConstants::InvalidID); EdgeIndex = Edges.Add(Edge); Boundary.GroupEdges.Add(EdgeIndex); GroupEdgeMinEidToGroupEdgeID.Add(MinEid, EdgeIndex); } } return true; } bool UE::Geometry::FGroupTopology::RebuildTopologyWithSpecificExtraCorners(const TSet& ExtraCornerVids) { // We temporarily replace the vert corner-forcing function with one that checks against the given vids, // then we revert after doing the build. TFunction OldCornerFunction = MoveTemp(ShouldAddExtraCornerAtVert); ShouldAddExtraCornerAtVert = [&ExtraCornerVids](const FGroupTopology&, int32 Vid, const FIndex2i&) { return ExtraCornerVids.Contains(Vid); }; bool bSuccess = RebuildTopology(); ShouldAddExtraCornerAtVert = MoveTemp(OldCornerFunction); return bSuccess; } bool FGroupTopology::GetGroupEdgeTangent(int GroupEdgeID, FVector3d& TangentOut) const { check(GroupEdgeID >= 0 && GroupEdgeID < Edges.Num()); const FGroupEdge& Edge = Edges[GroupEdgeID]; FVector3d StartPos = Mesh->GetVertex(Edge.Span.Vertices[0]); FVector3d EndPos = Mesh->GetVertex(Edge.Span.Vertices[Edge.Span.Vertices.Num()-1]); if (DistanceSquared(StartPos, EndPos) > 100 * FMathd::ZeroTolerance) { TangentOut = Normalized(EndPos - StartPos); return true; } else { TangentOut = FVector3d::UnitX(); return false; } } FFrame3d FGroupTopology::GetGroupFrame(int32 GroupID) const { FVector3d Centroid = FVector3d::Zero(); FVector3d Normal = FVector3d::Zero(); const FGroup& Face = Groups[GroupIDToGroupIndexMap[GroupID]]; for (int32 tid : Face.Triangles) { Centroid += Mesh->GetTriCentroid(tid); Normal += Mesh->GetTriNormal(tid); } Centroid /= (double)Face.Triangles.Num(); Normalize(Normal); return FFrame3d(Centroid, Normal); } FFrame3d FGroupTopology::GetSelectionFrame(const FGroupTopologySelection& Selection, FFrame3d* InitialLocalFrame) const { int32 NumCorners = Selection.SelectedCornerIDs.Num(); int32 NumEdges = Selection.SelectedEdgeIDs.Num(); int32 NumFaces = Selection.SelectedGroupIDs.Num(); FFrame3d StartFrame = (InitialLocalFrame) ? (*InitialLocalFrame) : FFrame3d(); if (NumEdges == 1) { int32 GroupEdgeID = Selection.GetASelectedEdgeID(); const TArray& Vertices = GetGroupEdgeVertices(GroupEdgeID); bool bIsSelfLoop = Vertices[0] == Vertices.Last(); if (!bIsSelfLoop) { // align Z axis of frame to face normal of one of the connected faces. int32 MeshEdgeID = GetGroupEdgeEdges(GroupEdgeID)[0]; FIndex2i EdgeTris = Mesh->GetEdgeT(MeshEdgeID); int32 UseFace = (EdgeTris.B != IndexConstants::InvalidID) ? FMath::Min(EdgeTris.A, EdgeTris.B) : EdgeTris.A; FVector3d FaceNormal = Mesh->GetTriNormal(UseFace); if (!FaceNormal.IsZero()) { StartFrame.AlignAxis(2, FaceNormal); } // align X axis along the edge, around the aligned Z axis FVector3d Tangent; if (GetGroupEdgeTangent(GroupEdgeID, Tangent)) { StartFrame.ConstrainedAlignAxis(0, Tangent, StartFrame.Z()); } StartFrame.Origin = GetEdgeMidpoint(GroupEdgeID); } else { StartFrame.Origin = GetEdgeAveragePosition(GroupEdgeID); // For self-loops, get polygon normal via newell's method FVector FaceNormal = FVector::ZeroVector; FVector Prev = Mesh->GetVertex(Vertices.Last()); for (int32 Idx = 0; Idx < Vertices.Num(); ++Idx) { FVector Cur = Mesh->GetVertex(Vertices[Idx]); FaceNormal += FVector((Cur.Y - Prev.Y) * (Prev.Z + Cur.Z), (Cur.Z - Prev.Z) * (Prev.X + Cur.X), (Cur.X - Prev.X) * (Prev.Y + Cur.Y)); Prev = Cur; } // If we found a valid plane normal, align the frame to it; otherwise leave the frame unchanged from default if (FaceNormal.Normalize()) { StartFrame.AlignAxis(2, FaceNormal); } } return StartFrame; } if (NumCorners == 1) { StartFrame.Origin = Mesh->GetVertex(Corners[Selection.GetASelectedCornerID()].VertexID); return StartFrame; } FVector3d AccumulatedOrigin = FVector3d::Zero(); FVector3d AccumulatedNormal = FVector3d::Zero(); int AccumCount = 0; for (int32 CornerID : Selection.SelectedCornerIDs) { AccumulatedOrigin += Mesh->GetVertex(GetCornerVertexID(CornerID)); AccumulatedNormal += FVector3d::UnitZ(); AccumCount++; } for (int32 EdgeID : Selection.SelectedEdgeIDs) { const FGroupEdge& Edge = Edges[EdgeID]; FVector3d StartPos = Mesh->GetVertex(Edge.Span.Vertices[0]); FVector3d EndPos = Mesh->GetVertex(Edge.Span.Vertices.Last()); // special case self-loops to be consistent with the single-edge loop special case, above if (Edge.Span.Vertices[0] == Edge.Span.Vertices.Last()) { AccumulatedOrigin += GetEdgeAveragePosition(EdgeID); } else { AccumulatedOrigin += 0.5 * (StartPos + EndPos); } AccumulatedNormal += FVector3d::UnitZ(); AccumCount++; } for (int32 GroupID : Selection.SelectedGroupIDs) { if (FindGroupByID(GroupID) != nullptr) { FFrame3d GroupFrame = GetGroupFrame(GroupID); AccumulatedOrigin += GroupFrame.Origin; AccumulatedNormal += GroupFrame.Z(); AccumCount++; } } FFrame3d AccumulatedFrame; if (AccumCount > 0) { AccumulatedOrigin /= (double)AccumCount; if (!AccumulatedNormal.Normalize()) { AccumulatedNormal = FVector3d::UnitZ(); } // We set our frame Z to be accumulated normal, and the other two axes are unconstrained, so // we want to set them to something that will make our frame generally more useful. If the normal // is aligned with world Z, then the entire frame might as well be aligned with world. double ZAlign = AccumulatedNormal.Dot(FVector3d::UnitZ()); if (FMath::Abs(ZAlign) > 1 - KINDA_SMALL_NUMBER) { if (ZAlign < 0) { // Rotate frame 180 degrees around X AccumulatedFrame = FFrame3d(AccumulatedOrigin, FVector3d::UnitX(), -FVector3d::UnitY(), -FVector3d::UnitZ()); } else { AccumulatedFrame = FFrame3d(AccumulatedOrigin, FQuaterniond::Identity()); } } else { // Otherwise, let's place one of the other axes into the XY plane so that the frame is more // useful for translation. We somewhat arbitrarily choose Y for this. FVector3d FrameY = Normalized(AccumulatedNormal.Cross(FVector3d::UnitZ())); // orthogonal to world Z and frame Z FVector3d FrameX = FrameY.Cross(AccumulatedNormal); // safe to not normalize because already orthogonal AccumulatedFrame = FFrame3d(AccumulatedOrigin, FrameX, FrameY, AccumulatedNormal); } } return AccumulatedFrame; } FAxisAlignedBox3d FGroupTopology::GetSelectionBounds( const FGroupTopologySelection& Selection, TFunctionRef TransformFunc) const { if (ensure(!Selection.IsEmpty()) == false) { return Mesh->GetBounds(); } FAxisAlignedBox3d Bounds = FAxisAlignedBox3d::Empty(); for (int32 CornerID : Selection.SelectedCornerIDs) { Bounds.Contain(TransformFunc(Mesh->GetVertex(GetCornerVertexID(CornerID)))); } for (int32 EdgeID : Selection.SelectedEdgeIDs) { const FGroupEdge& Edge = Edges[EdgeID]; for (int32 vid : Edge.Span.Vertices) { Bounds.Contain(TransformFunc(Mesh->GetVertex(vid))); } } for (int32 GroupID : Selection.SelectedGroupIDs) { for (int32 TriangleID : GetGroupTriangles(GroupID)) { FIndex3i TriVerts = Mesh->GetTriangle(TriangleID); Bounds.Contain(TransformFunc(Mesh->GetVertex(TriVerts.A))); Bounds.Contain(TransformFunc(Mesh->GetVertex(TriVerts.B))); Bounds.Contain(TransformFunc(Mesh->GetVertex(TriVerts.C))); } } return Bounds; } void FGroupTopology::GetSelectedTriangles(const FGroupTopologySelection& Selection, TArray& Triangles) const { for (int32 GroupID : Selection.SelectedGroupIDs) { for (int32 TriangleID : GetGroupTriangles(GroupID)) { Triangles.Add(TriangleID); } } } bool UE::Geometry::FGroupTopology::IsEdgeAngleSharp(const FDynamicMesh3* Mesh, int32 SharedVid, const FIndex2i& AttachedEids, double DotThreshold) { if (!ensure(Mesh && Mesh->IsEdge(AttachedEids.A) && Mesh->IsEdge(AttachedEids.B))) { return false; } // Gets vector pointing from the Vid along the edge. auto GetEdgeUnitVector = [Mesh, SharedVid](int32 Eid, FVector3d& VectorOut)->bool { FIndex2i EdgeVids = Mesh->GetEdgeV(Eid); // Make sure that the Vid is at EdgeVids.A if (EdgeVids.B == SharedVid) { Swap(EdgeVids.A, EdgeVids.B); } VectorOut = Mesh->GetVertex(EdgeVids.B) - Mesh->GetVertex(EdgeVids.A); return VectorOut.Normalize(KINDA_SMALL_NUMBER); }; FVector Edge1, Edge2; if (!GetEdgeUnitVector(AttachedEids.A, Edge1) || !GetEdgeUnitVector(AttachedEids.B, Edge2)) { // If either edge was degenerate, we don't want to consider the angle "sharp" return false; } return Edge1.Dot(Edge2) >= DotThreshold; } void FGroupTopology::GetAllVertexGroups(int32 VertexID, TArray& GroupsOut) const { for (int32 EdgeID : Mesh->VtxEdgesItr(VertexID)) { FIndex2i EdgeTris = Mesh->GetEdgeT(EdgeID); GroupsOut.AddUnique(GetGroupID(EdgeTris.A)); if (EdgeTris.B != FDynamicMesh3::InvalidID) { GroupsOut.AddUnique(GetGroupID(EdgeTris.B)); } } } FTriangleGroupTopology::FTriangleGroupTopology(const FDynamicMesh3* Mesh, bool bAutoBuild) : FGroupTopology(Mesh, false) { // Note that we can't rely on the call to RebuildTopology in the base class constructor // because virtual function call resolution is not active in constructors (so it would // call the base class RebuildTopology) if (bAutoBuild) { RebuildTopology(); } } bool FTriangleGroupTopology::RebuildTopology() { Groups.Reset(); Edges.Reset(); Corners.Reset(); int32 MaxGroupID = Mesh->MaxTriangleID(); // initialize groups GroupIDToGroupIndexMap.Reset(); GroupIDToGroupIndexMap.Init(-1, MaxGroupID); TArray GroupFaceCounts; GroupFaceCounts.Init(1, MaxGroupID); for (int tid : Mesh->TriangleIndicesItr()) { if (GroupIDToGroupIndexMap[tid] == -1) { FGroup NewGroup; NewGroup.GroupID = tid; NewGroup.Triangles.Add(tid); GroupIDToGroupIndexMap[tid] = Groups.Add(NewGroup); } } for (int vid : Mesh->VertexIndicesItr()) { FCorner Corner = { vid }; int NewCornerIndex = Corners.Num(); VertexIDToCornerIDMap.Add(vid, NewCornerIndex); Corners.Add(Corner); } for (FCorner& Corner : Corners) { GetAllVertexGroups(Corner.VertexID, Corner.NeighbourGroupIDs); } TArray MeshEdgeToGroupEdge; MeshEdgeToGroupEdge.Init(INDEX_NONE, Mesh->MaxEdgeID()); // construct boundary loops TArray SpanVertices; SpanVertices.SetNum(2); for (FGroup& Group : Groups) { // find FGroupEdges and uses to populate Group.Boundaries Group.Boundaries.SetNum(1); FGroupBoundary& Boundary0 = Group.Boundaries[0]; FIndex3i TriEdges = Mesh->GetTriEdges(Group.GroupID); for (int j = 0; j < 3; ++j) { int& GroupEdgeIndex = MeshEdgeToGroupEdge[TriEdges[j]]; if (GroupEdgeIndex == INDEX_NONE) { FGroupEdge NewGroupEdge = { MakeEdgeGroupsPair(TriEdges[j]) }; FIndex2i EdgeVerts = Mesh->GetEdgeV(TriEdges[j]); NewGroupEdge.Span = FEdgeSpan(Mesh); SpanVertices[0] = EdgeVerts.A; SpanVertices[1] = EdgeVerts.B; NewGroupEdge.Span.InitializeFromVertices(SpanVertices); NewGroupEdge.EndpointCorners = FIndex2i(GetCornerIDFromVertexID(SpanVertices[0]), GetCornerIDFromVertexID(SpanVertices[1])); check(NewGroupEdge.EndpointCorners.A != IndexConstants::InvalidID && NewGroupEdge.EndpointCorners.B != IndexConstants::InvalidID); GroupEdgeIndex = Edges.Add(NewGroupEdge); } Boundary0.GroupEdges.Add(GroupEdgeIndex); } // collect up .NeighbourGroupIDs and set .bIsOnBoundary // make all-neighbour-groups list at group level Boundary0.bIsOnBoundary = false; FIndex3i TriNbrTris = Mesh->GetTriNeighbourTris(Group.GroupID); for (int j = 0; j < 3; ++j) { if (TriNbrTris[j] != FDynamicMesh3::InvalidID) { Group.NeighbourGroupIDs.Add(TriNbrTris[j]); Boundary0.NeighbourGroupIDs.Add(TriNbrTris[j]); } else { Boundary0.bIsOnBoundary = true; } } } return true; }