// Copyright Epic Games, Inc. All Rights Reserved. #include "MeshSimplification.h" #include "MeshConstraintsUtil.h" #include "DynamicMesh/DynamicMeshAttributeSet.h" #include "Util/IndexUtil.h" #include "Async/ParallelFor.h" #include "Templates/UnrealTypeTraits.h" using namespace UE::Geometry; template QuadricErrorType TMeshSimplification::ComputeFaceQuadric(const int tid, FVector3d& nface, FVector3d& c, double& Area) const { // compute the new quadric for this tri. Mesh->GetTriInfo(tid, nface, Area, c); return FQuadricErrorType(nface, c); } // Face Quadric Error computation specialized for FAttrBasedQuadricErrord template<> FAttrBasedQuadricErrord TMeshSimplification::ComputeFaceQuadric(const int tid, FVector3d& nface, FVector3d& c, double& Area) const { // compute the new quadric for this tri. Mesh->GetTriInfo(tid, nface, Area, c); FVector3f n0; FVector3f n1; FVector3f n2; if (NormalOverlay != nullptr) { if (NormalOverlay->IsSetTriangle(tid)) { NormalOverlay->GetTriElements(tid, n0, n1, n2); } else { FVector3f FaceNormal = (FVector3f)Mesh->GetTriNormal(tid); n0 = n1 = n2 = FaceNormal; } } else { FIndex3i vids = Mesh->GetTriangle(tid); n0 = Mesh->GetVertexNormal(vids[0]); n1 = Mesh->GetVertexNormal(vids[1]); n2 = Mesh->GetVertexNormal(vids[2]); } FVector3d p0, p1, p2; Mesh->GetTriVertices(tid, p0, p1, p2); FVector3d n0d(n0.X, n0.Y, n0.Z); FVector3d n1d(n1.X, n1.Y, n1.Z); FVector3d n2d(n2.X, n2.Y, n2.Z); double attrweight = 16.; return FQuadricErrorType(p0, p1, p2, n0d, n1d, n2d, nface, c, attrweight); } template void TMeshSimplification::InitializeTriQuadrics() { const int NT = Mesh->MaxTriangleID(); triQuadrics.SetNum(NT); triAreas.SetNum(NT); // tested with ParallelFor - no measurable benefit //@todo parallel version //gParallel.BlockStartEnd(0, Mesh->MaxTriangleID - 1, (start_tid, end_tid) = > { FVector3d n, c; for (int tid : Mesh->TriangleIndicesItr()) { triQuadrics[tid] = ComputeFaceQuadric(tid, n, c, triAreas[tid]); } } template void TMeshSimplification::InitializeSeamQuadrics() { // early out if this feature isn't needed. if (!bAllowSeamCollapse) { return; } double EdgeWeight = this->SeamEdgeWeight; auto AddSeamQuadric = [EdgeWeight, this](int eid) { FDynamicMesh3::FEdge edge = Mesh->GetEdge(eid); FVector3d p0 = Mesh->GetVertex(edge.Vert[0]); FVector3d p1 = Mesh->GetVertex(edge.Vert[1]); // face normal FVector3d nA = Mesh->GetTriNormal(edge.Tri.A); // this constrains the point to a plane aligned with the edge and normal to the face FSeamQuadricType& seamQuadric = seamQuadrics.Add(eid, CreateSeamQuadric(p0, p1, nA)); // add the other side - this constrains the point to the line where the two planes intersect. if (edge.Tri.B != FDynamicMesh3::InvalidID) { FVector3d nB = Mesh->GetTriNormal(edge.Tri.B); seamQuadric.Add(CreateSeamQuadric(p0, p1, nB)); } seamQuadric.Scale(EdgeWeight); }; if (Constraints) // The edge constraints an entry for each seam, boundary, group boundary and material boundary { const auto& EdgeConstraints = Constraints->GetEdgeConstraints(); for (auto& ConstraintPair : EdgeConstraints) { int eid = ConstraintPair.Key; AddSeamQuadric(eid); } } else { const FDynamicMeshAttributeSet* Attributes = Mesh->Attributes(); for (int eid : Mesh->EdgeIndicesItr()) { bool bNeedsQuadric = Mesh->IsBoundaryEdge(eid); bNeedsQuadric = bNeedsQuadric || Mesh->IsGroupBoundaryEdge(eid); if (Attributes) { bNeedsQuadric = bNeedsQuadric || Attributes->IsMaterialBoundaryEdge(eid); bNeedsQuadric = bNeedsQuadric || Attributes->IsSeamEdge(eid); } if (bNeedsQuadric) { AddSeamQuadric(eid); } } } } template void TMeshSimplification::InitializeVertexQuadrics() { int NV = Mesh->MaxVertexID(); vertQuadrics.SetNum(NV); // tested with ParallelFor - no measurable benefit //gParallel.BlockStartEnd(0, Mesh->MaxVertexID - 1, (start_vid, end_vid) = > { for (int vid : Mesh->VertexIndicesItr()) { vertQuadrics[vid] = FQuadricErrorType::Zero(); for (int tid : Mesh->VtxTrianglesItr(vid)) { vertQuadrics[vid].Add(triAreas[tid], triQuadrics[tid]); } //check(TMathUtil.EpsilonEqual(0, vertQuadrics[i].Evaluate(Mesh->GetVertex(i)), TMathUtil.Epsilon * 10)); } } template QuadricErrorType TMeshSimplification::AssembleEdgeQuadric(const FDynamicMesh3::FEdge& edge) const { // form standard edge quadric as sum of the vertex quadrics for the edge endpoints QuadricErrorType EdgeQuadric(vertQuadrics[edge.Vert.A], vertQuadrics[edge.Vert.B]); if (!bRetainQuadricMemory) { // the edge.Tri faces are double counted. Remove one. const FIndex2i& Tris = edge.Tri; if (Tris.A != FDynamicMesh3::InvalidID) { EdgeQuadric.Add(-triAreas[Tris.A], triQuadrics[Tris.A]); } if (Tris.B != FDynamicMesh3::InvalidID) { EdgeQuadric.Add(-triAreas[Tris.B], triQuadrics[Tris.B]); } } if (bAllowSeamCollapse) { // lambda that adds any adjacent seam quadrics to the edge quadric auto AddSeamQuadricsToEdge = [&, this](int vid) { for (int eid : Mesh->VtxEdgesItr(vid)) { if (const FSeamQuadricType* seamQuadric = seamQuadrics.Find(eid)) { EdgeQuadric.AddSeamQuadric(*seamQuadric); } } }; // accumulate any adjacent seam quadrics onto this edge quadric. AddSeamQuadricsToEdge(edge.Vert.A); AddSeamQuadricsToEdge(edge.Vert.B); } return EdgeQuadric; } template void TMeshSimplification::InitializeQueue() { int NE = Mesh->EdgeCount(); int MaxEID = Mesh->MaxEdgeID(); EdgeQuadrics.SetNum(MaxEID); EdgeQueue.Initialize(MaxEID); TArray EdgeErrors; EdgeErrors.Init(FEdgeError{MAX_FLT, -1}, MaxEID); // @todo vertex quadrics can be computed in parallel //gParallel.BlockStartEnd(0, MaxEID - 1, (start_eid, end_eid) = > { //for (int eid = start_eid; eid <= end_eid; eid++) { for (int eid : Mesh->EdgeIndicesItr()) { FDynamicMesh3::FEdge edge = Mesh->GetEdge(eid); FQuadricErrorType Q = AssembleEdgeQuadric(edge); FVector3d opt = OptimalPoint(eid, Q, edge.Vert.A, edge.Vert.B); EdgeErrors[eid] = { (float)Q.Evaluate(opt), eid }; EdgeQuadrics[eid] = QEdge(eid, Q, opt); } // sorted pq insert is faster, so sort edge errors array and index map EdgeErrors.Sort(); // now do inserts int N = EdgeErrors.Num(); for (int i = 0; i < N; ++i) { int eid = EdgeErrors[i].eid; if (Mesh->IsEdge(eid)) { QEdge& edge = EdgeQuadrics[eid]; float error = EdgeErrors[i].error; EdgeQueue.Insert(eid, error); } } /* // previous code that does unsorted insert. This is marginally slower, but // might get even slower on larger meshes? have only tried up to about 350k. // (still, this function is not the bottleneck...) int cur_eid = StartEdges(); bool done = false; do { if (Mesh->IsEdge(cur_eid)) { QEdge edge = EdgeQuadrics[cur_eid]; double err = errList[cur_eid]; EdgeQueue.Enqueue(cur_eid, (float)err); } cur_eid = GetNextEdge(cur_eid, out done); } while (done == false); */ } template FVector3d TMeshSimplification::OptimalPoint(int eid, const FQuadricErrorType& q, int ea, int eb) { // if we would like to preserve boundary, we need to know that here // so that we properly score these edges if (bHaveBoundary && bPreserveBoundaryShape) { if (Mesh->IsBoundaryEdge(eid)) { const bool bModeAllowsVertMovement = (CollapseMode != ESimplificationCollapseModes::MinimalExistingVertexError); if (bModeAllowsVertMovement) { return (Mesh->GetVertex(ea) + Mesh->GetVertex(eb)) * 0.5; } // else MinimalExistingVertexError case below will choose one of the vertex locations } else { if (IsBoundaryVertex(ea)) { return Mesh->GetVertex(ea); } else if (IsBoundaryVertex(eb)) { return Mesh->GetVertex(eb); } } } // [TODO] if we have constraints, we should apply them here, for same reason as bdry above... switch (CollapseMode) { case ESimplificationCollapseModes::AverageVertexPosition: { return GetProjectedPoint((Mesh->GetVertex(ea) + Mesh->GetVertex(eb)) * 0.5); } break; case ESimplificationCollapseModes::MinimalExistingVertexError: { FVector3d va = Mesh->GetVertex(ea); FVector3d vb = Mesh->GetVertex(eb); double fa = q.Evaluate(va); double fb = q.Evaluate(vb); if (fa < fb) { return va; } else { return vb; } } break; case ESimplificationCollapseModes::MinimalQuadricPositionError: { FVector3d result = FVector3d::Zero(); if (q.OptimalPoint(result)) { return GetProjectedPoint(result); } // degenerate matrix, evaluate quadric at edge end and midpoints // (could do line search here...) FVector3d va = Mesh->GetVertex(ea); FVector3d vb = Mesh->GetVertex(eb); FVector3d c = GetProjectedPoint((va + vb) * 0.5); double fa = q.Evaluate(va); double fb = q.Evaluate(vb); double fc = q.Evaluate(c); double m = FMath::Min3(fa, fb, fc); if (m == fa) { return va; } else if (m == fb) { return vb; } return c; } break; default: // should never happen checkSlow(0); return FVector3d::Zero(); } } template void TMeshSimplification::UpdateNeighborhood(const FDynamicMesh3::FEdgeCollapseInfo& collapseInfo) { int kvid = collapseInfo.KeptVertex; int rvid = collapseInfo.RemovedVertex; FIndex2i removedTris = collapseInfo.RemovedTris; FIndex2i opposingVerts = collapseInfo.OpposingVerts; // --- Update the seam quadrics if (bAllowSeamCollapse) { FIndex2i removedEdges = collapseInfo.RemovedEdges; FIndex2i keptEdges = collapseInfo.KeptEdges; // update the map between edge id and seam quadrics // if constraints exist, they define the edges with seam quadrics // otherwise require kept edges to have a seam quadric if either // the kept or collapse edge had a seam quadric. if (Constraints) // quadrics on the constrained edges { if (Constraints->HasEdgeConstraint(keptEdges.A)) { seamQuadrics.Add(keptEdges.A); } else { seamQuadrics.Remove(keptEdges.A); } if (keptEdges.B != FDynamicMesh3::InvalidID) { if( Constraints->HasEdgeConstraint(keptEdges.B)) { seamQuadrics.Add(keptEdges.B); } else { seamQuadrics.Remove(keptEdges.B); } } } else // propagate any existing seam quadric requirements. { if (FSeamQuadricType* seamQuadric = seamQuadrics.Find(removedEdges.A)) { seamQuadrics.Add(keptEdges.A); } if (removedEdges.B != FDynamicMesh3::InvalidID) { if (FSeamQuadricType* seamQuadric = seamQuadrics.Find(removedEdges.B)) { seamQuadrics.Add(keptEdges.B); } } } // removed quadrics from deleted edges seamQuadrics.Remove(removedEdges.A); if (removedEdges.B != FDynamicMesh3::InvalidID) { seamQuadrics.Remove(removedEdges.B); } // update any seam quadrics adjacent to kvid to reflect changes in the seams double EdgeWeight = this->SeamEdgeWeight; for (int eid : Mesh->VtxEdgesItr(kvid)) { FDynamicMesh3::FEdge ne = Mesh->GetEdge(eid); // need to recompute this seam quadric if (FSeamQuadricType* seamQuadric = seamQuadrics.Find(eid)) { // rebuild the seam quadric FVector3d p0 = Mesh->GetVertex(ne.Vert[0]); FVector3d p1 = Mesh->GetVertex(ne.Vert[1]); // face normal FVector3d nA = Mesh->GetTriNormal(ne.Tri.A); // this constrains the point to a plane aligned with the edge and normal to the face *seamQuadric = CreateSeamQuadric(p0, p1, nA); // add the other side - this constrains the point to the line where the two planes intersect. if (ne.Tri.B != FDynamicMesh3::InvalidID) { FVector3d nB = Mesh->GetTriNormal(ne.Tri.B); seamQuadric->Add(CreateSeamQuadric(p0, p1, nB)); } seamQuadric->Scale(EdgeWeight); } } } // --- Update the vertex quadrics if (bRetainQuadricMemory) { // Quadric "memory" the retained vertex quadric is the sum of the two vert quadrics vertQuadrics[kvid] = QuadricErrorType(vertQuadrics[kvid], vertQuadrics[rvid]); } else { // compute the change in affected face quadrics, and then propagate // that change to the face adjacent verts. FVector3d n, c; double NewtriArea; // Update the triangle areas and quadrics that will have changed for (int tid : Mesh->VtxTrianglesItr(kvid)) { const double OldtriArea = triAreas[tid]; const FQuadricErrorType OldtriQuadric = triQuadrics[tid]; // compute the new quadric for this tri. FQuadricErrorType NewtriQuadric = ComputeFaceQuadric(tid, n, c, NewtriArea); // update the arrays that hold the current face area & quadric triAreas[tid] = NewtriArea; triQuadrics[tid] = NewtriQuadric; FIndex3i tri_vids = Mesh->GetTriangle(tid); // update the vert quadrics that are adjacent to vid. for (int32 i = 0; i < 3; ++i) { if (tri_vids[i] == kvid) continue; // correct the adjacent vertQuadrics vertQuadrics[tri_vids[i]].Add(-OldtriArea, OldtriQuadric); // subtract old quadric vertQuadrics[tri_vids[i]].Add(NewtriArea, NewtriQuadric); // add new quadric } } // remove the influence of the dead tris from the two verts that were opposing the collapsed edge { for (int i = 0; i < 2; ++i) { if (removedTris[i] != FDynamicMesh3::InvalidID) { const double oldArea = triAreas[removedTris[i]]; FQuadricErrorType oldQuadric = triQuadrics[removedTris[i]]; // subtract the quadric from the opposing vert vertQuadrics[opposingVerts[i]].Add(-oldArea, oldQuadric); // zero out the quadric & area for the removed tris. triQuadrics[removedTris[i]] = FQuadricErrorType::Zero(); triAreas[removedTris[i]] = 0.; } } } // Rebuild the quadric for the vert that was retained during the collapse. // NB: in the version with memory this quadric took the value of the edge quadric that collapsed. { FQuadricErrorType vertQuadric = FQuadricErrorType::Zero(); for (int tid : Mesh->VtxTrianglesItr(kvid)) { vertQuadric.Add(triAreas[tid], triQuadrics[tid]); } vertQuadrics[kvid] = vertQuadric; } } // --- Update all edge quadrics in the nbrhood // NB: this has to follow updating all potential seam quadrics adjacent to kvid // because an edge quadric gathers seam quadrics adjacent the ends if (bRetainQuadricMemory) { for (int eid : Mesh->VtxEdgesItr(kvid)) { FDynamicMesh3::FEdge ne = Mesh->GetEdge(eid); QuadricErrorType Q = AssembleEdgeQuadric(ne); FVector3d opt = OptimalPoint(eid, Q, ne.Vert.A, ne.Vert.B); float err = (float)Q.Evaluate(opt); EdgeQuadrics[eid] = QEdge(eid, Q, opt); if (EdgeQueue.Contains(eid)) { EdgeQueue.Update(eid, err); } else { EdgeQueue.Insert(eid, err); } } } else { TArray> EdgesToUpdate; for (int adjeid : Mesh->VtxEdgesItr(kvid)) { EdgesToUpdate.Add(adjeid); const FIndex2i Verts = Mesh->GetEdgeV(adjeid); int adjvid = (Verts[0] == kvid) ? Verts[1] : Verts[0]; if (adjvid != FDynamicMesh3::InvalidID) { for (int eid : Mesh->VtxEdgesItr(adjvid)) { if (eid != adjeid) { EdgesToUpdate.AddUnique(eid); } } } } for (int eid : EdgesToUpdate) { const FDynamicMesh3::FEdge edgeData = Mesh->GetEdge(eid); FQuadricErrorType Q = AssembleEdgeQuadric(edgeData); FVector3d opt = OptimalPoint(eid, Q, edgeData.Vert[0], edgeData.Vert[1]); float err = (float)Q.Evaluate(opt); EdgeQuadrics[eid] = QEdge(eid, Q, opt); if (EdgeQueue.Contains(eid)) { EdgeQueue.Update(eid, err); } else { EdgeQueue.Insert(eid, err); } } } } template void TMeshSimplification::Precompute(bool bMeshIsClosed) { bHaveBoundary = false; IsBoundaryVtxCache.SetNum(Mesh->MaxVertexID()); if (bMeshIsClosed == false) { for (int eid : Mesh->BoundaryEdgeIndicesItr()) { FIndex2i ev = Mesh->GetEdgeV(eid); IsBoundaryVtxCache[ev.A] = true; IsBoundaryVtxCache[ev.B] = true; bHaveBoundary = true; } } } template void TMeshSimplification::DoSimplify() { if (Mesh->TriangleCount() == 0) // badness if we don't catch this... { return; } if (Mesh->HasAttributes() && GetConstraints().IsSet() == false) { ensureMsgf(false, TEXT("Input Mesh has Attribute overlays but no Constraints are configured. Use FMeshConstraintsUtil::ConstrainAllBoundariesAndSeams() to create a Constraint Set for Attribute seams.")); } ProfileBeginPass(); ProfileBeginSetup(); Precompute(); if (Cancelled()) { return; } InitializeTriQuadrics(); if (Cancelled()) { return; } InitializeSeamQuadrics(); if (Cancelled()) { return; } InitializeVertexQuadrics(); if (Cancelled()) { return; } InitializeQueue(); if (Cancelled()) { return; } ProfileEndSetup(); ProfileBeginOps(); ProfileBeginCollapse(); while (EdgeQueue.GetCount() > 0) { // termination criteria if (SimplifyMode == ETargetModes::VertexCount) { if (Mesh->VertexCount() <= TargetCount) { break; } } else if (SimplifyMode == ETargetModes::MaxError) { float qe = EdgeQueue.GetFirstNodePriority(); if (FMath::Abs(qe) > MaxErrorAllowed) { break; } } else { if (Mesh->TriangleCount() <= TargetCount) { break; } } COUNT_ITERATIONS++; int eid = EdgeQueue.Dequeue(); if (Mesh->IsEdge(eid) == false) { continue; } if (Cancelled()) { return; } FDynamicMesh3::FEdgeCollapseInfo collapseInfo; ESimplificationResult result = CollapseEdge(eid, EdgeQuadrics[eid].collapse_pt, collapseInfo); if (result == ESimplificationResult::Ok_Collapsed) { // update the quadrics UpdateNeighborhood(collapseInfo); } else if (result == ESimplificationResult::Failed_IsolatedTriangle && Mesh->TriangleCount() > 2) { const FDynamicMesh3::FEdge Edge = Mesh->GetEdge(eid); RemoveIsolatedTriangle(Edge.Tri.A); } } ProfileEndCollapse(); ProfileEndOps(); if (Cancelled()) { return; } // [TODO] - consider, skip this when CollapseMode == ESimplificationCollapseModes::MinimalExistingVertexError ? Reproject(); ProfileEndPass(); } template void TMeshSimplification::SimplifyToTriangleCount(int nCount) { SimplifyMode = ETargetModes::TriangleCount; TargetCount = FMath::Max(1, nCount); MinEdgeLength = FMathd::MaxReal; MaxErrorAllowed = FMathf::MaxReal; DoSimplify(); } template void TMeshSimplification::SimplifyToVertexCount(int nCount) { SimplifyMode = ETargetModes::VertexCount; TargetCount = FMath::Max(3, nCount); MinEdgeLength = FMathd::MaxReal; MaxErrorAllowed = FMathf::MaxReal; DoSimplify(); } template void TMeshSimplification::SimplifyToEdgeLength(double minEdgeLen) { SimplifyMode = ETargetModes::MinEdgeLength; TargetCount = 1; MinEdgeLength = minEdgeLen; MaxErrorAllowed = FMathf::MaxReal; DoSimplify(); } template void TMeshSimplification::SimplifyToMaxError(double MaxError) { SimplifyMode = ETargetModes::MaxError; TargetCount = 1; MinEdgeLength = FMathd::MaxReal; MaxErrorAllowed = (float)MaxError; DoSimplify(); } template static bool IsDevelopableVertex(const FDynamicMesh3& Mesh, int32 VertexID, double DotTolerance, GetTriNormalFuncType GetTriNormalFunc) { FVector3d Normal1, Normal2; int32 Normal1Count = 0, Normal2Count = 0, OtherCount = 0; Mesh.EnumerateVertexTriangles(VertexID, [&](int32 tid) { FVector3d TriNormal = GetTriNormalFunc(tid); if (Normal1Count == 0) { Normal1 = TriNormal; Normal1Count++; return; } if (TriNormal.Dot(Normal1) > DotTolerance) { Normal1Count++; return; } if (Normal2Count == 0) { Normal2 = TriNormal; Normal2Count++; return; } if (TriNormal.Dot(Normal2) > DotTolerance) { Normal2Count++; return; } OtherCount++; }); return OtherCount == 0; } bool IsCollapsableDevelopableEdge(const FDynamicMesh3& Mesh, int32 CollapseEdgeID, int32 RemoveV, int32 KeepV, double DotTolerance, const TArray& TriNormals, const TArray IsBoundaryVtxCache) { FIndex2i CollapseEdgeT = Mesh.GetEdgeT(CollapseEdgeID); FVector3d Normal1 = TriNormals[CollapseEdgeT.A]; if (CollapseEdgeT.B == IndexConstants::InvalidID) { // If we're collapsing a boundary edge, the only way to avoid changing the shape is for RemoveV // to be flat and have exactly one other attached boundary edge that is colinear with this one. // Start by finding the other boundary edge and making sure that there is only one. bool bFoundSecondBoundaryEdge = false; for (int32 Eid : Mesh.VtxEdgesItr(RemoveV)) { if (Eid != CollapseEdgeID && Mesh.IsBoundaryEdge(Eid)) { if (bFoundSecondBoundaryEdge) { // Found more than one other boundary edge, so not collapsable return false; } bFoundSecondBoundaryEdge = true; // Verify that this second boundary edge is colinear with ours. FVector3d KeepVert = Mesh.GetVertex(KeepV); FVector3d RemoveVert = Mesh.GetVertex(RemoveV); int32 OtherV = IndexUtil::FindEdgeOtherVertex(Mesh.GetEdgeV(Eid), RemoveV); FVector3d OtherVert = Mesh.GetVertex(OtherV); if (!(Normalized(RemoveVert - OtherVert).Dot(Normalized(KeepVert - RemoveVert)) > DotTolerance)) { // Not colinear return false; } } } if (!bFoundSecondBoundaryEdge) { // Seems impossible for a vertex to have exactly one attached boundary edge return ensure(false); } // If we got to here, we found the other boundary edge, and we'll check for planarity further below. } else { // If this is not a boundary edge, then remove V must not be a boundary vertex, else we // would deform the boundary on collapse. if (IsBoundaryVtxCache[RemoveV]) { return false; } } FVector3d Normal2 = (CollapseEdgeT.B == IndexConstants::InvalidID) ? FVector3d::ZeroVector : TriNormals[CollapseEdgeT.B]; // planar case if (CollapseEdgeT.B == IndexConstants::InvalidID || Normal1.Dot(Normal2) > DotTolerance) { bool bIsFlat = true; Mesh.EnumerateVertexTriangles(RemoveV, [&](int32 tid) { if (TriNormals[tid].Dot(Normal1) < DotTolerance) { bIsFlat = false; } }); return bIsFlat; } // if we are not planar, we need to find the 'other' developable edge at RemoveV. // This edge must be aligned w/ our collapse edge and have the same normals FVector3d A = Mesh.GetVertex(RemoveV), B = Mesh.GetVertex(KeepV); FVector3d EdgeDir(B - A); Normalize(EdgeDir); int32 FoldEdges = 0, FlatEdges = 0, OtherEdges = 0; for (int32 eid : Mesh.VtxEdgesItr(RemoveV)) { if (eid != CollapseEdgeID) { FIndex2i EdgeT = Mesh.GetEdgeT(eid); if (EdgeT.B == IndexConstants::InvalidID) { // We already handled the cases where RemoveV is a boundary vert, so this shouldn't happen. return ensure(false); } FVector3d Normal3 = TriNormals[EdgeT.A]; FVector3d Normal4 = TriNormals[EdgeT.B]; FIndex2i OtherEdgeV = Mesh.GetEdgeV(eid); int32 OtherV = IndexUtil::FindEdgeOtherVertex(OtherEdgeV, RemoveV); FVector3d C = Mesh.GetVertex(OtherV); if ( Normalized(A-C).Dot(EdgeDir) > DotTolerance) { if ((Normal3.Dot(Normal1) > DotTolerance && Normal4.Dot(Normal2) > DotTolerance) || (Normal3.Dot(Normal2) > DotTolerance && Normal4.Dot(Normal1) > DotTolerance)) { FoldEdges++; } } else if ( Normal3.Dot(Normal4) > DotTolerance) { FlatEdges++; } else { OtherEdges++; } } } return (FoldEdges == 1 && OtherEdges == 0); } template void TMeshSimplification::SimplifyToMinimalPlanar( double CoplanarAngleTolDeg, TFunctionRef EdgeFilterPredicate) { #define RETURN_IF_CANCELLED if (Cancelled()) { return; } if (Mesh->TriangleCount() == 0) // badness if we don't catch this... { return; } // This function doesn't affect the shape of the boundary, so the value of bPreserveBoundaryShape // shouldn't matter. Yet in practice, having bPreserveBoundaryShape be true is problematic because // the related block in CollapseEdge() arbitrarily decides that just one of the verts of a boundary // edge can be collapsed to. // TODO: the above is probably a minor bug for other forms of simplification too, but fixing it requires // going through the details of other simplification methods. For SimplifyToMinimalPlanar, the simplest // solution is to just eliminate that factor as a concern, since it should be free to collapse along // colinear boundaries unless they are explicitly constrained. TGuardValue PreserveBoundaryShapeOverride(bPreserveBoundaryShape, false); // Sets to false, restores on exit // keep triangle normals TArray TriNormals; TArray DevelopableVerts; ProfileBeginPass(); ProfileBeginSetup(); Precompute(); RETURN_IF_CANCELLED; TriNormals.SetNum(Mesh->MaxTriangleID()); ParallelFor(Mesh->MaxTriangleID(), [&](int32 tid) { if (Mesh->IsTriangle(tid)) { TriNormals[tid] = Mesh->GetTriNormal(tid); } }); RETURN_IF_CANCELLED; DevelopableVerts.SetNum(Mesh->MaxVertexID()); double PlanarDotTol = FMathd::Cos( CoplanarAngleTolDeg * FMathd::DegToRad ); ParallelFor(Mesh->MaxVertexID(), [&](int32 vid) { if (Mesh->IsVertex(vid)) { DevelopableVerts[vid] = IsDevelopableVertex(*Mesh, vid, PlanarDotTol, [&](int32 tid) { return TriNormals[tid]; }); } }); RETURN_IF_CANCELLED; ProfileEndSetup(); ProfileBeginOps(); ProfileBeginCollapse(); TArray CollapseEdges; int32 MaxRounds = 50; int32 num_last_pass = 0; for (int ri = 0; ri < MaxRounds; ++ri) { num_last_pass = 0; // collect up edges we have identified for collapse CollapseEdges.Reset(); for (int32 eid : Mesh->EdgeIndicesItr()) { if (EdgeFilterPredicate(eid) == false) { continue; } FIndex2i ev = Mesh->GetEdgeV(eid); if (DevelopableVerts[ev.A] || DevelopableVerts[ev.B]) { CollapseEdges.Add(eid); } } FVector3d va = FVector3d::Zero(), vb = FVector3d::Zero(); for ( int32 eid : CollapseEdges ) { if (!Mesh->IsEdge(eid)) { continue; } RETURN_IF_CANCELLED; COUNT_ITERATIONS++; FIndex2i ev = Mesh->GetEdgeV(eid); FDynamicMesh3::FEdgeCollapseInfo CollapseInfo; ESimplificationResult Result = ESimplificationResult::Failed_OpNotSuccessful; // Try collapsing to vert B. if (DevelopableVerts[ev.A] && IsCollapsableDevelopableEdge(*Mesh, eid, ev.A, ev.B, PlanarDotTol, TriNormals, IsBoundaryVtxCache)) { Result = CollapseEdge(eid, Mesh->GetVertex(ev.B), CollapseInfo, ev.B); } // If that didn't work, try collapsing to vert A if (Result != ESimplificationResult::Ok_Collapsed && DevelopableVerts[ev.B] && IsCollapsableDevelopableEdge(*Mesh, eid, ev.B, ev.A, PlanarDotTol, TriNormals, IsBoundaryVtxCache)) { Result = CollapseEdge(eid, Mesh->GetVertex(ev.A), CollapseInfo, ev.A); } if (Result == ESimplificationResult::Ok_Collapsed) { ++num_last_pass; int vKeptID = CollapseInfo.KeptVertex; Mesh->EnumerateVertexTriangles(vKeptID, [&](int32 tid) { TriNormals[tid] = Mesh->GetTriNormal(tid); }); for (int32 vid : Mesh->VtxVerticesItr(vKeptID)) { DevelopableVerts[vid] = IsDevelopableVertex(*Mesh, vid, PlanarDotTol, [&](int32 tid) { return TriNormals[tid]; }); } DevelopableVerts[vKeptID] = IsDevelopableVertex(*Mesh, vKeptID, PlanarDotTol, [&](int32 tid) { return TriNormals[tid]; }); } } if (num_last_pass == 0) // converged { break; } } ProfileEndCollapse(); ProfileEndOps(); RETURN_IF_CANCELLED; Reproject(); ProfileEndPass(); #undef RETURN_IF_CANCELLED } template void TMeshSimplification::FastCollapsePass(double fMinEdgeLength, int nRounds, bool MeshIsClosedHint, uint32 MinTriangleCount) { if ((uint32)Mesh->TriangleCount() <= MinTriangleCount) // badness if we don't catch this... { return; } MinEdgeLength = fMinEdgeLength; double min_sqr = MinEdgeLength * MinEdgeLength; // we don't collapse on the boundary bHaveBoundary = false; ProfileBeginPass(); ProfileBeginSetup(); Precompute(MeshIsClosedHint); if (Cancelled()) { return; } ProfileEndSetup(); ProfileBeginOps(); ProfileBeginCollapse(); int N = Mesh->MaxEdgeID(); int num_last_pass = 0; for (int ri = 0; ri < nRounds; ++ri) { num_last_pass = 0; FVector3d va = FVector3d::Zero(), vb = FVector3d::Zero(); for (int eid = 0; eid < N; ++eid) { if ((!Mesh->IsEdge(eid)) || Mesh->IsBoundaryEdge(eid)) { continue; } if ((uint32)Mesh->TriangleCount() <= MinTriangleCount) { break; } if (Cancelled()) { return; } Mesh->GetEdgeV(eid, va, vb); if (DistanceSquared(va, vb) > min_sqr) { continue; } COUNT_ITERATIONS++; FVector3d midpoint = (va + vb) * 0.5; FDynamicMesh3::FEdgeCollapseInfo collapseInfo; ESimplificationResult result = CollapseEdge(eid, midpoint, collapseInfo); if (result == ESimplificationResult::Ok_Collapsed) { ++num_last_pass; } } if (num_last_pass == 0 || (uint32)Mesh->TriangleCount() <= MinTriangleCount) // converged { break; } } ProfileEndCollapse(); ProfileEndOps(); if (Cancelled()) { return; } Reproject(); ProfileEndPass(); } template bool TMeshSimplification::CanCollapseEdge(int edgeID, int a, int b, int c, int d, int t0, int t1, int& collapse_to) const { const bool bPreserveSeamTopology = (CollapseMode == ESimplificationCollapseModes::MinimalExistingVertexError); if (bAllowSeamCollapse && !bPreserveSeamTopology) { return CanCollapseVertex(edgeID, a, b, collapse_to); } // make sure that the retained edges from the collapsed triangles don't merge seams bool bCanCollapse = FMeshRefinerBase::CanCollapseEdge(edgeID, a, b, c, d, t0, t1, collapse_to); // make sure more general seam topology is preserved if (bCanCollapse && bAllowSeamCollapse && bPreserveSeamTopology) { if (!Constraints) { return bCanCollapse; } // NB: We have to be more restrictive with the MinimalQuadricPositionError mode // in order to preclude the possibility of a seam moving during collapse. // check if this edge is a seam if (Constraints->HasEdgeConstraint(edgeID)) { // examine local topology bool bCanCollapseSeam = true; if (const FDynamicMeshAttributeSet* Attributes = Mesh->Attributes()) { for (int i = 0; bCanCollapseSeam && i < Attributes->NumUVLayers(); ++i) { auto* Overlay = Attributes->GetUVLayer(i); bool bIsNonIntersecting; if (Overlay->IsSeamEdge(edgeID, &bIsNonIntersecting)) { bCanCollapseSeam = bCanCollapseSeam && bIsNonIntersecting; } } for (int i = 0; bCanCollapseSeam && i < Attributes->NumNormalLayers(); ++i) { auto* Overlay = Attributes->GetNormalLayer(i); bool bIsNonIntersecting; if (Overlay->IsSeamEdge(edgeID, &bIsNonIntersecting)) { bCanCollapseSeam = bCanCollapseSeam && bIsNonIntersecting; } } } bCanCollapse = bCanCollapseSeam; } else { // this edge was not a seam, but need to check if one or both ends are part of other seams // - this is done by checking for vertex constraint bool bVertexAOnSeam = Constraints->HasVertexConstraint(a); bool bVertexBOnSeam = Constraints->HasVertexConstraint(b); if (bVertexAOnSeam && bVertexBOnSeam) { bCanCollapse = false; } else if (bVertexAOnSeam) { if (collapse_to == -1) { collapse_to = a; } else if (collapse_to != a) { bCanCollapse = false; } } else if (bVertexBOnSeam) { if (collapse_to == -1) { collapse_to = b; } else if (collapse_to != b) { bCanCollapse = false; } } } } return bCanCollapse; } template ESimplificationResult TMeshSimplification::CollapseEdge(int edgeID, FVector3d vNewPos, FDynamicMesh3::FEdgeCollapseInfo& collapseInfo, int32 RequireKeepVert) { collapseInfo.KeptVertex = FDynamicMesh3::InvalidID; RuntimeDebugCheck(edgeID); FEdgeConstraint constraint = (!Constraints) ? FEdgeConstraint::Unconstrained() : Constraints->GetEdgeConstraint(edgeID); if (constraint.NoModifications()) { return ESimplificationResult::Ignored_EdgeIsFullyConstrained; } if (constraint.CanCollapse() == false) { return ESimplificationResult::Ignored_EdgeIsFullyConstrained; } // look up verts and tris for this edge if (Mesh->IsEdge(edgeID) == false) { return ESimplificationResult::Failed_NotAnEdge; } const FDynamicMesh3::FEdge Edge = Mesh->GetEdge(edgeID); int a = Edge.Vert[0], b = Edge.Vert[1], t0 = Edge.Tri[0], t1 = Edge.Tri[1]; bool bIsBoundaryEdge = (t1 == FDynamicMesh3::InvalidID); // look up 'other' verts c (from t0) and d (from t1, if it exists) FIndex3i T0tv = Mesh->GetTriangle(t0); int c = IndexUtil::FindTriOtherVtx(a, b, T0tv); FIndex3i T1tv = (bIsBoundaryEdge) ? FDynamicMesh3::InvalidTriangle : Mesh->GetTriangle(t1); int d = (bIsBoundaryEdge) ? FDynamicMesh3::InvalidID : IndexUtil::FindTriOtherVtx(a, b, T1tv); FVector3d vA = Mesh->GetVertex(a); FVector3d vB = Mesh->GetVertex(b); double edge_len_sqr = (vA - vB).SquaredLength(); if (edge_len_sqr > MinEdgeLength * MinEdgeLength) { return ESimplificationResult::Ignored_EdgeTooLong; } ProfileBeginCollapse(); // check if we should collapse, and also find which vertex we should retain // in cases where we have constraints/etc int collapse_to = -1; bool bCanCollapse = CanCollapseEdge(edgeID, a, b, c, d, t0, t1, collapse_to); if (bCanCollapse == false) { return ESimplificationResult::Ignored_Constrained; } const bool bMinimalVertexMode = (CollapseMode == ESimplificationCollapseModes::MinimalExistingVertexError); // if we have a boundary, we want to collapse to boundary if (bPreserveBoundaryShape && bHaveBoundary) { if (collapse_to != -1) { if ((IsBoundaryVertex(b) && collapse_to != b) || (IsBoundaryVertex(a) && collapse_to != a)) { return ESimplificationResult::Ignored_Constrained; } } if (!bMinimalVertexMode) // the minimal existing vertex error has already resolved this with more complicated logic { if (IsBoundaryVertex(b)) { collapse_to = b; } else if (IsBoundaryVertex(a)) { collapse_to = a; } } } if (RequireKeepVert == a || RequireKeepVert == b) { if (collapse_to != -1 && collapse_to != RequireKeepVert) { return ESimplificationResult::Ignored_Constrained; } collapse_to = RequireKeepVert; } // optimization: if edge cd exists, we cannot collapse or flip. look that up here? // funcs will do it internally... // (or maybe we can collapse if cd exists? edge-collapse doesn't check for it explicitly...) ESimplificationResult retVal = ESimplificationResult::Failed_OpNotSuccessful; int iKeep = b, iCollapse = a; bool bConstraintsSpecifyPosition = false; if (collapse_to != -1) { iKeep = collapse_to; iCollapse = (iKeep == a) ? b : a; // if constraints or collapse mode require a fixed position if (Constraints) { bConstraintsSpecifyPosition = bMinimalVertexMode || (!Constraints->GetVertexConstraint(collapse_to).bCanMove ); } } double collapse_t = 0; if (!bConstraintsSpecifyPosition) { checkSlow(!bMinimalVertexMode || (vNewPos == vA || vNewPos == vB)); // [TODO] maybe skip Projection call when !bModeAllowsVertMovment. vNewPos = GetProjectedCollapsePosition(iKeep, vNewPos); double EdgeLength = Distance(vA, vB); collapse_t = (EdgeLength < FMathd::ZeroTolerance) ? 0.5 : (Distance(vNewPos, Mesh->GetVertex(iKeep))) / EdgeLength; collapse_t = VectorUtil::Clamp(collapse_t, 0.0, 1.0); if (bMinimalVertexMode) { // this _should_ already be 0 or 1 with perfect precision. // round here to make sure later attribute lerps don't change values collapse_t = FMath::RoundToDouble(collapse_t); } // If vertex is not explicitly constrained, and geometric error constraint is requested, we check it here. // If the check with the predicted vNewPos fails, try a second time with the linear-interpolated point. // (could optionally do a line search here, and/or be smarter about avoiding duplicate work, although // it is small in the context of the larger algorithm) if (GeometricErrorConstraint != EGeometricErrorCriteria::None ) { if (CheckIfCollapseWithinGeometricTolerance(iKeep, iCollapse, vNewPos, t0, t1) == false) { // project new position back onto the edge vNewPos = (1.0-collapse_t)*Mesh->GetVertex(iKeep) + (collapse_t)*Mesh->GetVertex(iCollapse); if (CheckIfCollapseWithinGeometricTolerance(iKeep, iCollapse, vNewPos, t0, t1) == false) { ProfileEndCollapse(); return ESimplificationResult::Failed_GeometricDeviation; } } } } else { vNewPos = (collapse_to == a) ? vA : vB; // If geometric error constraint is requested, ensure that the fixed vNewPos satisfies the constraint. // This must be done, otherwise the free vertex being collapsed to a fixed vertex will be allowed // to violate the geometric constraint if (GeometricErrorConstraint != EGeometricErrorCriteria::None) { if (CheckIfCollapseWithinGeometricTolerance(iKeep, iCollapse, vNewPos, t0, t1) == false) { ProfileEndCollapse(); return ESimplificationResult::Failed_GeometricDeviation; } } } // check if this collapse will create a normal flip. Also checks // for invalid collapse nbrhood, since we are doing one-ring iter anyway. // [TODO] could we skip this one-ring check in CollapseEdge? pass in hints? if (CheckIfCollapseCreatesFlipOrInvalid(a, b, vNewPos, t0, t1) || CheckIfCollapseCreatesFlipOrInvalid(b, a, vNewPos, t0, t1)) { ProfileEndCollapse(); return ESimplificationResult::Ignored_CreatesFlip; } if (bPreventTinyTriangles && (CheckIfCollapseCreatesTinyTriangle(a, b, vNewPos, t0, t1) || CheckIfCollapseCreatesTinyTriangle(b, a, vNewPos, t0, t1))) { ProfileEndCollapse(); return ESimplificationResult::Ignored_CreatesTinyTriangle; } // lots of cases where we cannot collapse, but we should just let // Mesh sort that out, right? COUNT_COLLAPSES++; EMeshResult result = Mesh->CollapseEdge(iKeep, iCollapse, collapse_t, collapseInfo); if (result == EMeshResult::Ok) { Mesh->SetVertex(iKeep, vNewPos); if (Constraints) { Constraints->ClearEdgeConstraint(edgeID); auto ConstraintUpdator = [this](int cur_eid)->void { // Seam edge can never flip, it is never fully unconstrained EEdgeRefineFlags SeamEdgeConstraint = EEdgeRefineFlags::NoFlip; if (!bAllowSeamCollapse) { SeamEdgeConstraint = EEdgeRefineFlags((int)SeamEdgeConstraint | (int)EEdgeRefineFlags::NoCollapse); } FEdgeConstraint UpdatedEdgeConstraint; FVertexConstraint UpdatedVertexConstraintA; FVertexConstraint UpdatedVertexConstraintB; bool bHaveUpdate = FMeshConstraintsUtil::ConstrainEdgeBoundariesAndSeams(cur_eid, *Mesh, MeshBoundaryConstraint, GroupBoundaryConstraint, MaterialBoundaryConstraint, SeamEdgeConstraint, !bAllowSeamCollapse, UpdatedEdgeConstraint, UpdatedVertexConstraintA, UpdatedVertexConstraintB); if (bHaveUpdate) { FIndex2i EdgeVerts = Mesh->GetEdgeV(cur_eid); Constraints->SetOrUpdateEdgeConstraint(cur_eid, UpdatedEdgeConstraint); UpdatedVertexConstraintA.CombineConstraint(Constraints->GetVertexConstraint(EdgeVerts.A)); Constraints->SetOrUpdateVertexConstraint(EdgeVerts.A, UpdatedVertexConstraintA); UpdatedVertexConstraintB.CombineConstraint(Constraints->GetVertexConstraint(EdgeVerts.B)); Constraints->SetOrUpdateVertexConstraint(EdgeVerts.B, UpdatedVertexConstraintB); } }; if (Constraints->HasEdgeConstraint(collapseInfo.RemovedEdges.A)) { Constraints->ClearEdgeConstraint(collapseInfo.KeptEdges.A); Constraints->ClearEdgeConstraint(collapseInfo.RemovedEdges.A); ConstraintUpdator(collapseInfo.KeptEdges.A); } if (collapseInfo.RemovedEdges.B != FDynamicMesh3::InvalidID) { if (Constraints->HasEdgeConstraint(collapseInfo.RemovedEdges.B)) { Constraints->ClearEdgeConstraint(collapseInfo.KeptEdges.B); Constraints->ClearEdgeConstraint(collapseInfo.RemovedEdges.B); ConstraintUpdator(collapseInfo.KeptEdges.B); } } Constraints->ClearVertexConstraint(iCollapse); } OnEdgeCollapse(edgeID, iKeep, iCollapse, collapseInfo); DoDebugChecks(); retVal = ESimplificationResult::Ok_Collapsed; } else if (result == EMeshResult::Failed_CollapseTriangle) { retVal = ESimplificationResult::Failed_IsolatedTriangle; } ProfileEndCollapse(); return retVal; } template bool TMeshSimplification::CheckIfCollapseWithinGeometricTolerance(int vKeep, int vRemove, const FVector3d& NewPosition, int tc, int td) { if (GeometricErrorConstraint == EGeometricErrorCriteria::PredictedPointToProjectionTarget) { // currently assuming projection target is what we want to measure geometric error against if (ProjectionTarget() != nullptr) { double ToleranceSqr = GeometricErrorTolerance * GeometricErrorTolerance; // test new position to see if it is within geometric tolerance of projection surface FVector3d TargetPos = ProjectionTarget()->Project(NewPosition); double DistSqr = DistanceSquared(TargetPos, NewPosition); if (DistSqr > ToleranceSqr) { return false; } // test edge midpoints, except the edge being collapsed int32 CollapseEdgeID = Mesh->FindEdge(vKeep, vRemove); auto EdgeMidpointsWithinTolerance = [this, CollapseEdgeID, NewPosition, ToleranceSqr](int32 vid) { for ( int32 eid : Mesh->VtxEdgesItr(vid) ) { if (eid != CollapseEdgeID) { FIndex2i EdgeV = Mesh->GetEdgeV(eid); FVector3d OtherVertexPos = (EdgeV.A == vid) ? Mesh->GetVertex(EdgeV.B) : Mesh->GetVertex(EdgeV.A); FVector3d NewMidpoint = (OtherVertexPos + NewPosition) * 0.5; FVector3d MidpointTargetPos = ProjectionTarget()->Project(NewMidpoint); if (DistanceSquared(NewMidpoint, MidpointTargetPos) > ToleranceSqr) { return false; } } } return true; }; if (EdgeMidpointsWithinTolerance(vKeep) == false || EdgeMidpointsWithinTolerance(vRemove) == false) { return false; } // check tri centers, except the triangles being collapsed auto CentroidsWithinToleranceFunc = [this, vKeep, vRemove, tc, td, NewPosition, ToleranceSqr](int32 vid) { bool bInTolerance = true; Mesh->EnumerateVertexTriangles(vid, [&](int32 tid) { if (bInTolerance && tid != tc && tid != td) { FIndex3i Tri = Mesh->GetTriangle(tid); FVector3d NewCentroid = FVector3d::Zero(); for (int32 j = 0; j < 3; ++j) { NewCentroid += (Tri[j] == vRemove || Tri[j] == vKeep) ? NewPosition : Mesh->GetVertex(Tri[j]); } NewCentroid *= (1.0 / 3.0); FVector3d CentroidTargetPos = ProjectionTarget()->Project(NewCentroid); if (DistanceSquared(NewCentroid, CentroidTargetPos) > ToleranceSqr) { bInTolerance = false; } } }); return bInTolerance; }; if (CentroidsWithinToleranceFunc(vKeep) == false || CentroidsWithinToleranceFunc(vRemove) == false) { return false; } } } return true; } template bool TMeshSimplification::RemoveIsolatedTriangle(int tID) { if (!Mesh->IsTriangle(tID)) return true; FIndex3i tv = Mesh->GetTriangle(tID); bool bIsIsolated = true; for (int i = 0; i < 3; ++i) { for (int nbtr : Mesh->VtxTrianglesItr(tv[i])) { bIsIsolated = bIsIsolated && (nbtr == tID); } } if (bIsIsolated) { const FIndex3i TriEdges = Mesh->GetTriEdges(tID); if (Mesh->RemoveTriangle(tID) == EMeshResult::Ok) { if (Constraints) { Constraints->ClearEdgeConstraint(TriEdges.A); Constraints->ClearEdgeConstraint(TriEdges.B); Constraints->ClearEdgeConstraint(TriEdges.C); Constraints->ClearVertexConstraint(tv.A); Constraints->ClearVertexConstraint(tv.B); Constraints->ClearVertexConstraint(tv.C); } } OnRemoveIsolatedTriangle(tID); } return bIsIsolated; } template void TMeshSimplification::OnEdgeCollapse(int edgeID, int va, int vb, const FDynamicMesh3::FEdgeCollapseInfo& collapseInfo) { // this is for subclasses... } template void TMeshSimplification::OnRemoveIsolatedTriangle(int tId) { // this is for subclasses } // Project vertices onto projection target. // We can do projection in parallel if we have .net template void TMeshSimplification::FullProjectionPass() { auto project = [&](int vID) { if (IsVertexPositionConstrained(vID)) { return; } if (VertexControlF != nullptr && ((int)VertexControlF(vID) & (int)EVertexControl::NoProject) != 0) { return; } FVector3d curpos = Mesh->GetVertex(vID); FVector3d projected = ProjTarget->Project(curpos, vID); Mesh->SetVertex(vID, projected); }; ApplyToProjectVertices(project); // TODO: optionally do projection in parallel? } template void TMeshSimplification::ApplyToProjectVertices(const TFunction& apply_f) { for (int vid : Mesh->VertexIndicesItr()) { apply_f(vid); } } template void TMeshSimplification::ProjectVertex(int vID, IProjectionTarget* targetIn) { FVector3d curpos = Mesh->GetVertex(vID); FVector3d projected = targetIn->Project(curpos, vID); Mesh->SetVertex(vID, projected); } // used by collapse-edge to get projected position for new vertex template FVector3d TMeshSimplification::GetProjectedCollapsePosition(int vid, const FVector3d& vNewPos) { if (Constraints) { FVertexConstraint vc = Constraints->GetVertexConstraint(vid); if (vc.Target != nullptr) { return vc.Target->Project(vNewPos, vid); } if (vc.bCanMove == false) { return vNewPos; } } // no constraint applied, so if we have a target surface, project to that if (EnableInlineProjection() && ProjTarget != nullptr) { if (VertexControlF == nullptr || ((int)VertexControlF(vid) & (int)EVertexControl::NoProject) == 0) { return ProjTarget->Project(vNewPos, vid); } } return vNewPos; } // Custom behavior for FAttrBasedQuadric simplifier. template<> void TMeshSimplification::OnEdgeCollapse(int edgeID, int va, int vb, const FDynamicMesh3::FEdgeCollapseInfo& collapseInfo) { // Update the normal FAttrBasedQuadricErrord& Quadric = EdgeQuadrics[edgeID].q; FVector3d collapse_pt = EdgeQuadrics[edgeID].collapse_pt; FVector3d UpdatedNormald; Quadric.ComputeAttributes(collapse_pt, UpdatedNormald); FVector3f UpdatedNormal((float)UpdatedNormald.X, (float)UpdatedNormald.Y, (float)UpdatedNormald.Z); Normalize(UpdatedNormal); if (NormalOverlay != nullptr) { // Get all the elements associated with this vertex (could be more than one to account for split vertex data) TArray ElementIdArray; NormalOverlay->GetVertexElements(va, ElementIdArray); if (ElementIdArray.Num() > 1) { // keep whatever split normals are currently in the overlay. // @todo: normalize the split normals - since the values here result from a lerp return; } // at most one element for (int ElementId : ElementIdArray) { NormalOverlay->SetElement(ElementId, UpdatedNormal); } } else { Mesh->SetVertexNormal(va, UpdatedNormal); } } namespace UE { namespace Geometry { // These are explicit instantiations of the templates that are exported from the shared lib. // Only these instantiations of the template can be used. // This is necessary because we have placed most of the templated functions in this .cpp file, instead of the header. #if PLATFORM_COMPILER_CLANG && !PLATFORM_MICROSOFT template class DYNAMICMESH_API TMeshSimplification< FAttrBasedQuadricErrord >; template class DYNAMICMESH_API TMeshSimplification< FVolPresQuadricErrord >; template class DYNAMICMESH_API TMeshSimplification< FQuadricErrord >; #else template class TMeshSimplification< FAttrBasedQuadricErrord >; template class TMeshSimplification< FVolPresQuadricErrord >; template class TMeshSimplification< FQuadricErrord >; #endif } // end namespace UE::Geometry } // end namespace UE