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

746 lines
23 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
//#include "Operations/UniformTessellate.h"
#include "Operations/MeshClusterSimplifier.h"
#include "Async/ParallelFor.h"
#include "DynamicMesh/DynamicMesh3.h"
#include "DynamicMesh/DynamicMeshAttributeSet.h"
#include "VectorTypes.h"
namespace UE::MeshClusterSimplifyLocals
{
template<typename ElemType, int Dim, typename AttributeType>
void CopyAttribs(AttributeType* Result, const AttributeType* Source, TConstArrayView<int32> ResToSource, int32 Num)
{
ParallelFor(Num, [&ResToSource, &Source, &Result](int32 ResID)
{
int32 SourceID = ResToSource[ResID];
ElemType ToCopy[Dim];
Source->GetValue(SourceID, ToCopy);
Result->SetValue(ResID, ToCopy);
}
);
}
}
namespace UE::Geometry::MeshClusterSimplify
{
bool Simplify(const FDynamicMesh3& InMesh, FDynamicMesh3& ResultMesh, const FSimplifyOptions& SimplifyOptions)
{
TRACE_CPUPROFILER_EVENT_SCOPE(MeshClusterSimplify::Simplify);
// we build the result mesh by incrementally copying from the input mesh, so they shouldn't be the same mesh
if (!ensure(&ResultMesh != &InMesh))
{
return false;
}
ResultMesh.Clear();
const FDynamicMeshAttributeSet* InAttribs = InMesh.Attributes();
// We tag edges and vertices w/ the constraint level, abbreviated to EElemTag for convenience
using EElemTag = FSimplifyOptions::EConstraintLevel;
constexpr int32 NUM_TAGS = 3;
// TODO: optionally also compute some vertex curvature feature & sort by it, to favor capturing less flat parts of the input shape?
///
/// Step 1, Data Prep: Translate all mesh constraint options to simple per-edge and per-vertex tags, so we know what to try to especially preserve in the result
///
// Compute an Edge ID -> Constraint Level mapping
TArray<EElemTag> EdgeTags;
EdgeTags.SetNumUninitialized(InMesh.MaxEdgeID());
ParallelFor(InMesh.MaxEdgeID(), [&InMesh, &InAttribs, &SimplifyOptions, &EdgeTags](int32 EID)
{
if (!InMesh.IsEdge(EID))
{
return;
}
EElemTag UseTag = EElemTag::Free;
if ((uint8)SimplifyOptions.PreserveEdges.Boundary < (uint8)UseTag && InMesh.IsBoundaryEdge(EID))
{
UseTag = SimplifyOptions.PreserveEdges.Boundary;
}
if ((uint8)SimplifyOptions.PreserveEdges.PolyGroup < (uint8)UseTag && InMesh.IsGroupBoundaryEdge(EID))
{
UseTag = SimplifyOptions.PreserveEdges.PolyGroup;
}
if (InAttribs)
{
if ((uint8)SimplifyOptions.PreserveEdges.Material < (uint8)UseTag && InAttribs->IsMaterialBoundaryEdge(EID))
{
UseTag = SimplifyOptions.PreserveEdges.Material;
}
if ((uint8)SimplifyOptions.PreserveEdges.UVSeam < (uint8)UseTag)
{
for (int32 UVLayer = 0; UVLayer < InAttribs->NumUVLayers(); ++UVLayer)
{
if (InAttribs->GetUVLayer(UVLayer)->IsSeamEdge(EID))
{
UseTag = SimplifyOptions.PreserveEdges.UVSeam;
break;
}
}
}
if ((uint8)SimplifyOptions.PreserveEdges.TangentSeam < (uint8)UseTag)
{
for (int32 NormalLayer = 1; NormalLayer < InAttribs->NumNormalLayers(); ++NormalLayer)
{
if (InAttribs->GetNormalLayer(NormalLayer)->IsSeamEdge(EID))
{
UseTag = SimplifyOptions.PreserveEdges.TangentSeam;
break;
}
}
}
if ((uint8)SimplifyOptions.PreserveEdges.NormalSeam < (uint8)UseTag)
{
if (const FDynamicMeshNormalOverlay* Normals = InAttribs->PrimaryNormals())
{
if (Normals->IsSeamEdge(EID))
{
UseTag = SimplifyOptions.PreserveEdges.NormalSeam;
}
}
}
if ((uint8)SimplifyOptions.PreserveEdges.ColorSeam < (uint8)UseTag)
{
if (const FDynamicMeshColorOverlay* Colors = InAttribs->PrimaryColors())
{
if (Colors->IsSeamEdge(EID))
{
UseTag = SimplifyOptions.PreserveEdges.ColorSeam;
}
}
}
}
EdgeTags[EID] = UseTag;
});
TArray<EElemTag> VertexTags;
VertexTags.SetNumUninitialized(InMesh.MaxVertexID());
double CosBoundaryEdgeAngleTolerance = FMath::Cos(FMath::DegreesToRadians(FMath::Clamp(SimplifyOptions.FixBoundaryAngleTolerance, 0, 180)));
auto IsSeamIntersectionVertex =
[&InAttribs, &SimplifyOptions]
(int32 VID)
{
if (SimplifyOptions.PreserveEdges.UVSeam == EElemTag::Constrained)
{
for (int32 Layer = 0; Layer < InAttribs->NumUVLayers(); ++Layer)
{
if (InAttribs->GetUVLayer(Layer)->IsSeamIntersectionVertex(VID))
{
return true;
}
}
}
if (SimplifyOptions.PreserveEdges.NormalSeam == EElemTag::Constrained)
{
if (const FDynamicMeshNormalOverlay* Normals = InAttribs->PrimaryNormals())
{
if (Normals->IsSeamIntersectionVertex(VID))
{
return true;
}
}
}
if (SimplifyOptions.PreserveEdges.TangentSeam == EElemTag::Constrained)
{
for (int32 Layer = 1; Layer < InAttribs->NumNormalLayers(); ++Layer)
{
if (InAttribs->GetNormalLayer(Layer)->IsSeamIntersectionVertex(VID))
{
return true;
}
}
}
if (SimplifyOptions.PreserveEdges.ColorSeam == EElemTag::Constrained)
{
if (const FDynamicMeshColorOverlay* Colors = InAttribs->PrimaryColors())
{
if (Colors->IsSeamIntersectionVertex(VID))
{
return true;
}
}
}
return false;
};
ParallelFor(InMesh.MaxVertexID(),
[&InMesh, &InAttribs, &SimplifyOptions,
&VertexTags, &EdgeTags,
&IsSeamIntersectionVertex,
CosBoundaryEdgeAngleTolerance]
(int32 VID)
{
if (!InMesh.IsVertex(VID))
{
return;
}
int32 FixedCount = 0;
int32 ConstrainedCount = 0;
FVector3d BoundaryEdgeVert[2];
int32 FoundBoundaryEdgeVerts = 0;
InMesh.EnumerateVertexEdges(VID,
[&EdgeTags, VID, &FixedCount, &ConstrainedCount, &BoundaryEdgeVert,
&FoundBoundaryEdgeVerts, &SimplifyOptions, &InMesh]
(int32 EID)
{
FixedCount += int32(EdgeTags[EID] == EElemTag::Fixed);
if (EdgeTags[EID] == EElemTag::Constrained)
{
ConstrainedCount++;
if (SimplifyOptions.FixBoundaryAngleTolerance > 0)
{
if (InMesh.IsBoundaryEdge(EID))
{
if (FoundBoundaryEdgeVerts < 2)
{
FIndex2i EdgeV = InMesh.GetEdgeV(EID);
int32 OtherV = EdgeV.A == VID ? EdgeV.B : EdgeV.A;
BoundaryEdgeVert[FoundBoundaryEdgeVerts] = InMesh.GetVertex(OtherV);
}
FoundBoundaryEdgeVerts++;
}
}
}
}
);
if (FixedCount > 0)
{
VertexTags[VID] = EElemTag::Fixed;
return;
}
if (FoundBoundaryEdgeVerts == 2)
{
FVector3d CenterV = InMesh.GetVertex(VID);
FVector3d E1 = Normalized(BoundaryEdgeVert[0] - CenterV);
FVector3d E2 = Normalized(CenterV - BoundaryEdgeVert[1]);
if (E1.Dot(E2) < CosBoundaryEdgeAngleTolerance)
{
VertexTags[VID] = EElemTag::Fixed;
return;
}
}
if (ConstrainedCount > 0)
{
if (ConstrainedCount == 2 &&
// seams are a special case where we can have two constrained edges but still be at a seam intersection
// (e.g. at a vertex that joins two different types of seam)
(!InAttribs || !IsSeamIntersectionVertex(VID))
)
{
// constrain vertices along contiguous constrained edge paths
VertexTags[VID] = EElemTag::Constrained;
return;
}
else
{
// fix vertices at constraint intersections
VertexTags[VID] = EElemTag::Fixed;
return;
}
}
VertexTags[VID] = EElemTag::Free;
});
///
/// Step 2. Clustering: Grow vertex clusters out to the target edge length size
///
// Buckets of vertices to process -- vertices that are processed sooner are more likely to be directly included in the output
TStaticArray<TArray<int32>, NUM_TAGS> ProcessBuckets;
for (int32 VID : InMesh.VertexIndicesItr())
{
ProcessBuckets[(int32)VertexTags[VID]].Add(VID);
}
TArray<float> SourceDist;
TArray<int32> Source;
Source.Init(INDEX_NONE, InMesh.MaxVertexID());
SourceDist.Init(FMathf::MaxReal, InMesh.MaxVertexID());
auto TagVerticesByRegionGrowth =
[&Source, &SourceDist, &InMesh, &SimplifyOptions, &EdgeTags, &VertexTags]
(const TStaticArray<TArray<int32>, NUM_TAGS>& VertexIDBuckets)
{
// add all the fixed vertices as sources first, so they can't be claimed by other verts
for (int32 VID : VertexIDBuckets[(int32)EElemTag::Fixed])
{
Source[VID] = VID;
SourceDist[VID] = 0.f;
}
struct FWalk
{
int32 VID;
float Dist;
bool operator<(const FWalk& Other) const
{
return Dist < Other.Dist;
}
};
TArray<FWalk> HeapV;
// for the non-fixed vertices, progressively grow from vertices, in passes from more-constrained to less-constrained edges
for (uint8 TagIdx = 1; TagIdx < (uint8)NUM_TAGS; ++TagIdx)
{
for (uint8 BucketIdx = 0; BucketIdx <= TagIdx; ++BucketIdx)
{
const TArray<int32>& CurBucket = VertexIDBuckets[BucketIdx];
for (int32 InBucketIdx = 0; InBucketIdx < CurBucket.Num(); ++InBucketIdx)
{
int32 GrowFromVID = CurBucket[InBucketIdx];
int32& CurSourceVID = Source[GrowFromVID];
float& CurSourceDist = SourceDist[GrowFromVID];
// the vertex is unclaimed, claim it as a new source/kept vertex
if (CurSourceVID == INDEX_NONE)
{
CurSourceVID = GrowFromVID;
CurSourceDist = 0.f;
}
// if the vertex was claimed by another source in the current tag pass, no need to process it further
else if (CurSourceVID != GrowFromVID && (uint8)VertexTags[GrowFromVID] == TagIdx)
{
continue;
}
// vertex is either a new source, or previously claimed but we need to consider growing via less-constrained edges
// helper to add candidate verts to a heap
HeapV.Reset();
auto AddCandidates = [MaxDist = SimplifyOptions.TargetEdgeLength,
&HeapV, &InMesh, &SourceDist, &Source, &EdgeTags, &VertexTags, TagIdx]
(const FWalk& From)
{
// expand to one-ring
InMesh.EnumerateVertexEdges(From.VID,
[&From, MaxDist,
&HeapV, &InMesh, &SourceDist, &Source, &EdgeTags, &VertexTags, TagIdx]
(int32 EID)
{
if ((uint8)EdgeTags[EID] != TagIdx)
{
return;
}
FIndex2i EdgeV = InMesh.GetEdgeV(EID);
int32 ToVID = EdgeV.A == From.VID ? EdgeV.B : EdgeV.A;
if ((uint8)VertexTags[ToVID] < TagIdx || From.Dist >= SourceDist[ToVID])
{
// vertex was already claimed by more-constrained context, or is already as close (or closer) to another source
return;
}
// possible candidate, compute the actual distance and grow if close enough
FVector3d Pos = InMesh.GetVertex(ToVID);
FVector3d FromPos = InMesh.GetVertex(From.VID);
float NewDist = From.Dist + (float)FVector3d::Dist(Pos, FromPos);
if (NewDist < MaxDist && NewDist < SourceDist[ToVID])
{
// Viable candidate distance; add to heap
HeapV.HeapPush(FWalk{ ToVID, NewDist });
}
}
);
};
// initialize the heap w/ the neighbors of the initial grow-from vertex
FWalk Start{ GrowFromVID, CurSourceDist };
AddCandidates(Start);
while (!HeapV.IsEmpty())
{
FWalk CurWalk;
HeapV.HeapPop(CurWalk, EAllowShrinking::No);
// we already got to this vert from another place
if (SourceDist[CurWalk.VID] <= CurWalk.Dist)
{
continue;
}
// claim the vertex
SourceDist[CurWalk.VID] = CurWalk.Dist;
Source[CurWalk.VID] = CurSourceVID;
// search its (current-tag-level) edges for more verts to claim
AddCandidates(CurWalk);
}
}
}
}
};
TagVerticesByRegionGrowth(ProcessBuckets);
for (int32 Idx = 0; Idx < ProcessBuckets.Num(); ++Idx)
{
ProcessBuckets[Idx].Empty();
}
///
/// Step 3: Copy the cluster connectivity out to our ResultMesh
///
TArray<int32> ToResVID, FromResVID;
TArray<int32> ResultToSourceTri;
// If simplification introduces non-manifold edges, we can often recover by fixing more vertices and re-attempting the build.
// After MeshBuildAttempts tries, if still failing, we stop adding vertices and just duplicate vertices to add the non-manifold triangles.
// TODO: We could potentially analyze the cluster connectivity more carefully handle more degenerate cluster connectivity, more robustly.
// (if so -- it may be better to do so by analyzing the graph before building the ResultMesh, rather than this rebuilding approach!)
int32 MeshBuildAttempts = 2;
bool bResultHasDuplicateVertices = false;
while (MeshBuildAttempts-- > 0)
{
// clear mesh outputs
ToResVID.Reset();
FromResVID.Reset();
ResultToSourceTri.Reset();
ResultMesh.Clear();
bool bAllowDegenerate = MeshBuildAttempts <= 0;
// Array of vertex IDs to set to 'fixed' on a rebuild attempt
TArray<int32> SourceVIDToFix;
ToResVID.Init(INDEX_NONE, InMesh.MaxVertexID());
for (int32 VID = 0; VID < Source.Num(); ++VID)
{
if (Source[VID] == VID)
{
ToResVID[VID] = ResultMesh.AppendVertex(InMesh.GetVertex(VID));
// we need the reverse mapping if we're transferring seams
if (SimplifyOptions.bTransferAttributes)
{
FromResVID.Add(VID);
}
}
}
for (int32 TID : InMesh.TriangleIndicesItr())
{
FIndex3i Tri = InMesh.GetTriangle(TID);
FIndex3i SourceTri(Source[Tri.A], Source[Tri.B], Source[Tri.C]);
if (SourceTri.A != SourceTri.B && SourceTri.A != SourceTri.C && SourceTri.B != SourceTri.C)
{
FIndex3i ResTri(ToResVID[SourceTri.A], ToResVID[SourceTri.B], ToResVID[SourceTri.C]);
int32 ResultTID = ResultMesh.AppendTriangle(ResTri);
if (ResultTID == FDynamicMesh3::NonManifoldID)
{
if (bAllowDegenerate)
{
// TODO: only duplicate vertices on the non-manifold edge(s)
FIndex3i ExtraTri;
ExtraTri.A = ResultMesh.AppendVertex(ResultMesh.GetVertex(ResTri.A));
FromResVID.Add(SourceTri.A);
ExtraTri.B = ResultMesh.AppendVertex(ResultMesh.GetVertex(ResTri.B));
FromResVID.Add(SourceTri.B);
ExtraTri.C = ResultMesh.AppendVertex(ResultMesh.GetVertex(ResTri.C));
FromResVID.Add(SourceTri.C);
ResultTID = ResultMesh.AppendTriangle(ExtraTri);
bResultHasDuplicateVertices = true;
}
else
{
// Non-manifold edges can often be resolved by adding an extra vertex --
// mark the vertex with largest SourceDist for inclusion in the result mesh
int32 BestSubIdx = INDEX_NONE;
float BestDist = 0;
for (int32 SubIdx = 0; SubIdx < 3; ++SubIdx)
{
if (SourceDist[Tri[SubIdx]] > BestDist)
{
BestDist = SourceDist[Tri[SubIdx]];
BestSubIdx = SubIdx;
}
}
if (BestSubIdx != INDEX_NONE)
{
SourceVIDToFix.Add(Tri[BestSubIdx]);
}
}
}
if ((SimplifyOptions.bTransferAttributes || SimplifyOptions.bTransferGroups) && ResultTID >= 0)
{
checkSlow(ResultTID == ResultToSourceTri.Num()); // ResultMesh starts empty and should be compact
ResultToSourceTri.Add(TID);
}
}
}
// We marked some new vertices for inclusion in the result; tag them and re-try
if (!bAllowDegenerate && SourceVIDToFix.Num() > 0)
{
for (int32 VID : SourceVIDToFix)
{
VertexTags[VID] = EElemTag::Fixed;
}
ProcessBuckets[0] = MoveTemp(SourceVIDToFix);
TagVerticesByRegionGrowth(ProcessBuckets);
continue;
}
// Accept the result mesh triangulation
break;
}
///
/// Step 4: After accepting the final ResultMesh triangulation, copy the input mesh's attributes (UVs, materials, etc) over as well
///
if (SimplifyOptions.bTransferAttributes)
{
ResultMesh.EnableMatchingAttributes(InMesh);
if (InMesh.HasAttributes())
{
FDynamicMeshAttributeSet* ResultAttribs = ResultMesh.Attributes();
const bool bPreserveAnySeams =
SimplifyOptions.PreserveEdges.UVSeam != EElemTag::Free ||
SimplifyOptions.PreserveEdges.NormalSeam != EElemTag::Free ||
SimplifyOptions.PreserveEdges.TangentSeam != EElemTag::Free ||
SimplifyOptions.PreserveEdges.ColorSeam != EElemTag::Free;
// Seam mapping for overlays
{
// Compute a general wedge mapping that all the overlays can build from
// Map from ResultTID -> a source triangle per tri-vertex [aka wedge]
TArray<FIndex3i> ResultWedgeSourceTris;
// sub-indices per wedge
TArray<int8> SourceTriWedgeSubIndices;
ResultWedgeSourceTris.SetNumUninitialized(ResultMesh.MaxTriangleID());
SourceTriWedgeSubIndices.SetNumUninitialized(ResultMesh.MaxTriangleID() * 3);
ParallelFor(ResultMesh.MaxTriangleID(),
[&ResultMesh, &ResultWedgeSourceTris, &SourceTriWedgeSubIndices,
bPreserveAnySeams, &FromResVID, &ResultToSourceTri, &Source,
&VertexTags, &EdgeTags, &InMesh]
(int32 ResultTID)
{
TArray<int32> TriQ;
TSet<int32> LocalSeenTris;
FIndex3i ResultVIDs = ResultMesh.GetTriangle(ResultTID);
for (int32 SubIdx = 0; SubIdx < 3; ++SubIdx)
{
int32 ResultVID = ResultVIDs[SubIdx];
int32 SourceVID = FromResVID[ResultVID];
bool bFound = false;
// we're on a seam vertex, do a local search (w/out crossing seam edges) to from the init triangle to the source vertex
// to try to find the best tri to use as a wedge reference
if (VertexTags[SourceVID] != EElemTag::Free && bPreserveAnySeams)
{
TriQ.Reset();
LocalSeenTris.Reset();
int32 SourceTID = ResultToSourceTri[ResultTID];
TriQ.Add(SourceTID);
while (!TriQ.IsEmpty())
{
int32 SearchTID = TriQ.Pop(EAllowShrinking::No);
if (LocalSeenTris.Contains(SearchTID))
{
continue;
}
LocalSeenTris.Add(SearchTID);
FIndex3i Tri = InMesh.GetTriangle(SearchTID);
int32 FoundSubIdx = Tri.IndexOf(SourceVID);
if (FoundSubIdx != INDEX_NONE)
{
bFound = true;
ResultWedgeSourceTris[ResultTID][SubIdx] = SearchTID;
SourceTriWedgeSubIndices[ResultTID * 3 + SubIdx] = (int8)FoundSubIdx;
break;
}
// check we're still on a valid triangle that has a vert tagged w/ our source VID
FIndex3i SourceTri(Source[Tri.A], Source[Tri.B], Source[Tri.C]);
if (!SourceTri.Contains(SourceVID))
{
continue;
}
FIndex3i TriEdges = InMesh.GetTriEdges(SearchTID);
for (int32 EdgeSubIdx = 0; EdgeSubIdx < 3; ++EdgeSubIdx)
{
int32 WalkSourceEID = TriEdges[EdgeSubIdx];
if (EdgeTags[WalkSourceEID] == EElemTag::Free)
{
FIndex2i EdgeT = InMesh.GetEdgeT(WalkSourceEID);
int32 WalkTID = EdgeT.A == SearchTID ? EdgeT.B : EdgeT.A;
if (WalkTID != INDEX_NONE)
{
TriQ.Add(WalkTID);
}
}
}
}
}
if (!bFound)
{
// no seams, or search failed; just grab any triangle
int32 NbrTID = *InMesh.VtxTrianglesItr(SourceVID).begin();
checkSlow(NbrTID != INDEX_NONE); // should not be possible for a vert w/ no neighbors to end up as a source VID
ResultWedgeSourceTris[ResultTID][SubIdx] = NbrTID;
SourceTriWedgeSubIndices[ResultTID * 3 + SubIdx] = (int8)InMesh.GetTriangle(NbrTID).IndexOf(SourceVID);
}
}
}
);
// Helper to use the general wedge mapping to copy elements for a given overlay
auto OverlayTransfer =
[&ResultMesh, &ResultWedgeSourceTris, &SourceTriWedgeSubIndices,
bResultHasDuplicateVertices]
<typename OverlayType>
(OverlayType* ResultOverlay, const OverlayType* SourceOverlay)
{
TArray<int32> SourceToResElID;
SourceToResElID.Init(INDEX_NONE, SourceOverlay->MaxElementID());
// Note: Unfortunately can't parallelize this part easily; the overlay append and set both are not thread safe (due to ref counts)
for (int32 ResultTID : ResultMesh.TriangleIndicesItr())
{
FIndex3i ResultElemTri;
bool bHasUnsetSources = false;
for (int32 ResultSubIdx = 0; ResultSubIdx < 3; ++ResultSubIdx)
{
int32 SourceTID = ResultWedgeSourceTris[ResultTID][ResultSubIdx];
int8 SourceSubIdx = SourceTriWedgeSubIndices[ResultTID * 3 + ResultSubIdx];
int32 SourceElemID = SourceOverlay->GetTriangle(SourceTID)[SourceSubIdx];
if (SourceElemID == INDEX_NONE)
{
// if we mapped to an unset triangle in the source overlay, there is no element to copy
// we do not support partially-set triangles, so the whole result triangle will also be unset in this case
bHasUnsetSources = true;
break;
}
int32 UseElemID;
if (SourceToResElID[SourceElemID] == INDEX_NONE)
{
SourceToResElID[SourceElemID] = ResultOverlay->AppendElement(SourceOverlay->GetElement(SourceElemID));
UseElemID = SourceToResElID[SourceElemID];
}
else
{
UseElemID = SourceToResElID[SourceElemID];
// if we have duplicate vertices, may need to also duplicate the element
if (bResultHasDuplicateVertices)
{
if (ResultOverlay->GetParentVertex(UseElemID) != ResultMesh.GetTriangle(ResultTID)[ResultSubIdx])
{
UseElemID = ResultOverlay->AppendElement(SourceOverlay->GetElement(SourceElemID));
}
}
}
ResultElemTri[ResultSubIdx] = UseElemID;
}
if (!bHasUnsetSources)
{
ResultOverlay->SetTriangle(ResultTID, ResultElemTri);
}
}
};
for (int32 LayerIdx = 0; LayerIdx < InAttribs->NumUVLayers(); ++LayerIdx)
{
FDynamicMeshUVOverlay* ResultUVs = ResultAttribs->GetUVLayer(LayerIdx);
const FDynamicMeshUVOverlay* SourceUVs = InAttribs->GetUVLayer(LayerIdx);
OverlayTransfer(ResultUVs, SourceUVs);
}
for (int32 LayerIdx = 0; LayerIdx < InAttribs->NumNormalLayers(); ++LayerIdx)
{
OverlayTransfer(ResultAttribs->GetNormalLayer(LayerIdx), InAttribs->GetNormalLayer(LayerIdx));
}
if (InAttribs->HasPrimaryColors())
{
OverlayTransfer(ResultAttribs->PrimaryColors(), InAttribs->PrimaryColors());
}
}
for (int32 WeightLayerIdx = 0; WeightLayerIdx < InAttribs->NumWeightLayers(); ++WeightLayerIdx)
{
UE::MeshClusterSimplifyLocals::CopyAttribs<float, 1>(
ResultAttribs->GetWeightLayer(WeightLayerIdx),
InAttribs->GetWeightLayer(WeightLayerIdx),
FromResVID, ResultMesh.MaxVertexID()
);
}
for (int32 SculptLayerIdx = 0; SculptLayerIdx < InAttribs->NumSculptLayers(); ++SculptLayerIdx)
{
UE::MeshClusterSimplifyLocals::CopyAttribs<double, 3>(
ResultAttribs->GetSculptLayers()->GetLayer(SculptLayerIdx),
InAttribs->GetSculptLayers()->GetLayer(SculptLayerIdx),
FromResVID, ResultMesh.MaxVertexID()
);
}
for (int32 GroupLayerIdx = 0; GroupLayerIdx < InAttribs->NumPolygroupLayers(); ++GroupLayerIdx)
{
UE::MeshClusterSimplifyLocals::CopyAttribs<int32, 1>(
ResultAttribs->GetPolygroupLayer(GroupLayerIdx),
InAttribs->GetPolygroupLayer(GroupLayerIdx),
ResultToSourceTri, ResultMesh.MaxTriangleID()
);
}
if (const FDynamicMeshMaterialAttribute* InMats = InAttribs->GetMaterialID())
{
UE::MeshClusterSimplifyLocals::CopyAttribs<int32, 1>(
ResultAttribs->GetMaterialID(),
InMats,
ResultToSourceTri, ResultMesh.MaxTriangleID()
);
}
}
}
if (SimplifyOptions.bTransferGroups && InMesh.HasTriangleGroups())
{
ResultMesh.EnableTriangleGroups();
ParallelFor(ResultMesh.MaxTriangleID(),
[&ResultMesh, &InMesh, &ResultToSourceTri]
(int32 ResultTID)
{
checkSlow(ResultMesh.IsTriangle(ResultTID)); // ResultMesh is compact so all tris should be valid
int32 SourceTID = ResultToSourceTri[ResultTID];
ResultMesh.SetTriangleGroup(ResultTID, InMesh.GetTriangleGroup(SourceTID));
}
);
}
return true;
}
} // namespace UE::Geometry