Files
UnrealEngine/Engine/Plugins/Runtime/GeometryProcessing/Source/DynamicMesh/Private/Operations/SmoothHoleFiller.cpp
2025-05-18 13:04:45 +08:00

457 lines
14 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "Operations/SmoothHoleFiller.h"
#include "MeshBoundaryLoops.h"
#include "Selections/MeshFaceSelection.h"
#include "Selections/MeshVertexSelection.h"
#include "Selections/MeshEdgeSelection.h"
#include "Operations/SimpleHoleFiller.h"
#include "Operations/OffsetMeshRegion.h"
#include "Operations/MeshRegionOperator.h"
#include "Solvers/ConstrainedMeshSmoother.h"
#include "DynamicMesh/DynamicMeshChangeTracker.h"
#include "SubRegionRemesher.h"
#include "ProjectionTargets.h"
#include "MeshConstraintsUtil.h"
#include "DynamicMeshEditor.h"
using namespace UE::Geometry;
namespace
{
// hard-coded remeshing parameters
const int InitialRemeshPasses = 5;
const int PostSmoothRemeshPasses = 10;
const int SmoothSolveIterations = 2;
const bool bRemeshAfterSmooth = true;
const double RemeshingSmoothAlpha = 0.75;
/// Apply LaplacianMeshSmoother to subset of mesh triangles.
///
/// \param Mesh The mesh to smooth
/// \param InputTriangles Triangles defining code region to smooth. This region can grow by specifying nIncludeExteriorRings > 0.
/// \param nConstrainLoops Number of one-rings to soft-constrain inside the smooth region.
/// \param nIncludeExteriorRings Number of one-rings to grow the region.
/// \param bConstrainExteriorRings Whether to pin the vertices outside of the original "InputTriangles" region.
/// \param InteriorSmoothness Baseline "smoothness" scalar (\propto 1/weight) for vertices inside the initial region boundary.
/// \param BorderWeight Constraint weight for vertices on the initial region boundary
///
/// - Border of subset always has soft constraint with borderWeight, but is then snapped back to original vtx pos
/// after solve.
/// - nConstrainLoops inner loops are also soft-constrained, with weight falloff via square roots (defines continuity)
/// - interiorWeight is soft constraint added to all vertices
///
void RegionSmooth(FDynamicMesh3* Mesh,
const FMeshFaceSelection& InputTriangles,
int nConstrainLoops,
int nIncludeExteriorRings,
bool bConstrainExteriorRings,
double InteriorSmoothness,
double BorderWeight = 10.0)
{
TSet<int> FixedVerts;
FMeshFaceSelection SmoothTriangles = InputTriangles;
if (nIncludeExteriorRings > 0)
{
if (bConstrainExteriorRings)
{
// add constraints to vertices which are in expandVerts but not in startVerts (i.e. the "exterior ring"
// vertices)
FMeshEdgeSelection BoundaryEdges(Mesh);
BoundaryEdges.SelectBoundaryTriEdges(SmoothTriangles);
SmoothTriangles.ExpandToOneRingNeighbours(nIncludeExteriorRings);
FMeshVertexSelection StartVerts(Mesh);
StartVerts.SelectTriangleVertices(InputTriangles);
StartVerts.DeselectEdges(BoundaryEdges.AsArray());
FMeshVertexSelection ExpandVerts(Mesh);
ExpandVerts.SelectTriangleVertices(SmoothTriangles);
for (int ExpandedRegionVertexID : ExpandVerts.AsSet())
{
if (!StartVerts.IsSelected(ExpandedRegionVertexID))
{
FixedVerts.Add(ExpandedRegionVertexID);
}
}
}
else
{
SmoothTriangles.ExpandToOneRingNeighbours(nIncludeExteriorRings);
}
}
// Submesh
FMeshRegionOperator Region(Mesh, SmoothTriangles.AsArray());
FDynamicMesh3& SmoothMesh = Region.Region.GetSubmesh();
ELaplacianWeightScheme UseScheme = ELaplacianWeightScheme::IDTCotanget;
TUniquePtr<UE::Solvers::IConstrainedMeshSolver> Smoother = UE::MeshDeformation::ConstructConstrainedMeshSmoother(
UseScheme, SmoothMesh);
check(Smoother);
// map fixed verts to submesh
TSet<int> SubFixedVerts;
for (int BaseVertexID : FixedVerts)
{
SubFixedVerts.Add(Region.Region.MapVertexToSubmesh(BaseVertexID));
}
// Constrain borders
double Weight = BorderWeight;
TSet<int> Constrained;
for (int BaseVertexID : Region.Region.GetBaseBorderVertices())
{
int SubVertexID = Region.Region.MapVertexToSubmesh(BaseVertexID);
Smoother->AddConstraint(SubVertexID, Weight, SmoothMesh.GetVertex(SubVertexID), true);
Constrained.Add(SubVertexID);
}
if (Constrained.Num() > 0 && nConstrainLoops > 0)
{
Weight = FMath::Sqrt(Weight);
for (int k = 0; k < nConstrainLoops; ++k)
{
TSet<int> NextLayer;
for (int SubVertexID : Constrained)
{
for (int NeighborVertexID : SmoothMesh.VtxVerticesItr(SubVertexID))
{
if (Constrained.Contains(NeighborVertexID) == false)
{
if (Smoother->IsConstrained(NeighborVertexID) == false)
{
Smoother->AddConstraint(NeighborVertexID, Weight, SmoothMesh.GetVertex(NeighborVertexID),
SubFixedVerts.Contains(NeighborVertexID));
}
NextLayer.Add(NeighborVertexID);
}
}
}
Constrained.Append(NextLayer);
Weight = FMath::Sqrt(Weight);
}
}
// constraint weight for interior vertices away from border
// (this is an empirically-determined hack that seems to work OK to normalize the smoothing result for variable vertex count...)
double NonlinearT = InteriorSmoothness * InteriorSmoothness;
double ScaledPower = (NonlinearT / 50.0) * Mesh->VertexCount();
double InteriorWeight = (ScaledPower < FMathf::ZeroTolerance) ? 999999.0 : (1.0 / ScaledPower);
// soft constraint on all interior vertices, if requested
if (InteriorWeight > 0)
{
for (int VertexID : SmoothMesh.VertexIndicesItr())
{
if (Smoother->IsConstrained(VertexID) == false)
{
Smoother->AddConstraint(VertexID, InteriorWeight, SmoothMesh.GetVertex(VertexID), SubFixedVerts.Contains(VertexID));
}
}
}
else if (SubFixedVerts.Num() > 0)
{
for (int VertexID : SubFixedVerts)
{
if (Smoother->IsConstrained(VertexID) == false)
{
Smoother->AddConstraint(VertexID, 0, SmoothMesh.GetVertex(VertexID), true);
}
}
}
TArray<FVector3d> PositionBuffer;
bool bOK = Smoother->Deform(PositionBuffer);
for (int VertexID : SmoothMesh.VertexIndicesItr())
{
SmoothMesh.SetVertex(VertexID, PositionBuffer[VertexID]);
}
Region.BackPropropagateVertices(true);
}
}
FSmoothHoleFiller::FSmoothHoleFiller(FDynamicMesh3& Mesh, const FEdgeLoop& FillLoop) :
Mesh(Mesh),
FillLoop(FillLoop)
{
}
void FSmoothHoleFiller::ConstrainSubregionSeams(FSubRegionRemesher& Remesher, bool bConstrainROIBoundary)
{
FMeshConstraints Constraints;
const bool bAllowSplits = !bConstrainROIBoundary;
constexpr bool bAllowSmoothing = false;
// Constrain seam edges and vertices in the EdgeROI
FMeshConstraintsUtil::ConstrainSeamsInEdgeROI(Constraints,
Mesh,
Remesher.GetCurrentEdgeROI().Array(),
bAllowSplits,
bAllowSmoothing);
// Constrain TriangleROI boundaries in the EdgeROI
FMeshConstraintsUtil::ConstrainROIBoundariesInEdgeROI(Constraints, Mesh, Remesher.GetCurrentEdgeROI(),
Remesher.GetCurrentTriangleROI(), bAllowSplits, bAllowSmoothing);
// Finally, fully constrain all mesh boundaries in the EdgeROI. Do this so we don't disrupt another loop that we
// might want to fill later.
for (int EdgeID : Remesher.GetCurrentEdgeROI())
{
if (Mesh.IsBoundaryEdge(EdgeID))
{
Constraints.SetOrUpdateEdgeConstraint(EdgeID, FEdgeConstraint::FullyConstrained());
}
}
if (Constraints.HasConstraints())
{
Remesher.SetExternalConstraints(MoveTemp(Constraints));
}
}
void FSmoothHoleFiller::DefaultConfigureRemesher(FSubRegionRemesher& Remesher, bool bConstrainROIBoundary)
{
check(RemeshingTargetEdgeLength > 0.0);
Remesher.SetTargetEdgeLength(RemeshingTargetEdgeLength);
Remesher.SmoothSpeedT = RemeshingSmoothAlpha;
Remesher.bEnableSmoothing = (RemeshingSmoothAlpha > 0.0);
Remesher.SmoothType = FRemesher::ESmoothTypes::MeanValue;
ConstrainSubregionSeams(Remesher, bConstrainROIBoundary);
Remesher.ProjectionMode = FMeshRefinerBase::ETargetProjectionMode::NoProjection;
}
bool FSmoothHoleFiller::Fill(int32 GroupID)
{
// first do an easy hole fill
FSimpleHoleFiller Filler(&Mesh, FillLoop);
if (Filler.Fill(GroupID) == false)
{
return false;
}
if (FillLoop.Vertices.Num() <= 3)
{
NewTriangles = Filler.NewTriangles;
return true;
}
// Initialize target edge length
double MinLength, MaxLength, AvgLength;
TMeshQueries<FDynamicMesh3>::EdgeLengthStatsFromEdges(Mesh, FillLoop.Edges, MinLength, MaxLength, AvgLength);
check(FillOptions.FillDensityScalar > 0.0);
RemeshingTargetEdgeLength = AvgLength / FillOptions.FillDensityScalar;
// Get the initial triangle selection for remeshing/smoothing
FMeshFaceSelection TriangleSelection(&Mesh);
TriangleSelection.Select(Filler.NewTriangles);
NewTriangles = Filler.NewTriangles;
// if we aren't trying to stay inside hole, expand out a bit,
// which allows us to clean up ugly edges
if (!FillOptions.bConstrainToHoleInterior)
{
TriangleSelection.ExpandToOneRingNeighbours(FillOptions.RemeshingExteriorRegionWidth);
TriangleSelection.LocalOptimize(true, true);
}
// remesh the initial coarse fill region
if (InitialRemeshPasses > 0)
{
TUniquePtr<FSubRegionRemesher> Remesher;
if (FillOptions.bConstrainToHoleInterior)
{
Remesher = MakeUnique<FRestrictedSubRegionRemesher>(&Mesh, TriangleSelection.AsSet());
}
else
{
Remesher = MakeUnique<FSubRegionRemesher>(&Mesh);
FMeshVertexSelection VertexSelection(&Mesh, TriangleSelection); // All triangle vertices
Remesher->SetInitialVertexROI(VertexSelection.AsSet());
Remesher->InitializeFromVertexROI();
}
Remesher->UpdateROI();
DefaultConfigureRemesher(*Remesher, true);
for (int k = 0; k < InitialRemeshPasses; ++k)
{
if (k > 0)
{
Remesher->UpdateROI();
// If the ROI has expanded, it may include seam edges that were not constrained in the original Remesher set up
ConstrainSubregionSeams(*Remesher, true);
}
Remesher->BasicRemeshPass();
}
NewTriangles = Remesher->GetCurrentTriangleROI().Array();
TriangleSelection.DeselectAll();
TriangleSelection.Select(NewTriangles);
if (!FillOptions.bConstrainToHoleInterior)
{
TriangleSelection.LocalOptimize(true, true);
}
}
// Now iteratively smooth and remesh
if (FillOptions.bConstrainToHoleInterior)
{
for (int k = 0; k < SmoothSolveIterations; ++k)
{
bool bFinal = (k == (SmoothSolveIterations - 1));
SmoothAndRemeshPreserveRegion(TriangleSelection, bFinal);
TriangleSelection.DeselectAll();
TriangleSelection.Select(NewTriangles);
}
}
else
{
SmoothAndRemesh(TriangleSelection);
TriangleSelection.DeselectAll();
TriangleSelection.Select(NewTriangles);
}
// Filter NewTriangles by new groupID
TArray<int> AllRemeshedTriangles = NewTriangles;
NewTriangles.Reset();
for (int TriangleID : AllRemeshedTriangles)
{
if (Mesh.GetTriangleGroup(TriangleID) == GroupID)
{
NewTriangles.Emplace(TriangleID);
}
}
return true;
}
void FSmoothHoleFiller::SmoothAndRemeshPreserveRegion(FMeshFaceSelection& TriangleSelection, bool bFinal)
{
check(FillOptions.bConstrainToHoleInterior);
RegionSmooth(&Mesh, TriangleSelection, FillOptions.SmoothingInteriorRegionWidth,
FillOptions.SmoothingExteriorRegionWidth, true, FillOptions.InteriorSmoothness);
if (bRemeshAfterSmooth)
{
FRestrictedSubRegionRemesher Remesher(&Mesh, TriangleSelection.AsSet());
Remesher.UpdateROI();
DefaultConfigureRemesher(Remesher, true);
FDynamicMesh3 ProjectionTargetMeshCopy;
TUniquePtr<FDynamicMeshAABBTree3> ProjectionTargetSpatial = nullptr;
TUniquePtr<FMeshProjectionTarget> ProjectionTarget = nullptr;
if (bFinal && FillOptions.bProjectDuringRemesh)
{
// TODO: Get only a mesh subset for projection rather than copying the whole mesh
ProjectionTargetMeshCopy.Copy(Mesh, false, false, false, false);
ProjectionTargetSpatial = MakeUnique<FDynamicMeshAABBTree3>(&ProjectionTargetMeshCopy, true);
ProjectionTarget = MakeUnique<FMeshProjectionTarget>(&ProjectionTargetMeshCopy, ProjectionTargetSpatial.Get());
Remesher.SetProjectionTarget(ProjectionTarget.Get());
Remesher.ProjectionMode = FMeshRefinerBase::ETargetProjectionMode::AfterRefinement;
}
else
{
Remesher.ProjectionMode = FMeshRefinerBase::ETargetProjectionMode::NoProjection;
}
for (int k = 0; k < PostSmoothRemeshPasses; ++k)
{
if (k > 0)
{
Remesher.UpdateROI();
// If the ROI has expanded, it may include seam edges that were not constrained in the original Remesher set up
ConstrainSubregionSeams(Remesher, true);
}
Remesher.BasicRemeshPass();
}
NewTriangles = Remesher.GetCurrentTriangleROI().Array();
}
else
{
NewTriangles = TriangleSelection.AsArray();
}
}
void FSmoothHoleFiller::SmoothAndRemesh(FMeshFaceSelection& TriangleSelection)
{
check(!FillOptions.bConstrainToHoleInterior);
RegionSmooth(&Mesh, TriangleSelection, FillOptions.SmoothingInteriorRegionWidth,
FillOptions.SmoothingExteriorRegionWidth, false, FillOptions.InteriorSmoothness);
if (bRemeshAfterSmooth)
{
FSubRegionRemesher Remesher(&Mesh);
FMeshVertexSelection VertexSelection(&Mesh, TriangleSelection);
Remesher.SetInitialVertexROI(VertexSelection.AsSet());
Remesher.InitializeFromVertexROI();
Remesher.UpdateROI();
DefaultConfigureRemesher(Remesher, false);
FDynamicMesh3 ProjectionTargetMeshCopy;
TUniquePtr<FDynamicMeshAABBTree3> ProjectionTargetSpatial = nullptr;
TUniquePtr<FMeshProjectionTarget> ProjectionTarget = nullptr;
if (FillOptions.bProjectDuringRemesh)
{
// TODO: Get only a mesh subset for projection rather than copying the whole mesh
ProjectionTargetMeshCopy.Copy(Mesh, false, false, false, false);
ProjectionTargetSpatial = MakeUnique<FDynamicMeshAABBTree3>(&ProjectionTargetMeshCopy, true);
ProjectionTarget = MakeUnique<FMeshProjectionTarget>(&ProjectionTargetMeshCopy, ProjectionTargetSpatial.Get());
Remesher.SetProjectionTarget(ProjectionTarget.Get());
Remesher.ProjectionMode = FMeshRefinerBase::ETargetProjectionMode::AfterRefinement;
}
else
{
Remesher.ProjectionMode = FMeshRefinerBase::ETargetProjectionMode::NoProjection;
}
for (int k = 0; k < PostSmoothRemeshPasses; ++k)
{
if (k > 0)
{
Remesher.UpdateROI();
// If the ROI has expanded, it may include seam edges that were not constrained in the original Remesher set up
ConstrainSubregionSeams(Remesher, false);
}
Remesher.BasicRemeshPass();
}
NewTriangles = Remesher.GetCurrentTriangleROI().Array();
}
else
{
NewTriangles = TriangleSelection.AsArray();
}
}