Files
UnrealEngineUWP/Engine/Plugins/Experimental/GeometryProcessing/Source/DynamicMesh/Private/MeshSimplification.cpp
Ryan Schmidt ad9f0ea712 GeometryProcessing: add DynamicMeshAttributeSet::SplitAllBowties() function, splits bowties in NormalOverlay and UVOverlays
ModelingTools: Expose Simplify To Minimal Planar simplification mode in Simplify Tool. Add edge filter to this simplification mode in TMeshSimplification. Add support for returning all edges along intersection curves in FMeshBoolean and FMeshSelfUnion. Add option BooleanMeshesOp and SelfUnionMeshesOp to apply minimal-planar simplification along these cut/intersection edges. Expose as toggles in CSGMeshesTool and SelfUnionMeshesTool
#rb jimmy.andrews
#rnx
#jira none

[CL 14890297 by Ryan Schmidt in ue5-main branch]
2020-12-09 14:18:38 -04:00

1405 lines
35 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "MeshSimplification.h"
#include "DynamicMeshAttributeSet.h"
#include "Util/IndexUtil.h"
#include "Async/ParallelFor.h"
template <typename QuadricErrorType>
QuadricErrorType TMeshSimplification<QuadricErrorType>::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<FAttrBasedQuadricErrord>::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 <typename QuadricErrorType>
void TMeshSimplification<QuadricErrorType>::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 <typename QuadricErrorType>
void TMeshSimplification<QuadricErrorType>::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 <typename QuadricErrorType>
void TMeshSimplification<QuadricErrorType>::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 <typename QuadricErrorType>
QuadricErrorType TMeshSimplification<QuadricErrorType>::AssembleEdgeQuadric(const FDynamicMesh3::FEdge& edge) const
{
return QuadricErrorType(vertQuadrics[edge.Vert.A], vertQuadrics[edge.Vert.B]);
}
template<>
FAttrBasedQuadricErrord TMeshSimplification<FAttrBasedQuadricErrord>::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 <typename QuadricErrorType>
void TMeshSimplification<QuadricErrorType>::InitializeQueue()
{
int NE = Mesh->EdgeCount();
int MaxEID = Mesh->MaxEdgeID();
EdgeQuadrics.SetNum(MaxEID);
EdgeQueue.Initialize(MaxEID);
TArray<FEdgeError> 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 <typename QuadricErrorType>
FVector3d TMeshSimplification<QuadricErrorType>::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<FQuadricErrord>::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 <typename QuadricErrorType>
void TMeshSimplification<QuadricErrorType>::UpdateNeighbours(const FDynamicMesh3::FEdgeCollapseInfo& collapseInfo)
{
double EdgeWeight = this->SeamEdgeWeight;
int vid = collapseInfo.KeptVertex;
FIndex2i removedTris = collapseInfo.RemovedTris;
FIndex2i opposingVerts = collapseInfo.OpposingVerts;
TArray<int, TInlineAllocator<15>> AdjTris;
for (int tid : Mesh->VtxTrianglesItr(vid))
{
AdjTris.Add(tid);
}
TArray<int, TInlineAllocator<15>> 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<int, TInlineAllocator<64>> 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 <typename QuadricErrorType>
void TMeshSimplification<QuadricErrorType>::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 <typename QuadricErrorType>
void TMeshSimplification<QuadricErrorType>::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 <typename QuadricErrorType>
void TMeshSimplification<QuadricErrorType>::SimplifyToTriangleCount(int nCount)
{
SimplifyMode = ETargetModes::TriangleCount;
TargetCount = FMath::Max(1, nCount);
MinEdgeLength = FMathd::MaxReal;
MaxErrorAllowed = FMathf::MaxReal;
DoSimplify();
}
template <typename QuadricErrorType>
void TMeshSimplification<QuadricErrorType>::SimplifyToVertexCount(int nCount)
{
SimplifyMode = ETargetModes::VertexCount;
TargetCount = FMath::Max(3, nCount);
MinEdgeLength = FMathd::MaxReal;
MaxErrorAllowed = FMathf::MaxReal;
DoSimplify();
}
template <typename QuadricErrorType>
void TMeshSimplification<QuadricErrorType>::SimplifyToEdgeLength(double minEdgeLen)
{
SimplifyMode = ETargetModes::MinEdgeLength;
TargetCount = 1;
MinEdgeLength = minEdgeLen;
MaxErrorAllowed = FMathf::MaxReal;
DoSimplify();
}
template <typename QuadricErrorType>
void TMeshSimplification<QuadricErrorType>::SimplifyToMaxError(double MaxError)
{
SimplifyMode = ETargetModes::MaxError;
TargetCount = 1;
MinEdgeLength = FMathd::MaxReal;
MaxErrorAllowed = MaxError;
DoSimplify();
}
template<typename GetTriNormalFuncType>
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<typename GetTriNormalFuncType>
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);
if (EdgeT.B == IndexConstants::InvalidID)
{
return false; // abort if one of the edges of RemoveV is a boundary edge (?)
}
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 <typename QuadricErrorType>
void TMeshSimplification<QuadricErrorType>::SimplifyToMinimalPlanar(
double CoplanarAngleTolDeg,
TFunctionRef<bool(int32 EdgeID)> EdgeFilterPredicate)
{
#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<FVector3d> TriNormals;
TArray<bool> 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<int32> 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) == 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 <typename QuadricErrorType>
void TMeshSimplification<QuadricErrorType>::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 <typename QuadricErrorType>
ESimplificationResult TMeshSimplification<QuadricErrorType>::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 <typename QuadricErrorType>
void TMeshSimplification<QuadricErrorType>::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 <typename QuadricErrorType>
void TMeshSimplification<QuadricErrorType>::ApplyToProjectVertices(const TFunction<void(int)>& apply_f)
{
for (int vid : Mesh->VertexIndicesItr())
{
apply_f(vid);
}
}
template <typename QuadricErrorType>
void TMeshSimplification<QuadricErrorType>::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 <typename QuadricErrorType>
FVector3d TMeshSimplification<QuadricErrorType>::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<FAttrBasedQuadricErrord>::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<int> 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 >;