1326 lines
46 KiB
C#
1326 lines
46 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Diagnostics;
|
|
using System.Linq;
|
|
|
|
namespace Nanomesh
|
|
{
|
|
public partial class DecimateModifier
|
|
{
|
|
// Heuristics
|
|
internal static bool UpdateFarNeighbors = false;
|
|
internal static bool UpdateMinsOnCollapse = true;
|
|
internal static float MergeNormalsThresholdDegrees = 90;
|
|
internal static float NormalSimilarityThresholdDegrees = 60;
|
|
internal static float CollapseToMidpointPenalty = 0.4716252f;
|
|
internal static bool CollapseToEndpointsOnly = false;
|
|
internal static float UvSimilarityThreshold = 0.02f;
|
|
internal static float UvSeamAngleCos = 0.99f;
|
|
internal static bool BlockUvSeamVertices = true;
|
|
internal static float BoneWeightSimilarityThreshold = 0.85f;
|
|
internal static bool LimitCollapseEdgeLength = false;
|
|
internal static float MaxCollapseEdgeLength = float.PositiveInfinity;
|
|
internal static bool AllowBoundaryCollapses = false;
|
|
internal static float BodyCollisionPenetrationFactor = 0.75f;
|
|
|
|
// Constants
|
|
private const double _DeterminantEpsilon = 0.001f;
|
|
private const float _MinTriangleAreaRatio = 0.05f;
|
|
private const float _UvDirEpsilonSq = 1e-12f;
|
|
private const double _OFFSET_HARD = 1e6;
|
|
private const double _OFFSET_NOCOLLAPSE = 1e300;
|
|
|
|
// Instance
|
|
private ConnectedMesh _mesh;
|
|
private SymmetricMatrix[] _matrices;
|
|
private FastHashSet<EdgeCollapse> _pairs;
|
|
private LinkedHashSet<EdgeCollapse> _mins;
|
|
private int _lastProgress = int.MinValue;
|
|
private int _initialTriangleCount;
|
|
private float _mergeNormalsThresholdCos = MathF.Cos(MergeNormalsThresholdDegrees * MathF.PI / 180f);
|
|
private float _normalSimilarityThresholdCos = MathF.Cos(NormalSimilarityThresholdDegrees * MathF.PI / 180f);
|
|
private int _evaluatedEdges;
|
|
private int _collapsedEdges;
|
|
private int _rejectedBoneWeights;
|
|
private int _rejectedTopology;
|
|
private int _rejectedInversion;
|
|
private int _rejectedDegenerate;
|
|
private int _rejectedArea;
|
|
private int _rejectedFlip;
|
|
private int _rejectedBodyCollision;
|
|
private float[]? _bodyDistanceSq;
|
|
private float _bodyDistanceThresholdSq;
|
|
private Func<Vector3, float>? _bodyDistanceSqEvaluator;
|
|
private bool[]? _protectedVertices;
|
|
|
|
public ConnectedMesh Mesh => _mesh;
|
|
|
|
public DecimationStats GetStats()
|
|
=> new DecimationStats(
|
|
_evaluatedEdges,
|
|
_collapsedEdges,
|
|
_rejectedBoneWeights,
|
|
_rejectedTopology,
|
|
_rejectedInversion,
|
|
_rejectedDegenerate,
|
|
_rejectedArea,
|
|
_rejectedFlip,
|
|
_rejectedBodyCollision);
|
|
|
|
public void SetBodyCollision(float[]? bodyDistanceSq, float bodyDistanceThresholdSq, Func<Vector3, float>? bodyDistanceSqEvaluator = null)
|
|
{
|
|
_bodyDistanceSq = bodyDistanceSq;
|
|
_bodyDistanceThresholdSq = bodyDistanceThresholdSq;
|
|
_bodyDistanceSqEvaluator = bodyDistanceSqEvaluator;
|
|
}
|
|
|
|
public void SetProtectedVertices(bool[]? protectedVertices)
|
|
{
|
|
_protectedVertices = protectedVertices;
|
|
}
|
|
|
|
public void Initialize(ConnectedMesh mesh)
|
|
{
|
|
_mesh = mesh;
|
|
ResetStats();
|
|
|
|
_initialTriangleCount = mesh.FaceCount;
|
|
|
|
_matrices = new SymmetricMatrix[mesh.positions.Length];
|
|
_pairs = new FastHashSet<EdgeCollapse>();
|
|
_mins = new LinkedHashSet<EdgeCollapse>();
|
|
|
|
InitializePairs();
|
|
|
|
for (int p = 0; p < _mesh.PositionToNode.Length; p++)
|
|
{
|
|
if (_mesh.PositionToNode[p] != -1)
|
|
CalculateQuadric(p);
|
|
}
|
|
|
|
foreach (EdgeCollapse pair in _pairs)
|
|
{
|
|
CalculateError(pair);
|
|
}
|
|
}
|
|
|
|
public void DecimateToError(float maximumError)
|
|
{
|
|
while (GetPairWithMinimumError().error <= maximumError && _pairs.Count > 0)
|
|
{
|
|
Iterate();
|
|
}
|
|
}
|
|
|
|
public void DecimateToRatio(float targetTriangleRatio)
|
|
{
|
|
targetTriangleRatio = MathF.Clamp(targetTriangleRatio, 0f, 1f);
|
|
DecimateToPolycount((int)MathF.Round(targetTriangleRatio * _mesh.FaceCount));
|
|
}
|
|
|
|
public void DecimatePolycount(int polycount)
|
|
{
|
|
DecimateToPolycount((int)MathF.Round(_mesh.FaceCount - polycount));
|
|
}
|
|
|
|
public void DecimateToPolycount(int targetTriangleCount)
|
|
{
|
|
while (_mesh.FaceCount > targetTriangleCount && _pairs.Count > 0)
|
|
{
|
|
Iterate();
|
|
|
|
int progress = (int)MathF.Round(100f * (_initialTriangleCount - _mesh.FaceCount) / (_initialTriangleCount - targetTriangleCount));
|
|
if (progress >= _lastProgress + 10)
|
|
{
|
|
_lastProgress = progress;
|
|
}
|
|
}
|
|
}
|
|
|
|
public void Iterate()
|
|
{
|
|
EdgeCollapse pair = GetPairWithMinimumError();
|
|
while (pair != null && pair.error >= _OFFSET_NOCOLLAPSE)
|
|
{
|
|
_pairs.Remove(pair);
|
|
_mins.Remove(pair);
|
|
pair = GetPairWithMinimumError();
|
|
}
|
|
|
|
if (pair == null)
|
|
return;
|
|
|
|
Debug.Assert(_mesh.CheckEdge(_mesh.PositionToNode[pair.posA], _mesh.PositionToNode[pair.posB]));
|
|
|
|
_pairs.Remove(pair);
|
|
_mins.Remove(pair);
|
|
|
|
CollapseEdge(pair);
|
|
}
|
|
|
|
public double GetMinimumError()
|
|
{
|
|
return GetPairWithMinimumError()?.error ?? double.PositiveInfinity;
|
|
}
|
|
|
|
private EdgeCollapse GetPairWithMinimumError()
|
|
{
|
|
if (_mins.Count == 0)
|
|
ComputeMins();
|
|
|
|
LinkedHashSet<EdgeCollapse>.LinkedHashNode<EdgeCollapse> edge = _mins.First;
|
|
|
|
return edge?.Value;
|
|
}
|
|
|
|
private int MinsCount => MathF.Clamp(500, 0, _pairs.Count);
|
|
|
|
private void ComputeMins()
|
|
{
|
|
_mins = new LinkedHashSet<EdgeCollapse>(_pairs.OrderBy(x => x).Take(MinsCount));
|
|
}
|
|
|
|
private void InitializePairs()
|
|
{
|
|
_pairs.Clear();
|
|
_mins.Clear();
|
|
|
|
for (int p = 0; p < _mesh.PositionToNode.Length; p++)
|
|
{
|
|
int nodeIndex = _mesh.PositionToNode[p];
|
|
if (nodeIndex < 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
int sibling = nodeIndex;
|
|
do
|
|
{
|
|
int firstRelative = _mesh.nodes[sibling].relative;
|
|
int secondRelative = _mesh.nodes[firstRelative].relative;
|
|
|
|
EdgeCollapse pair = new EdgeCollapse(_mesh.nodes[firstRelative].position, _mesh.nodes[secondRelative].position);
|
|
|
|
_pairs.Add(pair);
|
|
|
|
Debug.Assert(_mesh.CheckEdge(_mesh.PositionToNode[pair.posA], _mesh.PositionToNode[pair.posB]));
|
|
|
|
} while ((sibling = _mesh.nodes[sibling].sibling) != nodeIndex);
|
|
}
|
|
}
|
|
|
|
private void CalculateQuadric(int position)
|
|
{
|
|
int nodeIndex = _mesh.PositionToNode[position];
|
|
|
|
Debug.Assert(nodeIndex >= 0);
|
|
Debug.Assert(!_mesh.nodes[nodeIndex].IsRemoved);
|
|
|
|
SymmetricMatrix symmetricMatrix = new SymmetricMatrix();
|
|
|
|
int sibling = nodeIndex;
|
|
do
|
|
{
|
|
Debug.Assert(_mesh.CheckRelatives(sibling));
|
|
|
|
Vector3 faceNormal = _mesh.GetFaceNormal(sibling);
|
|
double dot = Vector3.Dot(-faceNormal, _mesh.positions[_mesh.nodes[sibling].position]);
|
|
symmetricMatrix += new SymmetricMatrix(faceNormal.x, faceNormal.y, faceNormal.z, dot);
|
|
|
|
} while ((sibling = _mesh.nodes[sibling].sibling) != nodeIndex);
|
|
|
|
_matrices[position] = symmetricMatrix;
|
|
}
|
|
|
|
private readonly HashSet<int> _adjacentEdges = new HashSet<int>();
|
|
private readonly HashSet<int> _adjacentEdgesA = new HashSet<int>();
|
|
private readonly HashSet<int> _adjacentEdgesB = new HashSet<int>();
|
|
|
|
private IEnumerable<int> GetAdjacentPositions(int nodeIndex, int nodeAvoid)
|
|
{
|
|
_adjacentEdges.Clear();
|
|
|
|
int posToAvoid = _mesh.nodes[nodeAvoid].position;
|
|
|
|
int sibling = nodeIndex;
|
|
do
|
|
{
|
|
for (int relative = sibling; (relative = _mesh.nodes[relative].relative) != sibling;)
|
|
{
|
|
if (_mesh.nodes[relative].position != posToAvoid)
|
|
{
|
|
_adjacentEdges.Add(_mesh.nodes[relative].position);
|
|
}
|
|
}
|
|
} while ((sibling = _mesh.nodes[sibling].sibling) != nodeIndex);
|
|
|
|
return _adjacentEdges;
|
|
}
|
|
|
|
private void FillAdjacentPositions(int nodeIndex, int nodeAvoid, HashSet<int> output)
|
|
{
|
|
output.Clear();
|
|
|
|
int posToAvoid = _mesh.nodes[nodeAvoid].position;
|
|
|
|
int sibling = nodeIndex;
|
|
do
|
|
{
|
|
for (int relative = sibling; (relative = _mesh.nodes[relative].relative) != sibling;)
|
|
{
|
|
if (_mesh.nodes[relative].position != posToAvoid)
|
|
{
|
|
output.Add(_mesh.nodes[relative].position);
|
|
}
|
|
}
|
|
} while ((sibling = _mesh.nodes[sibling].sibling) != nodeIndex);
|
|
}
|
|
|
|
private void FillAdjacentPositionsByPos(int nodeIndex, int posToAvoid, HashSet<int> output)
|
|
{
|
|
output.Clear();
|
|
|
|
int sibling = nodeIndex;
|
|
do
|
|
{
|
|
for (int relative = sibling; (relative = _mesh.nodes[relative].relative) != sibling;)
|
|
{
|
|
int pos = _mesh.nodes[relative].position;
|
|
if (pos != posToAvoid)
|
|
{
|
|
output.Add(pos);
|
|
}
|
|
}
|
|
} while ((sibling = _mesh.nodes[sibling].sibling) != nodeIndex);
|
|
}
|
|
|
|
private double GetEdgeTopo(EdgeCollapse edge)
|
|
{
|
|
if (edge.Weight == -1)
|
|
{
|
|
edge.SetWeight(_mesh.GetEdgeTopo(_mesh.PositionToNode[edge.posA], _mesh.PositionToNode[edge.posB]));
|
|
}
|
|
return edge.Weight;
|
|
}
|
|
|
|
public static bool UseEdgeLength = true;
|
|
|
|
private void CalculateError(EdgeCollapse pair)
|
|
{
|
|
Debug.Assert(_mesh.CheckEdge(_mesh.PositionToNode[pair.posA], _mesh.PositionToNode[pair.posB]));
|
|
|
|
Vector3 posA = _mesh.positions[pair.posA];
|
|
Vector3 posB = _mesh.positions[pair.posB];
|
|
_evaluatedEdges++;
|
|
|
|
if (ShouldBlockBoneWeightCollapse(pair.posA, pair.posB))
|
|
{
|
|
_rejectedBoneWeights++;
|
|
pair.error = _OFFSET_NOCOLLAPSE;
|
|
return;
|
|
}
|
|
if (ShouldBlockNormalCollapse(pair.posA, pair.posB))
|
|
{
|
|
_rejectedTopology++;
|
|
pair.error = _OFFSET_NOCOLLAPSE;
|
|
return;
|
|
}
|
|
if (ShouldBlockUvCollapse(pair.posA, pair.posB))
|
|
{
|
|
_rejectedTopology++;
|
|
pair.error = _OFFSET_NOCOLLAPSE;
|
|
return;
|
|
}
|
|
if (IsProtectedVertex(pair.posA) || IsProtectedVertex(pair.posB))
|
|
{
|
|
_rejectedBodyCollision++;
|
|
pair.error = _OFFSET_NOCOLLAPSE;
|
|
return;
|
|
}
|
|
|
|
var edgeTopo = GetEdgeTopo(pair);
|
|
if (edgeTopo > 0d && !AllowBoundaryCollapses)
|
|
{
|
|
_rejectedTopology++;
|
|
pair.error = _OFFSET_NOCOLLAPSE;
|
|
return;
|
|
}
|
|
Vector3 posC = (posB + posA) / 2;
|
|
|
|
int nodeA = _mesh.PositionToNode[pair.posA];
|
|
int nodeB = _mesh.PositionToNode[pair.posB];
|
|
if (!CollapsePreservesTopology(pair))
|
|
{
|
|
_rejectedTopology++;
|
|
pair.error = _OFFSET_NOCOLLAPSE;
|
|
return;
|
|
}
|
|
if (!AllowBoundaryCollapses && (IsBoundaryVertex(nodeA) || IsBoundaryVertex(nodeB)))
|
|
{
|
|
_rejectedTopology++;
|
|
pair.error = _OFFSET_NOCOLLAPSE;
|
|
return;
|
|
}
|
|
|
|
double errorCollapseToO;
|
|
Vector3 posO = Vector3.PositiveInfinity;
|
|
|
|
// If a node is smooth (no hard edge connected, no uv break or no border), we can compute a quadric error
|
|
// Otherwise, we add up linear errors for every non smooth source.
|
|
// If both nodes of the edge are smooth, we can find the optimal position to collapse to by inverting the
|
|
// quadric matrix, otherwise, we pick the best between A, B, and the position in the middle, C.
|
|
|
|
SymmetricMatrix q = _matrices[pair.posA] + _matrices[pair.posB];
|
|
double det = q.DeterminantXYZ();
|
|
|
|
if (det > _DeterminantEpsilon || det < -_DeterminantEpsilon)
|
|
{
|
|
posO = new Vector3(
|
|
-1d / det * q.DeterminantX(),
|
|
+1d / det * q.DeterminantY(),
|
|
-1d / det * q.DeterminantZ());
|
|
errorCollapseToO = ComputeVertexError(q, posO.x, posO.y, posO.z);
|
|
}
|
|
else
|
|
{
|
|
errorCollapseToO = _OFFSET_NOCOLLAPSE;
|
|
}
|
|
|
|
double errorCollapseToA = ComputeVertexError(q, posA.x, posA.y, posA.z);
|
|
double errorCollapseToB = ComputeVertexError(q, posB.x, posB.y, posB.z);
|
|
double errorCollapseToC = ComputeVertexError(q, posC.x, posC.y, posC.z);
|
|
|
|
int pA = _mesh.nodes[nodeA].position;
|
|
int pB = _mesh.nodes[nodeB].position;
|
|
|
|
// We multiply by edge length to be agnotics with quadrics error.
|
|
// Otherwise it becomes too scale dependent
|
|
double length = (posB - posA).Length;
|
|
if (LimitCollapseEdgeLength && length > MaxCollapseEdgeLength)
|
|
{
|
|
_rejectedTopology++;
|
|
pair.error = _OFFSET_NOCOLLAPSE;
|
|
return;
|
|
}
|
|
|
|
foreach (int pD in GetAdjacentPositions(nodeA, nodeB))
|
|
{
|
|
Vector3 posD = _mesh.positions[pD];
|
|
EdgeCollapse edge = new EdgeCollapse(pA, pD);
|
|
if (_pairs.TryGetValue(edge, out EdgeCollapse realEdge))
|
|
{
|
|
double weight = GetEdgeTopo(realEdge);
|
|
errorCollapseToB += weight * length * ComputeLineicError(posB, posD, posA);
|
|
errorCollapseToC += weight * length * ComputeLineicError(posC, posD, posA);
|
|
}
|
|
}
|
|
|
|
foreach (int pD in GetAdjacentPositions(nodeB, nodeA))
|
|
{
|
|
Vector3 posD = _mesh.positions[pD];
|
|
EdgeCollapse edge = new EdgeCollapse(pB, pD);
|
|
if (_pairs.TryGetValue(edge, out EdgeCollapse realEdge))
|
|
{
|
|
double weight = GetEdgeTopo(realEdge);
|
|
errorCollapseToA += weight * length * ComputeLineicError(posA, posD, posB);
|
|
errorCollapseToC += weight * length * ComputeLineicError(posC, posD, posB);
|
|
}
|
|
}
|
|
|
|
errorCollapseToC *= CollapseToMidpointPenalty;
|
|
|
|
if (CollapseToEndpointsOnly)
|
|
{
|
|
errorCollapseToO = _OFFSET_NOCOLLAPSE;
|
|
errorCollapseToC = _OFFSET_NOCOLLAPSE;
|
|
}
|
|
|
|
if (CollapseToEndpointsOnly && _bodyDistanceSq != null && _bodyDistanceThresholdSq > 0f)
|
|
{
|
|
var hasA = TryGetBodyDistanceSq(pair.posA, out var distASq);
|
|
var hasB = TryGetBodyDistanceSq(pair.posB, out var distBSq);
|
|
var nearA = hasA && distASq <= _bodyDistanceThresholdSq;
|
|
var nearB = hasB && distBSq <= _bodyDistanceThresholdSq;
|
|
|
|
if (nearA && nearB)
|
|
{
|
|
if (distASq > distBSq)
|
|
{
|
|
errorCollapseToB = _OFFSET_NOCOLLAPSE;
|
|
}
|
|
else if (distBSq > distASq)
|
|
{
|
|
errorCollapseToA = _OFFSET_NOCOLLAPSE;
|
|
}
|
|
else
|
|
{
|
|
errorCollapseToA = _OFFSET_NOCOLLAPSE;
|
|
errorCollapseToB = _OFFSET_NOCOLLAPSE;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (nearA)
|
|
{
|
|
errorCollapseToA = _OFFSET_NOCOLLAPSE;
|
|
}
|
|
|
|
if (nearB)
|
|
{
|
|
errorCollapseToB = _OFFSET_NOCOLLAPSE;
|
|
}
|
|
}
|
|
|
|
if (hasA && hasB)
|
|
{
|
|
if (distASq > distBSq)
|
|
{
|
|
errorCollapseToB = _OFFSET_NOCOLLAPSE;
|
|
}
|
|
else if (distBSq > distASq)
|
|
{
|
|
errorCollapseToA = _OFFSET_NOCOLLAPSE;
|
|
}
|
|
}
|
|
|
|
if (errorCollapseToA >= _OFFSET_NOCOLLAPSE && errorCollapseToB >= _OFFSET_NOCOLLAPSE)
|
|
{
|
|
_rejectedBodyCollision++;
|
|
pair.error = _OFFSET_NOCOLLAPSE;
|
|
return;
|
|
}
|
|
}
|
|
|
|
if (!CollapseToEndpointsOnly && IsPointNearBody((posA + posB) * 0.5))
|
|
{
|
|
_rejectedBodyCollision++;
|
|
pair.error = _OFFSET_NOCOLLAPSE;
|
|
return;
|
|
}
|
|
|
|
MathUtils.SelectMin(
|
|
errorCollapseToO, errorCollapseToA, errorCollapseToB, errorCollapseToC,
|
|
posO, posA, posB, posC,
|
|
out pair.error, out pair.result);
|
|
|
|
pair.error = Math.Max(0d, pair.error);
|
|
|
|
if (!CollapseWillInvert(pair))
|
|
{
|
|
pair.error = _OFFSET_NOCOLLAPSE;
|
|
}
|
|
|
|
// TODO : Make it insensitive to model scale
|
|
}
|
|
|
|
private bool CollapsePreservesTopology(EdgeCollapse edge)
|
|
{
|
|
int nodeIndexA = _mesh.PositionToNode[edge.posA];
|
|
int nodeIndexB = _mesh.PositionToNode[edge.posB];
|
|
if (nodeIndexA < 0 || nodeIndexB < 0)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
FillAdjacentPositions(nodeIndexA, nodeIndexB, _adjacentEdgesA);
|
|
FillAdjacentPositions(nodeIndexB, nodeIndexA, _adjacentEdgesB);
|
|
|
|
int shared = 0;
|
|
foreach (var neighbor in _adjacentEdgesA)
|
|
{
|
|
if (_adjacentEdgesB.Contains(neighbor))
|
|
{
|
|
shared++;
|
|
if (shared > 2)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
return AllowBoundaryCollapses ? shared >= 1 : shared == 2;
|
|
}
|
|
|
|
private bool IsBoundaryVertex(int nodeIndex)
|
|
{
|
|
if (nodeIndex < 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
int sibling = nodeIndex;
|
|
do
|
|
{
|
|
for (int relative = sibling; (relative = _mesh.nodes[relative].relative) != sibling;)
|
|
{
|
|
if (_mesh.GetEdgeTopo(sibling, relative) >= ConnectedMesh.EdgeBorderPenalty)
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
} while ((sibling = _mesh.nodes[sibling].sibling) != nodeIndex);
|
|
|
|
return false;
|
|
}
|
|
|
|
private bool ShouldBlockBoneWeightCollapse(int posA, int posB)
|
|
{
|
|
if (_mesh.attributes is not MetaAttributeList<FfxivVertexAttribute> attrList)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
int nodeA = _mesh.PositionToNode[posA];
|
|
int nodeB = _mesh.PositionToNode[posB];
|
|
if (nodeA < 0 || nodeB < 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
bool hasWeights = false;
|
|
int siblingA = nodeA;
|
|
do
|
|
{
|
|
var attrA = (MetaAttribute<FfxivVertexAttribute>)attrList[_mesh.nodes[siblingA].attribute];
|
|
if ((attrA.attr0.flags & FfxivAttributeFlags.BoneWeights) != 0)
|
|
{
|
|
hasWeights = true;
|
|
int siblingB = nodeB;
|
|
do
|
|
{
|
|
var attrB = (MetaAttribute<FfxivVertexAttribute>)attrList[_mesh.nodes[siblingB].attribute];
|
|
if ((attrB.attr0.flags & FfxivAttributeFlags.BoneWeights) != 0
|
|
&& HasMatchingDominantBone(attrA.attr0.boneWeight, attrB.attr0.boneWeight)
|
|
&& GetBoneWeightOverlapNormalized(attrA.attr0.boneWeight, attrB.attr0.boneWeight) >= BoneWeightSimilarityThreshold)
|
|
{
|
|
return false;
|
|
}
|
|
} while ((siblingB = _mesh.nodes[siblingB].sibling) != nodeB);
|
|
}
|
|
} while ((siblingA = _mesh.nodes[siblingA].sibling) != nodeA);
|
|
|
|
return hasWeights;
|
|
}
|
|
|
|
private bool ShouldBlockUvCollapse(int posA, int posB)
|
|
{
|
|
if (_mesh.attributes is not MetaAttributeList<FfxivVertexAttribute> attrList)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var attrA = ((MetaAttribute<FfxivVertexAttribute>)attrList[posA]).attr0;
|
|
var attrB = ((MetaAttribute<FfxivVertexAttribute>)attrList[posB]).attr0;
|
|
var flags = attrA.flags | attrB.flags;
|
|
if ((flags & FfxivAttributeFlags.Uv0) == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var isSeam = IsUvSeamEdge(attrA.uv0, attrB.uv0);
|
|
if (!isSeam)
|
|
{
|
|
if (BlockUvSeamVertices && (HasUvSeamAtVertex(posA, posB, attrList, attrA) || HasUvSeamAtVertex(posB, posA, attrList, attrB)))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
if (!CheckUvSeamAngleAtVertex(posA, posB, attrList, attrA, attrB))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (!CheckUvSeamAngleAtVertex(posB, posA, attrList, attrB, attrA))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private bool ShouldBlockNormalCollapse(int posA, int posB)
|
|
{
|
|
if (_mesh.attributes is not MetaAttributeList<FfxivVertexAttribute> attrList)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var attrA = ((MetaAttribute<FfxivVertexAttribute>)attrList[posA]).attr0;
|
|
var attrB = ((MetaAttribute<FfxivVertexAttribute>)attrList[posB]).attr0;
|
|
if ((attrA.flags & FfxivAttributeFlags.Normal) == 0 || (attrB.flags & FfxivAttributeFlags.Normal) == 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var dot = Vector3F.Dot(attrA.normal, attrB.normal);
|
|
return dot < _normalSimilarityThresholdCos;
|
|
}
|
|
|
|
private static float UvDistanceSq(in Vector2F a, in Vector2F b)
|
|
{
|
|
var dx = a.x - b.x;
|
|
var dy = a.y - b.y;
|
|
return (dx * dx) + (dy * dy);
|
|
}
|
|
|
|
private static bool IsUvSeamEdge(in Vector2F uvA, in Vector2F uvB)
|
|
{
|
|
var thresholdSq = UvSimilarityThreshold * UvSimilarityThreshold;
|
|
return UvDistanceSq(uvA, uvB) > thresholdSq;
|
|
}
|
|
|
|
private bool HasUvSeamAtVertex(int posCenter, int posExclude, MetaAttributeList<FfxivVertexAttribute> attrList, in FfxivVertexAttribute attrCenter)
|
|
{
|
|
int nodeCenter = _mesh.PositionToNode[posCenter];
|
|
if (nodeCenter < 0)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
FillAdjacentPositionsByPos(nodeCenter, posExclude, _adjacentEdges);
|
|
foreach (int neighborPos in _adjacentEdges)
|
|
{
|
|
var attrNeighbor = ((MetaAttribute<FfxivVertexAttribute>)attrList[neighborPos]).attr0;
|
|
if (((attrNeighbor.flags | attrCenter.flags) & FfxivAttributeFlags.Uv0) == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (IsUvSeamEdge(attrCenter.uv0, attrNeighbor.uv0))
|
|
{
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private bool CheckUvSeamAngleAtVertex(int posCenter, int posOther, MetaAttributeList<FfxivVertexAttribute> attrList, in FfxivVertexAttribute attrCenter, in FfxivVertexAttribute attrOther)
|
|
{
|
|
int nodeCenter = _mesh.PositionToNode[posCenter];
|
|
if (nodeCenter < 0)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
FillAdjacentPositionsByPos(nodeCenter, posOther, _adjacentEdges);
|
|
|
|
int seamEdges = 1;
|
|
int otherSeamPos = -1;
|
|
|
|
foreach (int neighborPos in _adjacentEdges)
|
|
{
|
|
var attrNeighbor = ((MetaAttribute<FfxivVertexAttribute>)attrList[neighborPos]).attr0;
|
|
if (((attrNeighbor.flags | attrCenter.flags) & FfxivAttributeFlags.Uv0) == 0)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
if (IsUvSeamEdge(attrCenter.uv0, attrNeighbor.uv0))
|
|
{
|
|
seamEdges++;
|
|
otherSeamPos = neighborPos;
|
|
if (seamEdges > 2)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (otherSeamPos < 0)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var attrOtherSeam = ((MetaAttribute<FfxivVertexAttribute>)attrList[otherSeamPos]).attr0;
|
|
if (!TryNormalizeUvDirection(attrCenter.uv0, attrOther.uv0, out var dir1)
|
|
|| !TryNormalizeUvDirection(attrCenter.uv0, attrOtherSeam.uv0, out var dir2))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var dot = (dir1.x * dir2.x) + (dir1.y * dir2.y);
|
|
return dot >= UvSeamAngleCos;
|
|
}
|
|
|
|
private static bool TryNormalizeUvDirection(in Vector2F from, in Vector2F to, out Vector2F direction)
|
|
{
|
|
var dx = to.x - from.x;
|
|
var dy = to.y - from.y;
|
|
var lenSq = (dx * dx) + (dy * dy);
|
|
if (lenSq <= _UvDirEpsilonSq)
|
|
{
|
|
direction = default;
|
|
return false;
|
|
}
|
|
|
|
var invLen = 1f / MathF.Sqrt(lenSq);
|
|
direction = new Vector2F(dx * invLen, dy * invLen);
|
|
return true;
|
|
}
|
|
|
|
private bool TryGetBodyDistanceSq(int pos, out float distanceSq)
|
|
{
|
|
distanceSq = float.NaN;
|
|
if (_bodyDistanceSq == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if ((uint)pos >= (uint)_bodyDistanceSq.Length)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
distanceSq = _bodyDistanceSq[pos];
|
|
return !float.IsNaN(distanceSq);
|
|
}
|
|
|
|
private static float GetBoneWeightOverlapNormalized(in BoneWeight a, in BoneWeight b)
|
|
{
|
|
var overlap = GetBoneWeightOverlap(a, b);
|
|
var sumA = GetBoneWeightSum(a);
|
|
var sumB = GetBoneWeightSum(b);
|
|
var denom = MathF.Max(sumA, sumB);
|
|
if (denom <= 1e-6f)
|
|
{
|
|
return 1f;
|
|
}
|
|
|
|
return overlap / denom;
|
|
}
|
|
|
|
private static bool HasMatchingDominantBone(in BoneWeight a, in BoneWeight b)
|
|
{
|
|
var dominantA = GetDominantBoneIndex(a);
|
|
if (dominantA < 0)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var dominantB = GetDominantBoneIndex(b);
|
|
if (dominantB < 0)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return dominantA == dominantB;
|
|
}
|
|
|
|
private static int GetDominantBoneIndex(in BoneWeight weight)
|
|
{
|
|
var max = weight.weight0;
|
|
var index = weight.index0;
|
|
|
|
if (weight.weight1 > max)
|
|
{
|
|
max = weight.weight1;
|
|
index = weight.index1;
|
|
}
|
|
if (weight.weight2 > max)
|
|
{
|
|
max = weight.weight2;
|
|
index = weight.index2;
|
|
}
|
|
if (weight.weight3 > max)
|
|
{
|
|
max = weight.weight3;
|
|
index = weight.index3;
|
|
}
|
|
|
|
return max > 0f ? index : -1;
|
|
}
|
|
|
|
private static float GetBoneWeightOverlap(in BoneWeight a, in BoneWeight b)
|
|
{
|
|
float overlap = 0f;
|
|
AddSharedWeight(a.index0, a.weight0, b, ref overlap);
|
|
AddSharedWeight(a.index1, a.weight1, b, ref overlap);
|
|
AddSharedWeight(a.index2, a.weight2, b, ref overlap);
|
|
AddSharedWeight(a.index3, a.weight3, b, ref overlap);
|
|
return overlap;
|
|
}
|
|
|
|
private static float GetBoneWeightSum(in BoneWeight weight)
|
|
=> weight.weight0 + weight.weight1 + weight.weight2 + weight.weight3;
|
|
|
|
private static void AddSharedWeight(int index, float weight, in BoneWeight other, ref float overlap)
|
|
{
|
|
if (weight <= 0f)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (index == other.index0)
|
|
{
|
|
overlap += MathF.Min(weight, other.weight0);
|
|
}
|
|
else if (index == other.index1)
|
|
{
|
|
overlap += MathF.Min(weight, other.weight1);
|
|
}
|
|
else if (index == other.index2)
|
|
{
|
|
overlap += MathF.Min(weight, other.weight2);
|
|
}
|
|
else if (index == other.index3)
|
|
{
|
|
overlap += MathF.Min(weight, other.weight3);
|
|
}
|
|
}
|
|
|
|
// TODO : Fix this (doesn't seems to work properly
|
|
public bool CollapseWillInvert(EdgeCollapse edge)
|
|
{
|
|
int nodeIndexA = _mesh.PositionToNode[edge.posA];
|
|
int nodeIndexB = _mesh.PositionToNode[edge.posB];
|
|
Vector3 positionA = _mesh.positions[edge.posA];
|
|
Vector3 positionB = _mesh.positions[edge.posB];
|
|
var minAreaRatioSq = _MinTriangleAreaRatio * _MinTriangleAreaRatio;
|
|
|
|
int sibling = nodeIndexA;
|
|
do
|
|
{
|
|
int posC = _mesh.nodes[_mesh.nodes[sibling].relative].position;
|
|
int posD = _mesh.nodes[_mesh.nodes[_mesh.nodes[sibling].relative].relative].position;
|
|
|
|
if (posC == edge.posB || posD == edge.posB)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
Vector3F edgeAC = _mesh.positions[posC] - positionA;
|
|
Vector3F edgeAD = _mesh.positions[posD] - positionA;
|
|
Vector3F edgeCD = _mesh.positions[posD] - _mesh.positions[posC];
|
|
var normalBefore = Vector3F.Cross(edgeAC, edgeAD);
|
|
|
|
Vector3F edgeRC = _mesh.positions[posC] - edge.result;
|
|
Vector3F edgeRD = _mesh.positions[posD] - edge.result;
|
|
var normalAfter = Vector3F.Cross(edgeRC, edgeRD);
|
|
if (ShouldRejectBodyTriangle(edge.result, _mesh.positions[posC], _mesh.positions[posD]))
|
|
{
|
|
_rejectedBodyCollision++;
|
|
return false;
|
|
}
|
|
if (IsDegenerateTriangle(edgeAC, edgeAD, edgeCD, normalBefore)
|
|
|| IsDegenerateTriangle(edgeRC, edgeRD, edgeCD, normalAfter))
|
|
{
|
|
_rejectedDegenerate++;
|
|
_rejectedInversion++;
|
|
return false;
|
|
}
|
|
if (normalAfter.SqrMagnitude < normalBefore.SqrMagnitude * minAreaRatioSq)
|
|
{
|
|
_rejectedArea++;
|
|
_rejectedInversion++;
|
|
return false;
|
|
}
|
|
|
|
var dot = Vector3F.Dot(normalBefore, normalAfter);
|
|
if (dot <= 0f)
|
|
{
|
|
_rejectedFlip++;
|
|
_rejectedInversion++;
|
|
return false;
|
|
}
|
|
} while ((sibling = _mesh.nodes[sibling].sibling) != nodeIndexA);
|
|
|
|
sibling = nodeIndexB;
|
|
do
|
|
{
|
|
int posC = _mesh.nodes[_mesh.nodes[sibling].relative].position;
|
|
int posD = _mesh.nodes[_mesh.nodes[_mesh.nodes[sibling].relative].relative].position;
|
|
|
|
if (posC == edge.posA || posD == edge.posA)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
Vector3F edgeAC = _mesh.positions[posC] - positionB;
|
|
Vector3F edgeAD = _mesh.positions[posD] - positionB;
|
|
Vector3F edgeCD = _mesh.positions[posD] - _mesh.positions[posC];
|
|
var normalBefore = Vector3F.Cross(edgeAC, edgeAD);
|
|
|
|
Vector3F edgeRC = _mesh.positions[posC] - edge.result;
|
|
Vector3F edgeRD = _mesh.positions[posD] - edge.result;
|
|
var normalAfter = Vector3F.Cross(edgeRC, edgeRD);
|
|
if (ShouldRejectBodyTriangle(edge.result, _mesh.positions[posC], _mesh.positions[posD]))
|
|
{
|
|
_rejectedBodyCollision++;
|
|
return false;
|
|
}
|
|
if (IsDegenerateTriangle(edgeAC, edgeAD, edgeCD, normalBefore)
|
|
|| IsDegenerateTriangle(edgeRC, edgeRD, edgeCD, normalAfter))
|
|
{
|
|
_rejectedDegenerate++;
|
|
_rejectedInversion++;
|
|
return false;
|
|
}
|
|
if (normalAfter.SqrMagnitude < normalBefore.SqrMagnitude * minAreaRatioSq)
|
|
{
|
|
_rejectedArea++;
|
|
_rejectedInversion++;
|
|
return false;
|
|
}
|
|
|
|
var dot = Vector3F.Dot(normalBefore, normalAfter);
|
|
if (dot <= 0f)
|
|
{
|
|
_rejectedFlip++;
|
|
_rejectedInversion++;
|
|
return false;
|
|
}
|
|
} while ((sibling = _mesh.nodes[sibling].sibling) != nodeIndexB);
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// A |\
|
|
/// | \
|
|
/// |__\ X
|
|
/// | /
|
|
/// | /
|
|
/// B |/
|
|
/// </summary>
|
|
/// <param name="A"></param>
|
|
/// <param name="B"></param>
|
|
/// <param name="X"></param>
|
|
/// <returns></returns>
|
|
private double ComputeLineicError(in Vector3 A, in Vector3 B, in Vector3 X)
|
|
{
|
|
return Vector3.DistancePointLine(X, A, B);
|
|
}
|
|
|
|
private double ComputeVertexError(in SymmetricMatrix q, double x, double y, double z)
|
|
{
|
|
return q.m0 * x * x + 2 * q.m1 * x * y + 2 * q.m2 * x * z + 2 * q.m3 * x
|
|
+ q.m4 * y * y + 2 * q.m5 * y * z + 2 * q.m6 * y
|
|
+ q.m7 * z * z + 2 * q.m8 * z
|
|
+ q.m9;
|
|
}
|
|
|
|
private void InterpolateAttributes(EdgeCollapse pair)
|
|
{
|
|
int posA = pair.posA;
|
|
int posB = pair.posB;
|
|
|
|
int nodeIndexA = _mesh.PositionToNode[posA];
|
|
int nodeIndexB = _mesh.PositionToNode[posB];
|
|
|
|
Vector3 positionA = _mesh.positions[posA];
|
|
Vector3 positionB = _mesh.positions[posB];
|
|
|
|
HashSet<int> procAttributes = new HashSet<int>();
|
|
|
|
Vector3 positionN = pair.result;
|
|
double AN = Vector3.Magnitude(positionA - positionN);
|
|
double BN = Vector3.Magnitude(positionB - positionN);
|
|
double ratio = MathUtils.DivideSafe(AN, AN + BN);
|
|
|
|
/* // Other way (same results I think)
|
|
double ratio = 0;
|
|
double dot = Vector3.Dot(pair.result - positionA, positionB - positionA);
|
|
if (dot > 0)
|
|
ratio = Math.Sqrt(dot);
|
|
ratio /= (positionB - positionA).Length;
|
|
*/
|
|
|
|
// TODO : Probleme d'interpolation
|
|
|
|
|
|
int siblingOfA = nodeIndexA;
|
|
do // Iterator over faces around A
|
|
{
|
|
int relativeOfA = siblingOfA;
|
|
do // Circulate around face
|
|
{
|
|
if (_mesh.nodes[relativeOfA].position == posB)
|
|
{
|
|
if (!procAttributes.Add(_mesh.nodes[siblingOfA].attribute))
|
|
continue;
|
|
|
|
if (!procAttributes.Add(_mesh.nodes[relativeOfA].attribute))
|
|
continue;
|
|
|
|
if (_mesh.attributes != null && _mesh.attributeDefinitions.Length > 0)
|
|
{
|
|
IMetaAttribute attributeA = _mesh.attributes[_mesh.nodes[siblingOfA].attribute];
|
|
IMetaAttribute attributeB = _mesh.attributes[_mesh.nodes[relativeOfA].attribute];
|
|
|
|
for (int i = 0; i < _mesh.attributeDefinitions.Length; i++)
|
|
{
|
|
if (_mesh.attributeDefinitions[i].type == AttributeType.Normals)
|
|
{
|
|
Vector3F normalA = attributeA.Get<Vector3F>(i);
|
|
Vector3F normalB = attributeB.Get<Vector3F>(i);
|
|
|
|
float dot = Vector3F.Dot(normalA, normalB);
|
|
|
|
if (dot < _mergeNormalsThresholdCos)
|
|
{
|
|
continue;
|
|
}
|
|
}
|
|
|
|
_mesh.attributes.Interpolate(i, _mesh.nodes[siblingOfA].attribute, _mesh.nodes[relativeOfA].attribute, ratio);
|
|
}
|
|
}
|
|
}
|
|
} while ((relativeOfA = _mesh.nodes[relativeOfA].relative) != siblingOfA);
|
|
|
|
} while ((siblingOfA = _mesh.nodes[siblingOfA].sibling) != nodeIndexA);
|
|
|
|
|
|
/*
|
|
int attrIndex = _mesh.nodes[nodeIndexA].attribute;
|
|
|
|
int siblingOfA = nodeIndexA;
|
|
do
|
|
{
|
|
_mesh.nodes[siblingOfA].attribute = attrIndex;
|
|
} while ((siblingOfA = _mesh.nodes[siblingOfA].sibling) != nodeIndexA);
|
|
|
|
int siblingOfB = nodeIndexB;
|
|
do
|
|
{
|
|
_mesh.nodes[siblingOfB].attribute = attrIndex;
|
|
} while ((siblingOfB = _mesh.nodes[siblingOfB].sibling) != nodeIndexB);
|
|
*/
|
|
}
|
|
|
|
private readonly Dictionary<IMetaAttribute, int> _uniqueAttributes = new Dictionary<IMetaAttribute, int>();
|
|
|
|
private void MergeAttributes(int nodeIndex)
|
|
{
|
|
if (_mesh.attributeDefinitions.Length == 0)
|
|
return;
|
|
|
|
_uniqueAttributes.Clear();
|
|
|
|
int sibling = nodeIndex;
|
|
do
|
|
{
|
|
_uniqueAttributes.TryAdd(_mesh.attributes[_mesh.nodes[sibling].attribute], _mesh.nodes[sibling].attribute);
|
|
} while ((sibling = _mesh.nodes[sibling].sibling) != nodeIndex);
|
|
|
|
sibling = nodeIndex;
|
|
do
|
|
{
|
|
_mesh.nodes[sibling].attribute = _uniqueAttributes[_mesh.attributes[_mesh.nodes[sibling].attribute]];
|
|
} while ((sibling = _mesh.nodes[sibling].sibling) != nodeIndex);
|
|
}
|
|
|
|
private readonly HashSet<EdgeCollapse> _edgeToRefresh = new HashSet<EdgeCollapse>();
|
|
|
|
private void CollapseEdge(EdgeCollapse pair)
|
|
{
|
|
_collapsedEdges++;
|
|
int nodeIndexA = _mesh.PositionToNode[pair.posA];
|
|
int nodeIndexB = _mesh.PositionToNode[pair.posB];
|
|
|
|
int posA = pair.posA;
|
|
int posB = pair.posB;
|
|
|
|
// Remove all edges around A
|
|
int sibling = nodeIndexA;
|
|
//for (relative = sibling; relative != sibling; relative = _mesh.nodes[relative].relative)
|
|
//for (sibling = nodeIndexA; sibling != nodeIndexA; sibling = _mesh.nodes[sibling].sibling)
|
|
do
|
|
{
|
|
for (int relative = sibling; (relative = _mesh.nodes[relative].relative) != sibling;)
|
|
{
|
|
int posC = _mesh.nodes[relative].position;
|
|
EdgeCollapse pairAC = new EdgeCollapse(posA, posC);
|
|
// Todo : Optimization by only removing first pair (first edge)
|
|
if (_pairs.Remove(pairAC))
|
|
{
|
|
_mins.Remove(pairAC);
|
|
}
|
|
}
|
|
} while ((sibling = _mesh.nodes[sibling].sibling) != nodeIndexA);
|
|
|
|
// Remove all edges around B
|
|
sibling = nodeIndexB;
|
|
do
|
|
{
|
|
for (int relative = sibling; (relative = _mesh.nodes[relative].relative) != sibling;)
|
|
{
|
|
int posC = _mesh.nodes[relative].position;
|
|
EdgeCollapse pairBC = new EdgeCollapse(posB, posC);
|
|
if (_pairs.Remove(pairBC))
|
|
{
|
|
_mins.Remove(pairBC);
|
|
}
|
|
}
|
|
} while ((sibling = _mesh.nodes[sibling].sibling) != nodeIndexB);
|
|
|
|
// Interpolates attributes
|
|
InterpolateAttributes(pair);
|
|
|
|
// Collapse edge
|
|
int validNode = _mesh.CollapseEdge(nodeIndexA, nodeIndexB);
|
|
|
|
// A disconnected triangle has been collapsed, there are no edges to register
|
|
if (validNode < 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
posA = _mesh.nodes[validNode].position;
|
|
|
|
_mesh.positions[posA] = pair.result;
|
|
|
|
MergeAttributes(validNode);
|
|
|
|
CalculateQuadric(posA);
|
|
|
|
_edgeToRefresh.Clear();
|
|
|
|
sibling = validNode;
|
|
do
|
|
{
|
|
for (int relative = sibling; (relative = _mesh.nodes[relative].relative) != sibling;)
|
|
{
|
|
int posC = _mesh.nodes[relative].position;
|
|
_edgeToRefresh.Add(new EdgeCollapse(posA, posC));
|
|
|
|
if (UpdateFarNeighbors)
|
|
{
|
|
int sibling2 = relative;
|
|
while ((sibling2 = _mesh.nodes[sibling2].sibling) != relative)
|
|
{
|
|
int relative2 = sibling2;
|
|
while ((relative2 = _mesh.nodes[relative2].relative) != sibling2)
|
|
{
|
|
int posD = _mesh.nodes[relative2].position;
|
|
if (posD != posC)
|
|
{
|
|
_edgeToRefresh.Add(new EdgeCollapse(posC, posD));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} while ((sibling = _mesh.nodes[sibling].sibling) != validNode);
|
|
|
|
foreach (EdgeCollapse edge in _edgeToRefresh)
|
|
{
|
|
CalculateQuadric(edge.posB);
|
|
edge.SetWeight(-1);
|
|
_pairs.Remove(edge);
|
|
_pairs.Add(edge);
|
|
}
|
|
|
|
foreach (EdgeCollapse edge in _edgeToRefresh)
|
|
{
|
|
CalculateError(edge);
|
|
_mins.Remove(edge);
|
|
if (UpdateMinsOnCollapse)
|
|
{
|
|
_mins.AddMin(edge);
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ResetStats()
|
|
{
|
|
_evaluatedEdges = 0;
|
|
_collapsedEdges = 0;
|
|
_rejectedBoneWeights = 0;
|
|
_rejectedTopology = 0;
|
|
_rejectedInversion = 0;
|
|
_rejectedDegenerate = 0;
|
|
_rejectedArea = 0;
|
|
_rejectedFlip = 0;
|
|
_rejectedBodyCollision = 0;
|
|
}
|
|
|
|
private bool IsPointNearBody(in Vector3 point)
|
|
{
|
|
if (_bodyDistanceSqEvaluator == null || _bodyDistanceThresholdSq <= 0f)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var sq = _bodyDistanceSqEvaluator(point);
|
|
return !float.IsNaN(sq) && sq <= _bodyDistanceThresholdSq;
|
|
}
|
|
|
|
private bool IsPointNearBody(in Vector3 point, float thresholdSq)
|
|
{
|
|
if (_bodyDistanceSqEvaluator == null || thresholdSq <= 0f)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var sq = _bodyDistanceSqEvaluator(point);
|
|
return !float.IsNaN(sq) && sq <= thresholdSq;
|
|
}
|
|
|
|
private bool ShouldRejectBodyTriangle(in Vector3 a, in Vector3 b, in Vector3 c)
|
|
{
|
|
if (_bodyDistanceSqEvaluator == null || _bodyDistanceThresholdSq <= 0f)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var centroid = (a + b + c) / 3d;
|
|
if (!CollapseToEndpointsOnly)
|
|
{
|
|
return IsPointNearBody(centroid);
|
|
}
|
|
|
|
var penetrationFactor = MathF.Max(0f, BodyCollisionPenetrationFactor);
|
|
var penetrationThresholdSq = _bodyDistanceThresholdSq * penetrationFactor * penetrationFactor;
|
|
if (IsPointNearBody(centroid, penetrationThresholdSq))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var ab = (a + b) * 0.5;
|
|
var bc = (b + c) * 0.5;
|
|
var ca = (c + a) * 0.5;
|
|
return IsPointNearBody(ab, penetrationThresholdSq)
|
|
|| IsPointNearBody(bc, penetrationThresholdSq)
|
|
|| IsPointNearBody(ca, penetrationThresholdSq);
|
|
}
|
|
|
|
private bool IsProtectedVertex(int pos)
|
|
{
|
|
if (_protectedVertices == null)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return (uint)pos < (uint)_protectedVertices.Length && _protectedVertices[pos];
|
|
}
|
|
|
|
private static bool IsDegenerateTriangle(in Vector3F edge0, in Vector3F edge1, in Vector3F edge2, in Vector3F normal)
|
|
{
|
|
var maxEdgeSq = MathF.Max(edge0.SqrMagnitude, MathF.Max(edge1.SqrMagnitude, edge2.SqrMagnitude));
|
|
if (maxEdgeSq <= 0f)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
var minNormalSq = (float)(_DeterminantEpsilon * _DeterminantEpsilon) * maxEdgeSq * maxEdgeSq;
|
|
return normal.SqrMagnitude <= minNormalSq;
|
|
}
|
|
}
|
|
|
|
public readonly record struct DecimationStats(
|
|
int EvaluatedEdges,
|
|
int CollapsedEdges,
|
|
int RejectedBoneWeights,
|
|
int RejectedTopology,
|
|
int RejectedInversion,
|
|
int RejectedDegenerate,
|
|
int RejectedArea,
|
|
int RejectedFlip,
|
|
int RejectedBodyCollision);
|
|
}
|