Files
UnrealEngine/Engine/Plugins/Runtime/MeshModelingToolset/Source/ModelingComponents/Private/ConversionUtils/VolumeToDynamicMesh.cpp
2025-05-18 13:04:45 +08:00

242 lines
7.2 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "ConversionUtils/VolumeToDynamicMesh.h"
#include "CompGeom/PolygonTriangulation.h"
#include "ConstrainedDelaunay2.h"
#include "Polygon2.h"
#include "DynamicMesh/DynamicMesh3.h"
#include "DynamicMesh/MeshNormals.h"
#include "DynamicMesh/MeshTransforms.h"
#include "GameFramework/Volume.h"
#include "MeshBoundaryLoops.h"
#include "MeshQueries.h"
#include "MeshRegionBoundaryLoops.h"
#include "Model.h"
#include "DynamicMesh/Operations/MergeCoincidentMeshEdges.h"
#include "Operations/MinimalHoleFiller.h"
#include "Operations/PlanarFlipsOptimization.h"
#include "Components/BrushComponent.h"
#if WITH_EDITOR
#include "Engine/Polys.h"
#endif
using namespace UE::Geometry;
namespace VolumeToDynamicMeshLocals
{
/** Attempts to use delaunay triangulation to triangulate the polygon, and falls back to ear clipping if that fails. */
TArray<FIndex3i> TriangulatePolygon(const FPolygon2d& ToTriangulate)
{
// This first part is equivalent to calling ConstrainedDelaunayTriangulate<double>(ToTriangulate), except
// it allows us to properly check for failure.
TConstrainedDelaunay2<double> DelaunayTriangulation;
DelaunayTriangulation.FillRule = TConstrainedDelaunay2<double>::EFillRule::Positive;
DelaunayTriangulation.Add(ToTriangulate);
if (DelaunayTriangulation.Triangulate() && DelaunayTriangulation.Triangles.Num() > 0)
{
return DelaunayTriangulation.Triangles;
}
// If delaunay triangulation fails, use ear clipping.
TArray<FIndex3i> TrianglesOut;
PolygonTriangulation::TriangulateSimplePolygon(ToTriangulate.GetVertices(), TrianglesOut);
return TrianglesOut;
}
}
namespace UE {
namespace Conversion {
void VolumeToDynamicMesh(AVolume* Volume, FDynamicMesh3& Mesh,
const FVolumeToMeshOptions& Options)
{
UModel* Model = Volume->Brush;
if (Model == nullptr) // model is null in some cases, eg default physics volume
{
return;
}
BrushToDynamicMesh(*Model, Mesh, Options);
if (Options.bInWorldSpace)
{
FTransform3d XForm(Volume->GetTransform());
MeshTransforms::ApplyTransform(Mesh, XForm, true);
}
}
void BrushComponentToDynamicMesh(UBrushComponent* Component, UE::Geometry::FDynamicMesh3& Mesh, const FVolumeToMeshOptions& Options)
{
UModel* Model = Component->Brush;
if (Model == nullptr) // model is null in some cases, eg default physics volume
{
return;
}
BrushToDynamicMesh(*Model, Mesh, Options);
if (Options.bInWorldSpace)
{
FTransform3d XForm(Component->GetComponentTransform());
MeshTransforms::ApplyTransform(Mesh, XForm, true);
}
}
void BrushToDynamicMesh(UModel& BrushModel, UE::Geometry::FDynamicMesh3& Mesh, const FVolumeToMeshOptions& Options)
{
using namespace VolumeToDynamicMeshLocals;
Mesh.Clear();
// Note: Groups are also used to compute normals matching the input polygons
// If normals are requested but groups are not, groups will be discarded after normals are computed
if (Options.bSetGroups || Options.bGenerateNormals)
{
Mesh.EnableTriangleGroups();
}
#if WITH_EDITOR
// In the editor, the preferred source of geometry for a volume is the Polys array,
// which the bsp nodes are generated from, because the polys may be broken up into
// pieces unnecessarily as bsp nodes.
// Polys are planar polygons.
// We do not try to merge any vertices yet.
const TArray<FPoly>& Polygons = BrushModel.Polys->Element;
for (FPoly Poly : Polygons)
{
int32 NumVerts = Poly.Vertices.Num();
if (NumVerts < 3)
{
continue;
}
FVector3d Normal = (FVector3d)Poly.Normal;
FFrame3d Plane((FVector3d)Poly.Base, Normal);
TArray<int32> Vids;
FPolygon2d ToTriangulate;
Vids.SetNum(NumVerts);
for (int32 VertexIndex = 0; VertexIndex < NumVerts; ++VertexIndex)
{
FVector3d Point = (FVector3d)Poly.Vertices[VertexIndex];
Vids[VertexIndex] = Mesh.AppendVertex(Point);
ToTriangulate.AppendVertex(Plane.ToPlaneUV(Point, 2));
}
// Note that this call gives triangles with the reverse orientation compared to the
// polygon, but the polygons we get are oriented opposite of what we want (they
// are clockwise if the normal is towards us), so this ends up giving us the triangle
// orientation that we want.
TArray<FIndex3i> PolyTriangles = TriangulatePolygon(ToTriangulate);
int32 GroupID = FDynamicMesh3::InvalidID;
if (Options.bSetGroups)
{
GroupID = Mesh.AllocateTriangleGroup();
}
for (FIndex3i Tri : PolyTriangles)
{
Mesh.AppendTriangle(Vids[Tri.A], Vids[Tri.B], Vids[Tri.C], GroupID);
}
}
#else
// Each "BspNode" is a planar polygon, triangulate each polygon and accumulate in a mesh.
// Note that this does not make any attempt to weld vertices/edges
for (const FBspNode& Node : BrushModel.Nodes)
{
FVector3d Normal = (FVector3d)Node.Plane;
FFrame3d Plane(Node.Plane.W * Normal, Normal);
int32 NumVerts = (Node.NodeFlags & PF_TwoSided) ? Node.NumVertices / 2 : Node.NumVertices; // ??
if (NumVerts > 0)
{
TArray<int32> Vids;
FPolygon2d ToTriangulate;
Vids.SetNum(NumVerts);
for (int32 VertexIndex = 0; VertexIndex < NumVerts; ++VertexIndex)
{
const FVert& Vert = BrushModel.Verts[Node.iVertPool + VertexIndex];
FVector3d Point = (FVector3d)BrushModel.Points[Vert.pVertex];
Vids[VertexIndex] = Mesh.AppendVertex(Point);
ToTriangulate.AppendVertex(Plane.ToPlaneUV(Point, 2));
}
// Note that this call gives triangles with the reverse orientation compared to the
// polygon, but the polygons we get are oriented opposite of what we want (they
// are clockwise if the normal is towards us), so this ends up giving us the triangle
// orientation that we want.
TArray<FIndex3i> PolyTriangles = TriangulatePolygon(ToTriangulate);
int32 GroupID = FDynamicMesh3::InvalidID;
if (Options.bSetGroups)
{
GroupID = Mesh.AllocateTriangleGroup();
}
for (FIndex3i Tri : PolyTriangles)
{
Mesh.AppendTriangle(Vids[Tri.A], Vids[Tri.B], Vids[Tri.C], GroupID);
}
}
}
#endif
if (Options.bMergeVertices)
{
// Merge the mesh edges to create a closed solid
double MinLen, MaxLen, AvgLen;
TMeshQueries<FDynamicMesh3>::EdgeLengthStats(Mesh, MinLen, MaxLen, AvgLen);
FMergeCoincidentMeshEdges Merge(&Mesh);
Merge.MergeVertexTolerance = FMathd::Max(Merge.MergeVertexTolerance, MinLen * 0.1);
Merge.Apply();
// If the mesh is not closed, the merge failed or the volume had cracks/holes.
// Do trivial hole fills to ensure the output is solid (really want autorepair here)
if (Mesh.IsClosed() == false && Options.bAutoRepairMesh)
{
FMeshBoundaryLoops BoundaryLoops(&Mesh, true);
for (FEdgeLoop& Loop : BoundaryLoops.Loops)
{
FMinimalHoleFiller Filler(&Mesh, Loop);
Filler.Fill();
}
}
// try to flip towards better triangles in planar areas, should reduce/remove degenerate geo
if (Options.bOptimizeMesh)
{
FPlanarFlipsOptimization(&Mesh, 5).Apply(); // Do five passes
}
Mesh.CompactInPlace();
}
if (Options.bGenerateNormals)
{
Mesh.EnableAttributes();
FDynamicMeshNormalOverlay* Normals = Mesh.Attributes()->PrimaryNormals();
FMeshNormals::InitializeOverlayTopologyFromFaceGroups(&Mesh, Normals);
FMeshNormals::QuickRecomputeOverlayNormals(Mesh);
if (!Options.bSetGroups)
{
Mesh.DiscardTriangleGroups();
}
}
}
}}//end namespace UE::Conversion