Files
UnrealEngine/Engine/Plugins/Experimental/PlanarCutPlugin/Source/PlanarCut/Private/GeometryMeshConversion.cpp
2025-05-18 13:04:45 +08:00

3496 lines
120 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "GeometryMeshConversion.h"
#include "Curve/GeneralPolygon2.h"
#include "PlanarCut.h"
#include "Distance/DistLine3Segment3.h"
#include "PlanarCutPlugin.h"
#include "Distance/DistLine3Triangle3.h"
#include "Spatial/MeshSpatialSort.h"
#include "Distance/DistSegment3Triangle3.h"
#include "VertexConnectedComponents.h"
#include "CompGeom/PolygonTriangulation.h"
#include "GeometryCollection/GeometryCollectionAlgo.h"
#include "DisjointSet.h"
#include "DynamicMeshEditor.h"
#include "DynamicMesh/DynamicMeshAABBTree3.h"
#include "Selections/MeshConnectedComponents.h"
#include "DynamicMesh/MeshTransforms.h"
#include "Operations/MeshBoolean.h"
#include "Operations/MeshSelfUnion.h"
#include "DynamicMesh/Operations/MergeCoincidentMeshEdges.h"
#include "MeshBoundaryLoops.h"
#include "QueueRemesher.h"
#include "DynamicMesh/MeshNormals.h"
#include "DynamicMesh/MeshTangents.h"
#include "ConstrainedDelaunay2.h"
#include "Algo/Rotate.h"
#if defined(_MSC_VER) && USING_CODE_ANALYSIS
#pragma warning(push)
#pragma warning(disable : 6011)
#pragma warning(disable : 6387)
#pragma warning(disable : 6313)
#pragma warning(disable : 6294)
#endif
PRAGMA_DEFAULT_VISIBILITY_START
THIRD_PARTY_INCLUDES_START
#include <Eigen/Sparse>
#include <Eigen/Core>
#include <Eigen/SparseLU>
#include <Eigen/OrderingMethods>
#include <Eigen/Dense>
THIRD_PARTY_INCLUDES_END
PRAGMA_DEFAULT_VISIBILITY_END
#if defined(_MSC_VER) && USING_CODE_ANALYSIS
#pragma warning(pop)
#endif
#include <vector> // for Eigen sparse matrix construction
using namespace UE::Geometry;
namespace UE
{
namespace PlanarCut
{
// functions to setup geometry collection attributes on dynamic meshes
namespace AugmentedDynamicMesh
{
// An invalid color, to be replaced by neighboring valid colors (or DefaultVertexColor, if neighboring colors were not found)
// Use a large negative value as a clear unset / invalid value, but do not go all the way to -MaxReal (or overflow will break things)
const static FVector4f UnsetVertexColor = FVector4f(-FMathf::MaxReal * .25f, -FMathf::MaxReal * .25f, -FMathf::MaxReal * .25f, -FMathf::MaxReal * .25f);
const static FVector4f DefaultVertexColor = FVector4f(0, 0, 0, 1);
FName ColorAttribName = "ColorAttrib";
FName TangentUAttribName = "TangentUAttrib";
FName TangentVAttribName = "TangentVAttrib";
FName VisibleAttribName = "VisibleAttrib";
FName InternalAttribName = "InternalAttrib";
enum
{
MAX_NUM_UV_CHANNELS = 8,
};
FName UVChannelNames[MAX_NUM_UV_CHANNELS] = {
"UVAttrib0",
"UVAttrib1",
"UVAttrib2",
"UVAttrib3",
"UVAttrib4",
"UVAttrib5",
"UVAttrib6",
"UVAttrib7"
};
void EnableUVChannels(FDynamicMesh3& Mesh, int32 NumUVChannels, bool bResetExisting = false, bool bDisablePrevious = true)
{
Mesh.EnableAttributes();
if (!ensure(NumUVChannels <= MAX_NUM_UV_CHANNELS))
{
NumUVChannels = MAX_NUM_UV_CHANNELS;
}
for (int32 UVIdx = 0; UVIdx < NumUVChannels; UVIdx++)
{
if (!bResetExisting && Mesh.Attributes()->HasAttachedAttribute(UVChannelNames[UVIdx]))
{
continue;
}
Mesh.Attributes()->AttachAttribute(UVChannelNames[UVIdx], new TDynamicMeshVertexAttribute<float, 2>(&Mesh));
}
if (bDisablePrevious)
{
for (int32 UVIdx = NumUVChannels; UVIdx < MAX_NUM_UV_CHANNELS; UVIdx++)
{
Mesh.Attributes()->RemoveAttribute(UVChannelNames[UVIdx]);
}
}
}
int32 NumEnabledUVChannels(FDynamicMesh3& Mesh)
{
if (!Mesh.Attributes())
{
return 0;
}
for (int32 UVIdx = 0; UVIdx < MAX_NUM_UV_CHANNELS; UVIdx++)
{
if (!Mesh.Attributes()->HasAttachedAttribute(UVChannelNames[UVIdx]))
{
return UVIdx;
}
}
return MAX_NUM_UV_CHANNELS;
}
void Augment(FDynamicMesh3& Mesh, int32 NumUVChannels)
{
Mesh.EnableVertexNormals(FVector3f::UnitZ());
Mesh.EnableAttributes();
Mesh.Attributes()->EnableMaterialID();
Mesh.Attributes()->AttachAttribute(ColorAttribName, new TDynamicMeshVertexAttribute<float, 4>(&Mesh));
Mesh.Attributes()->AttachAttribute(TangentUAttribName, new TDynamicMeshVertexAttribute<float, 3>(&Mesh));
Mesh.Attributes()->AttachAttribute(TangentVAttribName, new TDynamicMeshVertexAttribute<float, 3>(&Mesh));
TDynamicMeshScalarTriangleAttribute<bool>* VisAttrib = new TDynamicMeshScalarTriangleAttribute<bool>(&Mesh);
VisAttrib->Initialize(true);
Mesh.Attributes()->AttachAttribute(VisibleAttribName, VisAttrib);
TDynamicMeshScalarTriangleAttribute<bool>* InternalAttrib = new TDynamicMeshScalarTriangleAttribute<bool>(&Mesh);
InternalAttrib->Initialize(true);
Mesh.Attributes()->AttachAttribute(InternalAttribName, InternalAttrib);
EnableUVChannels(Mesh, NumUVChannels);
}
void AddVertexColorAttribute(FDynamicMesh3& Mesh)
{
Mesh.Attributes()->AttachAttribute(ColorAttribName, new TDynamicMeshVertexAttribute<float, 4>(&Mesh));
}
bool IsAugmented(const FDynamicMesh3& Mesh)
{
return Mesh.HasAttributes()
&& Mesh.Attributes()->HasAttachedAttribute(ColorAttribName)
&& Mesh.Attributes()->HasAttachedAttribute(TangentUAttribName)
&& Mesh.Attributes()->HasAttachedAttribute(TangentVAttribName)
&& Mesh.Attributes()->HasAttachedAttribute(VisibleAttribName)
&& Mesh.Attributes()->HasMaterialID()
&& Mesh.HasVertexNormals();
}
void SetDefaultAttributes(FDynamicMesh3& Mesh, bool bGlobalVisibility)
{
checkSlow(IsAugmented(Mesh));
TDynamicMeshVertexAttribute<float, 4>* Colors =
static_cast<TDynamicMeshVertexAttribute<float, 4>*>(Mesh.Attributes()->GetAttachedAttribute(ColorAttribName));
TDynamicMeshVertexAttribute<float, 3>* Us =
static_cast<TDynamicMeshVertexAttribute<float, 3>*>(Mesh.Attributes()->GetAttachedAttribute(TangentUAttribName));
TDynamicMeshVertexAttribute<float, 3>* Vs =
static_cast<TDynamicMeshVertexAttribute<float, 3>*>(Mesh.Attributes()->GetAttachedAttribute(TangentVAttribName));
for (int VID : Mesh.VertexIndicesItr())
{
FVector3f N = Mesh.GetVertexNormal(VID);
FVector3f U, V;
VectorUtil::MakePerpVectors(N, U, V);
Us->SetValue(VID, U);
Vs->SetValue(VID, V);
Colors->SetValue(VID, UnsetVertexColor);
}
TDynamicMeshScalarTriangleAttribute<bool>* Visible =
static_cast<TDynamicMeshScalarTriangleAttribute<bool>*>(Mesh.Attributes()->GetAttachedAttribute(VisibleAttribName));
for (int TID : Mesh.TriangleIndicesItr())
{
Visible->SetNewValue(TID, bGlobalVisibility);
}
}
void SetVisibility(FDynamicMesh3& Mesh, int TID, bool bIsVisible)
{
checkSlow(IsAugmented(Mesh));
TDynamicMeshScalarTriangleAttribute<bool>* Visible =
static_cast<TDynamicMeshScalarTriangleAttribute<bool>*>(Mesh.Attributes()->GetAttachedAttribute(VisibleAttribName));
Visible->SetValue(TID, bIsVisible);
}
bool GetVisibility(const FDynamicMesh3& Mesh, int TID)
{
checkSlow(IsAugmented(Mesh));
const TDynamicMeshScalarTriangleAttribute<bool>* Visible =
static_cast<const TDynamicMeshScalarTriangleAttribute<bool>*>(Mesh.Attributes()->GetAttachedAttribute(VisibleAttribName));
return Visible->GetValue(TID);
}
void SetInternal(FDynamicMesh3& Mesh, int TID, bool bIsInternal)
{
checkSlow(IsAugmented(Mesh));
TDynamicMeshScalarTriangleAttribute<bool>* Internal =
static_cast<TDynamicMeshScalarTriangleAttribute<bool>*>(Mesh.Attributes()->GetAttachedAttribute(InternalAttribName));
Internal->SetValue(TID, bIsInternal);
}
bool GetInternal(const FDynamicMesh3& Mesh, int TID)
{
checkSlow(IsAugmented(Mesh));
const TDynamicMeshScalarTriangleAttribute<bool>* Internal =
static_cast<const TDynamicMeshScalarTriangleAttribute<bool>*>(Mesh.Attributes()->GetAttachedAttribute(InternalAttribName));
return Internal->GetValue(TID);
}
void SetUV(FDynamicMesh3& Mesh, int VID, FVector2f UV, int UVLayer)
{
if (!ensure(UVLayer < MAX_NUM_UV_CHANNELS))
{
return;
}
TDynamicMeshVertexAttribute<float, 2>* UVs =
static_cast<TDynamicMeshVertexAttribute<float, 2>*>(Mesh.Attributes()->GetAttachedAttribute(UVChannelNames[UVLayer]));
if (ensure(UVs))
{
UVs->SetValue(VID, UV);
}
}
void SetAllUV(FDynamicMesh3& Mesh, int VID, FVector2f UV, int NumUVLayers)
{
if (!ensure(NumUVLayers <= MAX_NUM_UV_CHANNELS))
{
NumUVLayers = MAX_NUM_UV_CHANNELS;
}
for (int32 Layer = 0; Layer < NumUVLayers; Layer++)
{
TDynamicMeshVertexAttribute<float, 2>* UVs =
static_cast<TDynamicMeshVertexAttribute<float, 2>*>(Mesh.Attributes()->GetAttachedAttribute(UVChannelNames[Layer]));
if (ensure(UVs))
{
UVs->SetValue(VID, UV);
}
}
}
void GetUV(const FDynamicMesh3& Mesh, int VID, FVector2f& UV, int UVLayer)
{
if (!ensure(UVLayer < MAX_NUM_UV_CHANNELS))
{
UV = FVector2f::Zero();
return;
}
const TDynamicMeshVertexAttribute<float, 2>* UVs =
static_cast<const TDynamicMeshVertexAttribute<float, 2>*>(Mesh.Attributes()->GetAttachedAttribute(UVChannelNames[UVLayer]));
if (ensure(UVs))
{
UVs->GetValue(VID, UV);
}
}
void SetTangent(FDynamicMesh3& Mesh, int VID, FVector3f Normal, FVector3f TangentU, FVector3f TangentV)
{
checkSlow(IsAugmented(Mesh));
TDynamicMeshVertexAttribute<float, 3>* Us =
static_cast<TDynamicMeshVertexAttribute<float, 3>*>(Mesh.Attributes()->GetAttachedAttribute(TangentUAttribName));
TDynamicMeshVertexAttribute<float, 3>* Vs =
static_cast<TDynamicMeshVertexAttribute<float, 3>*>(Mesh.Attributes()->GetAttachedAttribute(TangentVAttribName));
Us->SetValue(VID, TangentU);
Vs->SetValue(VID, TangentV);
}
void GetTangent(const FDynamicMesh3& Mesh, int VID, FVector3f& U, FVector3f& V)
{
checkSlow(IsAugmented(Mesh));
const TDynamicMeshVertexAttribute<float, 3>* Us =
static_cast<const TDynamicMeshVertexAttribute<float, 3>*>(Mesh.Attributes()->GetAttachedAttribute(TangentUAttribName));
const TDynamicMeshVertexAttribute<float, 3>* Vs =
static_cast<const TDynamicMeshVertexAttribute<float, 3>*>(Mesh.Attributes()->GetAttachedAttribute(TangentVAttribName));
FVector3f Normal = Mesh.GetVertexNormal(VID);
Us->GetValue(VID, U);
Vs->GetValue(VID, V);
}
FVector4f GetVertexColor(const FDynamicMesh3& Mesh, int VID)
{
checkSlow(IsAugmented(Mesh));
const TDynamicMeshVertexAttribute<float, 4>* Colors =
static_cast<const TDynamicMeshVertexAttribute<float, 4>*>(Mesh.Attributes()->GetAttachedAttribute(ColorAttribName));
FVector4f Color;
Colors->GetValue(VID, Color);
return Color;
}
void SetVertexColor(FDynamicMesh3& Mesh, int VID, FVector4f Color)
{
checkSlow(IsAugmented(Mesh));
TDynamicMeshVertexAttribute<float, 4>* Colors =
static_cast<TDynamicMeshVertexAttribute<float, 4>*>(Mesh.Attributes()->GetAttachedAttribute(ColorAttribName));
Colors->SetValue(VID, Color);
}
template<typename FOverlay, typename VecType>
void SplitOverlayHelper(UE::Geometry::FDynamicMesh3& Mesh, FOverlay* Overlay)
{
TArray<int32> ContigTris, ContigGroupLens;
TArray<int32> ToSplitTris;
TArray<bool> GroupIsLoop;
auto GetEID = [&Mesh, &Overlay](int32 VID, int32 TID) -> int32
{
FIndex3i Tri = Mesh.GetTriangle(TID);
int32 SubIdx = Tri.IndexOf(VID);
check(SubIdx >= 0);
return Overlay->GetTriangle(TID)[SubIdx];
};
// get overlay element, returning a zero vector for unset elements
auto GetElement = [&Overlay](int32 EID)
{
return EID > -1 ? Overlay->GetElement(EID) : VecType(EForceInit::ForceInitToZero);
};
for (int32 VID = 0, OrigMax = Mesh.MaxVertexID(); VID < OrigMax; VID++)
{
if (!Mesh.IsVertex(VID))
{
continue;
}
Mesh.GetVtxContiguousTriangles(VID, ContigTris, ContigGroupLens, GroupIsLoop);
int32 TriStartIdx = 0;
for (int32 GroupIdx = 0; GroupIdx < ContigGroupLens.Num(); GroupIdx++)
{
int32 GroupLen = ContigGroupLens[GroupIdx];
bool bIsLoop = GroupIsLoop[GroupIdx];
checkSlow(TriStartIdx < ContigTris.Num());
int32 TID0 = ContigTris[TriStartIdx];
int32 EID0 = GetEID(VID, TID0);
VecType LastElData = GetElement(EID0);
int32 LastEID = EID0;
bool bPastEID0 = false;
ToSplitTris.Reset();
for (int32 Idx = TriStartIdx + 1, EndIdx = TriStartIdx + GroupLen; Idx < EndIdx; Idx++)
{
int32 TID = ContigTris[Idx];
int32 EID = GetEID(VID, TID);
VecType ElData;
if (EID != LastEID &&
!VectorUtil::EpsilonEqual(ElData = GetElement(EID), LastElData, FMathf::ZeroTolerance))
{
if (ToSplitTris.Num() > 0)
{
FDynamicMesh3::FVertexSplitInfo SplitInfo;
Mesh.SplitVertex(VID, ToSplitTris, SplitInfo);
ToSplitTris.Reset();
}
LastEID = EID;
LastElData = ElData;
bPastEID0 = true;
}
if (bPastEID0)
{
ToSplitTris.Add(TID);
}
}
if (bPastEID0 && ToSplitTris.Num() > 0 && (!bIsLoop || LastEID != EID0))
{
// one final group at the end
FDynamicMesh3::FVertexSplitInfo SplitInfo;
Mesh.SplitVertex(VID, ToSplitTris, SplitInfo);
}
TriStartIdx += GroupLen;
}
}
}
void SplitOverlayAttributesToPerVertex(UE::Geometry::FDynamicMesh3& Mesh, bool bSplitUVs, bool bSplitNormalsTangents)
{
if (!ensure(Mesh.HasAttributes()))
{
return;
}
int32 NumUVLayers = FMath::Min(NumEnabledUVChannels(Mesh), Mesh.Attributes()->NumUVLayers());
if (bSplitUVs)
{
for (int32 LayerIdx = 0; LayerIdx < NumUVLayers; LayerIdx++)
{
FDynamicMeshUVOverlay* UVOverlay = Mesh.Attributes()->GetUVLayer(LayerIdx);
SplitOverlayHelper<FDynamicMeshUVOverlay, FVector2f>(Mesh, UVOverlay);
}
}
if (bSplitNormalsTangents)
{
for (int32 LayerIdx = 0; LayerIdx < Mesh.Attributes()->NumNormalLayers(); LayerIdx++)
{
SplitOverlayHelper<FDynamicMeshNormalOverlay, FVector3f>(Mesh, Mesh.Attributes()->GetNormalLayer(LayerIdx));
}
}
FDynamicMeshEditor Editor(&Mesh);
FDynamicMeshEditResult EditResult;
Editor.SplitBowties(EditResult);
// copy back attributes
for (int32 LayerIdx = 0; LayerIdx < NumUVLayers; LayerIdx++)
{
FDynamicMeshUVOverlay* UVOverlay = Mesh.Attributes()->GetUVLayer(LayerIdx);
TDynamicMeshVertexAttribute<float, 2>* UVs =
static_cast<TDynamicMeshVertexAttribute<float, 2>*>(Mesh.Attributes()->GetAttachedAttribute(UVChannelNames[LayerIdx]));
for (int32 EID : UVOverlay->ElementIndicesItr())
{
int32 ParentVID = UVOverlay->GetParentVertex(EID);
if (ParentVID != INDEX_NONE)
{
UVs->SetValue(ParentVID, UVOverlay->GetElement(EID));
}
}
}
if (Mesh.Attributes()->NumNormalLayers() > 0)
{
FDynamicMeshNormalOverlay* Overlay = Mesh.Attributes()->GetNormalLayer(0);
for (int32 EID : Overlay->ElementIndicesItr())
{
int32 ParentVID = Overlay->GetParentVertex(EID);
if (ParentVID != INDEX_NONE)
{
Mesh.SetVertexNormal(ParentVID, Overlay->GetElement(EID));
}
}
}
if (Mesh.Attributes()->HasTangentSpace())
{
TDynamicMeshVertexAttribute<float, 3>* Us =
static_cast<TDynamicMeshVertexAttribute<float, 3>*>(Mesh.Attributes()->GetAttachedAttribute(TangentUAttribName));
TDynamicMeshVertexAttribute<float, 3>* Vs =
static_cast<TDynamicMeshVertexAttribute<float, 3>*>(Mesh.Attributes()->GetAttachedAttribute(TangentVAttribName));
TDynamicMeshVertexAttribute<float, 3>* Tangents[] = { Us, Vs };
for (int Idx = 1; Idx < 3; Idx++)
{
FDynamicMeshNormalOverlay* Overlay = Mesh.Attributes()->GetNormalLayer(Idx);
TDynamicMeshVertexAttribute<float, 3>* Tangent = Tangents[Idx - 1];
for (int32 EID : Overlay->ElementIndicesItr())
{
int32 ParentVID = Overlay->GetParentVertex(EID);
if (ParentVID != INDEX_NONE)
{
Tangent->SetValue(ParentVID, Overlay->GetElement(EID));
}
}
}
}
}
void InitializeOverlayToPerVertexUVs(FDynamicMesh3& Mesh, int32 NumUVLayers, int32 FirstUVLayer)
{
Mesh.Attributes()->SetNumUVLayers(NumUVLayers);
for (int UVLayer = 0; UVLayer < NumUVLayers; ++UVLayer)
{
FDynamicMeshUVOverlay* UVs = Mesh.Attributes()->GetUVLayer(UVLayer);
UVs->ClearElements();
TArray<int> VertToUVMap;
VertToUVMap.SetNumUninitialized(Mesh.MaxVertexID());
for (int VID : Mesh.VertexIndicesItr())
{
FVector2f UV;
GetUV(Mesh, VID, UV, UVLayer + FirstUVLayer);
int UVID = UVs->AppendElement(UV);
VertToUVMap[VID] = UVID;
}
for (int TID : Mesh.TriangleIndicesItr())
{
FIndex3i Tri = Mesh.GetTriangle(TID);
Tri.A = VertToUVMap[Tri.A];
Tri.B = VertToUVMap[Tri.B];
Tri.C = VertToUVMap[Tri.C];
UVs->SetTriangle(TID, Tri);
}
}
}
void InitializeOverlayToPerVertexTangents(FDynamicMesh3& Mesh)
{
Mesh.Attributes()->EnableTangents();
FDynamicMeshNormalOverlay* TangentOverlays[2] = { Mesh.Attributes()->PrimaryTangents(), Mesh.Attributes()->PrimaryBiTangents() };
TangentOverlays[0]->ClearElements();
TangentOverlays[1]->ClearElements();
TArray<int> VertToTangentMap;
VertToTangentMap.SetNumUninitialized(Mesh.MaxVertexID());
for (int VID : Mesh.VertexIndicesItr())
{
FVector3f Tangents[2];
GetTangent(Mesh, VID, Tangents[0], Tangents[1]);
int TID = TangentOverlays[0]->AppendElement(&Tangents[0].X);
int TID2 = TangentOverlays[1]->AppendElement(&Tangents[1].X);
check(TID == TID2);
VertToTangentMap[VID] = TID;
}
for (int TID : Mesh.TriangleIndicesItr())
{
FIndex3i Tri = Mesh.GetTriangle(TID);
Tri.A = VertToTangentMap[Tri.A];
Tri.B = VertToTangentMap[Tri.B];
Tri.C = VertToTangentMap[Tri.C];
TangentOverlays[0]->SetTriangle(TID, Tri);
TangentOverlays[1]->SetTriangle(TID, Tri);
}
}
void ComputeTangents(FDynamicMesh3& Mesh, bool bOnlyInternalSurfaces,
bool bRecomputeNormals, bool bMakeSharpEdges, float SharpAngleDegrees)
{
bMakeSharpEdges = bMakeSharpEdges && bRecomputeNormals; // cannot make sharp edges if normals aren't supposed to change
FDynamicMeshNormalOverlay* Normals = Mesh.Attributes()->PrimaryNormals();
FMeshNormals::InitializeOverlayToPerVertexNormals(Normals, !bRecomputeNormals || bMakeSharpEdges);
// Copy per-vertex UVs to a UV overlay, because that's what the tangents code uses
// (TODO: consider making a tangent computation path that uses vertex normals / UVs)
// only need 1 UV layer unless we're round-tripping all the attributes through the mesh
int32 NeedNumUVLayers = bMakeSharpEdges ? NumEnabledUVChannels(Mesh) : 1;
InitializeOverlayToPerVertexUVs(Mesh, NeedNumUVLayers, 0);
FDynamicMeshUVOverlay* UVs = Mesh.Attributes()->PrimaryUV();
TDynamicMeshScalarTriangleAttribute<bool>* InternalAttrib =
static_cast<TDynamicMeshScalarTriangleAttribute<bool>*>(Mesh.Attributes()->GetAttachedAttribute(AugmentedDynamicMesh::InternalAttribName));
auto ShouldUpdateInternal = [&InternalAttrib, bOnlyInternalSurfaces](int TID)
{
return !bOnlyInternalSurfaces || InternalAttrib->GetValue(TID);
};
// To update the normals topology, we need to weld and re-split the whole mesh
if (bMakeSharpEdges)
{
FDynamicMeshNormalOverlay NormalsOverlayCopy(&Mesh);
// Apply coincident edge merge so that sharp normals can be smoothed where the edge angle is below the threshold
AugmentedDynamicMesh::InitializeOverlayToPerVertexTangents(Mesh);
FMergeCoincidentMeshEdges EdgeWelder(&Mesh);
EdgeWelder.Apply();
NormalsOverlayCopy.Copy(*Normals);
// need to re-topo where the materials match WhichMaterials
double NormalDotProdThreshold = FMathd::Cos(SharpAngleDegrees * FMathd::DegToRad);
FMeshNormals FaceNormals(&Mesh);
FaceNormals.ComputeTriangleNormals();
Normals->CreateFromPredicate([&](int VID, int TA, int TB) {
bool IA = InternalAttrib->GetValue(TA), IB = InternalAttrib->GetValue(TB);
if (IA != IB)
{
return false; // always split at an internal/external face boundary
}
bool bShouldUpdateTopo = !bOnlyInternalSurfaces || (IA && IB);
if (bShouldUpdateTopo) // in the region we're updating, don't split above dot threshold
{
return FaceNormals[TA].Dot(FaceNormals[TB]) > NormalDotProdThreshold;
}
else // in the region we're not updating, keep the original connectivity
{
return NormalsOverlayCopy.AreTrianglesConnected(TA, TB);
}
}, 0);
// copy back recomputed normals to the triangles that need updating, original normals to the triangles we don't want to change
FMeshNormals RecomputedNormals(&Mesh);
RecomputedNormals.RecomputeOverlayNormals(Normals);
for (int TID : Mesh.TriangleIndicesItr())
{
FIndex3i OverlayTri = Normals->GetTriangle(TID);
if (ShouldUpdateInternal(TID))
{
for (int SubIdx = 0; SubIdx < 3; SubIdx++)
{
int ElementID = OverlayTri[SubIdx];
Normals->SetElement(ElementID, (FVector3f)RecomputedNormals[ElementID]);
}
}
else
{
FIndex3i OldOverlayTri = NormalsOverlayCopy.GetTriangle(TID);
for (int SubIdx = 0; SubIdx < 3; SubIdx++)
{
int NewElementID = OverlayTri[SubIdx];
int OldElementID = OldOverlayTri[SubIdx];
Normals->SetElement(NewElementID, NormalsOverlayCopy.GetElement(OldElementID));
}
}
}
Mesh.CompactInPlace();
// Re-split vertices with different UVs/normals/tangents and transfer attributes back (required because we weld edges above)
AugmentedDynamicMesh::SplitOverlayAttributesToPerVertex(Mesh, true, true);
}
FComputeTangentsOptions Options;
Options.bAngleWeighted = true;
Options.bAveraged = true;
FMeshTangentsf Tangents(&Mesh);
Tangents.ComputeTriVertexTangents(Normals, UVs, Options);
const TArray<FVector3f>& TanU = Tangents.GetTangents();
const TArray<FVector3f>& TanV = Tangents.GetBitangents();
for (int TID : Mesh.TriangleIndicesItr())
{
if (!ShouldUpdateInternal(TID))
{
continue;
}
int TanIdxBase = TID * 3;
FIndex3i Tri = Mesh.GetTriangle(TID);
FIndex3i NormalElTri;
if (!bMakeSharpEdges) // if sharp edges, normals were already copied to the vertices, above
{
NormalElTri = Normals->GetTriangle(TID);
}
for (int Idx = 0; Idx < 3; Idx++)
{
int VID = Tri[Idx];
int TanIdx = TanIdxBase + Idx;
FVector3f VertexNormal = bMakeSharpEdges ? Mesh.GetVertexNormal(VID) : Normals->GetElement(NormalElTri[Idx]);
if (!bMakeSharpEdges && bRecomputeNormals)
{
Mesh.SetVertexNormal(VID, VertexNormal);
}
SetTangent(Mesh, VID, VertexNormal, TanU[TanIdx], TanV[TanIdx]);
}
}
}
// per component sampling is a rough heuristic to avoid doing geodesic distance but still get points on a 'thin' slice
void AddCollisionSamplesPerComponent(FDynamicMesh3& Mesh, double Spacing)
{
checkSlow(IsAugmented(Mesh));
FMeshConnectedComponents Components(&Mesh);
// TODO: if/when we switch to merged edges representation, pass a predicate here based on whether there's a normal seam ?
Components.FindConnectedTriangles();
TArray<TPointHashGrid3d<int>> KnownSamples; KnownSamples.Reserve(Components.Num());
for (int ComponentIdx = 0; ComponentIdx < Components.Num(); ComponentIdx++)
{
KnownSamples.Emplace(.5 * Spacing / FMathd::InvSqrt3, -1);
}
TArray<int> AlreadySeen; AlreadySeen.Init(-1, Mesh.MaxVertexID());
for (int ComponentIdx = 0; ComponentIdx < Components.Num(); ComponentIdx++)
{
FMeshConnectedComponents::FComponent& Component = Components.GetComponent(ComponentIdx);
for (int TID : Component.Indices)
{
FIndex3i Tri = Mesh.GetTriangle(TID);
for (int SubIdx = 0; SubIdx < 3; SubIdx++)
{
int VID = Tri[SubIdx];
if (AlreadySeen[VID] != ComponentIdx)
{
AlreadySeen[VID] = ComponentIdx;
KnownSamples[ComponentIdx].InsertPointUnsafe(VID, Mesh.GetVertex(VID));
}
}
}
}
AlreadySeen.Empty();
double SpacingThreshSq = Spacing * Spacing; // if points are more than Spacing apart, consider adding a new point between them
for (int ComponentIdx = 0; ComponentIdx < Components.Num(); ComponentIdx++)
{
FMeshConnectedComponents::FComponent& Component = Components.GetComponent(ComponentIdx);
for (int TID : Component.Indices)
{
FIndex3i TriVIDs = Mesh.GetTriangle(TID);
FTriangle3d Triangle;
Mesh.GetTriVertices(TID, Triangle.V[0], Triangle.V[1], Triangle.V[2]);
double EdgeLensSq[3];
int MaxEdgeIdx = 0;
double MaxEdgeLenSq = 0;
for (int i = 2, j = 0; j < 3; i = j++)
{
double EdgeLenSq = DistanceSquared(Triangle.V[i], Triangle.V[j]);
if (EdgeLenSq > MaxEdgeLenSq)
{
MaxEdgeIdx = i;
MaxEdgeLenSq = EdgeLenSq;
}
EdgeLensSq[i] = EdgeLenSq;
}
// if we found a too-long edge, we can try sampling the tri
if (MaxEdgeLenSq > SpacingThreshSq)
{
FVector3f Normal = (FVector3f)VectorUtil::Normal(Triangle.V[0], Triangle.V[1], Triangle.V[2]);
// Pick number of samples based on the longest edge
double LongEdgeLen = FMathd::Sqrt(MaxEdgeLenSq);
int Divisions = FMath::FloorToInt32(LongEdgeLen / Spacing);
double Factor = 1.0 / double(Divisions + 1);
int SecondEdgeIdx = (MaxEdgeIdx + 1) % 3;
int ThirdEdgeIdx = (MaxEdgeIdx + 2) % 3;
// Sample along the two longest edges first, then interpolate these samples
int SecondLongestEdgeIdx = SecondEdgeIdx;
if (EdgeLensSq[SecondEdgeIdx] < EdgeLensSq[ThirdEdgeIdx])
{
SecondLongestEdgeIdx = ThirdEdgeIdx;
}
int SecondLongestSecondEdgeIdx = (SecondLongestEdgeIdx + 1) % 3;
for (int DivI = 0; DivI < Divisions; DivI++)
{
double Along = (DivI + 1) * Factor;
FVector3d E1Bary(0, 0, 0), E2Bary(0, 0, 0);
E1Bary[MaxEdgeIdx] = Along;
E1Bary[SecondEdgeIdx] = 1 - Along;
E2Bary[SecondLongestEdgeIdx] = 1 - Along;
E2Bary[SecondLongestSecondEdgeIdx] = Along;
// Choose number of samples between the two edge points based on their distance
double AcrossDist = Distance( Triangle.BarycentricPoint(E1Bary), Triangle.BarycentricPoint(E2Bary) );
int DivisionsAcross = FMath::CeilToInt32(AcrossDist / Spacing);
double FactorAcross = 1.0 / double(DivisionsAcross + 1);
for (int DivJ = 0; DivJ < DivisionsAcross; DivJ++)
{
double AlongAcross = (DivJ + 1) * FactorAcross;
FVector3d Bary = UE::Geometry::Lerp(E1Bary, E2Bary, AlongAcross);
FVector3d SamplePos = Triangle.BarycentricPoint(Bary);
if (!KnownSamples[ComponentIdx].IsCellEmptyUnsafe(SamplePos)) // fast early out; def. have pt within radius
{
continue;
}
TPair<int, double> VIDDist = KnownSamples[ComponentIdx].FindNearestInRadius(SamplePos, Spacing * .5, [&Mesh, SamplePos](int VID)
{
return DistanceSquared(Mesh.GetVertex(VID), SamplePos);
});
// No point within radius Spacing/2 -> Add a new sample
if (VIDDist.Key == -1)
{
// no point within radius; can add a sample here
FVertexInfo Info(SamplePos, Normal);
int AddedVID = Mesh.AppendVertex(Info);
SetVertexColor(Mesh, AddedVID, DefaultVertexColor);
KnownSamples[ComponentIdx].InsertPointUnsafe(AddedVID, SamplePos);
}
}
}
}
}
}
}
/**
* Fill holes in non-welded meshes, w/ UVs that attempt to continue one of the local UV islands.
* Intended to clean holes erroneously left by FMeshBoolean (due to floating point error)
*
* Note this algo tries to continue UVs of internal faces only, where the valid UV areas aren't already
* pre-defined to match an artist-authored texture, but are automatically generated by simple projection
* per connected component.
* Hole filling with 'external' materials and UVs could risk having very stretched triangles in UV space.
* The hope is that holes are rare and small enough that it's ok to just use internal materials.
*
* @param Mesh Fill holes on this mesh
* @param CandidateEdges Only consider holes that touch these edges
*/
void FillHoles(FDynamicMesh3& Mesh, const TSet<int32>& CandidateEdges, double MinHoleArea = DOUBLE_KINDA_SMALL_NUMBER)
{
/// 1. Build a spatial hash of boundary vertices, so we can identify holes even though the mesh is not welded
double SnapDistance = 1e-03;
TPointHashGrid3d<int32> VertHash(SnapDistance * 10, -1);
TMap<int32, int32> CoincidentVerticesMap; // map every vertex in an overlapping cluster to a canonical single vertex for that cluster
TSet<int32> HashedVertices; // track what's already processed
auto AddVertexToHash = [&Mesh, SnapDistance, &VertHash, &CoincidentVerticesMap, &HashedVertices](int32 VID)
{
if (HashedVertices.Contains(VID))
{
return;
}
HashedVertices.Add(VID);
FVector3d VPos = Mesh.GetVertex(VID);
TPair<int32, double> Res = VertHash.FindNearestInRadius(VPos, SnapDistance, [&Mesh, &VPos](const int& VID)->double { return FVector3d::DistSquared(VPos, Mesh.GetVertex(VID)); });
if (Res.Key != INDEX_NONE)
{
int32 MapTo = Res.Key;
int32* WasMapped = CoincidentVerticesMap.Find(MapTo);
if (WasMapped)
{
MapTo = *WasMapped;
}
CoincidentVerticesMap.Add(VID, MapTo);
}
VertHash.InsertPointUnsafe(VID, VPos);
};
for (int32 EID : CandidateEdges)
{
FDynamicMesh3::FEdge Edge = Mesh.GetEdge(EID);
if (Edge.Tri.B == -1) // only consider boundary edges
{
AddVertexToHash(Edge.Vert.A);
AddVertexToHash(Edge.Vert.B);
}
}
/// 2. Find the edges w/ no paired reverse edge (i.e. the potential hole boundary edges)
using FPair = TPair<int32, int32>;
TMultiMap<int32, FPair> OpenEdges; // FPair contains: (Second vertex ID on edge, Edge ID)
auto CanonicalVID = [&CoincidentVerticesMap](int32 VID)
{
int32* Mapped = CoincidentVerticesMap.Find(VID);
return Mapped ? *Mapped : VID;
};
// remove an edge from OpenEdges, assuming the vertices are already passed through CanonicalVID()
auto RemoveCanonEdge = [&OpenEdges](int32 A, int32 B)
{
int32 NumRemovedPairs = 0;
for (TMultiMap<int32, FPair>::TKeyIterator It = OpenEdges.CreateKeyIterator(A); It; ++It)
{
if (It.Value().Key == B)
{
It.RemoveCurrent();
return true;
}
}
return false;
};
auto AddEdge = [&OpenEdges, &CanonicalVID, &RemoveCanonEdge](int32 A, int32 B, int32 EID)
{
A = CanonicalVID(A);
B = CanonicalVID(B);
if (A == B) // skip edges where the vertices were too close
{
return;
}
if (!RemoveCanonEdge(B, A))
{
OpenEdges.Add(A, FPair(B, EID));
}
};
for (int32 CandEID : CandidateEdges)
{
FDynamicMesh3::FEdge Edge = Mesh.GetEdge(CandEID);
if (Edge.Tri[1] == FDynamicMesh3::InvalidID)
{
int32 TID = Edge.Tri[0];
FIndex2i EdgeV = Mesh.GetOrientedBoundaryEdgeV(CandEID);
AddEdge(EdgeV.A, EdgeV.B, CandEID);
}
}
if (OpenEdges.Num() < 3)
{
return;
}
/// 3. Walk the open boundary edges to find the loops that define holes
TArray<int32> AllBoundaryVerts; // VIDs for all hole boundaries (w/ Num IDs at the start of each span)
TArray<int32> AllBoundaryEdges; // EIDs for hole boundaries (w/ same layout as AllBoundaryVerts)
int32 NumHoles = 0;
while (true)
{
const auto& EdgeItr = OpenEdges.CreateConstIterator();
if (!EdgeItr)
{
break;
}
int32 Start = EdgeItr.Key();
FPair WalkPair = EdgeItr.Value();
int32 Walk = WalkPair.Key;
int32 BdryStartIdx = AllBoundaryVerts.Add(-1);
int32 BdryEdgeStartIdx = AllBoundaryEdges.Add(-1);
checkSlow(BdryEdgeStartIdx == BdryStartIdx);
AllBoundaryVerts.Add(Start);
AllBoundaryEdges.Add(WalkPair.Value);
RemoveCanonEdge(Start, Walk);
while (Walk != Start) // this will either loop back to start or dead end (note every non-dead-end iter removes an edge from OpenEdges)
{
AllBoundaryVerts.Add(Walk);
FPair* Next = OpenEdges.Find(Walk);
if (!Next)
{
// dead-ended chain of edges, discard the vertices
AllBoundaryVerts.SetNum(BdryStartIdx);
AllBoundaryEdges.SetNum(BdryStartIdx);
break;
}
int32 NextVal = Next->Key;
AllBoundaryEdges.Add(Next->Value);
RemoveCanonEdge(Walk, NextVal);
Walk = NextVal;
}
int32 Count = AllBoundaryVerts.Num() - BdryStartIdx - 1;
if (Count < 3) // failed to add a boundary, try again next loop
{
AllBoundaryVerts.SetNum(BdryStartIdx);
AllBoundaryEdges.SetNum(BdryStartIdx);
continue;
}
AllBoundaryVerts[BdryStartIdx] = Count; // record length of added chain
AllBoundaryEdges[BdryStartIdx] = Count;
NumHoles++;
}
if (NumHoles == 0)
{
return;
}
check(AllBoundaryVerts.Num() == AllBoundaryEdges.Num());
/// Tag vertex connected components
FDisjointSet VertComponents(Mesh.MaxVertexID());
for (FIndex3i Tri : Mesh.TrianglesItr())
{
VertComponents.Union(Tri.A, Tri.B);
VertComponents.Union(Tri.B, Tri.C);
}
FDynamicMeshMaterialAttribute* MaterialIDs = Mesh.Attributes()->GetMaterialID();
/// 4. Compute stats on the holes: Their areas, and the neighboring component that best matches the hole normal
TArray<int32> HoleComponents; // which connected component to connect the new hole geometry to
HoleComponents.Init(-1, NumHoles);
TArray<FVector3d> HoleNormals;
HoleNormals.Init(FVector3d::ZeroVector, NumHoles);
for (int32 BIdx = 0, CurBdry = 0; BIdx < AllBoundaryVerts.Num(); BIdx += AllBoundaryVerts[BIdx] + 1, CurBdry++)
{
int32 ChainLen = AllBoundaryVerts[BIdx];
int32 EndIdx = BIdx + ChainLen + 1;
check(ChainLen > 2 && EndIdx <= AllBoundaryVerts.Num());
TMap<int32, FVector3d> ComponentToNormalMap;
FVector3d HoleNormal(0, 0, 0);
FVector3d POrigin = Mesh.GetVertex(AllBoundaryVerts[BIdx + 1]);
for (int32 LastIdx = EndIdx - 1, Idx = BIdx + 1; Idx < EndIdx; LastIdx = Idx++)
{
if (LastIdx != BIdx + 1 && Idx != BIdx + 1)
{
int32 V0 = AllBoundaryVerts[LastIdx];
int32 V1 = AllBoundaryVerts[Idx];
FVector3d P0 = Mesh.GetVertex(V0);
FVector3d P1 = Mesh.GetVertex(V1);
FVector3d Edge = P1 - P0;
FVector3d HoleTriNormal = Edge.Cross(P0 - POrigin);
HoleNormal += HoleTriNormal;
}
int32 EID01 = AllBoundaryEdges[LastIdx];
FDynamicMesh3::FEdge Edge01 = Mesh.GetEdge(EID01);
int32 TID01 = Edge01.Tri.A;
int32 MID01 = MaterialIDs->GetValue(TID01);
if (MID01 >= 0 && (MID01 % 2) == 0)
{
continue; // skip "outside" materials, where UVs are less predictable
}
int32 Component = (int32)VertComponents.Find(Edge01.Vert.A);
FVector3d Normal = Mesh.GetTriNormal(TID01);
FVector3d* AccNormal = ComponentToNormalMap.Find(Component);
if (AccNormal)
{
(*AccNormal) += Normal;
}
else
{
ComponentToNormalMap.Add(Component, Normal);
}
}
int32 BestComponent = -1;
double BestScore = -2; // Score is dot product of component avg triangle normal and hole normal
bool bHasNormal = HoleNormal.Normalize(DBL_EPSILON);
for (TPair<int32, FVector3d>& IDNormal : ComponentToNormalMap)
{
double Score;
if (IDNormal.Value.Normalize(DBL_EPSILON))
{
Score = HoleNormal.Dot(IDNormal.Value);
}
else
{
Score = -1;
}
if (Score > BestScore)
{
BestScore = Score;
BestComponent = IDNormal.Key;
}
}
if (!bHasNormal && BestComponent > -1)
{
HoleNormal = ComponentToNormalMap[BestComponent];
}
// Note: BestComponent could be -1; currently we ignore the hole in this case.
// This is the case where only the "outside" material is connected to the hole.
// it tends to only happen for e.g. singular degenerate triangles, which are
// better to ignore.
// If we'd instead like to fill these anyway, we could fall back to:
// BestComponent = VertComponents.Find(AllBoundaryVerts[BIdx + 1])
// Or we could change the hole fill logic to create a completely
// disconnected patch in this case.
HoleComponents[CurBdry] = BestComponent;
HoleNormals[CurBdry] = HoleNormal;
}
/// 5. Fill holes with new triangles
int32 NumUVs = NumEnabledUVChannels(Mesh);
TArray<int32> UseVIDs; // vertex IDs for the current hole
for (int32 BIdx = 0, CurBdry = 0; BIdx < AllBoundaryVerts.Num(); BIdx += AllBoundaryVerts[BIdx] + 1, CurBdry++)
{
if (HoleComponents[CurBdry] == -1)
{
// hole was solely on the "outside" material; this can indicate an open boundary that isn't actually a hole
// for now we skip these cases (See comment where HoleComponents are assigned, above)
continue;
}
int32 ChainLen = AllBoundaryVerts[BIdx];
int32 EndIdx = BIdx + ChainLen + 1;
check(ChainLen > 2 && EndIdx <= AllBoundaryVerts.Num());
TArray<FIndex3i> Triangles;
TArray<FVector3d> VertexPositions;
for (int Idx = BIdx + 1; Idx < EndIdx; Idx++)
{
VertexPositions.Add(Mesh.GetVertex(AllBoundaryVerts[Idx]));
}
PolygonTriangulation::TriangulateSimplePolygon(VertexPositions, Triangles, true);
double HoleArea = 0;
FVector3d LastNormal(0, 0, 0);
const double FlippedThreshold = -.5; // Allow some angle deviation but reject hole fills that are too folded
bool bHoleTriangulationIsFolded = false;
for (FIndex3i Tri : Triangles)
{
double TriArea;
FVector3d TriNormal = VectorUtil::NormalArea(VertexPositions[Tri.A], VertexPositions[Tri.B], VertexPositions[Tri.C], TriArea);
HoleArea += TriArea;
if (LastNormal.Dot(TriNormal) < FlippedThreshold)
{
bHoleTriangulationIsFolded = true;
break;
}
if (TriArea != 0.0)
{
LastNormal = TriNormal;
}
}
if (HoleArea < MinHoleArea || bHoleTriangulationIsFolded)
{
continue;
}
// find the hole material and edge matching the target vertex component
int32 HoleMaterial = -1;
FIndex2i HoleEdgeV; // reference edge for the hole
for (int32 LastIdx = EndIdx - 1, Idx = BIdx + 1; Idx < EndIdx; LastIdx = Idx++)
{
int32 V0 = AllBoundaryVerts[LastIdx];
int32 V1 = AllBoundaryVerts[Idx];
int32 EID01 = AllBoundaryEdges[LastIdx];
FDynamicMesh3::FEdge Edge = Mesh.GetEdge(EID01);
int32 Component01 = (int32)VertComponents.Find((uint32)Edge.Vert.A);
if (Edge.Tri.B == -1 && Component01 == HoleComponents[CurBdry]) // matching edge must still be a boundary edge
{
check(Edge.Vert.A != INDEX_NONE);
HoleMaterial = MaterialIDs->GetValue(Edge.Tri.A);
HoleEdgeV = Mesh.GetOrientedBoundaryEdgeV(EID01);
break;
}
}
// no boundary edge found with the target component ID
if (HoleEdgeV.A == INDEX_NONE)
{
continue;
}
// find a basis for UV projection (per UV channel)
FVector3d N = HoleNormals[CurBdry];
FVector3d Origin = Mesh.GetVertex(HoleEdgeV.A);
FVector3d T[8], B[8];
{
FVector3d Edge = Mesh.GetVertex(HoleEdgeV.B) - Origin;
float EdgeLen = (float)Edge.Length();
for (int32 UVIdx = 0; UVIdx < NumUVs; UVIdx++)
{
FVector2f UVA, UVB;
GetUV(Mesh, HoleEdgeV.A, UVA, UVIdx);
GetUV(Mesh, HoleEdgeV.B, UVB, UVIdx);
FVector2f UVEdge = UVB - UVA;
float UVEdgeLen = UVEdge.Length();
float UVScale = UVEdgeLen / EdgeLen;
double Angle = (double)FMath::Atan2(UVEdge.Y, UVEdge.X);
UE::Geometry::FQuaterniond RotT(N, -Angle, false);
UE::Geometry::FQuaterniond RotB(N, FMathd::HalfPi - Angle, false);
T[UVIdx] = RotT * Edge * UVScale;
B[UVIdx] = RotB * Edge * UVScale;
}
}
// Indices of vertices to be used for the hole fill
UseVIDs.Init(-1, ChainLen);
// Find the vertices that match the target component -- we can re-use these directly
for (int32 LastIdx = EndIdx - 1, Idx = BIdx + 1; Idx < EndIdx; LastIdx = Idx++)
{
int32 V0 = AllBoundaryVerts[LastIdx];
int32 V1 = AllBoundaryVerts[Idx];
int32 EID01 = AllBoundaryEdges[LastIdx];
if (!Mesh.IsBoundaryEdge(EID01))
{
continue;
}
int32 C0 = LastIdx - BIdx - 1;
int32 C1 = Idx - BIdx - 1;
FIndex2i EdgeV = Mesh.GetOrientedBoundaryEdgeV(EID01);
if (EdgeV.A >= 0 && HoleComponents[CurBdry] == (int32)VertComponents.Find(EdgeV.A))
{
UseVIDs[C0] = EdgeV.A;
UseVIDs[C1] = EdgeV.B;
}
}
// Copy color + tangents from the reference vertex (TODO: consider an average instead?)
FVector4f HoleColor;
FVector3f HoleTanU, HoleTanV;
GetTangent(Mesh, HoleEdgeV.A, HoleTanU, HoleTanV);
HoleColor = GetVertexColor(Mesh, HoleEdgeV.A);
int32 PrevMaxVID = Mesh.MaxVertexID();
// Create new vertices for all that weren't already assigned to an existing vertex
for (int32 Idx = BIdx + 1; Idx < EndIdx; Idx++)
{
int32 C = Idx - BIdx - 1;
if (UseVIDs[C] != -1)
{
continue;
}
int32 V = AllBoundaryVerts[Idx];
FVector3d P = Mesh.GetVertex(V);
UE::Geometry::FVertexInfo Info(P, (FVector3f)HoleNormals[CurBdry]);
int32 NewV = Mesh.AppendVertex(Info);
SetVertexColor(Mesh, NewV, HoleColor);
SetTangent(Mesh, NewV, (FVector3f)HoleNormals[CurBdry], HoleTanU, HoleTanV);
for (int32 UVIdx = 0; UVIdx < NumUVs; UVIdx++)
{
FVector3d Diff = P - Origin;
FVector2f UV((float)Diff.Dot(T[UVIdx]), (float)Diff.Dot(B[UVIdx]));
SetUV(Mesh, NewV, UV, UVIdx);
}
UseVIDs[C] = NewV;
}
for (FIndex3i Tri : Triangles)
{
int32 V0 = UseVIDs[Tri.A];
int32 V1 = UseVIDs[Tri.B];
int32 V2 = UseVIDs[Tri.C];
if (V0 == V1 || V1 == V2 || V0 == V2)
{
continue;
}
FVector3d P0 = Mesh.GetVertex(V0);
FVector3d P1 = Mesh.GetVertex(V1);
FVector3d P2 = Mesh.GetVertex(V2);
int32 TID = Mesh.AppendTriangle(V0, V1, V2);
// append can fail from a non-manifold edge; in that case add new vertices and try again
if (TID == FDynamicMesh3::NonManifoldID)
{
int32 SeparatedVIDs[]{ V0, V1, V2 };
for (int32 SubIdx = 0; SubIdx < 3; SubIdx++)
{
if (SeparatedVIDs[SubIdx] < PrevMaxVID)
{
int32 OrigV = SeparatedVIDs[SubIdx];
int32 NewV = Mesh.AppendVertex(Mesh, OrigV);
SetTangent(Mesh, NewV, (FVector3f)HoleNormals[CurBdry], HoleTanU, HoleTanV);
for (int32 UVIdx = 0; UVIdx < NumUVs; UVIdx++)
{
FVector2f UV;
GetUV(Mesh, OrigV, UV, UVIdx);
SetUV(Mesh, NewV, UV, UVIdx);
}
SeparatedVIDs[SubIdx] = NewV;
}
}
TID = Mesh.AppendTriangle(SeparatedVIDs[0], SeparatedVIDs[1], SeparatedVIDs[2]);
}
if (TID > -1)
{
MaterialIDs->SetValue(TID, HoleMaterial);
SetVisibility(Mesh, TID, true);
SetInternal(Mesh, TID, true);
}
}
}
}
} // namespace AugmentedDynamicMesh
namespace
{
/// Make a frame w/ Z aligned to the given normal
/// and X aligned to the major axis most perpendicular to the normal
FFrame3d AxisAlignedFrame(const FPlane& Plane)
{
FVector3d Origin = (FVector3d)Plane.GetOrigin();
FVector3d Normal = (FVector3d)Plane.GetNormal();
int32 MinIdx = UE::Geometry::MinAbsElementIndex(Normal);
FFrame3d Frame(Origin, Normal);
FVector3d Target(0, 0, 0);
Target[MinIdx] = 1;
Frame.ConstrainedAlignAxis(0, Target, Normal);
return Frame;
}
// Replace any unset vertex colors with colors from coincident or neighboring vertices (if bPropagateFromNeighbors) or DefaultVertexColor
void SetUnsetColors(FGeometryCollection* Collection, int32 FirstGeometryIndex, bool bPropagateFromNeighbors = true)
{
if (FirstGeometryIndex == INDEX_NONE)
{
return;
}
auto IsUnset = [](const FLinearColor& Color) -> bool
{
return Color.R < 0 || Color.G < 0 || Color.B < 0 || Color.A < 0;
};
constexpr float CopyColorDistance = 1e-03;
int32 NumGeo = Collection->NumElements(FGeometryCollection::GeometryGroup);
if (bPropagateFromNeighbors)
{
// Get vertex range for the geometry in and after FirstGeometryIndex
// Note: I think this will always just be the contiguous block of all vertices after the first geometry's start vertex,
// but explicitly find the range to be safe (and to make the code easier to adapt)
int32 StartV = Collection->VertexStart[FirstGeometryIndex];
int32 EndV = Collection->VertexCount[FirstGeometryIndex] + StartV;
for (int32 GeoIdx = FirstGeometryIndex + 1; GeoIdx < NumGeo; ++GeoIdx)
{
int32 GeoStart = Collection->VertexStart[GeoIdx];
if (GeoStart < StartV)
{
StartV = GeoStart;
}
else
{
int32 GeoEnd = GeoStart + Collection->VertexCount[GeoIdx];
EndV = FMath::Max(GeoEnd, EndV);
}
}
int32 NumV = EndV - StartV;
// A generic representation of the full vertex graph
// (To be split into connected components and solved as a linear system per component)
struct FLink
{
int32 V[2]{ -1, -1 };
float Wt = 1;
FLink()
{}
FLink(int32 A, int32 B, float Wt) : V{ A, B }, Wt(Wt)
{}
static FLink Edge(int32 A, int32 B)
{
// Note: Currently just using constant weight (per half-edge)
constexpr float EdgeWt = .5;
return FLink(A, B, EdgeWt);
}
};
TArray<FLink> Links; // connections between vertices -- note: always symmetric, duplicates are allowed
Links.Reserve(NumV * 3);
TArray<FVector4f> FixedColors;
TArray<float> FixedColorWts;
FixedColors.SetNumZeroed(NumV);
FixedColorWts.SetNumZeroed(NumV);
FVertexConnectedComponents Components(NumV); // Overall connected components (1 solve per component)
FVertexConnectedComponents VtxComps(NumV); // Vertex components (1 group of overlapping vertices per component)
for (int32 GeoIdx = FirstGeometryIndex; GeoIdx < NumGeo; ++GeoIdx)
{
int32 FStart = Collection->FaceStart[GeoIdx];
int32 FEnd = FStart + Collection->FaceCount[GeoIdx];
for (int32 FIdx = FStart; FIdx < FEnd; ++FIdx)
{
const FIntVector& Tri = Collection->Indices[FIdx];
// Note: This assumes triangles always have either fully set or fully unset vertices, so we only need to check one vertex
if (IsUnset(Collection->Color[Tri[0]]))
{
Components.ConnectVertices(Tri.X - StartV, Tri.Y - StartV);
Components.ConnectVertices(Tri.Y - StartV, Tri.Z - StartV);
Links.Add(FLink::Edge(Tri.X - StartV, Tri.Y - StartV));
Links.Add(FLink::Edge(Tri.Y - StartV, Tri.Z - StartV));
Links.Add(FLink::Edge(Tri.Z - StartV, Tri.X - StartV));
}
}
}
// put geometry in a shared coordinate space to spread color across geometry
TArray<FTransform> GlobalTransformArray;
GeometryCollectionAlgo::GlobalMatrices(Collection->Transform, Collection->Parent, GlobalTransformArray);
TArray<FVector3f> GlobalVertices;
GlobalVertices.SetNum(NumV);
for (int32 Idx = StartV; Idx < EndV; Idx++)
{
GlobalVertices[Idx - StartV] = (FVector3f)GlobalTransformArray[Collection->BoneMap[Idx]].TransformPosition(FVector(Collection->Vertex[Idx]));
}
// Create a hash grid of vertices with unset colors, and connect components for overlapping unset vertices
TPointHashGrid3f<int32> VertexHash(CopyColorDistance * 4.0f, INDEX_NONE);
TArray<int32> Neighbors;
for (int32 GeoIdx = FirstGeometryIndex; GeoIdx < NumGeo; ++GeoIdx)
{
int32 GeoStart = Collection->VertexStart[GeoIdx];
int32 GeoEnd = GeoStart + Collection->VertexCount[GeoIdx];
for (int32 Idx = GeoStart; Idx < GeoEnd; ++Idx)
{
FLinearColor Color = Collection->Color[Idx];
if (IsUnset(Color))
{
if (Components.GetComponentSize(Idx - StartV) == 1) // isolated vertex (not in any triangle), just leave as default color
{
Collection->Color[Idx] = FLinearColor(AugmentedDynamicMesh::DefaultVertexColor);
continue;
}
FVector3f Pt = GlobalVertices[Idx - StartV];
int32 SetID = Components.GetComponent(Idx - StartV);
Neighbors.Reset();
VertexHash.FindPointsInBall(Pt, CopyColorDistance, [&GlobalVertices, &Pt, StartV](int32 OtherIdx)
{
return DistanceSquared(Pt, GlobalVertices[OtherIdx - StartV]);
}, Neighbors);
for (int32 NbrIdx : Neighbors)
{
VtxComps.ConnectVertices(Idx - StartV, NbrIdx - StartV);
Components.ConnectVertices(SetID, NbrIdx - StartV);
}
VertexHash.InsertPointUnsafe(Idx, Pt);
}
}
}
TSet<int32> CanSolveComponents;
// Fix colors on vertices that are coincident to set colors
for (int32 GeoIdx = FirstGeometryIndex; GeoIdx < NumGeo; ++GeoIdx)
{
int32 GeoStart = Collection->VertexStart[GeoIdx];
int32 GeoEnd = GeoStart + Collection->VertexCount[GeoIdx];
for (int32 Idx = GeoStart; Idx < GeoEnd; ++Idx)
{
FLinearColor Color = Collection->Color[Idx];
if (!IsUnset(Color))
{
FVector3f Pt = GlobalVertices[Idx - StartV];
Neighbors.Reset();
VertexHash.FindPointsInBall(Pt, CopyColorDistance, [&GlobalVertices, &Pt, StartV](int32 OtherIdx)
{
return DistanceSquared(Pt, GlobalVertices[OtherIdx - StartV]);
}, Neighbors);
for (int32 NbrIdx : Neighbors)
{
int32 Component = Components.GetComponent(NbrIdx - StartV);
int32 ComponentSize = Components.GetComponentSize(NbrIdx - StartV);
if (ComponentSize == 1)
{
Collection->Color[NbrIdx] = Color; // Isolated vertex, no solve needed
}
CanSolveComponents.Add(Component);
int32 SharedVtx = VtxComps.GetComponent(NbrIdx - StartV);
FixedColors[SharedVtx] += FVector4f(Color.R, Color.G, Color.B, Color.A);
FixedColorWts[SharedVtx] += 1.0f;
}
}
}
}
// Average color at any vertex w/ a fixed color
for (int32 ColorIdx = 0; ColorIdx < FixedColors.Num(); ++ColorIdx)
{
float& Wt = FixedColorWts[ColorIdx];
if (Wt > 0.0f)
{
FixedColors[ColorIdx] /= Wt;
Wt = 1.0f;
}
}
TArray<int32> ToComponentMap;
ToComponentMap.Reserve(NumV);
TArray<float> DiagonalWts;
TArray<int32> ContigComponentVertices = Components.MakeContiguousComponentsArray(NumV);
// Solve for vertex colors per component
for (int32 ContigStart = 0, NextStart = -1; ContigStart < NumV; ContigStart = NextStart)
{
int32 ComponentID = Components.GetComponent(ContigComponentVertices[ContigStart]);
int32 ComponentSize = Components.GetComponentSize(ComponentID);
NextStart = ContigStart + ComponentSize;
if (ComponentSize == 1)
{
continue;
}
if (!CanSolveComponents.Contains(ComponentID))
{
continue;
}
using FSparseMatf = Eigen::SparseMatrix<float, Eigen::ColMajor>;
using FMatrixTripletf = Eigen::Triplet<float>;
std::vector<FMatrixTripletf> EntryTriplets;
// Make an index map for this component's vertices
ToComponentMap.Init(-1, NumV);
int32 NumToSolve = 0;
for (int32 ContigIdx = ContigStart; ContigIdx < NextStart; ++ContigIdx)
{
int32 LocalIdx = ContigComponentVertices[ContigIdx];
int32 GlobalIdx = LocalIdx + StartV;
int32 SharedIdx = VtxComps.GetComponent(LocalIdx);
if (FixedColorWts[SharedIdx] > 0)
{
FLinearColor Color(FixedColors[SharedIdx]);
Collection->Color[GlobalIdx] = Color; // copy fixed color out
continue; // has fixed color, do not include in solve
}
if (ToComponentMap[SharedIdx] != -1)
{
continue; // already mapped
}
ToComponentMap[SharedIdx] = NumToSolve;
++NumToSolve;
}
if (NumToSolve == 0)
{
continue;
}
FSparseMatf SparseMatrix(NumToSolve, NumToSolve);
Eigen::VectorXf X[4]{ Eigen::VectorXf(NumToSolve), Eigen::VectorXf(NumToSolve), Eigen::VectorXf(NumToSolve), Eigen::VectorXf(NumToSolve) };
Eigen::VectorXf B[4]{ Eigen::VectorXf(NumToSolve), Eigen::VectorXf(NumToSolve), Eigen::VectorXf(NumToSolve), Eigen::VectorXf(NumToSolve) };
for (int32 SubIdx = 0; SubIdx < 4; ++SubIdx)
{
B[SubIdx].setZero();
}
DiagonalWts.Reset(NumToSolve);
DiagonalWts.SetNumZeroed(NumToSolve, EAllowShrinking::No);
// Build the sparse matrix and rhs for the component
for (const FLink& Link : Links)
{
int32 SharedA = VtxComps.GetComponent(Link.V[0]);
int32 SharedB = VtxComps.GetComponent(Link.V[1]);
int32 LocalA = ToComponentMap[SharedA];
int32 LocalB = ToComponentMap[SharedB];
if (LocalA == INDEX_NONE)
{
if (LocalB == INDEX_NONE)
{
continue;
}
Swap(SharedA, SharedB);
Swap(LocalA, LocalB);
}
if (LocalB == INDEX_NONE)
{
if (FixedColorWts[SharedB] > 0)
{
FVector4f BVal = FixedColors[SharedB] * Link.Wt;
for (int32 SubIdx = 0; SubIdx < 4; ++SubIdx)
{
B[SubIdx][LocalA] += BVal[SubIdx];
}
DiagonalWts[LocalA] += Link.Wt;
}
}
else
{
EntryTriplets.push_back(FMatrixTripletf(LocalA, LocalB, -Link.Wt));
EntryTriplets.push_back(FMatrixTripletf(LocalB, LocalA, -Link.Wt));
DiagonalWts[LocalA] += Link.Wt;
DiagonalWts[LocalB] += Link.Wt;
}
}
for (int32 Idx = 0; Idx < NumToSolve; ++Idx)
{
EntryTriplets.push_back(FMatrixTripletf(Idx, Idx, DiagonalWts[Idx]));
}
// Solve linear system for internal colors
SparseMatrix.setFromTriplets(EntryTriplets.begin(), EntryTriplets.end());
SparseMatrix.makeCompressed();
Eigen::SparseLU<FSparseMatf, Eigen::COLAMDOrdering<int>> MatrixSolver;
MatrixSolver.isSymmetric(true);
MatrixSolver.analyzePattern(SparseMatrix);
MatrixSolver.factorize(SparseMatrix);
ParallelFor(4, [&](int32 Idx)
{
X[Idx] = MatrixSolver.solve(B[Idx]);
});
// Copy solved colors out
for (int32 ContigIdx = ContigStart; ContigIdx < NextStart; ++ContigIdx)
{
int32 LocalIdx = ContigComponentVertices[ContigIdx];
int32 GlobalIdx = LocalIdx + StartV;
int32 SharedIdx = VtxComps.GetComponent(LocalIdx);
int32 CompIdx = ToComponentMap[SharedIdx];
if (CompIdx != INDEX_NONE)
{
FVector4f SolvedColor;
for (int32 SubIdx = 0; SubIdx < 4; ++SubIdx)
{
// Note: make sure the solved color is non-negative, so solver error cannot make the color 'unset'
SolvedColor[SubIdx] = FMath::Max(X[SubIdx][CompIdx], 0.f);
}
Collection->Color[GlobalIdx] = FLinearColor(SolvedColor);
}
}
}
}
// Replace unset color with default color
for (int32 GeoIdx = FirstGeometryIndex; GeoIdx < NumGeo; ++GeoIdx)
{
int32 VStart = Collection->VertexStart[GeoIdx];
int32 VEnd = VStart + Collection->VertexCount[GeoIdx];
for (int32 Idx = VStart; Idx < VEnd; ++Idx)
{
if (IsUnset(Collection->Color[Idx]))
{
Collection->Color[Idx] = FLinearColor(AugmentedDynamicMesh::DefaultVertexColor);
}
}
}
}
}
void SetGeometryCollectionAttributes(FDynamicMesh3& Mesh, int32 NumUVLayers)
{
AugmentedDynamicMesh::Augment(Mesh, NumUVLayers);
}
void ClearCustomGeometryCollectionAttributes(UE::Geometry::FDynamicMesh3& Mesh)
{
Mesh.DiscardVertexNormals();
Mesh.Attributes()->RemoveAttribute(AugmentedDynamicMesh::ColorAttribName);
Mesh.Attributes()->RemoveAttribute(AugmentedDynamicMesh::TangentUAttribName);
Mesh.Attributes()->RemoveAttribute(AugmentedDynamicMesh::TangentVAttribName);
Mesh.Attributes()->RemoveAttribute(AugmentedDynamicMesh::VisibleAttribName);
Mesh.Attributes()->RemoveAttribute(AugmentedDynamicMesh::InternalAttribName);
AugmentedDynamicMesh::EnableUVChannels(Mesh, 0, false, true); // set 0 UV channels to remove UV attributes
}
FCellMeshes::FCellMeshes(int32 NumUVLayersIn, FRandomStream& RandomStream, const FPlanarCells& Cells, FAxisAlignedBox3d DomainBounds, double Grout, double ExtendDomain, bool bIncludeOutsideCell)
{
Init(NumUVLayersIn, RandomStream, Cells, DomainBounds, Grout, ExtendDomain, bIncludeOutsideCell);
}
FCellMeshes::FCellMeshes(int32 NumUVLayersIn, const FDynamicMesh3& SingleCutter, const FInternalSurfaceMaterials& Materials, TOptional<FTransform> Transform)
{
NumUVLayers = NumUVLayersIn;
SetNumCells(2);
CellMeshes[0].AugMesh = SingleCutter;
if (Transform.IsSet())
{
MeshTransforms::ApplyTransform(CellMeshes[0].AugMesh, FTransformSRT3d(Transform.GetValue()), true);
}
// Augment mesh if needed
if (!AugmentedDynamicMesh::IsAugmented(CellMeshes[0].AugMesh))
{
AugmentedDynamicMesh::Augment(CellMeshes[0].AugMesh, NumUVLayers);
// transfer overlay attributes to 'augmented attribute' format
AugmentedDynamicMesh::SplitOverlayAttributesToPerVertex(CellMeshes[0].AugMesh);
}
// Make sure color is unset
for (int VID : CellMeshes[0].AugMesh.VertexIndicesItr())
{
AugmentedDynamicMesh::SetVertexColor(CellMeshes[0].AugMesh, VID, AugmentedDynamicMesh::UnsetVertexColor);
}
// first mesh is the same as the second mesh, but will be subtracted b/c it's the "outside cell"
// TODO: special case this logic so we don't have to hold two copies of the exact same mesh!
CellMeshes[1].AugMesh = CellMeshes[0].AugMesh;
OutsideCellIndex = 1;
}
// Special function to just make the "grout" part of the planar mesh cells
// Used to make the multi-plane cuts with grout easier to implement
void FCellMeshes::MakeOnlyPlanarGroutCell(const FPlanarCells& Cells, FAxisAlignedBox3d DomainBounds, double Grout)
{
CellMeshes.Reset();
if (!ensure(Grout > 0) || !ensure(Cells.IsInfinitePlane()))
{
return;
}
float GlobalUVScale = Cells.InternalSurfaceMaterials.GlobalUVScale;
if (!ensure(GlobalUVScale > 0))
{
GlobalUVScale = 1;
}
SetNumCells(1);
bool bNoise = Cells.InternalSurfaceMaterials.NoiseSettings.IsSet();
double ExtendDomain = bNoise ? Cells.InternalSurfaceMaterials.NoiseSettings->Amplitude : 0;
DomainBounds.Expand(ExtendDomain);
CreateMeshesForSinglePlane(Cells, DomainBounds, bNoise, GlobalUVScale, Grout, true);
for (FCellInfo& CellInfo : CellMeshes)
{
AugmentedDynamicMesh::SetDefaultAttributes(CellInfo.AugMesh, Cells.InternalSurfaceMaterials.bGlobalVisibility);
}
}
void FCellMeshes::RemeshForNoise(FDynamicMesh3& Mesh, EEdgeRefineFlags EdgeFlags, double TargetEdgeLen)
{
FQueueRemesher Remesh(&Mesh);
Remesh.bPreventNormalFlips = true;
FMeshConstraints Constraints;
FMeshBoundaryLoops Boundary(&Mesh);
int LoopCount = Boundary.GetLoopCount();
if (!ensureMsgf(LoopCount == 1, TEXT("Expected to remesh a patch with a single boundary but found %d boundary loops"), LoopCount))
{
if (LoopCount == 0)
{
return;
}
}
for (int VID : Mesh.VertexIndicesItr())
{
FVertexConstraint FullyConstrain(true, false, VID);
Constraints.SetOrUpdateVertexConstraint(VID, FullyConstrain);
}
FEdgeConstraint EdgeConstraint(EdgeFlags);
for (int EID : Boundary[0].Edges)
{
Constraints.SetOrUpdateEdgeConstraint(EID, EdgeConstraint);
}
Remesh.SetExternalConstraints(Constraints);
Remesh.SetTargetEdgeLength(TargetEdgeLen);
Remesh.Precompute();
Remesh.FastestRemesh();
}
float FCellMeshes::OctaveNoise(const FVector& V, const FNoiseSettings& Settings)
{
int32 Octaves = Settings.Octaves;
float NoiseValue = 0;
float FreqScale = 1;
float AmpScale = 1;
for (int32 Octave = 0; Octave < Octaves; Octave++, FreqScale *= Settings.Lacunarity, AmpScale *= Settings.Persistence)
{
NoiseValue += FMath::PerlinNoise3D(V * FreqScale) * AmpScale;
}
return NoiseValue;
}
FVector FCellMeshes::NoiseVector(const FVector& Pos, const FNoiseSettings& Settings)
{
float Frequency = Settings.Frequency;
FVector Base = Pos * Frequency;
return FVector(
OctaveNoise(Base + NoiseOffsetX, Settings),
OctaveNoise(Base + NoiseOffsetY, Settings),
OctaveNoise(Base + NoiseOffsetZ, Settings)
) * Settings.Amplitude;
}
FVector3d FCellMeshes::NoiseDisplacement(const FVector3d& Pos, const FNoiseSettings& Settings)
{
FVector P = FVector(Pos);
return FVector3d(NoiseVector(P, Settings));
}
void FCellMeshes::ApplyNoise(FDynamicMesh3& Mesh, FVector3d Normal, const FNoiseSettings& Settings, bool bProjectBoundariesToNormal)
{
double Amplitude = (double)Settings.Amplitude;
double Frequency = (double)Settings.Frequency;
int32 Octaves = Settings.Octaves;
FVector3d Z = Normal * Amplitude;
for (int VID : Mesh.VertexIndicesItr())
{
FVector3d Pos = Mesh.GetVertex(VID);
FVector3d Displacement = NoiseDisplacement(Pos, Settings);
if (bProjectBoundariesToNormal || !Mesh.IsBoundaryVertex(VID))
{
// project displacement onto the normal direction
Displacement = Normal * Displacement.Dot(Normal);
}
Mesh.SetVertex(VID, Pos + Displacement);
}
}
void FCellMeshes::Init(int32 NumUVLayersIn, FRandomStream& RandomStream, const FPlanarCells& Cells, FAxisAlignedBox3d DomainBounds, double Grout, double ExtendDomain, bool bIncludeOutsideCell)
{
NumUVLayers = NumUVLayersIn;
InitEmpty(RandomStream);
float GlobalUVScale = Cells.InternalSurfaceMaterials.GlobalUVScale;
if (!ensure(GlobalUVScale > 0))
{
GlobalUVScale = 1;
}
int NumCells = Cells.NumCells;
bool bHasGroutCell = Grout > 0;
if (bIncludeOutsideCell && !Cells.IsInfinitePlane())
{
OutsideCellIndex = NumCells;
NumCells++;
}
SetNumCells(NumCells);
bool bNoise = Cells.InternalSurfaceMaterials.NoiseSettings.IsSet();
if (bNoise)
{
ExtendDomain += Cells.InternalSurfaceMaterials.NoiseSettings->Amplitude;
}
DomainBounds.Expand(ExtendDomain);
// special handling for the infinite plane case; we need to adapt this to be a closed volume
if (Cells.IsInfinitePlane())
{
CreateMeshesForSinglePlane(Cells, DomainBounds, bNoise, GlobalUVScale, Grout, false);
}
else
{
if (!bNoise) // bounded cells w/ no noise
{
CreateMeshesForBoundedPlanesWithoutNoise(NumCells, Cells, DomainBounds, bNoise, GlobalUVScale);
}
else // bounded cells with noise -- make each boundary plane separately so we can remesh them w/ noise vertices
{
CreateMeshesForBoundedPlanesWithNoise(NumCells, Cells, DomainBounds, bNoise, GlobalUVScale);
}
ApplyGeneralGrout(Grout);
}
// TODO: self-union on cells when it makes sense to do so (for non-single-plane inputs w/ high noise or possible untracked adjacencies)
/*for (FCellInfo& CellInfo : CellMeshes)
{
FMeshSelfUnion SelfUnion(&CellInfo.AugMesh);
// TODO: need to have an option in SelfUnion to not weld edges
SelfUnion.Compute();
}*/
for (FCellInfo& CellInfo : CellMeshes)
{
AugmentedDynamicMesh::SetDefaultAttributes(CellInfo.AugMesh, Cells.InternalSurfaceMaterials.bGlobalVisibility);
}
}
void FCellMeshes::ApplyGeneralGrout(double Grout)
{
if (Grout <= 0)
{
return;
}
// apply grout to all cells
for (int MeshIdx = 0; MeshIdx < CellMeshes.Num(); MeshIdx++)
{
if (MeshIdx == OutsideCellIndex)
{
continue;
}
FDynamicMesh3& Mesh = CellMeshes[MeshIdx].AugMesh;
// TODO: scale from mesh center of mass instead of the vertex centroid?
FVector3d VertexCentroid(0, 0, 0);
for (FVector3d V : Mesh.VerticesItr())
{
VertexCentroid += V;
}
VertexCentroid /= (double)Mesh.VertexCount();
FAxisAlignedBox3d Bounds = Mesh.GetBounds(true);
double BoundsSize = Bounds.MaxDim();
// currently just scale the meshes down so they leave half-a-grout worth of space on their longest axis
// or delete the mesh if it's so small that that would require a negative scale
// TODO: consider instead computing a true offset mesh
// (note that we don't currently have a good UV-preserving+sharp-edge-preserving way to do that)
double ScaleFactor = (BoundsSize - Grout * .5) / BoundsSize;
if (ScaleFactor < FMathd::ZeroTolerance * 1000)
{
// if the grout scale factor would be ~zero or negative, just clear the mesh instead
Mesh.Clear();
AugmentedDynamicMesh::Augment(Mesh, NumUVLayers);
}
else
{
MeshTransforms::Scale(Mesh, FVector3d::One() * ScaleFactor, VertexCentroid);
}
}
// create outside cell (if there is room for it) by appending all the other meshes
if (OutsideCellIndex != -1)
{
FDynamicMesh3& OutsideMesh = CellMeshes[OutsideCellIndex].AugMesh;
OutsideMesh.Clear();
AugmentedDynamicMesh::Augment(OutsideMesh, NumUVLayers);
FDynamicMeshEditor OutsideMeshEditor(&OutsideMesh);
for (int MeshIdx = 0; MeshIdx < CellMeshes.Num(); MeshIdx++)
{
if (MeshIdx == OutsideCellIndex)
{
continue;
}
FMeshIndexMappings IndexMaps;
OutsideMeshEditor.AppendMesh(&CellMeshes[MeshIdx].AugMesh, IndexMaps);
}
}
}
void FCellMeshes::AppendMesh(FDynamicMesh3& Base, FDynamicMesh3& ToAppend, bool bFlipped)
{
FDynamicMeshEditor Editor(&Base);
FMeshIndexMappings Mapping;
Editor.AppendMesh(&ToAppend, Mapping);
if (bFlipped)
{
for (int TID : ToAppend.TriangleIndicesItr())
{
Base.ReverseTriOrientation(Mapping.GetNewTriangle(TID));
}
for (int VID : ToAppend.VertexIndicesItr())
{
int BaseVID = Mapping.GetNewVertex(VID);
Base.SetVertexNormal(BaseVID, -Base.GetVertexNormal(BaseVID));
}
}
}
void FCellMeshes::CreateMeshesForBoundedPlanesWithoutNoise(int NumCells, const FPlanarCells& Cells, const FAxisAlignedBox3d& DomainBounds, bool bNoise, double GlobalUVScale)
{
for (int32 PlaneIdx = 0; PlaneIdx < Cells.PlaneCells.Num(); PlaneIdx++)
{
const TPair<int32, int32>& CellPair = Cells.PlaneCells[PlaneIdx];
FDynamicMesh3* Meshes[2]{ &CellMeshes[CellPair.Key].AugMesh, nullptr };
int32 OtherCell = CellPair.Value < 0 ? OutsideCellIndex : CellPair.Value;
int NumMeshes = OtherCell < 0 ? 1 : 2;
if (NumMeshes == 2)
{
Meshes[1] = &CellMeshes[OtherCell].AugMesh;
}
const TArray<int>& PlaneBoundary = Cells.PlaneBoundaries[PlaneIdx];
FVector3f Normal(Cells.Planes[PlaneIdx].GetNormal());
FFrame3d PlaneFrame = AxisAlignedFrame(Cells.Planes[PlaneIdx]);
FVertexInfo PlaneVertInfo;
PlaneVertInfo.bHaveC = true;
PlaneVertInfo.bHaveUV = false;
PlaneVertInfo.bHaveN = true;
int VertStart[2]{ -1, -1 };
for (int MeshIdx = 0; MeshIdx < NumMeshes; MeshIdx++)
{
PlaneVertInfo.Normal = Normal;
if (MeshIdx == 1 && OtherCell != OutsideCellIndex)
{
PlaneVertInfo.Normal *= -1.0f;
}
VertStart[MeshIdx] = Meshes[MeshIdx]->MaxVertexID();
FVector2f MinUV(FMathf::MaxReal, FMathf::MaxReal);
for (int BoundaryVertex : PlaneBoundary)
{
FVector3d Position = FVector3d(Cells.PlaneBoundaryVertices[BoundaryVertex]);
FVector2f UV = FVector2f(PlaneFrame.ToPlaneUV(Position));
MinUV.X = FMathf::Min(UV.X, MinUV.X);
MinUV.Y = FMathf::Min(UV.Y, MinUV.Y);
}
for (int BoundaryVertex : PlaneBoundary)
{
PlaneVertInfo.Position = FVector3d(Cells.PlaneBoundaryVertices[BoundaryVertex]);
FVector2f UV = (FVector2f(PlaneFrame.ToPlaneUV(PlaneVertInfo.Position)) - MinUV) * static_cast<float>(GlobalUVScale);
int VID = Meshes[MeshIdx]->AppendVertex(PlaneVertInfo);
AugmentedDynamicMesh::SetVertexColor(*Meshes[MeshIdx], VID, AugmentedDynamicMesh::UnsetVertexColor);
AugmentedDynamicMesh::SetAllUV(*Meshes[MeshIdx], VID, UV, NumUVLayers);
}
}
int MID = PlaneToMaterial(PlaneIdx);
if (Cells.AssumeConvexCells)
{
// put a fan
for (int V0 = 0, V1 = 1, V2 = 2; V2 < PlaneBoundary.Num(); V1 = V2++)
{
for (int MeshIdx = 0; MeshIdx < NumMeshes; MeshIdx++)
{
int Offset = VertStart[MeshIdx];
FIndex3i Tri(V0 + Offset, V1 + Offset, V2 + Offset);
if (MeshIdx == 1 && OtherCell != OutsideCellIndex)
{
Swap(Tri.B, Tri.C);
}
int TID = Meshes[MeshIdx]->AppendTriangle(Tri);
if (ensure(TID > -1))
{
Meshes[MeshIdx]->Attributes()->GetMaterialID()->SetNewValue(TID, MID);
}
}
}
}
else // cells may not be convex; cannot triangulate w/ fan
{
// Delaunay triangulate
FPolygon2f Polygon;
for (int V = 0; V < PlaneBoundary.Num(); V++)
{
FVector2f UV;
AugmentedDynamicMesh::GetUV(*Meshes[0], VertStart[0] + V, UV, 0);
Polygon.AppendVertex(UV);
}
FGeneralPolygon2f GeneralPolygon(Polygon);
FConstrainedDelaunay2f Triangulation;
Triangulation.FillRule = FConstrainedDelaunay2f::EFillRule::NonZero;
Triangulation.Add(GeneralPolygon);
Triangulation.Triangulate();
for (int MeshIdx = 0; MeshIdx < NumMeshes; MeshIdx++)
{
int Offset = VertStart[MeshIdx];
for (FIndex3i Triangle : Triangulation.Triangles)
{
Triangle.A += Offset;
Triangle.B += Offset;
Triangle.C += Offset;
if (MeshIdx == 1 && OtherCell != OutsideCellIndex)
{
Swap(Triangle.B, Triangle.C);
}
int TID = Meshes[MeshIdx]->AppendTriangle(Triangle);
if (ensure(TID > -1))
{
Meshes[MeshIdx]->Attributes()->GetMaterialID()->SetNewValue(TID, MID);
}
}
}
}
}
}
double FCellMeshes::GetSafeNoiseSpacing(float SurfaceArea, float TargetSpacing)
{
double MaxVerts = 1000000; // try to avoid creating more than ~ a million new vertices
double MinEdgeLen = FMathd::Sqrt((double)SurfaceArea / MaxVerts);
double Spacing = FMath::Max3(.001, MinEdgeLen, (double)TargetSpacing);
if (Spacing > TargetSpacing)
{
UE_LOG(LogPlanarCut, Warning,
TEXT("Requested spacing of noise points (surface resolution) of %f would require too many added vertices; Using %f instead."),
TargetSpacing, Spacing);
}
return Spacing;
}
void FCellMeshes::CreateMeshesForBoundedPlanesWithNoise(int NumCells, const FPlanarCells& Cells, const FAxisAlignedBox3d& DomainBounds, bool bNoise, double GlobalUVScale)
{
TArray<FDynamicMesh3> PlaneMeshes;
PlaneMeshes.SetNum(Cells.Planes.Num());
FName OriginalPositionAttribute = "OriginalPosition";
for (FDynamicMesh3& PlaneMesh : PlaneMeshes)
{
AugmentedDynamicMesh::EnableUVChannels(PlaneMesh, NumUVLayers);
PlaneMesh.EnableVertexNormals(FVector3f::UnitZ());
PlaneMesh.EnableAttributes();
PlaneMesh.Attributes()->EnableMaterialID();
PlaneMesh.Attributes()->AttachAttribute(OriginalPositionAttribute, new TDynamicMeshVertexAttribute<double, 3>(&PlaneMesh));
}
struct FPlaneIdxAndFlip
{
int32 PlaneIdx;
bool bIsFlipped;
};
TArray<TArray<FPlaneIdxAndFlip>> CellPlanes; // per cell, the planes that border that cell
CellPlanes.SetNum(NumCells);
for (int32 PlaneIdx = 0; PlaneIdx < Cells.PlaneCells.Num(); PlaneIdx++)
{
const TPair<int32, int32>& CellPair = Cells.PlaneCells[PlaneIdx];
int32 OtherCell = CellPair.Value < 0 ? OutsideCellIndex : CellPair.Value;
if (ensure(CellPlanes.IsValidIndex(CellPair.Key)))
{
CellPlanes[CellPair.Key].Add({ PlaneIdx, false });
}
if (CellPlanes.IsValidIndex(OtherCell))
{
CellPlanes[OtherCell].Add({ PlaneIdx, true });
}
}
// heuristic to protect against creating too many vertices on remeshing
float TotalArea = 0;
for (int32 PlaneIdx = 0; PlaneIdx < Cells.Planes.Num(); PlaneIdx++)
{
const TArray<int>& PlaneBoundary = Cells.PlaneBoundaries[PlaneIdx];
const FVector& V0 = Cells.PlaneBoundaryVertices[PlaneBoundary[0]];
FVector AreaVec = FVector::ZeroVector;
for (int32 V1Idx = 1, V2Idx = 2; V2Idx < PlaneBoundary.Num(); V1Idx = V2Idx++)
{
const FVector& V1 = Cells.PlaneBoundaryVertices[PlaneBoundary[V1Idx]];
const FVector& V2 = Cells.PlaneBoundaryVertices[PlaneBoundary[V2Idx]];
AreaVec += (V1 - V0) ^ (V2 - V1);
}
TotalArea += static_cast<float>(AreaVec.Size());
}
double Spacing = GetSafeNoiseSpacing(TotalArea, Cells.InternalSurfaceMaterials.NoiseSettings->PointSpacing);
ParallelFor(Cells.Planes.Num(), [this, OriginalPositionAttribute, &PlaneMeshes, &Cells, GlobalUVScale, Spacing](int32 PlaneIdx)
{
FDynamicMesh3& Mesh = PlaneMeshes[PlaneIdx];
const TArray<int>& PlaneBoundary = Cells.PlaneBoundaries[PlaneIdx];
FVector3f Normal(Cells.Planes[PlaneIdx].GetNormal());
FFrame3d PlaneFrame(Cells.Planes[PlaneIdx]);
FVertexInfo PlaneVertInfo;
PlaneVertInfo.bHaveC = true;
PlaneVertInfo.bHaveUV = false;
PlaneVertInfo.bHaveN = true;
PlaneVertInfo.Normal = Normal;
// UVs will be set below, after noise is added
// UnsetVertexColor will be set below
FPolygon2f Polygon;
for (int BoundaryVertex : PlaneBoundary)
{
PlaneVertInfo.Position = FVector3d(Cells.PlaneBoundaryVertices[BoundaryVertex]);
Polygon.AppendVertex((FVector2f)PlaneFrame.ToPlaneUV(PlaneVertInfo.Position));
Mesh.AppendVertex(PlaneVertInfo);
}
// we do a CDT here to give a slightly better start to remeshing; we could try simple ear clipping instead
FGeneralPolygon2f GeneralPolygon(Polygon);
FConstrainedDelaunay2f Triangulation;
Triangulation.FillRule = FConstrainedDelaunay2f::EFillRule::NonZero;
Triangulation.Add(GeneralPolygon);
Triangulation.Triangulate();
if (Triangulation.Triangles.Num() == 0) // fall back to ear clipping if the triangulation came back empty
{
PolygonTriangulation::TriangulateSimplePolygon(Polygon.GetVertices(), Triangulation.Triangles, false);
}
if (ensure(Triangulation.Triangles.Num() > 0))
{
int MID = PlaneToMaterial(PlaneIdx);
for (FIndex3i Triangle : Triangulation.Triangles)
{
int TID = Mesh.AppendTriangle(Triangle);
if (ensure(TID > -1))
{
Mesh.Attributes()->GetMaterialID()->SetNewValue(TID, MID);
}
}
RemeshForNoise(Mesh, EEdgeRefineFlags::SplitsOnly, Spacing);
TDynamicMeshVertexAttribute<double, 3>* OriginalPosns =
static_cast<TDynamicMeshVertexAttribute<double, 3>*>(Mesh.Attributes()->GetAttachedAttribute(OriginalPositionAttribute));
for (int VID : Mesh.VertexIndicesItr())
{
OriginalPosns->SetValue(VID, Mesh.GetVertex(VID));
}
ApplyNoise(Mesh, FVector3d(Normal), Cells.InternalSurfaceMaterials.NoiseSettings.GetValue());
FMeshNormals::QuickComputeVertexNormals(Mesh);
}
}, EParallelForFlags::None);
for (int CellIdx = 0; CellIdx < NumCells; CellIdx++)
{
FCellInfo& CellInfo = CellMeshes[CellIdx];
FDynamicMesh3& Mesh = CellInfo.AugMesh;
Mesh.Attributes()->AttachAttribute(OriginalPositionAttribute, new TDynamicMeshVertexAttribute<double, 3>(&Mesh));
bool bFlipForOutsideCell = CellIdx == OutsideCellIndex; // outside cell will be subtracted, and needs all planes flipped vs normal
for (FPlaneIdxAndFlip PlaneInfo : CellPlanes[CellIdx])
{
AppendMesh(Mesh, PlaneMeshes[PlaneInfo.PlaneIdx], PlaneInfo.bIsFlipped ^ bFlipForOutsideCell);
}
}
// resolve self-intersections
// build hash grid of mesh vertices so we correspond all same-pos vertices across touching meshes
TPointHashGrid3d<FIndex2i> MeshesVertices(FMathd::ZeroTolerance * 1000, FIndex2i::Invalid());
for (int CellIdx = 0; CellIdx < NumCells; CellIdx++)
{
FCellInfo& CellInfo = CellMeshes[CellIdx];
FDynamicMesh3& Mesh = CellInfo.AugMesh;
for (int VID : Mesh.VertexIndicesItr())
{
MeshesVertices.InsertPointUnsafe(FIndex2i(CellIdx, VID), Mesh.GetVertex(VID));
}
}
// repeatedly detect and resolve collisions until there are no more (or give up after too many iterations)
TArray<bool> CellUnmoved; CellUnmoved.Init(false, NumCells);
const int MaxIters = 10;
for (int Iters = 0; Iters < MaxIters; Iters++)
{
struct FUpdate
{
FIndex2i Tris;
TArray<FIndex2i> IDs;
FUpdate(int TriA = -1, int TriB = -1) : Tris(TriA, TriB)
{}
};
// todo: can parallelize?
TArray<TArray<FUpdate>> Updates; Updates.SetNum(NumCells);
bool bAnyUpdatesNeeded = false;
for (int CellIdx = 0; CellIdx < NumCells; CellIdx++)
{
if (CellUnmoved[CellIdx])
{
// if nothing moved since last time we resolved self intersections on this cell, don't need to process again
continue;
}
FDynamicMesh3& Mesh = CellMeshes[CellIdx].AugMesh;
FDynamicMeshAABBTree3 CellTree(&Mesh, true);
MeshIntersection::FIntersectionsQueryResult Intersections = CellTree.FindAllSelfIntersections(true);
for (MeshIntersection::FSegmentIntersection& Seg : Intersections.Segments)
{
// manually check for shared edges by vertex position because they might not be topologically connected
FIndex3i Tri[2]{ Mesh.GetTriangle(Seg.TriangleID[0]), Mesh.GetTriangle(Seg.TriangleID[1]) };
int MatchedVertices = 0;
for (int T0SubIdx = 0; T0SubIdx < 3; T0SubIdx++)
{
FVector3d V0 = Mesh.GetVertex(Tri[0][T0SubIdx]);
for (int T1SubIdx = 0; T1SubIdx < 3; T1SubIdx++)
{
FVector3d V1 = Mesh.GetVertex(Tri[1][T1SubIdx]);
if (DistanceSquared(V0, V1) < FMathd::ZeroTolerance)
{
MatchedVertices++;
break;
}
}
}
// no shared vertices: treat as a real collision
// (TODO: only skip shared edges? will need to do something to avoid shared vertices becoming collisions)
if (MatchedVertices < 1)
{
bAnyUpdatesNeeded = true;
FUpdate& Update = Updates[CellIdx].Emplace_GetRef(Seg.TriangleID[0], Seg.TriangleID[1]);
for (int TriIdx = 0; TriIdx < 2; TriIdx++)
{
for (int VSubIdx = 0; VSubIdx < 3; VSubIdx++)
{
int VIdx = Tri[TriIdx][VSubIdx];
FVector3d P = Mesh.GetVertex(VIdx);
FIndex2i IDs(CellIdx, VIdx);
MeshesVertices.FindPointsInBall(P, FMathd::ZeroTolerance, [this, P](FIndex2i IDs)
{
FVector3d Pos = CellMeshes[IDs.A].AugMesh.GetVertex(IDs.B);
return DistanceSquared(P, Pos);
}, Update.IDs);
}
}
}
}
}
if (!bAnyUpdatesNeeded)
{
break;
}
for (int CellIdx = 0; CellIdx < NumCells; CellIdx++)
{
CellUnmoved[CellIdx] = true;
}
// todo: maybe can parallelize if movements are not applied until after?
for (int CellIdx = 0; CellIdx < NumCells; CellIdx++)
{
FDynamicMesh3& Mesh = CellMeshes[CellIdx].AugMesh;
TDynamicMeshVertexAttribute<double, 3>* OriginalPosns =
static_cast<TDynamicMeshVertexAttribute<double, 3>*>(Mesh.Attributes()->GetAttachedAttribute(OriginalPositionAttribute));
auto InterpVert = [&Mesh, &OriginalPosns](int VID, double t)
{
FVector3d OrigPos, NoisePos;
OriginalPosns->GetValue(VID, OrigPos);
NoisePos = Mesh.GetVertex(VID);
return UE::Geometry::Lerp(OrigPos, NoisePos, t);
};
auto InterpTri = [&Mesh, &InterpVert](int TID, double t)
{
FIndex3i TriVIDs = Mesh.GetTriangle(TID);
FTriangle3d Tri;
for (int i = 0; i < 3; i++)
{
Tri.V[i] = InterpVert(TriVIDs[i], t);
}
return Tri;
};
auto TestIntersection = [&InterpTri](int TIDA, int TIDB, double t)
{
FIntrTriangle3Triangle3d TriTri(InterpTri(TIDA, t), InterpTri(TIDB, t));
return TriTri.Find();
};
// resolve tri-tri intersections on this cell's mesh (moving associated verts on other meshes as needed also)
for (FUpdate& Update : Updates[CellIdx])
{
double tsafe = 0;
double tbad = 1;
if (!TestIntersection(Update.Tris.A, Update.Tris.B, tbad))
{
continue;
}
for (int SearchSteps = 0; SearchSteps < 4; SearchSteps++)
{
double tmid = (tsafe + tbad) * .5;
if (TestIntersection(Update.Tris.A, Update.Tris.B, tmid))
{
tbad = tmid;
}
else
{
tsafe = tmid;
}
}
CellUnmoved[CellIdx] = false;
for (FIndex2i IDs : Update.IDs)
{
FVector3d OldPos = CellMeshes[IDs.A].AugMesh.GetVertex(IDs.B);
FVector3d NewPos;
if (IDs.A == CellIdx)
{
NewPos = InterpVert(IDs.B, tsafe);
Mesh.SetVertex(IDs.B, NewPos);
}
else
{
CellUnmoved[IDs.A] = false;
FDynamicMesh3& OtherMesh = CellMeshes[IDs.A].AugMesh;
TDynamicMeshVertexAttribute<double, 3>* OtherOriginalPosns =
static_cast<TDynamicMeshVertexAttribute<double, 3>*>(OtherMesh.Attributes()->GetAttachedAttribute(OriginalPositionAttribute));
FVector3d OrigPos;
OtherOriginalPosns->GetValue(IDs.B, OrigPos);
NewPos = UE::Geometry::Lerp(OrigPos, OldPos, tsafe);
OtherMesh.SetVertex(IDs.B, NewPos);
}
MeshesVertices.UpdatePoint(IDs, OldPos, NewPos);
}
}
}
}
// clear "original position" attribute now that we have removed self-intersections
// and set unset vertex colors
for (int CellIdx = 0; CellIdx < NumCells; CellIdx++)
{
FCellInfo& CellInfo = CellMeshes[CellIdx];
FDynamicMesh3& Mesh = CellInfo.AugMesh;
Mesh.Attributes()->RemoveAttribute(OriginalPositionAttribute);
for (int VID : Mesh.VertexIndicesItr())
{
AugmentedDynamicMesh::SetVertexColor(Mesh, VID, AugmentedDynamicMesh::UnsetVertexColor);
}
}
// recompute UVs using new positions after noise was applied + fixed
TArray<FVector2f> PlaneMinUVs; PlaneMinUVs.Init(FVector2f(FMathf::MaxReal, FMathf::MaxReal), Cells.Planes.Num());
TArray<FFrame3d> PlaneFrames; PlaneFrames.Reserve(Cells.Planes.Num());
for (int PlaneIdx = 0; PlaneIdx < Cells.Planes.Num(); PlaneIdx++)
{
PlaneFrames.Add(AxisAlignedFrame(Cells.Planes[PlaneIdx]));
}
// first pass to compute min UV for each plane
for (FCellInfo& CellInfo : CellMeshes)
{
FDynamicMesh3& Mesh = CellInfo.AugMesh;
FDynamicMeshMaterialAttribute* MaterialIDs = Mesh.Attributes()->GetMaterialID();
for (int TID : Mesh.TriangleIndicesItr())
{
int PlaneIdx = MaterialToPlane(MaterialIDs->GetValue(TID));
if (PlaneIdx > -1)
{
FIndex3i Tri = Mesh.GetTriangle(TID);
for (int Idx = 0; Idx < 3; Idx++)
{
FVector2f UV = FVector2f(PlaneFrames[PlaneIdx].ToPlaneUV(Mesh.GetVertex(Tri[Idx])));
FVector2f& MinUV = PlaneMinUVs[PlaneIdx];
MinUV.X = FMathf::Min(UV.X, MinUV.X);
MinUV.Y = FMathf::Min(UV.Y, MinUV.Y);
}
}
}
}
// second pass to actually set UVs
for (FCellInfo& CellInfo : CellMeshes)
{
FDynamicMesh3& Mesh = CellInfo.AugMesh;
FDynamicMeshMaterialAttribute* MaterialIDs = Mesh.Attributes()->GetMaterialID();
for (int TID : Mesh.TriangleIndicesItr())
{
int PlaneIdx = MaterialToPlane(MaterialIDs->GetValue(TID));
if (PlaneIdx > -1)
{
FIndex3i Tri = Mesh.GetTriangle(TID);
for (int Idx = 0; Idx < 3; Idx++)
{
FVector2f UV = ((FVector2f)PlaneFrames[PlaneIdx].ToPlaneUV(Mesh.GetVertex(Tri[Idx])) - PlaneMinUVs[PlaneIdx]) * static_cast<float>(GlobalUVScale);
for (int UVLayerIdx = 0; UVLayerIdx < NumUVLayers; UVLayerIdx++)
{
AugmentedDynamicMesh::SetUV(Mesh, Tri[Idx], UV, UVLayerIdx);
}
}
}
}
}
}
void FCellMeshes::CreateMeshesForSinglePlane(const FPlanarCells& Cells, const FAxisAlignedBox3d& DomainBounds, bool bNoise, double GlobalUVScale, double Grout, bool bOnlyGrout)
{
bool bHasGrout = Grout > 0;
int MID = PlaneToMaterial(0);
FPlane Plane = Cells.Planes[0];
FFrame3d PlaneFrame = AxisAlignedFrame(Plane);
FInterval1d ZRange;
FAxisAlignedBox2d XYRange;
for (int CornerIdx = 0; CornerIdx < 8; CornerIdx++)
{
FVector3d Corner = DomainBounds.GetCorner(CornerIdx);
XYRange.Contain(PlaneFrame.ToPlaneUV(Corner));
ZRange.Contain(Plane.PlaneDot(FVector(Corner)));
}
//if (FMathd::SignNonZero(ZRange.Min) == FMathd::SignNonZero(ZRange.Max))
//{
// // TODO: early out for plane that doesn't even intersect the domain bounding box?
//}
FDynamicMesh3 PlaneMesh(true, true, false, false);
AugmentedDynamicMesh::EnableUVChannels(PlaneMesh, NumUVLayers);
FVertexInfo PlaneVertInfo;
PlaneVertInfo.bHaveC = true;
PlaneVertInfo.bHaveN = true;
PlaneVertInfo.Normal = -FVector3f(Plane.GetNormal());
for (int CornerIdx = 0; CornerIdx < 4; CornerIdx++)
{
PlaneVertInfo.Position = PlaneFrame.FromPlaneUV(XYRange.GetCorner(CornerIdx));
FVector2f UV = FVector2f(XYRange.GetCorner(CornerIdx) - XYRange.Min) * static_cast<float>(GlobalUVScale);
int VID = PlaneMesh.AppendVertex(PlaneVertInfo);
AugmentedDynamicMesh::SetAllUV(PlaneMesh, VID, UV, NumUVLayers);
}
PlaneMesh.AppendTriangle(0, 1, 2);
PlaneMesh.AppendTriangle(0, 2, 3);
if (bNoise)
{
double Spacing = GetSafeNoiseSpacing(static_cast<float>(XYRange.Area()), Cells.InternalSurfaceMaterials.NoiseSettings->PointSpacing);
RemeshForNoise(PlaneMesh, EEdgeRefineFlags::SplitsOnly, Spacing);
ApplyNoise(PlaneMesh, PlaneFrame.GetAxis(2), Cells.InternalSurfaceMaterials.NoiseSettings.GetValue(), true);
FMeshNormals::QuickComputeVertexNormals(PlaneMesh);
}
TArray<int> PlaneBoundary, // loop of vertex IDs on the boundary of PlaneMesh (starting with vertex 0)
PlaneBoundaryCornerIndices; // indices of the corner vertices in the PlaneBoundary array
{
double Offset = ZRange.Max;
FMeshBoundaryLoops Boundary(&PlaneMesh);
checkSlow(Boundary.GetLoopCount() == 1);
int FirstIdx;
bool bFound = Boundary[0].Vertices.Find(0, FirstIdx);
checkSlow(bFound);
PlaneBoundary = Boundary[0].Vertices;
if (FirstIdx != 0)
{
Algo::Rotate(PlaneBoundary, FirstIdx);
}
checkSlow(PlaneBoundary[0] == 0);
PlaneBoundaryCornerIndices.Add(0);
int FoundIndices = 1;
for (int VIDIdx = 0; VIDIdx < PlaneBoundary.Num(); VIDIdx++)
{
int VID = PlaneBoundary[VIDIdx];
if (VID == FoundIndices)
{
FoundIndices++;
PlaneBoundaryCornerIndices.Add(VIDIdx);
}
}
}
FDynamicMesh3* Meshes[2];
if (!bOnlyGrout)
{
for (int Side = 0; Side < 2; Side++)
{
Meshes[Side] = &CellMeshes[Side].AugMesh;
*Meshes[Side] = PlaneMesh;
double Offset = ZRange.Max;
TArray<int> CapBoundary, CapBoundaryCornerIndices;
if (Side == 0)
{
Meshes[Side]->ReverseOrientation(true);
Offset = ZRange.Min;
}
PlaneVertInfo.Normal = FVector3f(Plane.GetNormal()) * (-1.0f + (float)Side * 2.0f);
FVector3d OffsetVec = FVector3d(Plane.GetNormal()) * Offset;
for (int CornerIdx = 0; CornerIdx < 4; CornerIdx++)
{
PlaneVertInfo.Position = Meshes[Side]->GetVertex(CornerIdx) + OffsetVec;
// UVs shouldn't matter for outer box vertices because they're outside of the domain by construction ...
CapBoundary.Add(Meshes[Side]->AppendVertex(PlaneVertInfo));
CapBoundaryCornerIndices.Add(CornerIdx);
}
int NewTris[2]{
Meshes[Side]->AppendTriangle(CapBoundary[0], CapBoundary[1], CapBoundary[2]),
Meshes[Side]->AppendTriangle(CapBoundary[0], CapBoundary[2], CapBoundary[3])
};
if (Side == 1)
{
Meshes[Side]->ReverseTriOrientation(NewTris[0]);
Meshes[Side]->ReverseTriOrientation(NewTris[1]);
}
FDynamicMeshEditor Editor(Meshes[Side]);
FDynamicMeshEditResult ResultOut;
Editor.StitchSparselyCorrespondedVertexLoops(PlaneBoundary, PlaneBoundaryCornerIndices, CapBoundary, CapBoundaryCornerIndices, ResultOut, Side == 0);
}
}
if (bHasGrout)
{
int GroutIdx = bOnlyGrout ? 0 : 2;
FDynamicMesh3* GroutMesh = &CellMeshes[GroutIdx].AugMesh;
FVector3d GroutOffset = (FVector3d)Plane.GetNormal() * (Grout * .5);
if (!bOnlyGrout)
{
for (int Side = 0; Side < 2; Side++)
{
// shift both sides out by Grout/2
MeshTransforms::Translate(*Meshes[Side], GroutOffset * (-1.0 + (double)Side * 2.0));
}
}
// make the center (grout) by stitching together two offset copies of PlaneMesh
*GroutMesh = PlaneMesh;
GroutMesh->ReverseOrientation(true);
MeshTransforms::Translate(*GroutMesh, GroutOffset);
FMeshIndexMappings IndexMaps;
FDynamicMeshEditor Editor(GroutMesh);
Editor.AppendMesh(&PlaneMesh, IndexMaps, [GroutOffset](int VID, const FVector3d& PosIn) {return PosIn - GroutOffset; });
TArray<int> AppendPlaneBoundary; AppendPlaneBoundary.Reserve(PlaneBoundary.Num());
TArray<int> RevBoundary = PlaneBoundary;
Algo::Reverse(RevBoundary);
for (int VID : RevBoundary)
{
AppendPlaneBoundary.Add(IndexMaps.GetNewVertex(VID));
}
FDynamicMeshEditResult ResultOut;
Editor.StitchVertexLoopsMinimal(RevBoundary, AppendPlaneBoundary, ResultOut);
}
// fix up custom attributes and material IDs for all meshes
for (int CellIdx = 0; CellIdx < CellMeshes.Num(); CellIdx++)
{
FDynamicMesh3& Mesh = CellMeshes[CellIdx].AugMesh;
// re-enable tangents and visibility attributes, since these are lost when we set the mesh to a copy of the plane mesh
AugmentedDynamicMesh::Augment(Mesh, NumUVLayers);
// Set all material IDs to the one plane's corresponding material ID
for (int TID : Mesh.TriangleIndicesItr())
{
Mesh.Attributes()->GetMaterialID()->SetNewValue(TID, MID);
}
}
}
template<typename TransformType>
void FDynamicMeshCollection::InitTemplate(const FGeometryCollection* Collection, TArrayView<const TransformType> Transforms, const TArrayView<const int32>& TransformIndices, FTransform TransformCollection, bool bSaveIsolatedVertices)
{
GeometryCollection::UV::FConstUVLayers UVLayers = GeometryCollection::UV::FindActiveUVLayers(*Collection);
int32 NumUVLayers = UVLayers.Num();
Meshes.Reset();
Bounds = FAxisAlignedBox3d::Empty();
for (int32 TransformIdx : TransformIndices)
{
int32 GeometryIdx = Collection->TransformToGeometryIndex[TransformIdx];
if (GeometryIdx == INDEX_NONE)
{
// only store the mesh if there is associated geometry
continue;
}
FTransformSRT3d CollectionToLocal;
if (bComponentSpaceTransforms)
{
CollectionToLocal = FTransformSRT3d(FTransform(Transforms[TransformIdx]) * TransformCollection);
}
else
{
CollectionToLocal = FTransformSRT3d(GeometryCollectionAlgo::GlobalMatrix(Transforms, TArrayView<const int32>(Collection->Parent.GetConstArray()), TransformIdx) * TransformCollection);
}
int32 AddedMeshIdx = Meshes.Add(new FMeshData(NumUVLayers));
FMeshData& MeshData = Meshes[AddedMeshIdx];
MeshData.TransformIndex = TransformIdx;
MeshData.FromCollection = FTransform(CollectionToLocal);
FDynamicMesh3& Mesh = MeshData.AugMesh;
Mesh.EnableAttributes();
Mesh.Attributes()->EnableMaterialID();
int32 VertexStart = Collection->VertexStart[GeometryIdx];
int32 VertexCount = Collection->VertexCount[GeometryIdx];
int32 FaceCount = Collection->FaceCount[GeometryIdx];
FVertexInfo VertexInfo;
VertexInfo.bHaveC = true;
VertexInfo.bHaveN = true;
VertexInfo.bHaveUV = false;
for (int32 Idx = VertexStart, N = VertexStart + VertexCount; Idx < N; Idx++)
{
VertexInfo.Position = CollectionToLocal.TransformPosition(FVector3d(Collection->Vertex[Idx]));
VertexInfo.Color = FVector3f(Collection->Color[Idx]);
VertexInfo.Normal = (FVector3f)CollectionToLocal.TransformVectorNoScale(FVector3d(Collection->Normal[Idx]));
int VID = Mesh.AppendVertex(VertexInfo);
AugmentedDynamicMesh::SetVertexColor(Mesh, VID, FVector4f(Collection->Color[Idx]));
AugmentedDynamicMesh::SetTangent(Mesh, VID, VertexInfo.Normal,
(FVector3f)CollectionToLocal.TransformVectorNoScale(FVector3d(Collection->TangentU[Idx])),
(FVector3f)CollectionToLocal.TransformVectorNoScale(FVector3d(Collection->TangentV[Idx])));
for (int32 UVLayer = 0; UVLayer < NumUVLayers; ++UVLayer)
{
AugmentedDynamicMesh::SetUV(Mesh, VID, UVLayers[UVLayer][Idx], UVLayer);
}
}
FIntVector VertexOffset(VertexStart, VertexStart, VertexStart);
for (int32 Idx = Collection->FaceStart[GeometryIdx], N = Collection->FaceStart[GeometryIdx] + FaceCount; Idx < N; Idx++)
{
if (bSkipInvisible && !Collection->Visible[Idx])
{
continue;
}
FIndex3i AddTri = FIndex3i(Collection->Indices[Idx] - VertexOffset);
int TID = Mesh.AppendTriangle(AddTri, 0);
if (TID == FDynamicMesh3::NonManifoldID)
{
// work around non-manifold triangles by copying the vertices
FIndex3i NewTri(-1, -1, -1);
for (int SubIdx = 0; SubIdx < 3; SubIdx++)
{
int NewVID = Mesh.AppendVertex(Mesh, AddTri[SubIdx]);
int32 SrcIdx = AddTri[SubIdx] + VertexStart;
AugmentedDynamicMesh::SetTangent(Mesh, NewVID,
Mesh.GetVertexNormal(NewVID), // TODO: we don't actually use the vertex normal; consider removing this arg from the function entirely
(FVector3f)CollectionToLocal.TransformVectorNoScale(FVector3d(Collection->TangentU[SrcIdx])),
(FVector3f)CollectionToLocal.TransformVectorNoScale(FVector3d(Collection->TangentV[SrcIdx])));
for (int32 UVLayer = 0; UVLayer < NumUVLayers; ++UVLayer)
{
AugmentedDynamicMesh::SetUV(Mesh, NewVID, UVLayers[UVLayer][SrcIdx], UVLayer);
}
NewTri[SubIdx] = NewVID;
}
TID = Mesh.AppendTriangle(NewTri, 0);
}
if (TID < 0)
{
continue;
}
if (bGenerateMeshToCollectionFaceMapping)
{
MeshData.AddMeshToCollectionFaceMapping(TID, Idx);
}
Mesh.Attributes()->GetMaterialID()->SetValue(TID, Collection->MaterialID[Idx]);
AugmentedDynamicMesh::SetVisibility(Mesh, TID, Collection->Visible[Idx]);
AugmentedDynamicMesh::SetInternal(Mesh, TID, Collection->Internal[Idx]);
// note: material index doesn't need to be passed through; will be rebuilt by a call to reindex materials once the cut mesh is returned back to geometry collection format
}
if (!bSaveIsolatedVertices)
{
FDynamicMeshEditor Editor(&Mesh);
Editor.RemoveIsolatedVertices();
}
Bounds.Contain(MeshData.GetCachedBounds());
// TODO: build spatial data (add this after setting up mesh boolean path that can use it)
//MeshData.Spatial.SetMesh(&Mesh);
}
}
void FDynamicMeshCollection::Init(const FGeometryCollection* Collection, TArrayView<const FTransform> Transforms, const TArrayView<const int32>& TransformIndices, FTransform TransformCollection, bool bSaveIsolatedVertices)
{
InitTemplate(Collection, Transforms, TransformIndices, TransformCollection, bSaveIsolatedVertices);
}
void FDynamicMeshCollection::Init(const FGeometryCollection* Collection, TArrayView<const FTransform3f> Transforms, const TArrayView<const int32>& TransformIndices, FTransform TransformCollection, bool bSaveIsolatedVertices)
{
InitTemplate(Collection, Transforms, TransformIndices, TransformCollection, bSaveIsolatedVertices);
}
void FDynamicMeshCollection::SetGeometryVisibility(FGeometryCollection* Collection, const TArray<int32>& GeometryIndices, bool bVisible)
{
TManagedArray<bool>& Visible = Collection->Visible;
for (int32 GeoIdx : GeometryIndices)
{
int32 FaceStart = Collection->FaceStart[GeoIdx];
int32 FaceEnd = FaceStart + Collection->FaceCount[GeoIdx];
for (int32 FaceIdx = FaceStart; FaceIdx < FaceEnd; FaceIdx++)
{
Visible[FaceIdx] = bVisible;
}
}
}
int32 FDynamicMeshCollection::CutWithMultiplePlanes(
const TArrayView<const FPlane>& Planes,
double Grout,
double CollisionSampleSpacing,
bool bSplitIslands,
int32 RandomSeed,
FGeometryCollection* Collection,
FInternalSurfaceMaterials& InternalSurfaceMaterials,
bool bSetDefaultInternalMaterialsFromCollection,
FProgressCancel* Progress
)
{
bool bHasGrout = Grout > 0;
int32 NumUVLayers = Collection->NumUVLayers();
FRandomStream RandomStream(RandomSeed);
const float PerPlaneWorkFrac = 1.0f / Planes.Num();
if (bHasGrout)
{
// For multi-plane cuts with grout specifically, the easiest path seems to be:
// 1. Build the "grout" section of each plane
// 2. Take the union of all those grout sections as the grout mesh
// 3. Use the generic CutWithCellMeshes path, where that grout mesh is both the inner and outside cell mesh
// (Note the outside cell mesh is subtracted, not intersected)
// (Note this relies on island splitting to separate all the pieces afterwards.)
FCellMeshes GroutCells(NumUVLayers, RandomStream);
GroutCells.SetNumCells(2);
FDynamicMesh3& GroutMesh = GroutCells.CellMeshes[0].AugMesh;
FDynamicMeshEditor GroutAppender(&GroutMesh);
FMeshIndexMappings IndexMaps;
for (int32 PlaneIdx = 0; PlaneIdx < Planes.Num(); PlaneIdx++)
{
FProgressCancel::FProgressScope MakeMeshScope(Progress, .1f * PerPlaneWorkFrac);
FPlanarCells PlaneCells(Planes[PlaneIdx]);
PlaneCells.InternalSurfaceMaterials = InternalSurfaceMaterials;
FCellMeshes PlaneGroutMesh(NumUVLayers, RandomStream);
PlaneGroutMesh.MakeOnlyPlanarGroutCell(PlaneCells, Bounds, Grout);
GroutAppender.AppendMesh(&PlaneGroutMesh.CellMeshes[0].AugMesh, IndexMaps);
if (Progress && Progress->Cancelled())
{
return INDEX_NONE;
}
}
FProgressCancel::FProgressScope GroutUnionScope(Progress, .3);
FMeshSelfUnion GroutUnion(&GroutMesh);
GroutUnion.bSimplifyAlongNewEdges = true;
GroutUnion.bWeldSharedEdges = false;
GroutUnion.Compute();
GroutUnionScope.Done();
if (Progress && Progress->Cancelled())
{
return INDEX_NONE;
}
FProgressCancel::FProgressScope GroutCutScope = FProgressCancel::CreateScopeTo(Progress, 1);
// first mesh is the same as the second mesh, but will be subtracted b/c it's the "outside cell"
GroutCells.CellMeshes[1].AugMesh = GroutMesh;
GroutCells.OutsideCellIndex = 1;
TArray<TPair<int32, int32>> CellConnectivity;
CellConnectivity.Add(TPair<int32, int32>(0, -1));
return CutWithCellMeshes(InternalSurfaceMaterials, CellConnectivity, GroutCells, bSplitIslands, Collection, bSetDefaultInternalMaterialsFromCollection, CollisionSampleSpacing);
}
TArray<TUniquePtr<FMeshData>> ToCut;
// copy initial surfaces
for (FMeshData& MeshData : Meshes)
{
ToCut.Add(MakeUnique<FMeshData>(MeshData));
}
for (int32 PlaneIdx = 0; PlaneIdx < Planes.Num(); PlaneIdx++)
{
if (Progress && Progress->Cancelled())
{
return INDEX_NONE;
}
FProgressCancel::FProgressScope OnePlaneScope(Progress, PerPlaneWorkFrac);
FPlanarCells PlaneCells(Planes[PlaneIdx]);
PlaneCells.InternalSurfaceMaterials = InternalSurfaceMaterials;
double OnePercentExtend = Bounds.MaxDim() * .01;
FCellMeshes CellMeshes(NumUVLayers, RandomStream, PlaneCells, Bounds, 0, OnePercentExtend, false);
// TODO: we could do these cuts in parallel (will takes some rework of how results are added to the ToCut array)
for (int32 ToCutIdx = 0, ToCutNum = ToCut.Num(); ToCutIdx < ToCutNum; ToCutIdx++)
{
FMeshData& Surface = *ToCut[ToCutIdx];
int32 TransformIndex = Surface.TransformIndex;
FTransform FromCollection = Surface.FromCollection;
FAxisAlignedBox3d Box = Surface.GetCachedBounds();
if (InternalSurfaceMaterials.NoiseSettings.IsSet())
{
Box.Expand(InternalSurfaceMaterials.NoiseSettings->Amplitude);
}
if (!FMath::PlaneAABBIntersection(Planes[PlaneIdx], FBox(Box)))
{
continue;
}
TArray<TUniquePtr<FMeshData>> BoolResults;
for (int ResultIdx = 0; ResultIdx < 2; ResultIdx++)
{
BoolResults.Add(MakeUnique<FMeshData>(NumUVLayers));
BoolResults[ResultIdx]->TransformIndex = TransformIndex;
BoolResults[ResultIdx]->FromCollection = FromCollection;
}
check(CellMeshes.CellMeshes.Num() == 2);
bool bKeepResults = true;
for (int32 CellIdx = 0; CellIdx < 2; CellIdx++)
{
FCellMeshes::FCellInfo& Cell = CellMeshes.CellMeshes[CellIdx];
FMeshBoolean::EBooleanOp Op = FMeshBoolean::EBooleanOp::Intersect;
if (CellIdx == CellMeshes.OutsideCellIndex)
{
Op = FMeshBoolean::EBooleanOp::Difference;
}
FMeshBoolean Boolean(&Surface.AugMesh, &Cell.AugMesh, &BoolResults[CellIdx]->AugMesh, Op);
Boolean.bSimplifyAlongNewEdges = true;
Boolean.PreserveUVsOnlyForMesh = 0; // slight warping of the autogenerated cell UVs generally doesn't matter
Boolean.bWeldSharedEdges = false;
Boolean.bTrackAllNewEdges = true;
if (!Boolean.Compute())
{
// TODO: do something about failure cases? e.g. try auto-filling small holes?
// note: failure cases won't be detected at all unless we weld edges,
// which will require re-working how tangents are carried through
}
if (BoolResults[CellIdx]->AugMesh.TriangleCount() == 0)
{
bKeepResults = false;
break;
}
constexpr bool bFillHoles = false; // TODO: consider enabling hole filler optionally, or improving the hole filler and enabling it all the time
if (bFillHoles)
{
AugmentedDynamicMesh::FillHoles(BoolResults[CellIdx]->AugMesh, Boolean.AllNewEdges);
}
}
if (bKeepResults)
{
ToCut[ToCutIdx] = MoveTemp(BoolResults[0]);
int32 NewIdx = ToCut.Add(MoveTemp(BoolResults[1]));
// indices of all boolean result meshes (may be more than two due to splitting disconnected components)
TArray<int32, TInlineAllocator<4>> ResultIndices = { ToCutIdx, NewIdx };
// corresponding parent indices for each result mesh
TArray<int32, TInlineAllocator<4>> ParentIndices = { 0, 1 };
TArray<FDynamicMesh3> SplitMeshes;
for (int UnsplitIdx = 0; UnsplitIdx < 2; UnsplitIdx++)
{
if (bSplitIslands && SplitIslands(ToCut[ResultIndices[UnsplitIdx]]->AugMesh, SplitMeshes))
{
ToCut[ResultIndices[UnsplitIdx]]->SetMesh(SplitMeshes[0]);
for (int32 Idx = 1; Idx < SplitMeshes.Num(); Idx++)
{
const FDynamicMesh3& Mesh = SplitMeshes[Idx];
ResultIndices.Add(ToCut.Add(MakeUnique<FMeshData>(Mesh, TransformIndex, FromCollection)));
ParentIndices.Add(UnsplitIdx);
}
}
}
} // iteration over meshes to cut
} // iteration over cutting planes
}
TMultiMap<int32, int32> ParentTransformToChildren;
for (int32 ToCutIdx = 0; ToCutIdx < ToCut.Num(); ToCutIdx++)
{
ParentTransformToChildren.Add(ToCut[ToCutIdx]->TransformIndex, ToCutIdx);
}
TArray<int32> ToCutIdxToGeometryIdx; ToCutIdxToGeometryIdx.Init(-1, ToCut.Num());
TArray<int32> ToCutIndices;
int32 FirstCreatedIndex = -1;
TArray<int32> GeometryForRemoval;
for (FMeshData& MeshData : Meshes)
{
int32 GeometryIdx = Collection->TransformToGeometryIndex[MeshData.TransformIndex];
int32 InternalMaterialID = bSetDefaultInternalMaterialsFromCollection ? InternalSurfaceMaterials.GetDefaultMaterialIDForGeometry(*Collection, GeometryIdx) : InternalSurfaceMaterials.GlobalMaterialID;
ToCutIndices.Reset();
ParentTransformToChildren.MultiFind(MeshData.TransformIndex, ToCutIndices);
// if there's only one mesh here, i.e. it didn't get cut at all
if (ToCutIndices.Num() <= 1)
{
continue;
}
// remove old parent geometry
GeometryForRemoval.Add(GeometryIdx);
// add newly created geometry as children
int32 SubPartIdx = 0;
for (int32 ToCutIdx : ToCutIndices)
{
FDynamicMesh3& Mesh = ToCut[ToCutIdx]->AugMesh;
FString BoneName = GetBoneName(*Collection, ToCut[ToCutIdx]->TransformIndex, SubPartIdx++);
int32 CreatedGeometryIdx = AppendToCollection(ToCut[ToCutIdx]->FromCollection, Mesh, CollisionSampleSpacing, ToCut[ToCutIdx]->TransformIndex, BoneName, *Collection, InternalMaterialID);
ToCutIdxToGeometryIdx[ToCutIdx] = CreatedGeometryIdx;
if (FirstCreatedIndex == -1)
{
FirstCreatedIndex = CreatedGeometryIdx;
}
}
}
if (Progress && Progress->Cancelled())
{
return INDEX_NONE;
}
SetUnsetColors(Collection, FirstCreatedIndex);
int32 NewFirstIndex = FirstCreatedIndex;
constexpr bool bRemoveOldGeometry = false; // if false, we just hide the geometry that we've replaced by fractured child geometry, rather than remove it
if (GeometryForRemoval.Num() > 0)
{
if (bRemoveOldGeometry)
{
GeometryForRemoval.Sort();
FManagedArrayCollection::FProcessingParameters ProcessingParams;
#if !UE_BUILD_DEBUG
ProcessingParams.bDoValidation = false;
#endif
Collection->RemoveElements(FGeometryCollection::GeometryGroup, GeometryForRemoval, ProcessingParams);
NewFirstIndex -= GeometryForRemoval.Num();
}
else
{
SetGeometryVisibility(Collection, GeometryForRemoval, false);
}
}
return NewFirstIndex;
}
int32 FDynamicMeshCollection::SplitAllIslands(FGeometryCollection* Collection, double CollisionSampleSpacing)
{
int32 FirstIdx = -1;
TArray<int32> GeometryForRemoval;
for (FMeshData& Surface : Meshes)
{
int32 SrcGeometryIdx = Collection->TransformToGeometryIndex[Surface.TransformIndex];
int32 CreatedGeometryIdx = -1;
TArray<FDynamicMesh3> Islands;
if (SplitIslands(Surface.AugMesh, Islands))
{
for (int32 i = 0; i < Islands.Num(); i++)
{
FDynamicMesh3& Island = Islands[i];
FString BoneName = GetBoneName(*Collection, Surface.TransformIndex, i);
constexpr int32 InternalMaterialID = 0; // Note: there won't be new internal faces (to assign materials) from a split, so this value won't affect the output here
CreatedGeometryIdx = AppendToCollection(Surface.FromCollection, Island, CollisionSampleSpacing, Surface.TransformIndex, BoneName, *Collection, InternalMaterialID);
if (FirstIdx == -1)
{
FirstIdx = CreatedGeometryIdx;
}
}
GeometryForRemoval.Add(SrcGeometryIdx);
}
}
int32 NewFirstIdx = FirstIdx;
// remove or hide superfluous geometry
constexpr bool bRemoveOldGeometry = false; // if false, we just hide the geometry that we've replaced by fractured child geometry, rather than remove it
if (GeometryForRemoval.Num() > 0)
{
if (bRemoveOldGeometry)
{
GeometryForRemoval.Sort();
FManagedArrayCollection::FProcessingParameters ProcessingParams;
#if !UE_BUILD_DEBUG
ProcessingParams.bDoValidation = false;
#endif
Collection->RemoveElements(FGeometryCollection::GeometryGroup, GeometryForRemoval, ProcessingParams);
NewFirstIdx -= GeometryForRemoval.Num();
}
else
{
SetGeometryVisibility(Collection, GeometryForRemoval, false);
}
}
return NewFirstIdx;
}
int32 FDynamicMeshCollection::CutWithCellMeshes(const FInternalSurfaceMaterials& InternalSurfaceMaterials, const TArray<TPair<int32, int32>>& CellConnectivity, FCellMeshes& CellMeshes, bool bSplitIslands, FGeometryCollection* Collection, bool bSetDefaultInternalMaterialsFromCollection, double CollisionSampleSpacing)
{
// TODO: should we do these cuts in parallel, and the appends sequentially below?
int32 FirstIdx = -1;
int BadCount = 0;
TArray<int32> GeometryForRemoval;
TArray<FAxisAlignedBox3d> CellBounds; CellBounds.Reserve(CellMeshes.CellMeshes.Num());
for (int CellIdx = 0; CellIdx < CellMeshes.CellMeshes.Num(); CellIdx++)
{
CellBounds.Add(CellMeshes.CellMeshes[CellIdx].AugMesh.GetBounds(true));
}
for (FMeshData& Surface : Meshes)
{
int32 GeometryIdx = Collection->TransformToGeometryIndex[Surface.TransformIndex];
TArray<TUniquePtr<FDynamicMesh3>> BooleanResults; BooleanResults.SetNum(CellMeshes.CellMeshes.Num());
FAxisAlignedBox3d SurfaceBounds = Surface.GetCachedBounds();
ParallelFor(CellMeshes.CellMeshes.Num(), [&BooleanResults, &CellMeshes, &CellBounds, &Surface, &SurfaceBounds](int32 CellIdx)
{
FCellMeshes::FCellInfo& Cell = CellMeshes.CellMeshes[CellIdx];
if (CellBounds[CellIdx].Intersects(SurfaceBounds))
{
BooleanResults[CellIdx] = MakeUnique<FDynamicMesh3>();
FDynamicMesh3& AugBoolResult = *BooleanResults[CellIdx];
FMeshBoolean::EBooleanOp Op = FMeshBoolean::EBooleanOp::Intersect;
if (CellIdx == CellMeshes.OutsideCellIndex)
{
Op = FMeshBoolean::EBooleanOp::Difference;
}
FMeshBoolean Boolean(&Surface.AugMesh, &Cell.AugMesh, &AugBoolResult, Op);
Boolean.bSimplifyAlongNewEdges = true;
Boolean.PreserveUVsOnlyForMesh = 0; // slight warping of the autogenerated cell UVs generally doesn't matter
Boolean.bWeldSharedEdges = false;
Boolean.bTrackAllNewEdges = true;
if (!Boolean.Compute())
{
// TODO: do something about failure cases? e.g. try auto-filling small holes?
// note: failure cases won't be detected at all unless we weld edges,
// which will require re-working how tangents are carried through
}
constexpr bool bFillHoles = false; // TODO: consider enabling hole filler optionally, or improving the hole filler and enabling it all the time
if (bFillHoles)
{
AugmentedDynamicMesh::FillHoles(AugBoolResult, Boolean.AllNewEdges);
}
}
}, EParallelForFlags::None);
int32 NonEmptyResults = 0;
for (const TUniquePtr<FDynamicMesh3>& AugBoolResult : BooleanResults)
{
if (AugBoolResult.IsValid() && AugBoolResult->TriangleCount() > 0)
{
NonEmptyResults++;
}
}
if (NonEmptyResults > 1) // only write to geometry collection if more than one result was non-empty
{
TSet<int32> PlanesInOutput;
TMultiMap<int32, int32> CellToGeometry;
TMap<int32, int32> GeometryToResultMesh;
int32 SubPartIndex = 0;
int32 InternalMaterialID = bSetDefaultInternalMaterialsFromCollection ? InternalSurfaceMaterials.GetDefaultMaterialIDForGeometry(*Collection, GeometryIdx) : InternalSurfaceMaterials.GlobalMaterialID;
for (int32 CellIdx = 0; CellIdx < CellMeshes.CellMeshes.Num(); CellIdx++)
{
if (BooleanResults[CellIdx].IsValid() && BooleanResults[CellIdx]->TriangleCount() > 0)
{
FDynamicMesh3& AugBoolResult = *BooleanResults[CellIdx];
for (int TID : AugBoolResult.TriangleIndicesItr())
{
int MID = AugBoolResult.Attributes()->GetMaterialID()->GetValue(TID);
int32 PlaneIdx = CellMeshes.MaterialToPlane(MID);
if (PlaneIdx >= 0)
{
PlanesInOutput.Add(PlaneIdx);
}
}
int32 CreatedGeometryIdx = -1;
TArray<FDynamicMesh3> Islands;
if (bSplitIslands && SplitIslands(AugBoolResult, Islands))
{
for (int32 i = 0; i < Islands.Num(); i++)
{
FDynamicMesh3& Island = Islands[i];
FString BoneName = GetBoneName(*Collection, Surface.TransformIndex, SubPartIndex++);
CreatedGeometryIdx = AppendToCollection(Surface.FromCollection, Island, CollisionSampleSpacing, Surface.TransformIndex, BoneName, *Collection, InternalMaterialID);
CellToGeometry.Add(CellIdx, CreatedGeometryIdx);
if (i > 0)
{
GeometryToResultMesh.Add(CreatedGeometryIdx, BooleanResults.Add(MakeUnique<FDynamicMesh3>(Island)));
}
else
{
*BooleanResults[CellIdx] = Island;
GeometryToResultMesh.Add(CreatedGeometryIdx, CellIdx);
}
if(FirstIdx == -1)
{
FirstIdx = CreatedGeometryIdx;
}
}
}
else
{
FString BoneName = GetBoneName(*Collection, Surface.TransformIndex, SubPartIndex++);
CreatedGeometryIdx = AppendToCollection(Surface.FromCollection, AugBoolResult, CollisionSampleSpacing, Surface.TransformIndex, BoneName, *Collection, InternalMaterialID);
CellToGeometry.Add(CellIdx, CreatedGeometryIdx);
GeometryToResultMesh.Add(CreatedGeometryIdx, CellIdx);
if(FirstIdx == -1)
{
FirstIdx = CreatedGeometryIdx;
}
}
}
}
// remove old geom
GeometryForRemoval.Add(GeometryIdx);
}
}
SetUnsetColors(Collection, FirstIdx);
int32 NewFirstIdx = FirstIdx;
// remove or hide superfluous geometry
constexpr bool bRemoveOldGeometry = false; // if false, we just hide the geometry that we've replaced by fractured child geometry, rather than remove it
if (GeometryForRemoval.Num() > 0)
{
if (bRemoveOldGeometry)
{
GeometryForRemoval.Sort();
FManagedArrayCollection::FProcessingParameters ProcessingParams;
#if !UE_BUILD_DEBUG
ProcessingParams.bDoValidation = false;
#endif
Collection->RemoveElements(FGeometryCollection::GeometryGroup, GeometryForRemoval, ProcessingParams);
NewFirstIdx -= GeometryForRemoval.Num();
}
else
{
SetGeometryVisibility(Collection, GeometryForRemoval, false);
}
}
return NewFirstIdx;
}
void FDynamicMeshCollection::FillVertexHash(const FDynamicMesh3& Mesh, TPointHashGrid3d<int>& VertHash)
{
for (int VID : Mesh.VertexIndicesItr())
{
FVector3d V = Mesh.GetVertex(VID);
VertHash.InsertPointUnsafe(VID, V);
}
}
bool FDynamicMeshCollection::IsNeighboring(
UE::Geometry::FDynamicMesh3& MeshA, const UE::Geometry::TPointHashGrid3d<int>& VertHashA, const UE::Geometry::FAxisAlignedBox3d& BoundsA,
UE::Geometry::FDynamicMesh3& MeshB, const UE::Geometry::TPointHashGrid3d<int>& VertHashB, const UE::Geometry::FAxisAlignedBox3d& BoundsB)
{
UE::Geometry::FDynamicMesh3* Mesh[2]{ &MeshA, &MeshB };
const UE::Geometry::TPointHashGrid3d<int>* VertHash[2]{ &VertHashA, &VertHashB };
if (!ensure(Mesh[0] && Mesh[1] && VertHash[0] && VertHash[1]))
{
return false;
}
if (!BoundsA.Intersects(BoundsB))
{
return false;
}
int A = 0, B = 1;
if (Mesh[0]->VertexCount() > Mesh[1]->VertexCount())
{
Swap(A, B);
}
FDynamicMesh3& RefMesh = *Mesh[B];
for (const FVector3d& V : Mesh[A]->VerticesItr())
{
TPair<int, double> Nearest = VertHash[B]->FindNearestInRadius(V, FMathd::ZeroTolerance * 10, [&RefMesh, &V](int VID)
{
return DistanceSquared(RefMesh.GetVertex(VID), V);
});
if (Nearest.Key != -1)
{
return true;
}
}
return false;
}
// Split mesh into connected components, including implicit connections by co-located vertices
bool FDynamicMeshCollection::SplitIslands(FDynamicMesh3& Source, TArray<FDynamicMesh3>& SeparatedMeshes, double SnapDistance)
{
TPointHashGrid3d<int> VertHash(SnapDistance * 10, -1);
FDisjointSet VertComponents(Source.MaxVertexID());
// Add Source vertices to hash & disjoint sets
for (int VID : Source.VertexIndicesItr())
{
FVector3d Pt = Source.GetVertex(VID);
VertHash.EnumeratePointsInBall(Pt, SnapDistance, [&Source, Pt](int OtherVID) {return DistanceSquared(Pt, Source.GetVertex(OtherVID)); }, [&](int32 NbrVID, double DSq) -> bool
{
VertComponents.UnionSequential(VID, NbrVID);
return true;
});
VertHash.InsertPointUnsafe(VID, Pt);
}
for (FIndex3i Tri : Source.TrianglesItr())
{
VertComponents.Union(Tri.A, Tri.B);
VertComponents.Union(Tri.B, Tri.C);
}
bool bWasSplit = FDynamicMeshEditor::SplitMesh(&Source, SeparatedMeshes, [&Source, &VertComponents](int TID)
{
return (int)VertComponents.Find(Source.GetTriangle(TID).A);
});
if (bWasSplit)
{
// disconnected components that are contained inside other components need to be re-merged
TMeshSpatialSort<FDynamicMesh3> SpatialSort(SeparatedMeshes);
SpatialSort.NestingMethod = TMeshSpatialSort<FDynamicMesh3>::ENestingMethod::InLargestParent;
SpatialSort.bOnlyNestNegativeVolumes = false;
SpatialSort.bOnlyParentPostiveVolumes = true;
SpatialSort.Compute();
TArray<bool> KeepMeshes; KeepMeshes.Init(true, SeparatedMeshes.Num());
for (TMeshSpatialSort<FDynamicMesh3>::FMeshNesting& Nest : SpatialSort.Nests)
{
FDynamicMeshEditor Editor(&SeparatedMeshes[Nest.OuterIndex]);
FMeshIndexMappings Mappings;
for (int Inner : Nest.InnerIndices)
{
Editor.AppendMesh(&SeparatedMeshes[Inner], Mappings);
KeepMeshes[Inner] = false;
}
}
for (int Idx = 0; Idx < SeparatedMeshes.Num(); Idx++)
{
if (!KeepMeshes[Idx])
{
SeparatedMeshes.RemoveAtSwap(Idx, EAllowShrinking::No);
KeepMeshes.RemoveAtSwap(Idx, EAllowShrinking::No);
Idx--;
}
}
}
return bWasSplit;
}
void FDynamicMeshCollection::AddCollisionSamples(double CollisionSampleSpacing)
{
for (int32 MeshIdx = 0; MeshIdx < Meshes.Num(); MeshIdx++)
{
AugmentedDynamicMesh::AddCollisionSamplesPerComponent(Meshes[MeshIdx].AugMesh, CollisionSampleSpacing);
}
}
// Update all geometry in a GeometryCollection w/ the meshes in the MeshCollection
// Resizes the GeometryCollection as needed
bool FDynamicMeshCollection::UpdateAllCollections(FGeometryCollection& Collection)
{
bool bAllSucceeded = true;
int32 NumGeometry = Collection.NumElements(FGeometryCollection::GeometryGroup);
TArray<int32> NewFaceCounts, NewVertexCounts;
NewFaceCounts.SetNumUninitialized(NumGeometry);
NewVertexCounts.SetNumUninitialized(NumGeometry);
for (int32 GeomIdx = 0; GeomIdx < Collection.FaceCount.Num(); GeomIdx++)
{
NewFaceCounts[GeomIdx] = Collection.FaceCount[GeomIdx];
NewVertexCounts[GeomIdx] = Collection.VertexCount[GeomIdx];
}
for (int MeshIdx = 0; MeshIdx < Meshes.Num(); MeshIdx++)
{
FDynamicMeshCollection::FMeshData& MeshData = Meshes[MeshIdx];
int32 GeomIdx = Collection.TransformToGeometryIndex[MeshData.TransformIndex];
NewFaceCounts[GeomIdx] = MeshData.AugMesh.TriangleCount();
NewVertexCounts[GeomIdx] = MeshData.AugMesh.VertexCount();
}
bool bDoValidation = false;
#if UE_BUILD_DEBUG
bDoValidation = true;
#endif
GeometryCollectionAlgo::ResizeGeometries(&Collection, NewFaceCounts, NewVertexCounts, bDoValidation);
for (int MeshIdx = 0; MeshIdx < Meshes.Num(); MeshIdx++)
{
FDynamicMeshCollection::FMeshData& MeshData = Meshes[MeshIdx];
FDynamicMesh3& Mesh = MeshData.AugMesh;
int32 GeometryIdx = Collection.TransformToGeometryIndex[MeshData.TransformIndex];
bool bSucceeded = UpdateCollection(MeshData.FromCollection, Mesh, GeometryIdx, Collection, -1);
bAllSucceeded &= bSucceeded;
}
SetUnsetColors(&Collection, 0, false);
return bAllSucceeded;
}
// Update an existing geometry in a collection w/ a new mesh (w/ the same number of faces and vertices!)
bool FDynamicMeshCollection::UpdateCollection(const FTransform& FromCollection, FDynamicMesh3& Mesh, int32 GeometryIdx, FGeometryCollection& Output, int32 InternalMaterialID)
{
if (!Mesh.IsCompact())
{
Mesh.CompactInPlace(nullptr);
}
int32 OldVertexCount = Output.VertexCount[GeometryIdx];
int32 OldTriangleCount = Output.FaceCount[GeometryIdx];
int32 UVLayerCount = AugmentedDynamicMesh::NumEnabledUVChannels(Mesh);
Output.SetNumUVLayers(UVLayerCount);
GeometryCollection::UV::FUVLayers OutputUVLayers = GeometryCollection::UV::FindActiveUVLayers(Output);
int32 NewVertexCount = Mesh.VertexCount();
int32 NewTriangleCount = Mesh.TriangleCount();
if (!ensure(OldVertexCount == NewVertexCount) || !ensure(OldTriangleCount == NewTriangleCount))
{
return false;
}
int32 VerticesStart = Output.VertexStart[GeometryIdx];
int32 FacesStart = Output.FaceStart[GeometryIdx];
int32 TransformIdx = Output.TransformIndex[GeometryIdx];
for (int32 VID = 0; VID < Mesh.MaxVertexID(); VID++)
{
checkSlow(Mesh.IsVertex(VID)); // mesh is compact
int32 CopyToIdx = VerticesStart + VID;
Output.Vertex[CopyToIdx] = (FVector3f)FromCollection.InverseTransformPosition(FVector(Mesh.GetVertex(VID)));
Output.Normal[CopyToIdx] = (FVector3f)FromCollection.InverseTransformVectorNoScale(FVector(Mesh.GetVertexNormal(VID)));
for (int32 UVLayer = 0; UVLayer < UVLayerCount; ++UVLayer)
{
FVector2f UV;
AugmentedDynamicMesh::GetUV(Mesh, VID, UV, UVLayer);
OutputUVLayers[UVLayer][CopyToIdx] = UV;
}
FVector3f TangentU, TangentV;
AugmentedDynamicMesh::GetTangent(Mesh, VID, TangentU, TangentV);
Output.TangentU[CopyToIdx] = (FVector3f)FromCollection.InverseTransformVectorNoScale(FVector(TangentU));
Output.TangentV[CopyToIdx] = (FVector3f)FromCollection.InverseTransformVectorNoScale(FVector(TangentV));
Output.Color[CopyToIdx] = FLinearColor(AugmentedDynamicMesh::GetVertexColor(Mesh, VID));
// Bone map is set based on the transform of the new geometry
Output.BoneMap[CopyToIdx] = TransformIdx;
}
FIntVector VertexStartOffset(VerticesStart);
for (int32 TID = 0; TID < Mesh.MaxTriangleID(); TID++)
{
checkSlow(Mesh.IsTriangle(TID));
int32 CopyToIdx = FacesStart + TID;
Output.Visible[CopyToIdx] = AugmentedDynamicMesh::GetVisibility(Mesh, TID);
int MaterialID = Mesh.Attributes()->GetMaterialID()->GetValue(TID);
// negative material IDs are, by convention, indications of new (internal) geometry, positive are copied through
Output.Internal[CopyToIdx] = MaterialID < 0 ? true : AugmentedDynamicMesh::GetInternal(Mesh, TID);
Output.MaterialID[CopyToIdx] = MaterialID < 0 ? InternalMaterialID : MaterialID;
Output.Indices[CopyToIdx] = FIntVector(Mesh.GetTriangle(TID)) + VertexStartOffset;
}
if (Output.BoundingBox.Num())
{
Output.BoundingBox[GeometryIdx].Init();
for (int32 Idx = VerticesStart; Idx < VerticesStart + Output.VertexCount[GeometryIdx]; ++Idx)
{
Output.BoundingBox[GeometryIdx] += (FVector)Output.Vertex[Idx];
}
}
return true;
}
int32 FDynamicMeshCollection::AppendToCollection(const FTransform& FromCollection, FDynamicMesh3& Mesh, double CollisionSampleSpacing, int32 TransformParent, FString BoneName, FGeometryCollection& Output, int32 InternalMaterialID)
{
if (Mesh.TriangleCount() == 0)
{
return -1;
}
if (!Mesh.IsCompact())
{
Mesh.CompactInPlace(nullptr);
}
if (CollisionSampleSpacing > 0)
{
AugmentedDynamicMesh::AddCollisionSamplesPerComponent(Mesh, CollisionSampleSpacing);
}
int32 NewGeometryStartIdx = Output.FaceStart.Num();
int32 OriginalVertexNum = Output.Vertex.Num();
int32 OriginalFaceNum = Output.Indices.Num();
int32 UVLayerCount = AugmentedDynamicMesh::NumEnabledUVChannels(Mesh);
if (!ensure(UVLayerCount > 0))
{
UVLayerCount = 1;
}
int32 GeometryIdx = Output.AddElements(1, FGeometryCollection::GeometryGroup);
int32 TransformIdx = Output.AddElements(1, FGeometryCollection::TransformGroup);
int32 NumTriangles = Mesh.TriangleCount();
int32 NumVertices = Mesh.VertexCount();
check(NumTriangles > 0);
check(Mesh.IsCompact());
Output.FaceCount[GeometryIdx] = NumTriangles;
Output.FaceStart[GeometryIdx] = OriginalFaceNum;
Output.VertexCount[GeometryIdx] = NumVertices;
Output.VertexStart[GeometryIdx] = OriginalVertexNum;
Output.TransformIndex[GeometryIdx] = TransformIdx;
Output.TransformToGeometryIndex[TransformIdx] = GeometryIdx;
if (TransformParent > -1)
{
Output.BoneName[TransformIdx] = BoneName;
Output.BoneColor[TransformIdx] = Output.BoneColor[TransformParent];
Output.Parent[TransformIdx] = TransformParent;
Output.Children[TransformParent].Add(TransformIdx);
Output.SimulationType[TransformParent] = FGeometryCollection::ESimulationTypes::FST_Clustered;
}
Output.Transform[TransformIdx] = FTransform3f::Identity;
Output.SimulationType[TransformIdx] = FGeometryCollection::ESimulationTypes::FST_Rigid;
int32 FacesStart = Output.AddElements(NumTriangles, FGeometryCollection::FacesGroup);
int32 VerticesStart = Output.AddElements(NumVertices, FGeometryCollection::VerticesGroup);
Output.SetNumUVLayers(UVLayerCount);
GeometryCollection::UV::FUVLayers OutputUVLayers = GeometryCollection::UV::FindActiveUVLayers(Output);
for (int32 VID = 0; VID < Mesh.MaxVertexID(); VID++)
{
checkSlow(Mesh.IsVertex(VID)); // mesh is compact
int32 CopyToIdx = VerticesStart + VID;
Output.Vertex[CopyToIdx] = (FVector3f)FromCollection.InverseTransformPosition(FVector(Mesh.GetVertex(VID)));
Output.Normal[CopyToIdx] = (FVector3f)FromCollection.InverseTransformVectorNoScale(FVector(Mesh.GetVertexNormal(VID)));
for (int32 UVLayer = 0; UVLayer < UVLayerCount; ++UVLayer)
{
FVector2f UV;
AugmentedDynamicMesh::GetUV(Mesh, VID, UV, UVLayer);
OutputUVLayers[UVLayer][CopyToIdx] = UV;
}
FVector3f TangentU, TangentV;
AugmentedDynamicMesh::GetTangent(Mesh, VID, TangentU, TangentV);
Output.TangentU[CopyToIdx] = (FVector3f)FromCollection.InverseTransformVectorNoScale(FVector(TangentU));
Output.TangentV[CopyToIdx] = (FVector3f)FromCollection.InverseTransformVectorNoScale(FVector(TangentV));
Output.Color[CopyToIdx] = FLinearColor(AugmentedDynamicMesh::GetVertexColor(Mesh, VID));
// Bone map is set based on the transform of the new geometry
Output.BoneMap[CopyToIdx] = TransformIdx;
}
FIntVector VertexStartOffset(VerticesStart);
for (int32 TID = 0; TID < Mesh.MaxTriangleID(); TID++)
{
checkSlow(Mesh.IsTriangle(TID));
int32 CopyToIdx = FacesStart + TID;
Output.Visible[CopyToIdx] = AugmentedDynamicMesh::GetVisibility(Mesh, TID);
int MaterialID = Mesh.Attributes()->GetMaterialID()->GetValue(TID);
// negative material IDs are, by convention, indications of new (internal) geometry, positive are copied through
Output.Internal[CopyToIdx] = MaterialID < 0 ? true : AugmentedDynamicMesh::GetInternal(Mesh, TID);
Output.MaterialID[CopyToIdx] = MaterialID < 0 ? InternalMaterialID : MaterialID;
Output.Indices[CopyToIdx] = FIntVector(Mesh.GetTriangle(TID)) + VertexStartOffset;
}
if (Output.BoundingBox.Num())
{
Output.BoundingBox[GeometryIdx].Init();
for (int32 Idx = OriginalVertexNum; Idx < Output.Vertex.Num(); ++Idx)
{
Output.BoundingBox[GeometryIdx] += (FVector)Output.Vertex[Idx];
}
}
return GeometryIdx;
}
}} // namespace UE::PlanarCut