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 _pairs; private LinkedHashSet _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? _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? 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(); _mins = new LinkedHashSet(); 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.LinkedHashNode edge = _mins.First; return edge?.Value; } private int MinsCount => MathF.Clamp(500, 0, _pairs.Count); private void ComputeMins() { _mins = new LinkedHashSet(_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 _adjacentEdges = new HashSet(); private readonly HashSet _adjacentEdgesA = new HashSet(); private readonly HashSet _adjacentEdgesB = new HashSet(); private IEnumerable 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 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 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 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)attrList[_mesh.nodes[siblingA].attribute]; if ((attrA.attr0.flags & FfxivAttributeFlags.BoneWeights) != 0) { hasWeights = true; int siblingB = nodeB; do { var attrB = (MetaAttribute)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 attrList) { return false; } var attrA = ((MetaAttribute)attrList[posA]).attr0; var attrB = ((MetaAttribute)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 attrList) { return false; } var attrA = ((MetaAttribute)attrList[posA]).attr0; var attrB = ((MetaAttribute)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 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)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 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)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)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; } /// /// A |\ /// | \ /// |__\ X /// | / /// | / /// B |/ /// /// /// /// /// 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 procAttributes = new HashSet(); 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(i); Vector3F normalB = attributeB.Get(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 _uniqueAttributes = new Dictionary(); 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 _edgeToRefresh = new HashSet(); 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); }