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

470 lines
15 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "Operations/PNTriangles.h"
#include "IndexTypes.h"
#include "VectorTypes.h"
#include "DynamicMesh/MeshNormals.h"
#include "DynamicMesh/DynamicMesh3.h"
#include "Async/ParallelFor.h"
#include "Util/ProgressCancel.h"
#include "Operations/UniformTessellate.h"
using namespace UE::Geometry;
namespace FPNTrianglesLocals
{
/**
* PN Triangle is represented by a control points cage. Each edge has 2 control points and
* 1 control point is inside the triangle. Additionally, each edge contains one control point
* of the normal component. We separate control points into 2 categories to avoid repeating
* computations since control points on edges are shared.
*/
using FTriangleControlPoint = FVector3d;
struct FEdgeControlPoints
{
FVector3d Point1;
FVector3d Point2;
FVector3d NormalTriA;
FVector3d NormalTriB;
};
struct FControlPoints
{
TArray<FEdgeControlPoints> OnEdges; // Map edge ID to the edge control points
TArray<FTriangleControlPoint> OnTriangles; // Map triangle ID to the triangle center control point
};
/**
* Compute PN Triangle control points and optionally their normal component.
*
* @param Mesh The mesh used to compute the per-triangle control points.
* @param UseNormals If normals are disabled for the Mesh then pass manually computed normals.
* @param bComputePNNormals Compute control points for the normal component for calculating quadratically varying normals.
* @param ProgressCancel Set this to be able to cancel running operation.
* @param OutControlPoints Contains all the control points for the Mesh.
*
* @return true if the operation succeeded, false if it failed or was canceled by the user.
*/
bool ComputeControlPoints(FControlPoints& OutControlPoints,
const FDynamicMesh3& Mesh,
const FMeshNormals* UseNormals,
const bool bComputePNNormals,
FProgressCancel* ProgressCancel)
{
if (!Mesh.HasAttributes() && !Mesh.HasVertexNormals() && UseNormals == nullptr)
{
return false; // Mesh must have normals
}
// We know ahead of time the total number of control points:
// 2 control points per edge and one in the middle of each triangle.
OutControlPoints.OnEdges.SetNum(Mesh.MaxEdgeID());
OutControlPoints.OnTriangles.SetNum(Mesh.MaxTriangleID());
auto ComputeControlPoint = [](const FVector3d& Vertex1, const FVector3d& Vertex2, const FVector3f& Normal)
{
FVector3d Edge12 = Vertex2 - Vertex1;
double Weight12 = Edge12.Dot(FVector3d(Normal));
FVector3d Point = (2.0*Vertex1 + Vertex2 - Weight12*FVector3d(Normal))/3.0;
return Point;
};
auto ComputeControlNormal = [](const FVector3d& Vertex1, const FVector3d& Vertex2, const FVector3f& Normal1, const FVector3f& Normal2)
{
FVector3d Edge12 = Vertex2 - Vertex1;
FVector3f Normal21 = Normal2 + Normal1;
double Divisor = Edge12.Dot(Edge12);
FVector3f Normal;
if (FMath::IsNearlyZero(Divisor))
{
Normal = 0.5f * Normal21; // If edge is degenerate then simply interpolate between the normals
}
else
{
double NWeight12 = 2.0*Edge12.Dot(FVector3d(Normal21))/Divisor;
Normal = Normalized(Normal21 - NWeight12*FVector3f(Edge12));
}
return Normal;
};
// First compute only the control points on the edges
ParallelFor(Mesh.MaxEdgeID(), [&](int32 EID)
{
if (ProgressCancel && ProgressCancel->Cancelled())
{
return;
}
if (Mesh.IsEdge(EID))
{
FIndex2i EdgeV = Mesh.GetEdgeV(EID);
FVector3d Vertex1 = Mesh.GetVertex(EdgeV.A);
FVector3d Vertex2 = Mesh.GetVertex(EdgeV.B);
FIndex2i EdgeTri = Mesh.GetEdgeT(EID);
FVector3f Normal1, Normal2;
if (Mesh.HasAttributes())
{
const FDynamicMeshNormalOverlay* NormalOverlay = Mesh.Attributes()->PrimaryNormals();
if (NormalOverlay->IsSetTriangle(EdgeTri.A))
{
NormalOverlay->GetElementAtVertex(EdgeTri.A, EdgeV.A, Normal1);
NormalOverlay->GetElementAtVertex(EdgeTri.A, EdgeV.B, Normal2);
}
else
{
Normal1 = static_cast<FVector3f>(Mesh.GetTriNormal(EdgeTri.A));
Normal2 = Normal1;
}
}
else
{
Normal1 = (UseNormals != nullptr) ? (FVector3f)(*UseNormals)[EdgeV.A] : Mesh.GetVertexNormal(EdgeV.A);
Normal2 = (UseNormals != nullptr) ? (FVector3f)(*UseNormals)[EdgeV.B] : Mesh.GetVertexNormal(EdgeV.B);
}
FEdgeControlPoints& ControlPnts = OutControlPoints.OnEdges[EID];
if (bComputePNNormals)
{
ControlPnts.NormalTriA = FVector3d(ComputeControlNormal(Vertex1, Vertex2, Normal1, Normal2));
}
FVector3d Point1ForTriA = ComputeControlPoint(Vertex1, Vertex2, Normal1);
FVector3d Point2ForTriA = ComputeControlPoint(Vertex2, Vertex1, Normal2);
if (Mesh.HasAttributes() && Mesh.Attributes()->PrimaryNormals())
{
const FDynamicMeshNormalOverlay* NormalOverlay = Mesh.Attributes()->PrimaryNormals();
if (EdgeTri.B != FDynamicMesh3::InvalidID && NormalOverlay->IsSeamEdge(EID))
{
if (NormalOverlay->IsSetTriangle(EdgeTri.B))
{
NormalOverlay->GetElementAtVertex(EdgeTri.B, EdgeV.A, Normal1);
NormalOverlay->GetElementAtVertex(EdgeTri.B, EdgeV.B, Normal2);
}
else
{
Normal1 = static_cast<FVector3f>(Mesh.GetTriNormal(EdgeTri.B));
Normal2 = Normal1;
}
FVector3d Point1ForTriB = ComputeControlPoint(Vertex1, Vertex2, Normal1);
FVector3d Point2ForTriB = ComputeControlPoint(Vertex2, Vertex1, Normal2);
// Following the core idea of Crack-free PN Triangles we average the control point we'd like
// with the control point our neighbor triangle would like.
Point1ForTriA = (Point1ForTriA + Point1ForTriB) / 2.0;
Point2ForTriA = (Point2ForTriA + Point2ForTriB) / 2.0;
if (bComputePNNormals)
{
ControlPnts.NormalTriB = FVector3d(ComputeControlNormal(Vertex1, Vertex2, Normal1, Normal2));
}
}
}
ControlPnts.Point1 = Point1ForTriA;
ControlPnts.Point2 = Point2ForTriA;
}
});
if (ProgressCancel && ProgressCancel->Cancelled())
{
return false;
}
// Compute the center control point for each triangle
ParallelFor(Mesh.MaxTriangleID(), [&](int32 TID)
{
if (ProgressCancel && ProgressCancel->Cancelled())
{
return;
}
if (Mesh.IsTriangle(TID))
{
const FIndex3i& TriEdgeID = Mesh.GetTriEdgesRef(TID);
FVector3d ControlMidpoint = FVector3d::Zero();
for (int EIdx = 0; EIdx < 3; ++EIdx)
{
const FEdgeControlPoints& ControlPnts = OutControlPoints.OnEdges[TriEdgeID[EIdx]];
ControlMidpoint += ControlPnts.Point1 + ControlPnts.Point2;
}
ControlMidpoint /= 6.0;
const FIndex3i& TriVertID = Mesh.GetTriangleRef(TID);
FVector3d VertexMidpoint = (Mesh.GetVertex(TriVertID.A) +
Mesh.GetVertex(TriVertID.B) +
Mesh.GetVertex(TriVertID.C))/3.0;
OutControlPoints.OnTriangles[TID] = ControlMidpoint + (ControlMidpoint - VertexMidpoint)/2.0;
}
});
if (ProgressCancel && ProgressCancel->Cancelled())
{
return false;
}
return true;
}
/**
* Tessellate the mesh by recursively subdividing it using loop-style subdivision. Keep track of the new vertices
* added and which original triangles (before tessellation) they belong to. New vertices on original non-boundary
* edges can belong to either of the original triangles that share the edge.
*
* @param Mesh The mesh we are tessellating.
* @param Level How many times we are recursively subdividing the Mesh.
* @param ProgressCancel Set this to be able to cancel running operation.
* @param OutNewVertices Array of tuples of the new vertex ID and the original triangle ID the vertex belongs to.
* @param OutMesh Result of the tessellation.
*
* @return true if the operation succeeded, false if it failed or was canceled by the user.
*/
bool TessellateMesh(const FDynamicMesh3& Mesh,
const int32 Level,
FProgressCancel* ProgressCancel,
TArray<FIndex2i>& OutNewVertices,
FDynamicMesh3& OutMesh)
{
checkSlow(Level >= 0);
if (Level < 0)
{
return false;
}
else if (Level == 0)
{
return true; // nothing to do
}
FUniformTessellate Tessellator(&Mesh, &OutMesh);
Tessellator.TessellationNum = Level;
Tessellator.bComputeMappings = true;
if (Tessellator.Validate() != EOperationValidationResult::Ok)
{
return false;
}
if (Tessellator.Compute() == false)
{
return false;
}
const int32 NewVertCount = Tessellator.VertexEdgeMap.Num() + Tessellator.VertexTriangleMap.Num();
OutNewVertices.Reserve(NewVertCount);
for (auto It = Tessellator.VertexEdgeMap.CreateConstIterator(); It; ++It)
{
FIndex2i EdgeTri = It.Value();
OutNewVertices.Add(FIndex2i(It.Key(), EdgeTri.A));
}
for (auto It = Tessellator.VertexTriangleMap.CreateConstIterator(); It; ++It)
{
OutNewVertices.Add(FIndex2i(It.Key(), It.Value()));
}
checkSlow(OutNewVertices.Num() == NewVertCount);
return true;
}
/**
* Displace the vertices created from the tessellation using the cubic patch formula based on their barycentric
* coordinates.
*
* @param OriginalMesh The original mesh (before tessellation).
* @param FControlPoints PN Triangle control points.
* @param VerticesToDisplace Array of vertices into the Mesh that we are displacing.
* @param bComputePNNormals Should we be computing quadratically varying normals using control points.
* @param ProgressCancel Set this to be able to cancel running operation.
* @param Mesh The tessellated mesh whose vertices we are displacing.
*
* @return true if the operation succeeded, false if it failed or was canceled by the user.
*/
bool DisplaceAndSetQuadraticNormals(const FDynamicMesh3& OriginalMesh,
const FControlPoints& FControlPoints,
const TArray<FIndex2i>& VerticesToDisplace,
const bool bComputePNNormals,
FProgressCancel* ProgressCancel,
FDynamicMesh3& Mesh)
{
bool bHasVertexNormals = Mesh.HasVertexNormals();
bool bHasAttributes = Mesh.HasAttributes();
// Iterate over every new vertex and compute its displacement and optionally a normal
ParallelFor(VerticesToDisplace.Num(), [&](int32 IDX)
{
if (ProgressCancel && ProgressCancel->Cancelled())
{
return;
}
FIndex2i NewVtx = VerticesToDisplace[IDX];
int VertexID = NewVtx[0]; // ID of the new vertex added with tessellation
int OriginalTriangleID = NewVtx[1]; // ID of the original triangle new vertex belongs to
// Get the topology information of the original triangle
FIndex3i TriVertex = OriginalMesh.GetTriangle(OriginalTriangleID);
FIndex3i TriEdges = OriginalMesh.GetTriEdges(OriginalTriangleID);
FVector3d Bary = VectorUtil::BarycentricCoords(Mesh.GetVertexRef(VertexID), OriginalMesh.GetVertexRef(TriVertex.A),
OriginalMesh.GetVertexRef(TriVertex.B), OriginalMesh.GetVertexRef(TriVertex.C));
FVector3d BarySquared = Bary*Bary;
// Displaced vertex. First compute contribution of the original control points at the original vertices
FVector3d NewPos = Bary[0]*BarySquared[0]*OriginalMesh.GetVertexRef(TriVertex.A) +
Bary[1]*BarySquared[1]*OriginalMesh.GetVertexRef(TriVertex.B) +
Bary[2]*BarySquared[2]*OriginalMesh.GetVertexRef(TriVertex.C);
// Compute contribution of the control points at the edges
for (int EIDX = 0; EIDX < 3; ++EIDX)
{
int EID = TriEdges[EIDX];
FIndex2i EdgeV = OriginalMesh.GetEdgeRef(EID).Vert;
const FEdgeControlPoints& ControlPnts = FControlPoints.OnEdges[EID];
// Check that the orientation of the edge is consistent so we use the correct barycentric values
int EdgeVtx1 = EdgeV[0];
int EdgeVtx2 = EdgeV[1];
IndexUtil::OrientTriEdgeAndFindOtherVtx(EdgeVtx1, EdgeVtx2, TriVertex);
int BaryIdx1 = EdgeV[0] == EdgeVtx1 ? EIDX : (EIDX + 1) % 3;
int BaryIdx2 = EdgeV[0] == EdgeVtx1 ? (EIDX + 1) % 3 : EIDX;
NewPos += (3.0*BarySquared[BaryIdx1]*Bary[BaryIdx2])*ControlPnts.Point1 +
(3.0*BarySquared[BaryIdx2]*Bary[BaryIdx1])*ControlPnts.Point2;
}
// Finally compute contribution of the single control point inside the triangle
NewPos += (6.0*Bary[0]*Bary[1]*Bary[2])*FControlPoints.OnTriangles[OriginalTriangleID];
Mesh.SetVertex(VertexID, NewPos);
if (bComputePNNormals)
{
if (bHasVertexNormals)
{
// Compute contribution of the normal control points at the original vertices
FVector3d NewNormal(BarySquared[0]*OriginalMesh.GetVertexNormal(TriVertex.A) +
BarySquared[1]*OriginalMesh.GetVertexNormal(TriVertex.B) +
BarySquared[2]*OriginalMesh.GetVertexNormal(TriVertex.C));
// Compute contribution of the normal control points at the edges
for (int EIDX = 0; EIDX < 3; ++EIDX)
{
int EID = TriEdges[EIDX];
const FEdgeControlPoints& ControlPnts = FControlPoints.OnEdges[EID];
NewNormal += Bary[EIDX]*Bary[(EIDX + 1) % 3]*ControlPnts.NormalTriA;
}
Normalize(NewNormal);
Mesh.SetVertexNormal(VertexID, FVector3f(NewNormal));
}
if (bHasAttributes)
{
//TODO: Compute per-element quadractic PN normals
}
}
});
if (ProgressCancel && ProgressCancel->Cancelled())
{
return false;
}
return true;
}
}
bool FPNTriangles::Compute()
{
using namespace FPNTrianglesLocals;
check(TessellationLevel >= 0);
check(Mesh != nullptr);
if (TessellationLevel < 0 || Mesh == nullptr)
{
return false;
}
if (TessellationLevel == 0)
{
return true; // nothing to do
}
// Compute per-vertex normals if no normals exist
FMeshNormals Normals;
bool bHasNormals = Mesh->HasVertexNormals() || (Mesh->HasAttributes() && Mesh->Attributes()->PrimaryNormals() != nullptr);
if (bHasNormals == false)
{
Normals = FMeshNormals(Mesh);
Normals.ComputeVertexNormals();
}
FMeshNormals* UseNormals = (bHasNormals) ? nullptr : &Normals;
// Compute PN triangle control points for each flat triangle of the original mesh
FControlPoints FControlPoints;
// TODO: We disable quadratically varying normal computation until we fully support seam normal overlay edges.
// We might remove them completely since using quadratically varying normals would imply that we should be using
// the same approach when computing normals inside the shaders for lighting computations
// (see Tessellation on Any Budget, GDC, 2011).
const bool bComputePNNormals = false;
bool bOk = ComputeControlPoints(FControlPoints, *Mesh, UseNormals, bComputePNNormals, Progress);
if (bOk == false)
{
return false;
}
// Tessellate the original mesh
TArray<FIndex2i> NewVertices;
FDynamicMesh3 ResultMesh;
bOk = TessellateMesh(*Mesh, TessellationLevel, Progress, NewVertices, ResultMesh);
if (bOk == false)
{
return false;
}
// Compute displacement and optionally quadratically varying normals
bOk = DisplaceAndSetQuadraticNormals(*Mesh, FControlPoints, NewVertices, bComputePNNormals, Progress, ResultMesh);
if (bOk == false)
{
return false;
}
if (bRecalculateNormals && bComputePNNormals == false)
{
if (ResultMesh.HasAttributes())
{
FMeshNormals NewNormals(&ResultMesh);
FDynamicMeshNormalOverlay* NormalOverlay = ResultMesh.Attributes()->PrimaryNormals();
NewNormals.RecomputeOverlayNormals(NormalOverlay);
NewNormals.CopyToOverlay(NormalOverlay);
}
else if (ResultMesh.HasVertexNormals())
{
FMeshNormals::QuickComputeVertexNormals(ResultMesh);
}
}
Mesh->Copy(ResultMesh);
return true;
}