// 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 WedgeQuadricType; typedef TQuadricCache QuadricCacheType; typedef typename MeshVertType::BasicAttrContainerType BasicAttrContainerType; typedef typename MeshVertType::AttrContainerType AttrContainerType; typedef TTuple EdgeUpdateTuple; typedef TArray > EdgeUpdateTupleArray; typedef TArray > EdgePtrArray; typedef TArray > TriPtrArray; typedef TArray > 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 ) * 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 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* 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 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(*TopEdgePtr->v1); // prune any SIMP_REMOVED verts from the v1 link list. MeshManager.PruneVerts(*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