Files
UnrealEngine/Engine/Plugins/Experimental/SkeletalReduction/Source/Private/SkeletalSimplifier.h
2025-05-18 13:04:45 +08:00

704 lines
23 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once
#include "Containers/BinaryHeap.h"
#include "SkeletalSimplifierQuadricCache.h"
namespace SkeletalSimplifier
{
/**
* Simple terminator class. This could be expanded on to include an interrupter to allow the user
* to halt execution.
*/
class FSimplifierTerminatorBase
{
public:
FSimplifierTerminatorBase(int32 MinTri, int32 MinVert, float MaxCost)
: MaxFeatureCost(MaxCost)
, MinTriNumToRetain(MinTri)
, MinVertNumToRetain(MinVert)
{}
// return true if the simplifier should terminate.
inline bool operator()(const int32 TriNum, const int32 VertNum, const float SqrError)
{
if (TriNum < MinTriNumToRetain || VertNum < MinVertNumToRetain || SqrError > MaxFeatureCost)
{
return true;
}
return false;
}
float MaxFeatureCost;
int32 MinTriNumToRetain;
int32 MinVertNumToRetain;
};
/**
* Termination criterion for simplifier
*/
class FSimplifierTerminator : public FSimplifierTerminatorBase
{
public:
FSimplifierTerminator(uint32 MinTri, uint32 MaxTri, uint32 MinVert, uint32 MaxVert, float MaxCost, float MaxDist)
: FSimplifierTerminatorBase(MinTri, MinVert, MaxCost)
, MaxTriNumToRetain(MaxTri)
, MaxVertNumToRetain(MaxVert)
, MaxDistance(MaxDist)
{}
// return true if the simplifier should terminate.
inline bool operator()(const uint32 TriNum, const uint32 VertNum, const float SqrError)
{
if (FSimplifierTerminatorBase::operator()(TriNum, VertNum, SqrError) && TriNum < MaxTriNumToRetain && VertNum < MaxVertNumToRetain)
{
return true;
}
else
{
return false;
}
}
uint32 MaxTriNumToRetain;
uint32 MaxVertNumToRetain;
float MaxDistance;
};
/**
* Class to allow a single weight value to be applied to all the sparse attributes.
*/
class UniformWeights
{
public:
UniformWeights() : Weight(0.)
{}
UniformWeights(double w) : Weight(w)
{}
UniformWeights(const UniformWeights& other) : Weight(other.Weight) {}
double GetElement(int32) const { return Weight; }
UniformWeights& operator=(const UniformWeights& other)
{
Weight = other.Weight;
return *this;
}
private:
double Weight;
};
/**
* The core simplifier. This does all the work
*
*/
class FMeshSimplifier
{
typedef typename SimpVertType::TriIterator TriIterator;
typedef Quadrics::TFaceQuadric<MeshVertType, UniformWeights> WedgeQuadricType;
typedef TQuadricCache<WedgeQuadricType> QuadricCacheType;
typedef typename MeshVertType::BasicAttrContainerType BasicAttrContainerType;
typedef typename MeshVertType::AttrContainerType AttrContainerType;
typedef TTuple<SimpVertType*, SimpVertType*, MeshVertType> EdgeUpdateTuple;
typedef TArray<EdgeUpdateTuple, TInlineAllocator<16> > EdgeUpdateTupleArray;
typedef TArray<SimpEdgeType*, TInlineAllocator<32> > EdgePtrArray;
typedef TArray<SimpTriType*, TInlineAllocator<16> > TriPtrArray;
typedef TArray<SimpVertType*, TInlineAllocator<16> > VertPtrArray;
public:
typedef typename BasicAttrContainerType::DenseVecDType DenseVecDType;
typedef typename BasicAttrContainerType::WeightArrayType WeightArrayType;
typedef typename WedgeQuadricType::SparseWeightContainerType SparseWeightContainerType;
public:
FMeshSimplifier(const MeshVertType* InSrcVerts, const uint32 InNumSrcVerts,
const uint32* InSrcIndexes, const uint32 InNumSrcIndexes,
const float CoAlignmentLimit,
const float VolumeImportance,
const bool bVolumePreservation,
const bool bEnforceBoundaries,
const bool bMergeCoincidentVertBones,
const bool bUseLegacyAttrGradient);
~FMeshSimplifier();
/**
* Weight used to insure that when a boundary edge collapses, the resulting vertex wont be far from that edge.
*/
void SetBoundaryConstraintWeight(const double Weight);
/**
* Set Quadric weights for the standard attributes ( the attributes that are stored in TBasicVertexAttrs<NumTexCoords> )
* The order of the weights corresponds to the order of the attributes :
* Normal (3 weights), Tangent (3 weights ), BiTangent (3 weights), Color (3 weights), TextureCoords( 2 * NumTexCoords weights).
*/
void SetAttributeWeights(const DenseVecDType& Weights);
/**
* Set Quadric weights for sparse attributes
*/
void SetSparseAttributeWeights(const SparseWeightContainerType& SparseWeights);
/**
* Lock mesh boundary edges to prevent simplification by using the vertex tag ESimpElementFlags::SIMP_LOCKED
*/
void SetBoundaryLocked();
/**
* Lock edges that connect different colors to prevent simplification by using the vertex tag ESimpElementFlags::SIMP_LOCKED
*/
void SetColorEdgeLocked(float Threshold = 1.e-3);
/**
* Lock vertices at corners of very simple 8-vert boxes.
*/
void SetBoxCornersLocked();
/**
* Simplify the mesh. Termination of the simplification is controlled by the TiminationCriterion class
* The terminator must implement
*
* struct Terminator
* {
* inline bool operator()(const int32 TriNum, const float SqrError);
* };
*/
template <typename TerminationCriterionType>
float SimplifyMesh(TerminationCriterionType TerminationCriterion);
/**
* The number of verts of the mesh once simplified (or at any stage)
*/
int GetNumVerts() const { return MeshManager.ReducedNumVerts; }
/**
* The number of tris in the mesh once simplified (or at any stage)
*/
int GetNumTris() const { return MeshManager.ReducedNumTris; }
/**
* The fraction of non-manifold edges (edges with more than 2 adjacent tris).
*/
float FractionNonManifoldEdges() { return MeshManager.FractionNonManifoldEdges(); }
/**
* Export a copy of the simplified mesh.
*
* @param Verts - pointer to an array to populate. Should be least GetNumVerts() in size
* @param Indexes - pointer to index buffer to populate. Should be at least 3 * GetNumTris() in size
* @param bWeldVtxColorAttrs - Weld verts attributes (colors) that may have been artificially split by the reduction algorithm.
* note: this could be extended to the other attributes (e.g. normals, uvs )
* @param LockedVerts - optional pointer to array. On return the array will hold the indices of any locked verts.
*/
void OutputMesh(MeshVertType* Verts, uint32* Indexes, bool bWeldVtxColorAttrs = true, TArray<int32>* LockedVerts = NULL);
protected:
// Populate the cost heap by evaluating the collapse cost of each edge.
// NB: must be called before simplification
void InitCosts();
// returns the dot product between current face normal and the face normal after vertToUpdate has been moved to newPos
float CalculateNormalShift(const SimpTriType& tri, const SimpVertType* vertToUpdate, const FVector& newPos) const;
void ComputeEdgeCollapseVerts(SimpEdgeType* edge, EdgeUpdateTupleArray& newVerts);
void ComputeEdgeCollapseVertsAndFixBones(SimpEdgeType* edge, EdgeUpdateTupleArray& newVerts);
double ComputeEdgeCollapseVertsAndCost(SimpEdgeType* edge, EdgeUpdateTupleArray& newVerts);
FVector ComputeEdgeCollapseVertsPos(SimpEdgeType* edge, EdgeUpdateTupleArray& newVerts, TArray< WedgeQuadricType, TInlineAllocator<16> >& quadrics, Quadrics::FEdgeQuadric& edgeQuadric);
void ComputeEdgeCollapseVertsAndQuadrics(SimpEdgeType* edge, EdgeUpdateTupleArray& newVerts, Quadrics::FEdgeQuadric& newEdgeQuadric, TArray< WedgeQuadricType, TInlineAllocator<16> >& newQuadrics);
double ComputeEdgeCollapseCost(SimpEdgeType* edge);
// Quadric based on attributes and position (see section 4 in Hoppe's "New Quadric Metric for Simplifying Meshes.")
WedgeQuadricType GetWedgeQuadric(SimpVertType* v);
// Quadric based on sharp edges. (See section 5 in Hoppe's "New Quadric Metric for Simplifying Meshes.")
// This includes an edge quadric for every single-sided edge that shares this vert
Quadrics::FEdgeQuadric GetEdgeQuadric(SimpVertType* v);
void DirtyTriQuadricCache(TriPtrArray& DirtyTri);
void DirtyVertAndEdgeQuadricsCache(VertPtrArray& DirtyVerts);
//
void UpdateEdgeCollapseCost(EdgePtrArray& DirtyEdges);
int32 CountDegenerates() const; // Included for testing.
// Snap the interpolated vertex color in the third tuple element to the closest of the other two
void SnapToClosestSourceVertexColor(EdgeUpdateTuple& UpdateTuple);
// Returns the closer (AColor or BColor) from the SampleColor
const FLinearColor& SnapToClosestColor(const FLinearColor& AColor, const FLinearColor& BColor, const FLinearColor& SampleColor) const;
protected:
// --- Weights for quadric simplification
// The weights for the standard attributes
DenseVecDType BasicAttrWeights;
// The weights for the bones
SparseWeightContainerType AdditionalAttrWeights;
// --- Magic numbers that penalize undesirable simplifications.
// prevent high valence verts.
uint32 DegreeLimit = 24;
double DegreePenalty = 100.0f;
// prevent edge folding
double invalidPenalty = 1.e6;
// critical angle by which the normal of a triangle is allowed to changed during a collapse.
double coAlignmentLimit = .0871557f;
double VolumeImportance;
bool bPreserveVolume;
bool bCheckBoneBoundaries;
bool bMergeBonesOnCoincidentVerts;
double BoundaryConstraintWeight = 256.;
// --- End Magic numbers
// forces the simplifier to use the old attribute interpolation gradient method. Needed for backward compatibilty with certain cloth assets
bool bUseLegacyAttrGrad;
// Heap that maps edges to the cost of collapse
FBinaryHeap<double> CollapseCostHeap;
// Manage the quadrics
// Linear arrays that map quadrics to vertices, faces and edges.
QuadricCacheType QuadricCache;
// The mesh manager - holds a mesh that supports the topology queries we need and the collapse methods.
FSimplifierMeshManager MeshManager;
private:
FMeshSimplifier();
};
FORCEINLINE void FMeshSimplifier::DirtyTriQuadricCache(TriPtrArray& DirtyTriArray)
{
for (SimpTriType* tri : DirtyTriArray)
{
//if (tri->TestFlags(SIMP_REMOVED)) continue;
QuadricCache.DirtyTriQuadric(tri);
}
}
FORCEINLINE void FMeshSimplifier::DirtyVertAndEdgeQuadricsCache(VertPtrArray& DirtyVertArray)
{
for (SimpVertType* vert : DirtyVertArray)
{
//if (vert->TestFlags(SIMP_REMOVED)) continue;
const uint32 VertIdx = MeshManager.GetVertIndex(vert);
QuadricCache.DirtyVertQuadric(VertIdx);
QuadricCache.DirtyEdgeQuadric(VertIdx);
}
}
FORCEINLINE FMeshSimplifier::WedgeQuadricType FMeshSimplifier::GetWedgeQuadric(SimpVertType* v)
{
const auto TriQuadricFatory = [this](const SimpTriType& tri)->FMeshSimplifier::WedgeQuadricType
{
return FMeshSimplifier::WedgeQuadricType(
tri.verts[0]->vert, tri.verts[1]->vert, tri.verts[2]->vert,
this->BasicAttrWeights, this->AdditionalAttrWeights, bUseLegacyAttrGrad);
};
return QuadricCache.GetWedgeQuadric(v, TriQuadricFatory);
}
FORCEINLINE Quadrics::FEdgeQuadric FMeshSimplifier::GetEdgeQuadric(SimpVertType* v)
{
const double Weight = BoundaryConstraintWeight;
const auto EdgeQuadricFactory = [this, Weight](const FVector& Pos0, const FVector& Pos1, const FVector& Normal)->Quadrics::FEdgeQuadric
{
return Quadrics::FEdgeQuadric(Pos0, Pos1, Normal, Weight);
};
Quadrics::FEdgeQuadric Quadric = QuadricCache.GetEdgeQuadric(v, EdgeQuadricFactory);
return Quadric;
}
FORCEINLINE void FMeshSimplifier::ComputeEdgeCollapseVertsAndQuadrics(SimpEdgeType* edge,
EdgeUpdateTupleArray& EdgeAndNewVertArray,
Quadrics::FEdgeQuadric& newEdgeQuadric,
TArray< WedgeQuadricType, TInlineAllocator<16> >& newWedgeQuadrics)
{
FVector newPos = ComputeEdgeCollapseVertsPos(edge, EdgeAndNewVertArray, newWedgeQuadrics, newEdgeQuadric);
// update all the collapsed verts with the new location
for (int i = 0; i < newWedgeQuadrics.Num(); ++i)
{
MeshVertType& simpVert = EdgeAndNewVertArray[i].Get<2>();
simpVert.GetPos() = (FVector3f)newPos;
// If the area is non-degenerate: by default the elements in newVertPair are copies of the original v1 verts.
if (newWedgeQuadrics[i].TotalArea() > 1.e-6)
{
// calculate vert attributes from the new position
newWedgeQuadrics[i].CalcAttributes(simpVert, BasicAttrWeights, AdditionalAttrWeights);
simpVert.Correct();
}
}
}
FORCEINLINE const FLinearColor& FMeshSimplifier::SnapToClosestColor(const FLinearColor& AColor, const FLinearColor& BColor, const FLinearColor& SampleColor) const
{
// compute the squared distance in linear space - ignoring the alpha channel.
float ADistSqrd = (AColor.R - SampleColor.R)*(AColor.R - SampleColor.R) + (AColor.G - SampleColor.G)*(AColor.G - SampleColor.G) + (AColor.B - SampleColor.B)*(AColor.B - SampleColor.B);
float BDistSqrt = (BColor.R - SampleColor.R)*(BColor.R - SampleColor.R) + (BColor.G - SampleColor.G)*(BColor.G - SampleColor.G) + (BColor.B - SampleColor.B)*(BColor.B - SampleColor.B);
// select the closest
return (ADistSqrd > BDistSqrt) ? BColor : AColor;
}
FORCEINLINE void FMeshSimplifier::SnapToClosestSourceVertexColor(EdgeUpdateTuple& UpdateTuple)
{
MeshVertType& VertAttributes = UpdateTuple.Get<2>();
FLinearColor& InterpolatedColor = VertAttributes.BasicAttributes.Color;
const SimpVertType* AVtxPtr = UpdateTuple.Get<0>();
const SimpVertType* BVtxPtr = UpdateTuple.Get<1>();
if (AVtxPtr != nullptr && BVtxPtr != nullptr)
{
// update to the closest source color.
InterpolatedColor = SnapToClosestColor(AVtxPtr->vert.BasicAttributes.Color, BVtxPtr->vert.BasicAttributes.Color, InterpolatedColor);
}
}
FORCEINLINE void FMeshSimplifier::UpdateEdgeCollapseCost(EdgePtrArray& DirtyEdges)
{
uint32 NumEdges = DirtyEdges.Num();
// update edges
for (uint32 i = 0; i < NumEdges; i++)
{
SimpEdgeType* edge = DirtyEdges[i];
if (edge->TestFlags(SIMP_REMOVED))
continue;
double cost = ComputeEdgeCollapseCost(edge);
SimpEdgeType* e = edge;
do {
uint32 EdgeIndex = MeshManager.GetEdgeIndex(e);
if (CollapseCostHeap.IsPresent(EdgeIndex))
{
CollapseCostHeap.Update(cost, EdgeIndex);
}
e = e->next;
} while (e != edge);
}
}
template< typename TerminationCriterionType>
float FMeshSimplifier::SimplifyMesh(TerminationCriterionType TerminationCriterion)
{
TRACE_CPUPROFILER_EVENT_SCOPE(FMeshSimplifier::SimplifyMesh);
// Build the cost heap
InitCosts();
// Do the distance check?
const bool bCheckDistance = (TerminationCriterion.MaxDistance < FLT_MAX);
TriPtrArray DirtyTris;
VertPtrArray DirtyVerts;
EdgePtrArray DirtyEdges;
double maxError = 0.0f;
float distError = 0.0f;
while (CollapseCostHeap.Num() > 0)
{
// get the next vertex to collapse
uint32 TopIndex = CollapseCostHeap.Top();
const double error = CollapseCostHeap.GetKey(TopIndex);
// Check for termination.
{
const int32 numTris = MeshManager.ReducedNumTris;
const int32 numVerts = MeshManager.ReducedNumVerts;
if (TerminationCriterion(numTris, numVerts, error) || distError > TerminationCriterion.MaxDistance)
{
break;
}
}
maxError = FMath::Max(maxError, error);
CollapseCostHeap.Pop();
// Pointer to the candidate edge (link list) for collapse
SimpEdgeType* TopEdgePtr = MeshManager.GetEdgePtr(TopIndex);
checkSlow(TopEdgePtr);
// Gather all the edges that are really in this group.
EdgePtrArray CoincidentEdges;
MeshManager.GetEdgesInGroup(*TopEdgePtr, CoincidentEdges);
if (MeshManager.HasLockedVerts(TopEdgePtr) || MeshManager.IsLockedGroup(CoincidentEdges))
{
// this edge shouldn't be collapsed.
continue;
}
// Before changing any of the mesh topology, we capture lists
// of the tris, verts, and edges that may need new quadrics
// computed to updated edge collapse values.
MeshManager.GetAdjacentTopology(*TopEdgePtr, DirtyTris, DirtyVerts, DirtyEdges);
const int32 NumCoincidentEdges = CoincidentEdges.Num();
// Remove any edges in from this group that happen to be degenerate (no adjacent triangles)
// and capture the Idx of the dead edges. Also removes the edges from the edge-index heap.
FSimplifierMeshManager::IdxArray InvalidCostIdxArray; // Keep track of the Idx to remove from the cost heap
const int32 NumRemovedEdges = MeshManager.RemoveEdgeIfInvalid(CoincidentEdges, InvalidCostIdxArray);
// if none of the edges in this group were valid, just continue.
if (NumCoincidentEdges == NumRemovedEdges)
{
continue;
}
// The representative edge in our edge group was removed. Replace it with another from the group that is valid.
if (MeshManager.IsRemoved(TopEdgePtr) || MeshManager.IsInvalid(TopEdgePtr))
{
// try to find a valid edge in the batch..
for (SimpEdgeType* EPtr : CoincidentEdges)
{
if (EPtr && !MeshManager.IsRemoved(EPtr))
{
TopEdgePtr = EPtr;
break;
}
}
}
// continue if no edge actually exists.
if (MeshManager.IsRemoved(TopEdgePtr) || MeshManager.IsInvalid(TopEdgePtr))
{
continue;
}
// update Edge->v1 to new locations : move verts to new verts
{
// Copy the edge->v1->verts and update the location & attributes
// capturing the corresponding SimpVert.
// The VertexUpdate Array is built by adding
// 1) the two vertices for each edge
// 2) Elements from TopEdge->v0 vert group not already added
// 3) Elements from TopEdge->v1 vert group not already added
EdgeUpdateTupleArray VertexUpdateArray;
ComputeEdgeCollapseVertsAndFixBones(TopEdgePtr, VertexUpdateArray); // re-targets using closest bone
// Compute the distance of the new vertex to each plane of the affected triangles.
if (bCheckDistance)
{
const FVector NewPos = (FVector)VertexUpdateArray[0].Get<2>().GetPos();
float Dist = 0.f;
for (SimpTriType* Tri : DirtyTris)
{
FVector TriNorm = (FVector)Tri->GetNormal();
FVector PosToTri = NewPos - (FVector)Tri->verts[0]->GetPos();
Dist = FMath::Max(FMath::Abs(FVector::DotProduct(TriNorm, PosToTri)), Dist);
}
distError = FMath::Max(distError, Dist);
}
// Update both verts in the mesh to have the new location and attribute values.
for (int32 i = 0, Imax = VertexUpdateArray.Num(); i < Imax; ++i)
{
EdgeUpdateTuple& EdgeUpdate = VertexUpdateArray[i];
// New vertex attribute values for the collapsed vertex
const MeshVertType& CollapsedAttributes = EdgeUpdate.Get<2>();
// update the first edge vertex
SimpVertType* VtxPtr = EdgeUpdate.Get<0>();
if (VtxPtr != nullptr)
{
MeshManager.UpdateVertexAttributes(*VtxPtr, CollapsedAttributes);
}
// update the second edge vertex
VtxPtr = EdgeUpdate.Get<1>();
if (VtxPtr != nullptr)
{
MeshManager.UpdateVertexAttributes(*VtxPtr, CollapsedAttributes);
}
}
}
// This manages the complicated logic of making sure the attribute element IDs after the collapse
// will be in the correct state for a collapse of of a split attribute or non-split attribute. This applies
// to the vertices in the edges, and the loose vertices that don't share a triangle
// with the a vertex on the opposite end of the edge.
MeshManager.UpdateVertexAttriuteIDs(CoincidentEdges);
// collapse all edges by moving edge->v0 to edge->v1
{
// All positions and attributes should be fixed now, but we need to update the
// mesh connectivity.
for (int i = 0; i < NumCoincidentEdges; ++i)
{
SimpEdgeType* EdgePtr = CoincidentEdges[i];
bool bSkip = !EdgePtr || MeshManager.IsRemoved(EdgePtr);
if (bSkip) continue;
// Collapse the edge, delete triangles that become degenerate
// and update any edges that used to include v0 to include v1 now.
// V1 acquires any lock from v0
bool bCollapsed = MeshManager.CollapseEdge(EdgePtr, InvalidCostIdxArray);
// this edge has been collapsed. Remove it from the CoincidentEdges array
if (bCollapsed)
{
CoincidentEdges[i] = NULL;
}
// NB: two edges in this group may have shared a single vertex.
// so the above collapse could have also collapsed other edges in the group.
MeshManager.RemoveEdgeIfInvalid(CoincidentEdges, InvalidCostIdxArray);
}
// add v0 remainder verts to v1
{
// I'm not totally okay with this.. this adds all the v0 verts to the v1 group,
// resulting in v1, v1', v1''.. v0, v0', ..
// but doesn't change their Position()
MeshManager.MergeGroups(TopEdgePtr->v0, TopEdgePtr->v1);
// if any of the verts in the group is locked, make them all locked
MeshManager.PropagateFlag<SIMP_LOCKED>(*TopEdgePtr->v1);
// prune any SIMP_REMOVED verts from the v1 link list.
MeshManager.PruneVerts<SIMP_REMOVED>(*TopEdgePtr->v1);
}
}
// Dirty the quadric cache
// NB: these early out if the objects are marked as removed.
// i think that is too clever and should be changed so they
// always dirty everything in the list.
DirtyTriQuadricCache(DirtyTris);
DirtyVertAndEdgeQuadricsCache(DirtyVerts);
// If a dirty tri has zero area, remove it and tag as SIMP_REMOVED
MeshManager.RemoveIfDegenerate(DirtyTris);
// Tag verts that aren't part of tris ad SIMP_REMOVED
// and remove them from the mesh vert groups.
MeshManager.RemoveIfDegenerate(DirtyVerts);
// Remove edges with out verts and update
// the cost & heap for all the remaining dirty edges.
// this triggers updates of the tri and vert cached quadrics.
MeshManager.RemoveIfDegenerate(DirtyEdges, InvalidCostIdxArray);
// If an edge collapses on a triangle, the other two edges are now
// one.. this accounts for that sort of thing.
MeshManager.RebuildEdgeLinkLists(DirtyEdges);
// Update the cost heap by removing the dead edges we just pruned.
int32 NumRemoved = InvalidCostIdxArray.Num();
for (int32 i = 0; i < NumRemoved; ++i)
{
CollapseCostHeap.Remove(InvalidCostIdxArray[i]);
}
// update the Quadric Caches associated with the dirty edges
// and update the heap.
UpdateEdgeCollapseCost(DirtyEdges);
DirtyTris.Reset();
DirtyVerts.Reset();
DirtyEdges.Reset();
}
// remove degenerate triangles
// not sure why this happens
int32 NumTrisIJustRemoved =
MeshManager.RemoveDegenerateTris();
// remove orphaned verts
int32 NumVertsIJustRemoved =
MeshManager.RemoveDegenerateVerts();
return (bCheckDistance) ? distError : maxError;
}
}; // End namespace SkeletalSimplifier