Files
UnrealEngineUWP/Engine/Plugins/Runtime/GeometryProcessing/Source/DynamicMesh/Private/Operations/LocalPlanarSimplify.cpp
jimmy andrews f63cdc13af Update FLocalPlanarSimplify to support simplification along straight creases in a mesh, where the sides of the crease may optionally belong to different PolyGroups
Add an edge simplification operation to the PolyEdit tool

#rb semion.piskarev
#preflight 63ee9a0f3c1eb56f055442ac

[CL 24276322 by jimmy andrews in ue5-main branch]
2023-02-16 21:55:32 -05:00

515 lines
18 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "Operations/LocalPlanarSimplify.h"
#include "DynamicMesh/DynamicMeshAttributeSet.h"
#include "DynamicMesh/DynamicMesh3.h"
using namespace UE::Geometry;
bool FLocalPlanarSimplify::IsFlat(const FDynamicMesh3& Mesh, int VID, double DotTolerance, FVector3d& OutFirstNormal)
{
bool bHasFirst = false;
bool bIsFlat = true;
Mesh.EnumerateVertexTriangles(VID, [&Mesh, DotTolerance, &OutFirstNormal, &bHasFirst, &bIsFlat](int32 TID)
{
if (!bIsFlat)
{
return;
}
FVector3d Normal = Mesh.GetTriNormal(TID);
if (!bHasFirst)
{
OutFirstNormal = Normal;
bHasFirst = true;
}
else
{
bIsFlat = bIsFlat && Normal.Dot(OutFirstNormal) >= DotTolerance;
}
});
return bIsFlat;
}
bool FLocalPlanarSimplify::IsDevelopableAlongEdge(const FDynamicMesh3& Mesh, int EID, int VID, double DotTolerance, FVector3d& NormalA, bool& bIsFlat)
{
FIndex2i EdgeT = Mesh.GetEdgeT(EID);
NormalA = Mesh.GetTriNormal(EdgeT.A);
bIsFlat = true;
FVector3d NormalB;
if (EdgeT.B != FDynamicMesh3::InvalidID)
{
NormalB = Mesh.GetTriNormal(EdgeT.B);
// Only consider a second normal if the across-edge triangle normal doesn't match the first normal
if (NormalA.Dot(NormalB) < DotTolerance)
{
bIsFlat = false;
}
}
bool bIsDevelopable = true;
Mesh.EnumerateVertexTriangles(VID, [&bIsDevelopable, EdgeT, &Mesh, NormalA, NormalB, DotTolerance, bIsFlat](int32 TID)
{
if (!bIsDevelopable || EdgeT.Contains(TID))
{
return;
}
FVector3d Normal = Mesh.GetTriNormal(TID);
bIsDevelopable = Normal.Dot(NormalA) >= DotTolerance;
if (!bIsDevelopable && !bIsFlat) // if we didn't match first normal, test the second normal (if we have one)
{
bIsDevelopable = Normal.Dot(NormalB) >= DotTolerance;
}
});
bIsFlat = bIsFlat && bIsDevelopable;
return bIsDevelopable;
}
/**
* Test if a given edge collapse would cause a triangle flip or other unacceptable decrease in mesh quality
*/
bool FLocalPlanarSimplify::CollapseWouldHurtTriangleQuality(
const FDynamicMesh3& Mesh, const FVector3d& RemoveVNormal,
int32 RemoveV, const FVector3d& RemoveVPos, int32 KeepV, const FVector3d& KeepVPos,
double TryToImproveTriQualityThreshold, bool bHasMultipleNormals
)
{
double WorstQualityNewTriangle = FMathd::MaxReal;
bool bIsHurt = false;
Mesh.EnumerateVertexTriangles(RemoveV,
[&Mesh, &bIsHurt, &KeepVPos, RemoveV, KeepV, &RemoveVNormal,
TryToImproveTriQualityThreshold, &WorstQualityNewTriangle, bHasMultipleNormals](int32 TID)
{
if (bIsHurt)
{
return;
}
FIndex3i Tri = Mesh.GetTriangle(TID);
// Skip re-computing the triangle normal for flat surfaces
FVector3d TriNormal = bHasMultipleNormals ? Mesh.GetTriNormal(TID) : RemoveVNormal;
FVector3d Verts[3];
for (int Idx = 0; Idx < 3; Idx++)
{
int VID = Tri[Idx];
if (VID == KeepV)
{
// this tri has both RemoveV and KeepV, so it'll be removed and we don't need to consider it
return;
}
else if (VID == RemoveV)
{
// anything at RemoveV is reconnected to KeepV's position
Verts[Idx] = KeepVPos;
}
else
{
// it's not on the collapsed edge so it stays still
Verts[Idx] = Mesh.GetVertex(Tri[Idx]);
}
}
FVector3d Edge1(Verts[1] - Verts[0]);
FVector3d Edge2(Verts[2] - Verts[0]);
FVector3d VCross(Edge2.Cross(Edge1));
double EdgeFlipTolerance = 1.e-5;
double Area2 = Normalize(VCross);
if (TryToImproveTriQualityThreshold > 0)
{
FVector3d Edge3(Verts[2] - Verts[1]);
double MaxLenSq = FMathd::Max3(Edge1.SquaredLength(), Edge2.SquaredLength(), Edge3.SquaredLength());
double Quality = Area2 / (MaxLenSq + FMathd::ZeroTolerance);
WorstQualityNewTriangle = FMathd::Min(Quality, WorstQualityNewTriangle);
}
bIsHurt = VCross.Dot(TriNormal) <= EdgeFlipTolerance;
}
);
// note tri quality was computed as 2*Area / MaxEdgeLenSquared
// so need to multiply it by 2/sqrt(3) to get actual aspect ratio
if (!bIsHurt && WorstQualityNewTriangle * 2 * FMathd::InvSqrt3 < TryToImproveTriQualityThreshold)
{
// we found a bad tri; tentatively switch to rejecting this edge collapse
bIsHurt = true;
// but if there was an even worse tri in the original neighborhood, accept the collapse after all
Mesh.EnumerateVertexTriangles(RemoveV, [&Mesh, &bIsHurt, WorstQualityNewTriangle](int TID)
{
if (!bIsHurt) // early out if we already found an originally-worse tri
{
return;
}
FVector3d A, B, C;
Mesh.GetTriVertices(TID, A, B, C);
FVector3d E1 = B - A, E2 = C - A, E3 = C - B;
double Area2 = E1.Cross(E2).Length();
double MaxLenSq = FMathd::Max3(E1.SquaredLength(), E2.SquaredLength(), E3.SquaredLength());
double Quality = Area2 / (MaxLenSq + FMathd::ZeroTolerance);
if (Quality < WorstQualityNewTriangle)
{
bIsHurt = false;
}
}
);
}
return bIsHurt;
}
/**
* Test if a given edge collapse would change the mesh shape or UVs unacceptably
*/
bool FLocalPlanarSimplify::CollapseWouldChangeShapeOrUVs(
const FDynamicMesh3& Mesh, const TSet<int>& PathEdgeSet, double DotTolerance, int SourceEID,
int32 RemoveV, const FVector3d& RemoveVPos, int32 KeepV, const FVector3d& KeepVPos, const FVector3d& EdgeDir,
bool bPreserveTriangleGroups, bool bPreserveUVsForMesh, bool bPreserveVertexUVs, bool bPreserveOverlayUVs,
float UVEqualThresholdSq, bool bPreserveVertexNormals, float NormalEqualCosThreshold)
{
// Search the edges connected to the vertex to find one in the boundary set that points in the opposite direction
// If we don't find that edge, or if there are other boundary/seam edges attached, we can't remove this vertex
// We also can't remove the vertex if doing so would distort the UVs
bool bHasBadEdge = false;
int OpposedEdge = -1;
FIndex2i EdgeT = Mesh.GetEdgeT(SourceEID);
int SourceGroupID = Mesh.GetTriangleGroup(EdgeT.A);
int OtherGroupID = -1;
// Note: In many cases (e.g. plane cut, new edge insertions) we know there will be only one group,
// so we could use a slightly faster path with bAllowTwoGroups==false. But this is probably not
// a significant performance difference, so for now we just expose the allows-two-group path.
constexpr bool bAllowTwoGroups = true;
if (bAllowTwoGroups && EdgeT.B != FDynamicMesh3::InvalidID)
{
OtherGroupID = Mesh.GetTriangleGroup(EdgeT.B);
}
Mesh.EnumerateVertexEdges(RemoveV,
[&](int32 VertEID)
{
if (bHasBadEdge || VertEID == SourceEID)
{
return;
}
FDynamicMesh3::FEdge Edge = Mesh.GetEdge(VertEID);
if (bPreserveTriangleGroups && Mesh.HasTriangleGroups())
{
int GroupA = Mesh.GetTriangleGroup(Edge.Tri.A);
int GroupB = Edge.Tri.B == FDynamicMesh3::InvalidID ? SourceGroupID : Mesh.GetTriangleGroup(Edge.Tri.B);
if (
(GroupA != SourceGroupID && (!bAllowTwoGroups || GroupA != OtherGroupID)) ||
(GroupB != SourceGroupID && (!bAllowTwoGroups || GroupB != OtherGroupID))
)
{
// RemoveV is bordering too many groups, so the edge collapse would change the shape of the groups
bHasBadEdge = true;
return;
}
}
// it's a path edge; check if it's the opposite-facing one we need
if (PathEdgeSet.Contains(VertEID))
{
if (OpposedEdge != -1)
{
bHasBadEdge = true;
return;
}
FIndex2i OtherEdgeV = Edge.Vert;
int OtherV = IndexUtil::FindEdgeOtherVertex(OtherEdgeV, RemoveV);
FVector3d OtherVPos = Mesh.GetVertex(OtherV);
FVector3d OtherEdgeDir = OtherVPos - RemoveVPos;
if (Normalize(OtherEdgeDir) == 0)
{
// collapsing degenerate edges above should prevent this
bHasBadEdge = true;
return; // break instead of continue to skip the whole edge
}
if (OtherEdgeDir.Dot(EdgeDir) <= -DotTolerance)
{
OpposedEdge = VertEID;
}
else
{
bHasBadEdge = true;
return;
}
// LerpT used for vertex normal and UV interpolation
float LerpT = float((OtherVPos - RemoveVPos).Dot(OtherEdgeDir) / (OtherVPos - KeepVPos).Dot(OtherEdgeDir));
if (bPreserveVertexNormals && Mesh.HasVertexNormals())
{
FVector3f OtherN = Mesh.GetVertexNormal(OtherV);
FVector3f RemoveN = Mesh.GetVertexNormal(RemoveV);
FVector3f KeepN = Mesh.GetVertexNormal(KeepV);
if (Normalized(Lerp(OtherN, KeepN, LerpT)).Dot(Normalized(RemoveN)) < NormalEqualCosThreshold)
{
bHasBadEdge = true;
return;
}
}
// Controls whether any UV distortion is tested
if (!bPreserveUVsForMesh)
{
return;
}
if (bPreserveVertexUVs && Mesh.HasVertexUVs())
{
FVector2f OtherUV = Mesh.GetVertexUV(OtherV);
FVector2f RemoveUV = Mesh.GetVertexUV(RemoveV);
FVector2f KeepUV = Mesh.GetVertexUV(KeepV);
if ( DistanceSquared( Lerp(OtherUV, KeepUV, LerpT), RemoveUV) > UVEqualThresholdSq)
{
bHasBadEdge = true;
return;
}
}
if (bPreserveOverlayUVs && Mesh.HasAttributes())
{
int NumLayers = Mesh.Attributes()->NumUVLayers();
FIndex2i SourceEdgeTris = Mesh.GetEdgeT(SourceEID);
FIndex2i OppEdgeTris = Edge.Tri;
// returns true if collapse will result in acceptable overlay UVs
auto CanCollapseOverlayUVs = [&Mesh, KeepV, RemoveV, OtherV, NumLayers, LerpT, UVEqualThresholdSq](int SourceEdgeTID, int OppEdgeTID) -> bool
{
FIndex3i SourceBaseTri = Mesh.GetTriangle(SourceEdgeTID);
FIndex3i OppBaseTri = Mesh.GetTriangle(OppEdgeTID);
int KeepSourceIdx = IndexUtil::FindTriIndex(KeepV, SourceBaseTri);
int RemoveSourceIdx = IndexUtil::FindTriIndex(RemoveV, SourceBaseTri);
int OtherOppIdx = IndexUtil::FindTriIndex(OtherV, OppBaseTri);
if (!ensure(KeepSourceIdx != -1 && RemoveSourceIdx != -1 && OtherOppIdx != -1))
{
return false;
}
// get the UVs per overlay off the triangle(s) attached the two edges
for (int UVLayerIdx = 0; UVLayerIdx < NumLayers; UVLayerIdx++)
{
const FDynamicMeshUVOverlay* UVs = Mesh.Attributes()->GetUVLayer(UVLayerIdx);
if (UVs->ElementCount() < 3)
{
// overlay is not actually in use; skip it
continue;
}
FIndex3i SourceT = UVs->GetTriangle(SourceEdgeTID);
FIndex3i OppT = UVs->GetTriangle(OppEdgeTID);
int KeepE = SourceT[KeepSourceIdx];
int RemoveE = SourceT[RemoveSourceIdx];
int OtherE = OppT[OtherOppIdx];
if (KeepE == -1 || RemoveE == -1 || OtherE == -1)
{
// overlay is not set on relevant triangles; skip it
continue;
}
FVector2f OtherUV = UVs->GetElement(OtherE);
FVector2f RemoveUV = UVs->GetElement(RemoveE);
FVector2f KeepUV = UVs->GetElement(KeepE);
if (DistanceSquared(Lerp(OtherUV, KeepUV, LerpT), RemoveUV) > UVEqualThresholdSq)
{
return false;
}
}
return true;
};
// If we're not on a boundary, check if we're on a seam -- and if so, test that
// the collapse won't change the UVs on the other side of the seam
if (SourceEdgeTris.B != -1 || OppEdgeTris.B != -1)
{
// the edges must be both boundary or neither
if (SourceEdgeTris.B == -1 || OppEdgeTris.B == -1)
{
bHasBadEdge = true;
return;
}
// likewise must be both seam or neither
bool bSourceIsSeam = Mesh.Attributes()->IsSeamEdge(SourceEID);
bool bOtherIsSeam = Mesh.Attributes()->IsSeamEdge(VertEID);
if (bSourceIsSeam != bOtherIsSeam)
{
bHasBadEdge = true;
return;
}
// if both are seam, make sure that the A and B sides are consistent,
// then test the UVs on the other side of the seam
if (bSourceIsSeam)
{
// test whether the A/B sides are consistent for SourceEdgeTris and OppEdgeTris,
// based on the ordering of the remove and keep/other vertices in each triangle
FIndex3i SourceBaseTri = Mesh.GetTriangle(SourceEdgeTris.A);
FIndex3i OppBaseTri = Mesh.GetTriangle(OppEdgeTris.A);
int32 SrcTriRmVSubIdx = IndexUtil::FindTriIndex(RemoveV, SourceBaseTri);
int32 OppTriRmVSubIdx = IndexUtil::FindTriIndex(RemoveV, OppBaseTri);
// Test whether the Triangle "A" for the Source Edge is wound such that the 'Remove' Vertex is before the 'Keep' Vertex
bool bSrcAOrderIsRmThenKeep = SourceBaseTri[(SrcTriRmVSubIdx + 1) % 3] == KeepV;
// Test whether the Triangle "A" for the Opposite Edge is wound such that the 'Remove' Vertex is before the 'Other' Vertex
bool bOppAOrderIsRmThenOther = OppBaseTri[(OppTriRmVSubIdx + 1) % 3] == OtherV;
// If the two "A" sides are on the same side of the seam, we expect these two vertex ordering bools *not* to match --
// i.e., the oriented edges should be "Keep Remove, then Remove Other" or "Other Remove, then Remove Keep"
// If the bools match, we swap the A and B of the Opposite Edge Triangles, to move the A side back to the same side of the seam
// as its Source Edge Triangle counterpart.
if (bSrcAOrderIsRmThenKeep == bOppAOrderIsRmThenOther)
{
Swap(OppEdgeTris.A, OppEdgeTris.B);
}
if (!CanCollapseOverlayUVs(SourceEdgeTris.B, OppEdgeTris.B))
{
bHasBadEdge = true;
return;
}
}
}
if (!CanCollapseOverlayUVs(SourceEdgeTris.A, OppEdgeTris.A))
{
bHasBadEdge = true;
return;
}
}
}
else // it wasn't in the boundary edge set; check if it's one that would prevent us from safely removing the vertex
{
if (Mesh.IsBoundaryEdge(VertEID) || (Mesh.HasAttributes() && Mesh.Attributes()->IsSeamEdge(VertEID)))
{
bHasBadEdge = true;
}
}
});
return bHasBadEdge;
}
void FLocalPlanarSimplify::SimplifyAlongEdges(FDynamicMesh3& Mesh, TSet<int32>& InOutEdges, TUniqueFunction<void(const DynamicMeshInfo::FEdgeCollapseInfo&)> ProcessCollapse) const
{
double DotTolerance = FMathd::Cos(SimplificationAngleTolerance * FMathd::DegToRad);
// For some simplification passes we can safely only consider locally-flat vertices for simplification, ignoring "straight crease" cases because
// the new edges that we consider simplifying were generated by cutting across flat triangles. This makes the simplification logic a bit simpler.
// You can use this variable to toggle that path.
// TODO: We could consider exposing this variable as a parameter (but it does not seem likely to affect performance enough to really be worth doing so)
constexpr bool bOnlySimplifyWhereFlat = false;
// Save the input list of edges to iterate over, so we can edit the set w/ new edges while safely iterating over the old ones
TArray<int32> CutBoundaryEdgesArray = InOutEdges.Array();
int NumCollapses = 0, CollapseIters = 0;
int MaxCollapseIters = 1; // TODO: is there a case where we need more iterations? Perhaps if we add some triangle quality criteria?
while (CollapseIters < MaxCollapseIters)
{
int LastNumCollapses = NumCollapses;
for (int EID : CutBoundaryEdgesArray)
{
// this can happen if a collapse removes another cut boundary edge
// (which can happen e.g. if you have a degenerate (colinear) tri flat on the cut boundary)
if (!Mesh.IsEdge(EID))
{
continue;
}
FDynamicMesh3::FEdge Edge = Mesh.GetEdge(EID);
// track whether the neighborhood of the vertex is flat
bool Developable[2]{ false, false };
// normals for each flat vertex
FVector3d FlatNormals[2]{ FVector3d::Zero(), FVector3d::Zero() };
bool Flat[2]{ false, false };
int NumDevelopable = 0;
for (int VIdx = 0; VIdx < 2; VIdx++)
{
if (bOnlySimplifyWhereFlat)
{
Flat[VIdx] = Developable[VIdx] = IsFlat(Mesh, Edge.Vert[VIdx], DotTolerance, FlatNormals[VIdx]);
}
else
{
Developable[VIdx] = IsDevelopableAlongEdge(Mesh, EID, Edge.Vert[VIdx], DotTolerance, FlatNormals[VIdx], Flat[VIdx]);
}
if (Developable[VIdx])
{
NumDevelopable++;
}
}
if (NumDevelopable == 0)
{
continue;
}
// see if we can collapse to remove either vertex
for (int RemoveVIdx = 0; RemoveVIdx < 2; RemoveVIdx++)
{
if (!Developable[RemoveVIdx])
{
continue;
}
int KeepVIdx = 1 - RemoveVIdx;
FVector3d RemoveVPos = Mesh.GetVertex(Edge.Vert[RemoveVIdx]);
FVector3d KeepVPos = Mesh.GetVertex(Edge.Vert[KeepVIdx]);
FVector3d EdgeDir = KeepVPos - RemoveVPos;
if (Normalize(EdgeDir) == 0) // 0 is returned as a special case when the edge was too short to normalize
{
// This case is often avoided by collapsing degenerate edges in a separate pre-pass, so we just skip over it here for now
// TODO: Consider adding degenerate edge-collapse logic here
break; // break instead of continue to skip the whole edge
}
bool bHasBadEdge = false; // will be set if either mesh can't collapse the edge
int RemoveV = Edge.Vert[RemoveVIdx];
int KeepV = Edge.Vert[KeepVIdx];
int SourceEID = EID;
bHasBadEdge = bHasBadEdge || CollapseWouldHurtTriangleQuality(
Mesh, FlatNormals[RemoveVIdx], RemoveV, RemoveVPos, KeepV, KeepVPos, TryToImproveTriQualityThreshold, !Flat[RemoveVIdx]);
bHasBadEdge = bHasBadEdge || CollapseWouldChangeShapeOrUVs(
Mesh, InOutEdges, DotTolerance,
SourceEID, RemoveV, RemoveVPos, KeepV, KeepVPos, EdgeDir, bPreserveTriangleGroups,
true, bPreserveVertexUVs, bPreserveOverlayUVs, UVDistortTolerance * UVDistortTolerance,
bPreserveVertexNormals, FMathf::Cos(NormalDistortTolerance * FMathf::DegToRad));
if (bHasBadEdge)
{
continue;
}
FDynamicMesh3::FEdgeCollapseInfo CollapseInfo;
EMeshResult CollapseResult = Mesh.CollapseEdge(KeepV, RemoveV, 0, CollapseInfo);
if (CollapseResult == EMeshResult::Ok)
{
if (ProcessCollapse)
{
ProcessCollapse(CollapseInfo);
}
NumCollapses++;
InOutEdges.Remove(CollapseInfo.CollapsedEdge);
InOutEdges.Remove(CollapseInfo.RemovedEdges[0]);
if (CollapseInfo.RemovedEdges[1] != -1)
{
InOutEdges.Remove(CollapseInfo.RemovedEdges[1]);
}
}
break; // if we got through to trying to collapse the edge, don't try to collapse from the other vertex.
}
}
CutBoundaryEdgesArray = InOutEdges.Array();
if (NumCollapses == LastNumCollapses)
{
break;
}
CollapseIters++;
}
}