// Copyright Epic Games, Inc. All Rights Reserved. #include "Operations/GeodesicPath.h" #include "Operations/MeshGeodesicSurfaceTracer.h" using namespace UE::Geometry; // -- FEdgePath methods ---// int32 FEdgePath::GetHeadSegmentID() const { NodeType* Node = PathLinkList.GetHead(); if (!Node) { return InvalidID; } int32 SID = Node->GetValue().SID; return SID; } int32 FEdgePath::GetTailSegmentID() const { NodeType* Node = PathLinkList.GetTail(); if (!Node) { return InvalidID; } int32 SID = Node->GetValue().SID; return SID; } FEdgePath::FDirectedSegment FEdgePath::GetSegment(int32 SID) const { FDirectedSegment Result = { InvalidID, InvalidID }; if (!IsSegment(SID)) { return Result; } const FSegmentAndSID& SegmentAndSID = SIDtoNode[SID]->GetValue(); Result.EID = SegmentAndSID.EID; Result.HeadIndex = SegmentAndSID.HeadIndex; return Result; } int32 FEdgePath::GetPrevSegmentID(int32 SID) const { if (!IsSegment(SID)) { return InvalidID; } const NodeType* Node = SIDtoNode[SID]; const NodeType* PrevNode = Node->GetPrevNode(); if (PrevNode) { return PrevNode->GetValue().SID; } else { return InvalidID; } } int32 FEdgePath::GetNextSegmentID(int32 SID) const { if (!IsSegment(SID)) { return InvalidID; } const NodeType* Node = SIDtoNode[SID]; const NodeType* NextNode = Node->GetNextNode(); if (NextNode) { return NextNode->GetValue().SID; } else { return InvalidID; } } int32 FEdgePath::AppendSegment(const FDirectedSegment& Segment) { // record the mesh edge as a new segment const int32 SID = DirectedSegmentRefCounts.Allocate(); FSegmentAndSID SegmentAndSID = {{Segment.EID, Segment.HeadIndex}, SID}; PathLinkList.AddTail(SegmentAndSID); NodeType* Node = PathLinkList.GetTail(); SIDtoNode.InsertAt(Node, SID); return SID; } int32 FEdgePath::InsertSegmentBefore(const FEdgePath::FDirectedSegment& Segment, int32 SegmentIDToInsertBefore) { NodeType* NodeToInsertBefore = (SegmentIDToInsertBefore > -1) ? SIDtoNode[SegmentIDToInsertBefore] : nullptr; int32 SID = DirectedSegmentRefCounts.Allocate(); FSegmentAndSID SegmentAndSID = {{Segment.EID, Segment.HeadIndex}, SID}; PathLinkList.InsertNode(SegmentAndSID, NodeToInsertBefore); NodeType* Node = (NodeToInsertBefore) ? NodeToInsertBefore->GetPrevNode() : PathLinkList.GetTail(); SIDtoNode.InsertAt(Node, SID); return SID; } void FEdgePath::RemoveSegment(int32 SID) { if (!IsSegment(SID)) { return; } NodeType* Node = SIDtoNode[SID]; PathLinkList.RemoveNode(Node); // does delete SIDtoNode[SID] = nullptr; DirectedSegmentRefCounts.Decrement(SID); } /** * Visits a subset of the triangle neighborhood of vertex CVID, * i.e. faces in CW or CCW-order about vertex CVID starting with edge StartEID and ending at either a boundary or edge EndEID, * returns false if a boundary is encountered. * * At each face a Visitor functor is called. * * @param EdgeFlipMesh - IntrinsicEdgeFlip mesh * @param StartEID - edge incident on CVID * @param EndEID - edge incident on CVID * @param CVID - vertex at center when visiting triangle neighborhood * @param Visitor - functor called on each tri between StartEID and EndEID * @param bClockwise - specifies the direction of travel around VID as clockwise (CW) or counter clockwise (CCW). * * the functor Visitor(int32 TriID, int32 IndexOfCenterVID, FIndex3i& TriEIDs) * TriID - the triangle visited * IndexOfCenterVID - the corner index of the specified VID in the triangle * TriEIDs - edge IDs for the triangle. * */ template bool VisitWedgeTriangles(const EdgeFlipMeshType& EdgeFlipMesh, const int32 StartEID, const int32 EndEID, const int32 CVID, FunctorType& Visitor, bool bClockwise = false) { constexpr static int InvalidID = IndexConstants::InvalidID; int32 CurEID = StartEID; FIndex2i CurEdgeT = EdgeFlipMesh.GetEdgeT(CurEID); int32 CurTID = CurEdgeT[0]; FIndex3i CurTriEIDs = EdgeFlipMesh.GetTriEdges(CurTID); int32 IndexOf = CurTriEIDs.IndexOf(CurEID); // make sure we are starting with the triangle on the correct side of the start edge for clockwise / counter clockwise traversal if (( bClockwise && (EdgeFlipMesh.GetTriangle(CurTID)[IndexOf] == CVID)) || (!bClockwise && (EdgeFlipMesh.GetTriangle(CurTID)[IndexOf] != CVID))) { CurTID = CurEdgeT[1]; if (CurTID == InvalidID) { return false; } CurTriEIDs = EdgeFlipMesh.GetTriEdges(CurTID); IndexOf = CurTriEIDs.IndexOf(CurEID); } const int32 NextEdgeOffset = (bClockwise) ? 1 : 2; do { Visitor(CurTID, IndexOf, CurTriEIDs); // advance to next edge CurEID = CurTriEIDs[(NextEdgeOffset + IndexOf) % 3]; CurEdgeT = EdgeFlipMesh.GetEdgeT(CurEID); CurTID = (CurEdgeT[0] == CurTID) ? CurEdgeT[1] : CurEdgeT[0]; if (CurTID == InvalidID) { return (CurEID == EndEID); } CurTriEIDs = EdgeFlipMesh.GetTriEdges(CurTID); IndexOf = CurTriEIDs.IndexOf(CurEID); } while (CurEID != EndEID); return true; } // -- Deformable Edge Path --// FDeformableEdgePath::FDeformableEdgePath(const FDynamicMesh3& SurfaceMeshIn, const TArray& PathAsDirectedSegments) : EdgeFlipMesh(SurfaceMeshIn) , PathLength(0.) , NumFlips(0) { for (const FEdgePath::FDirectedSegment& DirectedSegment : PathAsDirectedSegments) { const int32 EID = DirectedSegment.EID; if (!EdgeFlipMesh.IsEdge(EID)) { continue; } // compare with last segment eliminate double-back const int32 TailSID = EdgePath.GetTailSegmentID(); const FEdgePath::FDirectedSegment TailSegment = EdgePath.GetSegment(TailSID); if (TailSegment.EID == DirectedSegment.EID && TailSegment.HeadIndex != DirectedSegment.HeadIndex) { EdgePath.RemoveSegment(TailSID); } else { int32 SID = EdgePath.AppendSegment(DirectedSegment); TArray& Segments = EIDToSIDsMap.FindOrAdd(EID); Segments.Add(SID); UpdateJointAndQueue(SID); } } // no segments in this path. if (EdgePath.NumSegments() == 0) { return; } // compute the initial length of the path const int32 MaxSID = EdgePath.MaxSegmentID(); for (int32 SID = EdgePath.GetHeadSegmentID(); SID < MaxSID; ++SID) { if (!EdgePath.IsSegment(SID)) { continue; } const int32 EID = EdgePath.GetSegment(SID).EID; const double EdgeLength = EdgeFlipMesh.GetEdgeLength(EID); PathLength += EdgeLength; } } void FDeformableEdgePath::Minimize(FDeformableEdgePath::FEdgePathDeformationInfo& DeformedPathInfo, const int32 MaxNumIterations) { DeformedPathInfo = FEdgePathDeformationInfo(); DeformedPathInfo.OriginalLength = PathLength; constexpr double AngleEpsilon = 1.e-3; int32 NumIterations = 0; while (JointAngleQueue.Num() > 0 && MaxNumIterations > NumIterations) { // get ID of Outgoing segment in the joint, and the associated smaller internal angle. const uint32 QueueSegmentID = JointAngleQueue.Top(); const double InternalAngle = JointAngleQueue.GetKey(QueueSegmentID); JointAngleQueue.Pop(); if (InternalAngle > TMathUtilConstants::Pi - AngleEpsilon) { // the smaller internal angle was greater than Pi, this joint can't be further straightened continue; } const int32 SID = int32(QueueSegmentID); FPathJoint& PathJoint = PathJoints[SID]; if (!IsJointFlexible(PathJoint)) { continue; } DeformJoint(PathJoint); NumIterations++; } DeformedPathInfo.NumIterations = NumIterations; DeformedPathInfo.NumEdgeFlips = NumFlips; DeformedPathInfo.FinalLength = PathLength; } void FDeformableEdgePath::DeformJoint(FDeformableEdgePath::FPathJoint& PathJoint) { const int32 JointVID = PathJoint.VID; const int32 OutgoingSID = PathJoint.OutgoingSID; const int32 IncomingSID = EdgePath.GetPrevSegmentID(OutgoingSID); if (IncomingSID < 0 || JointVID == InvalidID) // a joint is composed of incoming and outgoing edges meeting at VID { return; } const int32 IncomingEID = EdgePath.GetSegment(IncomingSID).EID; const int32 OutgoingEID = EdgePath.GetSegment(OutgoingSID).EID; TArray DeformedPath; switch (PathJoint.InteriorSide) { case ESide::Right: { const bool bClockwise = false; DeformJoint(IncomingEID, OutgoingEID, JointVID, DeformedPath, bClockwise); } break; case ESide::Left: { const bool bClockwise = true; DeformJoint(IncomingEID, OutgoingEID, JointVID, DeformedPath, bClockwise); } break; default: check(0); } ReplaceJointWithPath(PathJoint, DeformedPath); } bool FDeformableEdgePath::DeformJoint( const int32 IncomingEID, const int32 OutgoingEID, int32 JointVID, TArray& DeformedPath, const bool bClockwise ) { bool Result = OuterArcFlipEdges(IncomingEID, OutgoingEID, JointVID, bClockwise); // collect deformed path if (Result) { const int32 HeadOffset = (bClockwise) ? 1 : 2; auto PathAccumulator = [this, HeadOffset, &DeformedPath, bClockwise, JointVID](int32 TriID, int32 IndexOfEID, FIndex3i& TriEIDs) { const int32 IndexOfCenterVID = (bClockwise) ? (IndexOfEID + 1) % 3 : IndexOfEID; const int32 OpposingEID = TriEIDs[(IndexOfCenterVID + 1) % 3]; const FIndex3i TriVIDs = EdgeFlipMesh.GetTriangle(TriID); checkSlow(JointVID == TriVIDs[IndexOfCenterVID]); const int32 HeadVID = TriVIDs[(IndexOfCenterVID + HeadOffset) % 3]; const FIndex2i OpposingEdgeV = EdgeFlipMesh.GetEdgeV(OpposingEID); FEdgePath::FDirectedSegment DirectedSegment; DirectedSegment.EID = OpposingEID; DirectedSegment.HeadIndex = (OpposingEdgeV.A == HeadVID) ? 0 : 1; checkSlow(OpposingEdgeV[DirectedSegment.HeadIndex] == HeadVID); DeformedPath.Add(DirectedSegment); }; Result = VisitWedgeTriangles(EdgeFlipMesh, IncomingEID, OutgoingEID, JointVID, PathAccumulator, bClockwise); checkSlow(Result); // since OuterArcFlipEdges was true, this should be too. } return Result; } bool FDeformableEdgePath::IsJointFlexible(const FDeformableEdgePath::FPathJoint& Joint) const { const int32 SID = Joint.OutgoingSID; const int32 PrevSID = EdgePath.GetPrevSegmentID(SID); if ( SID < 0 || PrevSID < 0) { return false; } // Get the Interior Side and JointVID const int32 JointVID = Joint.VID; const ESide InteriorSide = Joint.InteriorSide; bool bIsBlocked = false; int32 StartEID = EdgePath.GetSegment(SID).EID; int32 EndEID = EdgePath.GetSegment(PrevSID).EID; const bool bClockwise = (InteriorSide == ESide::Right); auto OccupiedEdgeTestor = [&bIsBlocked, StartEID, EndEID, this](int32 TriID, int32 IndexOfCenterVID, FIndex3i& TriEIDs) { for (int32 i = 0; i < 3 && !bIsBlocked; ++i) { const int32 CurEID = TriEIDs[i]; if (CurEID == StartEID || CurEID == EndEID) { continue; } const TArray* SegmentArray = EIDToSIDsMap.Find(CurEID); bIsBlocked = (SegmentArray) || bIsBlocked; } }; // check StartEID and EndEID. These should have one entry each. // To prevent the introduction of a path crossing, this prevents the local joint from being deformed // in the case that other segments of the path have doubled back on either of these edges. // [todo] when multiple path segments share the same edge, ascertain their relative interleaving (e.g. left to right) // so we can identify situations when the local joint can be deformed without producing a crossing. { if (const TArray* SegmentArray = EIDToSIDsMap.Find(StartEID)) { bIsBlocked = bIsBlocked ||(SegmentArray->Num() > 1); } if (const TArray* SegmentArray = EIDToSIDsMap.Find(EndEID)) { bIsBlocked = bIsBlocked || (SegmentArray->Num() > 1); } } const bool bHasBoundary = !VisitWedgeTriangles(EdgeFlipMesh, StartEID, EndEID, JointVID, OccupiedEdgeTestor, bClockwise); return (!bHasBoundary && !bIsBlocked); } void FDeformableEdgePath::UpdateJointAndQueue(int32 SID) { int32 PrevSID = EdgePath.GetPrevSegmentID(SID); // a joint is composed of cur segment and prev segment if (PrevSID != InvalidID) { const int32 JointVID = SegmentHeadVID(PrevSID); checkSlow(JointVID == SegmentTailVID(SID)); const int32 IncomingEID = EdgePath.GetSegment(PrevSID).EID; const int32 OutgoingEID = EdgePath.GetSegment(SID).EID; double LAngle = 0., RAngle = 0.; ComputeWedgeAngles(IncomingEID, OutgoingEID, JointVID, LAngle, RAngle); const ESide InteriorSide = (LAngle < RAngle) ? ESide::Left : ESide::Right; // encode the joint FPathJoint PathJoint = { SID, InteriorSide, JointVID }; PathJoints.InsertAt(PathJoint, SID); uint32 QueueSegmentID = uint32(SID); double SortKey = FMath::Min(LAngle, RAngle); if ( JointAngleQueue.IsPresent(QueueSegmentID) ) { JointAngleQueue.Update(SortKey, QueueSegmentID); } else { JointAngleQueue.Add(SortKey, QueueSegmentID); } } } void FDeformableEdgePath::RemoveSegment(int32 SID) { if (SID < 0) { return; } const int32 EID = EdgePath.GetSegment(SID).EID; JointAngleQueue.Remove(SID); EdgePath.RemoveSegment(SID); if (TArray* SegmentArray = EIDToSIDsMap.Find(EID)) { SegmentArray->RemoveSwap(SID); if (SegmentArray->Num() == 0) { EIDToSIDsMap.Remove(EID); } } // subtract length of segment from total path length PathLength -= EdgeFlipMesh.GetEdgeLength(EID); checkSlow(!EdgePath.IsSegment(SID)); } void FDeformableEdgePath::ReplaceJointWithPath(const FDeformableEdgePath::FPathJoint PathJoint, const TArray& PathAsDirectedEdges) { int32 OutgoingSID = PathJoint.OutgoingSID; if (OutgoingSID < 0) { return; } // The Joint is comprised of AtoB and BtoC int32 IncomingSID = EdgePath.GetPrevSegmentID(OutgoingSID); // segment for first edge element after Joint. int32 NextSegmentSID = EdgePath.GetNextSegmentID(OutgoingSID); // Note this can be InvalidID, and that is ok // remove the joint RemoveSegment(IncomingSID); RemoveSegment(OutgoingSID); // add path before NextSegment if (NextSegmentSID != InvalidID) { for (const FEdgePath::FDirectedSegment& DirectedSegment : PathAsDirectedEdges) { int32 NewSID = EdgePath.InsertSegmentBefore(DirectedSegment, NextSegmentSID); PathLength += EdgeFlipMesh.GetEdgeLength(DirectedSegment.EID); UpdateJointAndQueue(NewSID); } // update the joint that now connects NextEdgeNode to the last NewNode. UpdateJointAndQueue(NextSegmentSID); } else // add to tail { for (const FEdgePath::FDirectedSegment& DirectedSegment : PathAsDirectedEdges) { int32 NewSID = EdgePath.AppendSegment(DirectedSegment); PathLength += EdgeFlipMesh.GetEdgeLength(DirectedSegment.EID); UpdateJointAndQueue(NewSID); } } } bool FDeformableEdgePath::OuterArcFlipEdges(int32 StartEID, int32 EndEID, int32 CVID, bool bClockwise) { int32 CurEID = StartEID; FIndex2i CurEdgeT = EdgeFlipMesh.GetEdgeT(CurEID); int32 CurTID = CurEdgeT[0]; FIndex3i CurTriEIDs = EdgeFlipMesh.GetTriEdges(CurTID); int32 IndexOf = CurTriEIDs.IndexOf(CurEID); // make sure we are starting with the triangle on the correct side of the start edge for clockwise / counter clockwise traversal if (( bClockwise && (EdgeFlipMesh.GetTriangle(CurTID)[IndexOf] == CVID)) || (!bClockwise && (EdgeFlipMesh.GetTriangle(CurTID)[IndexOf] != CVID))) { CurTID = CurEdgeT[1]; if (CurTID == InvalidID) { return false; } CurTriEIDs = EdgeFlipMesh.GetTriEdges(CurTID); IndexOf = CurTriEIDs.IndexOf(CurEID); } const int32 NextEdgeOffset = (bClockwise) ? 1 : 2; int32 NextEID = CurTriEIDs[(IndexOf + NextEdgeOffset) % 3]; while (NextEID != EndEID) { FIndex2i PreFlipT = EdgeFlipMesh.GetEdgeT(NextEID); // The ID of the triangle adjacent to CurEID after flipping NextEID // relies on the logic in FIntrinsicEdgeFlipMesh::FlipEdgeTopology().. if that changes this will break int32 TmpTID = CurTID; if (bClockwise) { TmpTID = (CurTID == PreFlipT[1]) ? PreFlipT[0] : PreFlipT[1]; } IntrinsicMeshType::FEdgeFlipInfo EdgeFlipInfo; UE::Geometry::EMeshResult Result = EdgeFlipMesh.FlipEdge(NextEID, EdgeFlipInfo); if (Result == UE::Geometry::EMeshResult::Failed_IsBoundaryEdge) { return false; } if (Result == UE::Geometry::EMeshResult::Ok) { this->NumFlips++; CurTID = TmpTID; CurTriEIDs = EdgeFlipMesh.GetTriEdges(CurTID); IndexOf = CurTriEIDs.IndexOf(CurEID); checkSlow(IndexOf != -1); // would only fail if the TmpID logic above is wrong. NextEID = CurTriEIDs[(IndexOf + NextEdgeOffset) % 3]; } else { // didn't flip edge. advance to next triangle on other side of that edge CurEID = NextEID; CurTID = (PreFlipT[0] == CurTID) ? PreFlipT[1] : PreFlipT[0]; CurTriEIDs = EdgeFlipMesh.GetTriEdges(CurTID); IndexOf = CurTriEIDs.IndexOf(CurEID); NextEID = CurTriEIDs[(IndexOf + NextEdgeOffset) % 3]; } } return true; } void FDeformableEdgePath::ComputeWedgeAngles(int32 IncomingEID, int32 OutgoingEID, int32 CenterVID, double& LeftSideAngle, double& RightSideAngle) const { const bool bClockwise = false; double WedgeAngle = 0.; auto WedgeAngleAccumulator = [this, &WedgeAngle](int32 TID, int32 IndexOf, const FIndex3i& TriEIDs) { WedgeAngle += EdgeFlipMesh.GetTriInternalAngleR(TID, IndexOf); }; bool bLeftContainsBoundary = !VisitWedgeTriangles(EdgeFlipMesh, OutgoingEID, IncomingEID, CenterVID, WedgeAngleAccumulator, bClockwise); double LeftWedgeAngle = WedgeAngle; WedgeAngle = 0.; bool bRightContainsBoundary = !VisitWedgeTriangles(EdgeFlipMesh, IncomingEID, OutgoingEID, CenterVID, WedgeAngleAccumulator, bClockwise); double RightWedgeAngle = WedgeAngle; LeftSideAngle = (bLeftContainsBoundary) ? TMathUtilConstants::MaxReal : LeftWedgeAngle; RightSideAngle = (bRightContainsBoundary) ? TMathUtilConstants::MaxReal : RightWedgeAngle; } TArray FDeformableEdgePath::AsSurfacePoints(double CoalesceThreshold) const { TArray PathSurfacePoints; const int32 NumIntrinsicPathSegments = EdgePath.NumSegments(); // empty path case if (NumIntrinsicPathSegments == 0) { return PathSurfacePoints; } IntrinsicMeshType::FEdgeCorrespondence EdgeCorrespondence = EdgeFlipMesh.ComputeEdgeCorrespondence(); int32 SID = EdgePath.GetHeadSegmentID(); while (SID != InvalidID) { const int32 NextSID = EdgePath.GetNextSegmentID(SID); const bool bIsLastSegment = (NextSID == InvalidID); const int32 EID = EdgePath.GetSegment(SID).EID; const bool bReverseEdge = (EdgePath.GetSegment(SID).HeadIndex == 0); // get intrinsic edge as a sequence of surface points, note each segment starts and ends at a surface mesh vertex but may cross several surface mesh edges. TArray SegmentSurfacePoints = EdgeCorrespondence.TraceEdge(EID, CoalesceThreshold, bReverseEdge); PathSurfacePoints.Append(MoveTemp(SegmentSurfacePoints)); if (!bIsLastSegment) { // delete last element since it will be the same as the first element in the next segment PathSurfacePoints.Pop(); } SID = NextSID; } return MoveTemp(PathSurfacePoints); } double UE::Geometry::SumPathLength(const FDeformableEdgePath& DeformableEdgePath) { double TotalPathLength = 0; const FEdgePath& EdgePath = DeformableEdgePath.GetEdgePath(); const FDeformableEdgePath::IntrinsicMeshType& EdgeFlipMesh = DeformableEdgePath.GetIntrinsicMesh(); const int32 NumIntrinsicPathSegments = EdgePath.NumSegments(); // empty path case if (NumIntrinsicPathSegments == 0) { return TotalPathLength; } int32 SID = EdgePath.GetHeadSegmentID(); while (SID != FDeformableEdgePath::InvalidID) { const int32 EID = EdgePath.GetSegment(SID).EID; const double SegmentLength = EdgeFlipMesh.GetEdgeLength(EID); TotalPathLength += SegmentLength; SID = EdgePath.GetNextSegmentID(SID); } return TotalPathLength; }