using System; using System.Collections.Generic; using UnityEngine; namespace Framework.Geometry.Volumes.Fitting { /// Fits 3D planes to meshes public static class Plane3Fitter { #region enum VectorComponent /// Individual comopnents of a 3D vector public enum VectorComponent { /// The X coordinate of a vector X, /// The Y coordinate of a vector Y, /// The Z coordinate of a vector Z } #endregion // enum VectorComponent /// Fits an axis aligned plane to a set of points /// Points to which the axis aligned plane will be fitted /// Axis of the plane's normal vector /// /// Distance beyond which outliers will be incrementally eliminated /// /// The offset of the plane on the specified axis public static float FitAxisAlignedPlane( IList points, VectorComponent axis, float maximumOutlierDelta = 0.01f ) { // Extract the component on which the plane should be fitted. // The other vector components quite simply don't matter :) ArraySegment sortedValues; { float[] values = demuxVectorComponent(points, axis); Array.Sort(values); sortedValues = new ArraySegment(values, 0, values.Length); } return findHighestDensity(sortedValues, maximumOutlierDelta); } /// Finds the highest density of values within the specified values /// /// Values of which the point of highest density will be found /// /// /// Distance beyond which outliers will be incrementally eliminated /// /// The point of highest density private static float findHighestDensity( ArraySegment sortedValues, float maximumOutlierDelta = 0.01f ) { int startIndex = sortedValues.Offset; int endIndex = startIndex + sortedValues.Count - 1; // Calculate the sum of all remaining values. We will use this to find the mean // of the values so we can see which outliers to eliminate. float sum = sortedValues.Array[startIndex]; for(int index = startIndex + 1; index <= endIndex; ++index) { sum += sortedValues.Array[index]; } // Now remove outliers until all of the remaining points are within the maximum // outlier distance, then return the median of those points. float median; for(;;) { median = sum / (endIndex - startIndex + 1); // Each iteration, eliminate the furthest point. We need to go point-by-point // because if enough points are outliers, this would move the median in // the first iterations so far that the point of highest density would be // an outlier, too. float leftDistance = Math.Abs(median - sortedValues.Array[startIndex]); float rightDistance = Math.Abs(sortedValues.Array[endIndex] - median); if(leftDistance > rightDistance) { if(leftDistance < maximumOutlierDelta) { break; } sum -= sortedValues.Array[startIndex]; ++startIndex; } else { if(rightDistance < maximumOutlierDelta) { break; } sum -= sortedValues.Array[endIndex]; --endIndex; } } //Debug.Log("Median averaged on " + (endIndex - startIndex + 1) + " samples."); return median; } #if false /// /// Removes elements from a sorted list until the remaining values fall within /// a specified range /// /// List from which elements will be removed /// Minimum value to keep in the list (inclusive) /// Maximum value to keep in the list (exclusive) private static void Trim( ref ArraySegment sortedValues, float min, float max ) { int startIndex = sortedValues.Offset; int endIndex = startIndex + sortedValues.Count - 1; // Remove values that are smaller than the minimum value while(sortedValues.Array[startIndex] < min) { ++startIndex; if(startIndex > endIndex) { sortedValues = new ArraySegment(sortedValues.Array, sortedValues.Offset, 0); return; } } // Remove values that are larger than the maximum value while(sortedValues.Array[endIndex] >= max) { --endIndex; if(endIndex < startIndex) { sortedValues = new ArraySegment(sortedValues.Array, sortedValues.Offset, 0); return; } } sortedValues = new ArraySegment( sortedValues.Array, startIndex, (endIndex - startIndex + 1) ); } #endif /// Demuxes an individual component from a set of vectors /// Vector that will have one of their components extracted /// Vector component that will be demuxed /// An array of all the values of the specified vector component private static float[] demuxVectorComponent( IList vectors, VectorComponent component ) { int vectorCount = vectors.Count; var values = new float[vectorCount]; switch(component) { case VectorComponent.X: { for(int index = 0; index < vectorCount; ++index) { values[index] = vectors[index].x; } break; } case VectorComponent.Y: { for(int index = 0; index < vectorCount; ++index) { values[index] = vectors[index].y; } break; } case VectorComponent.Z: { for(int index = 0; index < vectorCount; ++index) { values[index] = vectors[index].z; } break; } default: { throw new ArgumentException("Unsupported vector component index", "component"); } } return values; } } } // namespace Framework.Geometry.Volumes.Fitting