// ╔════════════════════════════════════════════════════════════════╗ // ║ Copyright © 2025 NWH Coding d.o.o. All rights reserved. ║ // ║ Licensed under Unity Asset Store Terms of Service: ║ // ║ https://unity.com/legal/as-terms ║ // ║ Use permitted only in compliance with the License. ║ // ║ Distributed "AS IS", without warranty of any kind. ║ // ╚════════════════════════════════════════════════════════════════╝ /****************************************************************************** * * The MIT License (MIT) * * MIConvexHull, Copyright (c) 2015 David Sehnal, Matthew Campbell * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. * *****************************************************************************/ #region using System.Collections.Generic; #endregion namespace NWH.DWP2.MiConvexHull { /* * Main part of the algorithm * Basic idea: * - Create the initial hull (done in Initialize.cs) * * For each face there are "vertices beyond" which are "visible" from it. * If there are no such vertices, the face is on the hull. * * - While there is at least one face with at least one "vertex beyond": * * Pick the furthest beyond vertex * * For this vertex: * > find all faces that are visible from it (TagAffectedFaces) * > remove them and replace them with a "cone" created by the vertex and the boundary * of the affected faces, and for each new face, compute "beyond vertices" * (CreateCone + CommitCone) * * + Implement it in way that is fast, but hard to understand and maintain. */ /// /// Class ConvexHullAlgorithm. /// internal partial class ConvexHullAlgorithm { /// /// Tags all faces seen from the current vertex with 1. /// /// The current face. private void TagAffectedFaces(ConvexFaceInternal currentFace) { AffectedFaceBuffer.Clear(); AffectedFaceBuffer.Add(currentFace.Index); TraverseAffectedFaces(currentFace.Index); } /// /// Recursively traverse all the relevant faces. /// /// The current face. private void TraverseAffectedFaces(int currentFace) { TraverseStack.Clear(); TraverseStack.Push(currentFace); AffectedFaceFlags[currentFace] = true; while (TraverseStack.Count > 0) { ConvexFaceInternal top = FacePool[TraverseStack.Pop()]; for (int i = 0; i < NumOfDimensions; i++) { int adjFace = top.AdjacentFaces[i]; if (!AffectedFaceFlags[adjFace] && MathHelper.GetVertexDistance(CurrentVertex, FacePool[adjFace]) >= PlaneDistanceTolerance) { AffectedFaceBuffer.Add(adjFace); AffectedFaceFlags[adjFace] = true; TraverseStack.Push(adjFace); } } } } /// /// Creates a new deferred face. /// /// The face. /// Index of the face. /// The pivot. /// Index of the pivot. /// The old face. /// DeferredFace. private DeferredFace MakeDeferredFace(ConvexFaceInternal face, int faceIndex, ConvexFaceInternal pivot, int pivotIndex, ConvexFaceInternal oldFace) { DeferredFace ret = ObjectManager.GetDeferredFace(); ret.Face = face; ret.FaceIndex = faceIndex; ret.Pivot = pivot; ret.PivotIndex = pivotIndex; ret.OldFace = oldFace; return ret; } /// /// Connect faces using a connector. /// /// The connector. private void ConnectFace(FaceConnector connector) { uint index = connector.HashCode % Constants.ConnectorTableSize; ConnectorList list = ConnectorTable[index]; for (FaceConnector current = list.First; current != null; current = current.Next) { if (FaceConnector.AreConnectable(connector, current, NumOfDimensions)) { list.Remove(current); FaceConnector.Connect(current, connector); current.Face = null; connector.Face = null; ObjectManager.DepositConnector(current); ObjectManager.DepositConnector(connector); return; } } list.Add(connector); } /// /// Removes the faces "covered" by the current vertex and adds the newly created ones. /// /// true if XXXX, false otherwise. private bool CreateCone() { int currentVertexIndex = CurrentVertex; ConeFaceBuffer.Clear(); for (int fIndex = 0; fIndex < AffectedFaceBuffer.Count; fIndex++) { int oldFaceIndex = AffectedFaceBuffer[fIndex]; ConvexFaceInternal oldFace = FacePool[oldFaceIndex]; // Find the faces that need to be updated int updateCount = 0; for (int i = 0; i < NumOfDimensions; i++) { int af = oldFace.AdjacentFaces[i]; if (!AffectedFaceFlags[af]) // Tag == false when oldFaces does not contain af { UpdateBuffer[updateCount] = af; UpdateIndices[updateCount] = i; ++updateCount; } } for (int i = 0; i < updateCount; i++) { ConvexFaceInternal adjacentFace = FacePool[UpdateBuffer[i]]; int oldFaceAdjacentIndex = 0; int[] adjFaceAdjacency = adjacentFace.AdjacentFaces; for (int j = 0; j < adjFaceAdjacency.Length; j++) { if (oldFaceIndex == adjFaceAdjacency[j]) { oldFaceAdjacentIndex = j; break; } } int forbidden = UpdateIndices[i]; // Index of the face that corresponds to this adjacent face int oldVertexIndex; int[] vertices; int newFaceIndex = ObjectManager.GetFace(); ConvexFaceInternal newFace = FacePool[newFaceIndex]; vertices = newFace.Vertices; for (int j = 0; j < NumOfDimensions; j++) { vertices[j] = oldFace.Vertices[j]; } oldVertexIndex = vertices[forbidden]; int orderedPivotIndex; // correct the ordering if (currentVertexIndex < oldVertexIndex) { orderedPivotIndex = 0; for (int j = forbidden - 1; j >= 0; j--) { if (vertices[j] > currentVertexIndex) { vertices[j + 1] = vertices[j]; } else { orderedPivotIndex = j + 1; break; } } } else { orderedPivotIndex = NumOfDimensions - 1; for (int j = forbidden + 1; j < NumOfDimensions; j++) { if (vertices[j] < currentVertexIndex) { vertices[j - 1] = vertices[j]; } else { orderedPivotIndex = j - 1; break; } } } vertices[orderedPivotIndex] = CurrentVertex; if (!MathHelper.CalculateFacePlane(newFace, Center)) { return false; } ConeFaceBuffer.Add(MakeDeferredFace(newFace, orderedPivotIndex, adjacentFace, oldFaceAdjacentIndex, oldFace)); } } return true; } /// /// Commits a cone and adds a vertex to the convex hull. /// private void CommitCone() { // Fill the adjacency. for (int i = 0; i < ConeFaceBuffer.Count; i++) { DeferredFace face = ConeFaceBuffer[i]; ConvexFaceInternal newFace = face.Face; ConvexFaceInternal adjacentFace = face.Pivot; ConvexFaceInternal oldFace = face.OldFace; int orderedPivotIndex = face.FaceIndex; newFace.AdjacentFaces[orderedPivotIndex] = adjacentFace.Index; adjacentFace.AdjacentFaces[face.PivotIndex] = newFace.Index; // let there be a connection. for (int j = 0; j < NumOfDimensions; j++) { if (j == orderedPivotIndex) { continue; } FaceConnector connector = ObjectManager.GetConnector(); connector.Update(newFace, j, NumOfDimensions); ConnectFace(connector); } // the id adjacent face on the hull? If so, we can use simple method to find beyond vertices. if (adjacentFace.VerticesBeyond.Count == 0) { FindBeyondVertices(newFace, oldFace.VerticesBeyond); } // it is slightly more effective if the face with the lower number of beyond vertices comes first. else if (adjacentFace.VerticesBeyond.Count < oldFace.VerticesBeyond.Count) { FindBeyondVertices(newFace, adjacentFace.VerticesBeyond, oldFace.VerticesBeyond); } else { FindBeyondVertices(newFace, oldFace.VerticesBeyond, adjacentFace.VerticesBeyond); } // This face will definitely lie on the hull if (newFace.VerticesBeyond.Count == 0) { ConvexFaces.Add(newFace.Index); UnprocessedFaces.Remove(newFace); ObjectManager.DepositVertexBuffer(newFace.VerticesBeyond); newFace.VerticesBeyond = EmptyBuffer; } else // Add the face to the list { UnprocessedFaces.Add(newFace); } // recycle the object. ObjectManager.DepositDeferredFace(face); } // Recycle the affected faces. for (int fIndex = 0; fIndex < AffectedFaceBuffer.Count; fIndex++) { int face = AffectedFaceBuffer[fIndex]; UnprocessedFaces.Remove(FacePool[face]); ObjectManager.DepositFace(face); } } /// /// Check whether the vertex v is beyond the given face. If so, add it to beyondVertices. /// /// The face. /// The beyond vertices. /// The v. private void IsBeyond(ConvexFaceInternal face, IndexBuffer beyondVertices, int v) { double distance = MathHelper.GetVertexDistance(v, face); if (distance >= PlaneDistanceTolerance) { if (distance > MaxDistance) { // If it's within the tolerance distance, use the lex. larger point if (distance - MaxDistance < PlaneDistanceTolerance) { // todo: why is this LexCompare necessary. Would seem to favor x over y over z (etc.)? if (LexCompare(v, FurthestVertex) > 0) { MaxDistance = distance; FurthestVertex = v; } } else { MaxDistance = distance; FurthestVertex = v; } } beyondVertices.Add(v); } } /// /// Compares the values of two vertices. The return value (-1, 0 or +1) are found /// by first checking the first coordinate and then progressing through the rest. /// In this way {2, 8} will be a "-1" (less than) {3, 1}. /// /// The base vertex index, u. /// The compared vertex index, v. /// System.Int32. private int LexCompare(int u, int v) { int uOffset = u * NumOfDimensions, vOffset = v * NumOfDimensions; for (int i = 0; i < NumOfDimensions; i++) { double x = Positions[uOffset + i], y = Positions[vOffset + i]; int comp = x.CompareTo(y); if (comp != 0) { return comp; } } return 0; } /// /// Used by update faces. /// /// The face. /// The beyond. /// The beyond1. private void FindBeyondVertices(ConvexFaceInternal face, IndexBuffer beyond, IndexBuffer beyond1) { IndexBuffer beyondVertices = BeyondBuffer; MaxDistance = double.NegativeInfinity; FurthestVertex = 0; int v; for (int i = 0; i < beyond1.Count; i++) { VertexVisited[beyond1[i]] = true; } VertexVisited[CurrentVertex] = false; for (int i = 0; i < beyond.Count; i++) { v = beyond[i]; if (v == CurrentVertex) { continue; } VertexVisited[v] = false; IsBeyond(face, beyondVertices, v); } for (int i = 0; i < beyond1.Count; i++) { v = beyond1[i]; if (VertexVisited[v]) { IsBeyond(face, beyondVertices, v); } } face.FurthestVertex = FurthestVertex; // Pull the old switch a roo (switch the face beyond buffers) IndexBuffer temp = face.VerticesBeyond; face.VerticesBeyond = beyondVertices; if (temp.Count > 0) { temp.Clear(); } BeyondBuffer = temp; } /// /// Finds the beyond vertices. /// /// The face. /// The beyond. private void FindBeyondVertices(ConvexFaceInternal face, IndexBuffer beyond) { IndexBuffer beyondVertices = BeyondBuffer; MaxDistance = double.NegativeInfinity; FurthestVertex = 0; int v; for (int i = 0; i < beyond.Count; i++) { v = beyond[i]; if (v == CurrentVertex) { continue; } IsBeyond(face, beyondVertices, v); } face.FurthestVertex = FurthestVertex; // Pull the old switch a roo (switch the face beyond buffers) IndexBuffer temp = face.VerticesBeyond; face.VerticesBeyond = beyondVertices; if (temp.Count > 0) { temp.Clear(); } BeyondBuffer = temp; } /// /// Recalculates the centroid of the current hull. /// private void UpdateCenter() { for (int i = 0; i < NumOfDimensions; i++) { Center[i] *= ConvexHullSize; } ConvexHullSize += 1; double f = 1.0 / ConvexHullSize; int co = CurrentVertex * NumOfDimensions; for (int i = 0; i < NumOfDimensions; i++) { Center[i] = f * (Center[i] + Positions[co + i]); } } /// /// Removes the last vertex from the center. /// private void RollbackCenter() { for (int i = 0; i < NumOfDimensions; i++) { Center[i] *= ConvexHullSize; } ConvexHullSize -= 1; double f = ConvexHullSize > 0 ? 1.0 / ConvexHullSize : 0.0; int co = CurrentVertex * NumOfDimensions; for (int i = 0; i < NumOfDimensions; i++) { Center[i] = f * (Center[i] - Positions[co + i]); } } /// /// Handles singular vertex. /// private void HandleSingular() { RollbackCenter(); SingularVertices.Add(CurrentVertex); // This means that all the affected faces must be on the hull and that all their "vertices beyond" are // singular. for (int fIndex = 0; fIndex < AffectedFaceBuffer.Count; fIndex++) { ConvexFaceInternal face = FacePool[AffectedFaceBuffer[fIndex]]; IndexBuffer vb = face.VerticesBeyond; for (int i = 0; i < vb.Count; i++) { SingularVertices.Add(vb[i]); } ConvexFaces.Add(face.Index); UnprocessedFaces.Remove(face); ObjectManager.DepositVertexBuffer(face.VerticesBeyond); face.VerticesBeyond = EmptyBuffer; } } /// /// Get a vertex coordinate. In order to reduce speed, all vertex coordinates /// have been placed in a single array. /// /// The vertex index. /// The index of the dimension. /// System.Double. private double GetCoordinate(int vIndex, int dimension) { return Positions[vIndex * NumOfDimensions + dimension]; } #region Returning the Results in the proper format /// /// Gets the hull vertices. /// /// The type of the t vertex. /// The data. /// TVertex[]. private TVertex[] GetHullVertices(IList data) { int cellCount = ConvexFaces.Count; int hullVertexCount = 0; for (int i = 0; i < NumberOfVertices; i++) { VertexVisited[i] = false; } for (int i = 0; i < cellCount; i++) { int[] vs = FacePool[ConvexFaces[i]].Vertices; for (int j = 0; j < vs.Length; j++) { int v = vs[j]; if (!VertexVisited[v]) { VertexVisited[v] = true; hullVertexCount++; } } } TVertex[] result = new TVertex[hullVertexCount]; for (int i = 0; i < NumberOfVertices; i++) { if (VertexVisited[i]) { result[--hullVertexCount] = data[i]; } } return result; } /// /// Finds the convex hull and creates the TFace objects. /// /// The type of the t face. /// The type of the t vertex. /// TFace[]. private TFace[] GetConvexFaces() where TFace : ConvexFace, new() where TVertex : IVertex { IndexBuffer faces = ConvexFaces; int cellCount = faces.Count; TFace[] cells = new TFace[cellCount]; for (int i = 0; i < cellCount; i++) { ConvexFaceInternal face = FacePool[faces[i]]; TVertex[] vertices = new TVertex[NumOfDimensions]; for (int j = 0; j < NumOfDimensions; j++) { vertices[j] = (TVertex)Vertices[face.Vertices[j]]; } cells[i] = new TFace { Vertices = vertices, Adjacency = new TFace[NumOfDimensions], Normal = IsLifted ? null : face.Normal, }; face.Tag = i; } for (int i = 0; i < cellCount; i++) { ConvexFaceInternal face = FacePool[faces[i]]; TFace cell = cells[i]; for (int j = 0; j < NumOfDimensions; j++) { if (face.AdjacentFaces[j] < 0) { continue; } cell.Adjacency[j] = cells[FacePool[face.AdjacentFaces[j]].Tag]; } // Fix the vertex orientation. if (face.IsNormalFlipped) { TVertex tempVert = cell.Vertices[0]; cell.Vertices[0] = cell.Vertices[NumOfDimensions - 1]; cell.Vertices[NumOfDimensions - 1] = tempVert; TFace tempAdj = cell.Adjacency[0]; cell.Adjacency[0] = cell.Adjacency[NumOfDimensions - 1]; cell.Adjacency[NumOfDimensions - 1] = tempAdj; } } return cells; } #endregion } }