// Copyright Epic Games, Inc. All Rights Reserved. #include "MeshSimplification.h" #include "DynamicMeshAttributeSet.h" #include "Util/IndexUtil.h" #include "Async/ParallelFor.h" 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) { NormalOverlay->GetTriElements(tid, n0, n1, n2); } 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(); if (!Attributes) { return; } 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)); } // for each seam edge, add the seam quadric to its verts. for (auto& seamQuadric : seamQuadrics) { int eid = seamQuadric.Key; FIndex2i vids = Mesh->GetEdgeV(eid); vertQuadrics[vids[0]].AddSeamQuadric(seamQuadric.Value); vertQuadrics[vids[1]].AddSeamQuadric(seamQuadric.Value); } } template QuadricErrorType TMeshSimplification::AssembleEdgeQuadric(const FDynamicMesh3::FEdge& edge) const { return QuadricErrorType(vertQuadrics[edge.Vert.A], vertQuadrics[edge.Vert.B]); } template<> FAttrBasedQuadricErrord TMeshSimplification::AssembleEdgeQuadric(const FDynamicMesh3::FEdge& edge) const { FAttrBasedQuadricErrord Q(vertQuadrics[edge.Vert.A], vertQuadrics[edge.Vert.B]); // the edge.Tri faces are double counted. Remove one. const FIndex2i& Tris = edge.Tri; if (Tris.A != FDynamicMesh3::InvalidID) { Q.Add(-triAreas[Tris.A], triQuadrics[Tris.A]); } if (Tris.B != FDynamicMesh3::InvalidID) { Q.Add(-triAreas[Tris.B], triQuadrics[Tris.B]); } return Q; } template void TMeshSimplification::InitializeQueue() { int NE = Mesh->EdgeCount(); int MaxEID = Mesh->MaxEdgeID(); EdgeQuadrics.SetNum(MaxEID); EdgeQueue.Initialize(MaxEID); TArray EdgeErrors; EdgeErrors.SetNum(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)) { return (Mesh->GetVertex(ea) + Mesh->GetVertex(eb)) * 0.5; } 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(); } } // update queue weight for each edge in vertex one-ring template <> void DYNAMICMESH_API TMeshSimplification::UpdateNeighbours(const FDynamicMesh3::FEdgeCollapseInfo& collapseInfo) { int vid = collapseInfo.KeptVertex; double EdgeWeight = this->SeamEdgeWeight; for (int eid : Mesh->VtxEdgesItr(vid)) { FDynamicMesh3::FEdge ne = Mesh->GetEdge(eid); // update the seam quadric and vert quadric to reflect the new seams if (bAllowSeamCollapse) { // need to recompute this seam quadric if (FSeamQuadricType* seamQuadric = seamQuadrics.Find(eid)) { // subtract the old seam quadric from adj verts vertQuadrics[ne.Vert[0]].SubtractSeamQuadric(*seamQuadric); vertQuadrics[ne.Vert[1]].SubtractSeamQuadric(*seamQuadric); // 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); // add the seam quadric to the adj verts vertQuadrics[ne.Vert[0]].AddSeamQuadric(*seamQuadric); vertQuadrics[ne.Vert[1]].AddSeamQuadric(*seamQuadric); } } FQuadricErrord 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); } } } // update queue weight for each edge in vertex one-ring. Memoryless template void TMeshSimplification::UpdateNeighbours(const FDynamicMesh3::FEdgeCollapseInfo& collapseInfo) { double EdgeWeight = this->SeamEdgeWeight; int vid = collapseInfo.KeptVertex; FIndex2i removedTris = collapseInfo.RemovedTris; FIndex2i opposingVerts = collapseInfo.OpposingVerts; TArray> AdjTris; for (int tid : Mesh->VtxTrianglesItr(vid)) { AdjTris.Add(tid); } TArray> AdjEdges; for (int eid : Mesh->VtxEdgesItr(vid)) { AdjEdges.Add(eid); } // This is the faster version that selectively updates the one-ring { // 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 : AdjTris) { 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] == vid) 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 : AdjTris) { vertQuadric.Add(triAreas[tid], triQuadrics[tid]); } vertQuadrics[vid] = vertQuadric; } if (bAllowSeamCollapse) { for (int eid : AdjEdges) { // need to recompute this seam quadric if (FSeamQuadricType* seamQuadric = seamQuadrics.Find(eid)) { const FDynamicMesh3::FEdge ne = Mesh->GetEdge(eid); // subtract the old seam quadric from adj verts if (ne.Vert[0] == vid) // note we have reset the vertQuadric at the retained vertex so it doesn't have a contribution from the old seamQ { vertQuadrics[ne.Vert[1]].SubtractSeamQuadric(*seamQuadric); } else { check(ne.Vert[1] == vid); vertQuadrics[ne.Vert[0]].SubtractSeamQuadric(*seamQuadric); } // 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); // add the seam quadric to the adj verts vertQuadrics[ne.Vert[0]].AddSeamQuadric(*seamQuadric); vertQuadrics[ne.Vert[1]].AddSeamQuadric(*seamQuadric); } } } } // Update all the edges { TArray> EdgesToUpdate; for (int adjeid : AdjEdges) { EdgesToUpdate.Add(adjeid); const FIndex2i Verts = Mesh->GetEdgeV(adjeid); int adjvid = (Verts[0] == vid) ? 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) { vertQuadrics[collapseInfo.KeptVertex] = EdgeQuadrics[eid].q; UpdateNeighbours(collapseInfo); } } ProfileEndCollapse(); ProfileEndOps(); if (Cancelled()) { return; } 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 = 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; } template static bool IsCollapsableDevelopableEdge(const FDynamicMesh3& Mesh, int32 CollapseEdgeID, int32 RemoveV, int32 KeepV, double DotTolerance, GetTriNormalFuncType GetTriNormalFunc) { FIndex2i CollapseEdgeT = Mesh.GetEdgeT(CollapseEdgeID); FVector3d Normal1 = GetTriNormalFunc(CollapseEdgeT.A); FVector3d Normal2 = GetTriNormalFunc(CollapseEdgeT.B); // assuming is that RemoveV is developable vertex...should check? // planar case if (Normal1.Dot(Normal2) > DotTolerance) { bool bIsFlat = true; Mesh.EnumerateVertexTriangles(RemoveV, [&](int32 tid) { if (GetTriNormalFunc(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); EdgeDir.Normalize(); int32 FoldEdges = 0, FlatEdges = 0, OtherEdges = 0; for (int32 eid : Mesh.VtxEdgesItr(RemoveV)) { if (eid != CollapseEdgeID) { FIndex2i EdgeT = Mesh.GetEdgeT(eid); FVector3d Normal3 = GetTriNormalFunc(EdgeT.A); FVector3d Normal4 = GetTriNormalFunc(EdgeT.B); FIndex2i OtherEdgeV = Mesh.GetEdgeV(eid); int32 OtherV = IndexUtil::FindEdgeOtherVertex(OtherEdgeV, RemoveV); FVector3d C = Mesh.GetVertex(OtherV); if ((A-C).Normalized().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) { #define RETURN_IF_CANCELLED if (Cancelled()) { return; } if (Mesh->TriangleCount() == 0) // badness if we don't catch this... { return; } // we don't collapse on the boundary bHaveBoundary = false; // 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()) { 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) == false) || Mesh->IsBoundaryEdge(eid) ) { continue; } RETURN_IF_CANCELLED; COUNT_ITERATIONS++; FIndex2i ev = Mesh->GetEdgeV(eid); bool bDevelopableA = DevelopableVerts[ev.A]; bool bDevelopableB = DevelopableVerts[ev.B]; if (bDevelopableA || bDevelopableB) // this may change during execution as edges are collapsed { if (! bDevelopableA) // any other preference for verts? { Swap(ev.A, ev.B); } bool bIsCollapsible = IsCollapsableDevelopableEdge(*Mesh, eid, ev.A, ev.B, PlanarDotTol, [&](int32 tid) { return TriNormals[tid]; }); if (bIsCollapsible) { FDynamicMesh3::FEdgeCollapseInfo collapseInfo; ESimplificationResult result; result = CollapseEdge(eid, Mesh->GetVertex(ev.B), collapseInfo, ev.B); 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]; }); } else if (bDevelopableA && bDevelopableB && IsCollapsableDevelopableEdge(*Mesh, eid, ev.B, ev.A, PlanarDotTol, [&](int32 tid) { return TriNormals[tid]; }) ) { // we can try collapsing to A 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) { if (Mesh->TriangleCount() == 0) // 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 (Cancelled()) { return; } Mesh->GetEdgeV(eid, va, vb); if (va.DistanceSquared(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) // converged { break; } } ProfileEndCollapse(); ProfileEndOps(); if (Cancelled()) { return; } Reproject(); ProfileEndPass(); } 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; } // 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 (IsBoundaryVertex(b)) { collapse_to = b; } else if (IsBoundaryVertex(a)) { collapse_to = a; } } if (RequireKeepVert == a || RequireKeepVert == b) { if (collapse_to >= 0 && 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 bCanMove = true; if (collapse_to != -1) { iKeep = collapse_to; iCollapse = (iKeep == a) ? b : a; // if constraints require a fixed position if (Constraints) { bCanMove = Constraints->GetVertexConstraint(collapse_to).bCanMove; } } double collapse_t = 0; if (bCanMove) { vNewPos = GetProjectedCollapsePosition(iKeep, vNewPos); double div = vA.Distance(vB); collapse_t = (div < FMathd::ZeroTolerance) ? 0.5 : (vNewPos.Distance(Mesh->GetVertex(iKeep))) / div; collapse_t = VectorUtil::Clamp(collapse_t, 0.0, 1.0); } else { vNewPos = (collapse_to == a) ? vA : vB; } // 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; } // 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); Constraints->ClearEdgeConstraint(collapseInfo.RemovedEdges.A); if (collapseInfo.RemovedEdges.B != FDynamicMesh3::InvalidID) { Constraints->ClearEdgeConstraint(collapseInfo.RemovedEdges.B); } Constraints->ClearVertexConstraint(iCollapse); } OnEdgeCollapse(edgeID, iKeep, iCollapse, collapseInfo); DoDebugChecks(); retVal = ESimplificationResult::Ok_Collapsed; } ProfileEndCollapse(); return retVal; } // 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(UpdatedNormald.X, UpdatedNormald.Y, UpdatedNormald.Z); UpdatedNormal.Normalize(); 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); } } // 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. template class DYNAMICMESH_API TMeshSimplification< FAttrBasedQuadricErrord >; template class DYNAMICMESH_API TMeshSimplification< FVolPresQuadricErrord >; template class DYNAMICMESH_API TMeshSimplification< FQuadricErrord >;