using System; using System.Collections.Generic; using UnityEngine; namespace Unity.XR.CoreUtils { /// /// Utility methods for common geometric operations. /// public static class GeometryUtils { // Used in approximate equality checks const float k_TwoPi = Mathf.PI * 2f; // constants/cached constructions for Vector/UV operations static readonly Vector3 k_Up = Vector3.up; static readonly Vector3 k_Forward = Vector3.forward; static readonly Vector3 k_Zero = Vector3.zero; static readonly Quaternion k_VerticalCorrection = Quaternion.AngleAxis(180.0f, k_Up); const float k_MostlyVertical = 0.95f; // Local method use only -- created here to reduce garbage collection. Collections must be cleared before use static readonly List k_HullEdgeDirections = new List(); static readonly HashSet k_HullIndices = new HashSet(); /// /// Finds the side of a polygon closest to a specified world space position. /// /// Vertices defining the outline of a polygon. /// The position in space to find the two closest outline vertices to. /// The coordinates of the first vertex of the nearest side is assigned to this `out` parameter. /// The coordinates of the second vertex of the nearest side is assigned to this `out` parameter. /// True if a nearest edge could be found. public static bool FindClosestEdge(List vertices, Vector3 point, out Vector3 vertexA, out Vector3 vertexB) { var vertexCount = vertices.Count; if (vertexCount < 1) { vertexA = Vector3.zero; vertexB = Vector3.zero; return false; } var shortestSqrDistance = float.MaxValue; var closestVertA = Vector3.zero; var closestVertB = Vector3.zero; for (var i = 0; i < vertexCount; i++) { var vert = vertices[i]; var nextVert = vertices[(i + 1) % vertices.Count]; var closestPointOnEdge = ClosestPointOnLineSegment(point, vert, nextVert); var sqrDistanceToEdge = Vector3.SqrMagnitude(point - closestPointOnEdge); if (sqrDistanceToEdge < shortestSqrDistance) { shortestSqrDistance = sqrDistanceToEdge; closestVertA = vert; closestVertB = nextVert; } } vertexA = closestVertA; vertexB = closestVertB; return true; } /// /// Finds the point on a polygon perimeter farthest from a specified point in space. /// /// Vertices defining the outline of a polygon. /// The position in world space to find the furthest intersection point. /// A world space position of a point on the polygon that is as far from the input point as possible. /// Returns if contains less than tree points. public static Vector3 PointOnOppositeSideOfPolygon(List vertices, Vector3 point) { const float oppositeSideBufferScale = 100.0f; var vertexCount = vertices.Count; if (vertexCount < 3) return Vector3.zero; var a = vertices[0]; var b = vertices[1]; var c = vertices[2]; var normal = Vector3.Cross(b - a, c - a).normalized; var center = Vector3.zero; foreach (var vertex in vertices) { center += vertex; } center *= 1f / vertexCount; var toPoint = Vector3.ProjectOnPlane(point - center, normal); var lengthMinusOne = vertexCount - 1; for (var i = 0; i < vertexCount; i++) { var vertexA = vertices[i]; var aNeighbor = i == lengthMinusOne ? a : vertices[i + 1]; var aLineVector = aNeighbor - vertexA; ClosestTimesOnTwoLines(vertexA, aLineVector, center, -toPoint * oppositeSideBufferScale, out var s, out var t); if (t >= 0 && s >= 0 && s <= 1) { return vertexA + aLineVector * s; } } return Vector3.zero; } /// /// Generates a standard triangle buffer with a given number of indices and adds it to the specified list. /// /// /// Set to reverse the normal winding order. /// /// Example winding orders: /// /// | **Normal winding** | **Reverse winding** | /// | :----------------- | :------------------ | /// | 0, 1, 2, | 0, 2, 1, /// | 0, 2, 3, | 0, 3, 2, /// | 0, 3, 4, | 0, 4, 3, /// | ... | ... | /// /// The list to which to add the triangle buffer. The list is not cleared. /// The number of perimeter vertices. /// (Optional) Whether to reverse the winding order of the vertices. public static void TriangulatePolygon(List indices, int vertCount, bool reverse = false) { vertCount -= 2; indices.EnsureCapacity(vertCount * 3); if (reverse) { for (var i = 0; i < vertCount; i++) { indices.Add(0); indices.Add(i + 2); indices.Add(i + 1); } } else { for (var i = 0; i < vertCount; i++) { indices.Add(0); indices.Add(i + 1); indices.Add(i + 2); } } } /// /// Finds the times at which two linear trajectories are the closest to each other. /// /// /// Two trajectories which may or may not intersect have a time along each path which minimizes the distance /// between trajectories. This function finds those two times. The same logic applies to line segments, where /// the one point is the starting position, and the second point is the position at t = 1. /// /// Starting point of object a /// Velocity (direction and magnitude) of object a /// Starting point of object b /// Velocity (direction and magnitude) of object b /// The time along trajectory a /// The time along trajectory b /// (Optional) epsilon value for parallel lines test /// False if the lines are parallel, otherwise true public static bool ClosestTimesOnTwoLines(Vector3 positionA, Vector3 velocityA, Vector3 positionB, Vector3 velocityB, out float s, out float t, double parallelTest = double.Epsilon) { // Cast dot products to doubles because parallel test can fail on some hardware (iOS) var a = (double)Vector3.Dot(velocityA, velocityA); var b = (double)Vector3.Dot(velocityA, velocityB); var e = (double)Vector3.Dot(velocityB, velocityB); var d = a * e - b * b; //lines are parallel if (Math.Abs(d) < parallelTest) { s = 0; t = 0; return false; } var r = positionA - positionB; var c = Vector3.Dot(velocityA, r); var f = Vector3.Dot(velocityB, r); s = (float)((b * f - c * e) / d); t = (float)((a * f - c * b) / d); return true; } /// /// Finds the times of closest approach between two non-parallel trajectories. /// /// /// Two trajectories, which may or may not intersect, have a time along each path that minimizes the distance /// between trajectories. This function finds those two times. The same logic applies to line segments, where /// the one point is the starting position, and the second point is the position at t = 1. /// This function ignores the y components. /// /// Starting point of object A. /// Velocity (direction and magnitude) of object A. /// Starting point of object B. /// Velocity (direction and magnitude) of object B. /// Set to the calculated time of closest approach along trajectory A. /// Set to the calculated time of closest approach along trajectory B. /// (Optional) A custom epsilon value for teh parallel lines test. /// False if the lines are parallel, otherwise true. public static bool ClosestTimesOnTwoLinesXZ(Vector3 positionA, Vector3 velocityA, Vector3 positionB, Vector3 velocityB, out float s, out float t, double parallelTest = double.Epsilon) { // Cast dot products to doubles because parallel test can fail on some hardware (iOS) var a = (double)(velocityA.x * velocityA.x + velocityA.z * velocityA.z); var b = (double)(velocityA.x * velocityB.x + velocityA.z * velocityB.z); var e = (double)(velocityB.x * velocityB.x + velocityB.z * velocityB.z); var d = a * e - b * b; //lines are parallel if (Math.Abs(d) < parallelTest) { s = 0; t = 0; return false; } var r = positionA - positionB; var c = velocityA.x * r.x + velocityA.z * r.z; var f = velocityB.x * r.x + velocityB.z * r.z; s = (float)((b * f - c * e) / d); t = (float)((a * f - c * b) / d); return true; } /// /// Finds the closest points between two line segments. /// /// /// If the two line segments are parallel, then and /// are set to the midpoint of the respective line segments. /// /// Starting point of segment A. /// Vector from point a to the end point of segment A. /// Starting point of segment B. /// Vector from point b to the end point of segment B. /// Set to the coordinates of the point along segment A that is closest to any point on segment B. /// Set to the coordinates of the point along segment B that is closest to any point on segment A. /// (Optional) epsilon value for parallel lines test /// True if the line segments are parallel, false otherwise public static bool ClosestPointsOnTwoLineSegments(Vector3 a, Vector3 aLineVector, Vector3 b, Vector3 bLineVector, out Vector3 resultA, out Vector3 resultB, double parallelTest = double.Epsilon) { var parallel = !ClosestTimesOnTwoLines(a, aLineVector, b, bLineVector, out var s, out var t, parallelTest); if (s > 0 && s <= 1 && t > 0 && t <= 1) { resultA = a + aLineVector * s; resultB = b + bLineVector * t; } else { // Edge cases (literally--we are checking each of the four endpoints against the opposite segment) var bNeighbor = b + bLineVector; var aNeighbor = a + aLineVector; var aOnB = ClosestPointOnLineSegment(a, b, bNeighbor); var aNeighborOnB = ClosestPointOnLineSegment(aNeighbor, b, bNeighbor); var minDist = Vector3.Distance(a, aOnB); resultA = a; resultB = aOnB; var nextDist = Vector3.Distance(aNeighbor, aNeighborOnB); if (nextDist < minDist) { resultA = aNeighbor; resultB = aNeighborOnB; minDist = nextDist; } var bOnA = ClosestPointOnLineSegment(b, a, aNeighbor); nextDist = Vector3.Distance(b, bOnA); if (nextDist < minDist) { resultA = bOnA; resultB = b; minDist = nextDist; } var bNeighborOnA = ClosestPointOnLineSegment(bNeighbor, a, aNeighbor); nextDist = Vector3.Distance(bNeighbor, bNeighborOnA); if (nextDist < minDist) { resultA = bNeighborOnA; resultB = bNeighbor; } if (parallel) { if (Vector3.Dot(aLineVector, bLineVector) > 0) { t = Vector3.Dot(bNeighbor - a, aLineVector.normalized) * 0.5f; var midA = a + aLineVector.normalized * t; var midB = bNeighbor + bLineVector.normalized * -t; if (t > 0 && t < aLineVector.magnitude) { resultA = midA; resultB = midB; } } else { t = Vector3.Dot(aNeighbor - bNeighbor, aLineVector.normalized) * 0.5f; var midA = aNeighbor + aLineVector.normalized * -t; var midB = bNeighbor + bLineVector.normalized * -t; if (t > 0 && t < aLineVector.magnitude) { resultA = midA; resultB = midB; } } } } return parallel; } /// /// Returns the point along a line segment closest to a given point. /// /// The point to test against the line segment. /// The first point of the line segment. /// The second point of the line segment. /// The point along the line segment closest to . public static Vector3 ClosestPointOnLineSegment(Vector3 point, Vector3 a, Vector3 b) { var segment = b - a; var direction = segment.normalized; var projection = Vector3.Dot(point - a, direction); if (projection < 0) return a; if (projection * projection > segment.sqrMagnitude) return b; return a + projection * direction; } /// /// Finds the closest points of the perimeters of two polygons. /// /// Vertices defining the outline of polygon A. /// Vertices defining the outline of polygon B. /// The point on polygon A closest to an edge of polygon B. /// The point on polygon B closest to an edge of polygon A. /// The minimum distance between closest approaches used to detect parallel line segments. public static void ClosestPolygonApproach(List verticesA, List verticesB, out Vector3 pointA, out Vector3 pointB, float parallelTest = 0f) { pointA = default; pointB = default; var closest = float.MaxValue; var aCount = verticesA.Count; var bCount = verticesB.Count; var aCountMinusOne = aCount - 1; var bCountMinusOne = bCount - 1; var firstVertexA = verticesA[0]; var firstVertexB = verticesB[0]; for (var i = 0; i < aCount; i++) { var vertexA = verticesA[i]; var aNeighbor = i == aCountMinusOne ? firstVertexA : verticesA[i + 1]; var aLineVector = aNeighbor - vertexA; for (var j = 0; j < bCount; j++) { var vertexB = verticesB[j]; var bNeighbor = j == bCountMinusOne ? firstVertexB : verticesB[j + 1]; var bLineVector = bNeighbor - vertexB; var parallel = ClosestPointsOnTwoLineSegments(vertexA, aLineVector, vertexB, bLineVector, out var a, out var b, parallelTest); var dist = Vector3.Distance(a, b); if (parallel) { var delta = dist - closest; if (delta < parallelTest) { closest = dist - parallelTest; pointA = a; pointB = b; } } else if (dist < closest) { closest = dist; pointA = a; pointB = b; } } } } /// /// Determines if a point is inside of a polygon on the XZ plane. (The y value is not used.) /// /// The point to test. /// Vertices defining the outline of a polygon. /// True if the point is inside the polygon, false otherwise. public static bool PointInPolygon(Vector3 testPoint, List vertices) { // Sanity check - not enough bounds vertices = nothing to be inside of if (vertices.Count < 3) return false; // Check how many lines this test point collides with going in one direction // Odd = Inside, Even = Outside var collisions = 0; var vertexCounter = 0; var startPoint = vertices[vertices.Count - 1]; // We recenter the test point around the origin to simplify the math a bit startPoint.x -= testPoint.x; startPoint.z -= testPoint.z; var currentSide = false; if (!MathUtility.ApproximatelyZero(startPoint.z)) { currentSide = startPoint.z < 0f; } else { // We need a definitive side of the horizontal axis to start with (since we need to know when we // cross it), so we go backwards through the vertices until we find one that does not lie on the horizontal for (var i = vertices.Count - 2; i >= 0; --i) { var vertZ = vertices[i].z; vertZ -= testPoint.z; if (!MathUtility.ApproximatelyZero(vertZ)) { currentSide = vertZ < 0f; break; } } } while (vertexCounter < vertices.Count) { var endPoint = vertices[vertexCounter]; endPoint.x -= testPoint.x; endPoint.z -= testPoint.z; var startToEnd = endPoint - startPoint; var edgeSqrMagnitude = startToEnd.sqrMagnitude; if (MathUtility.ApproximatelyZero(startToEnd.x * endPoint.z - startToEnd.z * endPoint.x) && startPoint.sqrMagnitude <= edgeSqrMagnitude && endPoint.sqrMagnitude <= edgeSqrMagnitude) { // This line goes through the start point, which means the point is on an edge of the polygon return true; } // Ignore lines that end at the horizontal axis if (!MathUtility.ApproximatelyZero(endPoint.z)) { var nextSide = endPoint.z < 0f; if (nextSide != currentSide) { currentSide = nextSide; // If we've crossed the horizontal, check if the origin is to the left of the line if ((startPoint.x * endPoint.z - startPoint.z * endPoint.x) / -(startPoint.z - endPoint.z) > 0) collisions++; } } startPoint = endPoint; vertexCounter++; } return collisions % 2 > 0; } /// /// Determines if a point is inside of a convex polygon and lies on the surface. /// /// The point to test. /// Vertices defining the outline of the polygon. The polygon must be convex. /// The vertices must be coplanar, but can lie on any arbitrary plane. /// True if the point is inside the polygon and coplanar, false otherwise. public static bool PointInPolygon3D(Vector3 testPoint, List vertices) { // Not enough bounds vertices = nothing to be inside of if (vertices.Count < 3) return false; // Compute the sum of the angles between the test point and each pair of edge points double angleSum = 0; for (var vertIndex = 0; vertIndex < vertices.Count; vertIndex++) { var toA = vertices[vertIndex] - testPoint; var toB = vertices[(vertIndex + 1) % vertices.Count] - testPoint; var sqrDistances = toA.sqrMagnitude * toB.sqrMagnitude; // Use sqrMagnitude, take sqrt of result later if (sqrDistances <= MathUtility.EpsilonScaled) // On a vertex { return true; } double cosTheta = Vector3.Dot(toA, toB) / Mathf.Sqrt(sqrDistances); var angle = Math.Acos(cosTheta); angleSum += angle; } // The sum will only be 2*PI if the point is on the plane of the polygon and on the interior const float radiansCompareThreshold = 0.01f; return Mathf.Abs((float)angleSum - k_TwoPi) < radiansCompareThreshold; } /// /// Returns the point on a plane closest to a specified point. /// /// The plane normal. (It does not need to be normalized.) /// Any point on the plane. /// The point to test. /// The point on the plane closest to . public static Vector3 ProjectPointOnPlane(Vector3 planeNormal, Vector3 planePoint, Vector3 point) { var distance = -Vector3.Dot(planeNormal.normalized, point - planePoint); return point + planeNormal.normalized * distance; } /// /// Finds the smallest convex polygon in the XZ plane that contains . /// /// /// Based on algorithm outlined in /// /// Gift Wrapping Convex Hull Algorithm With Unity Implementation. /// /// Points used to find the convex hull. The y coordinates of these points are ignored. /// The vertices that define the smallest convex polygon are assigned to this list. The list is not cleared. /// True if has at least 3 elements, false otherwise. public static bool ConvexHull2D(List points, List hull) { if (points.Count < 3) return false; k_HullIndices.Clear(); var pointsCount = points.Count; var leftmostPointIndex = 0; for (var i = 1; i < pointsCount; ++i) { var point = points[i]; var pointX = point.x; var pointZ = point.z; var leftMost = points[leftmostPointIndex]; var leftmostX = leftMost.x; var leftmostZ = leftMost.z; // As we traverse the outermost points, if we find 3 or more collinear points then we skip points that // fall in the middle. So if our starting point falls in the middle of a line, it will always be skipped // and our loop's end condition will never be met. So if there are multiple leftmost points, we want to // use the point that has the minimum Z. if (pointX < leftmostX || MathUtility.Approximately(pointX, leftmostX) && pointZ < leftmostZ) leftmostPointIndex = i; } // Starting from the leftmost point, move clockwise along outermost points until we are back at the starting point. var currentIndex = leftmostPointIndex; do { var currentPoint = points[currentIndex]; hull.Add(currentPoint); k_HullIndices.Add(currentIndex); // This loop is where we find the next outermost point (next point on the hull clockwise). // To do this we start with a point "p" which is an arbitrary entry in "points". // We iterate through each point "q" in "points". If "q" is to the left of the line from // the current point to "p", then "p" takes on the value of "q" and we continue iterating. // By the end of iteration, "p" will be the next point on the hull because no point is more to the left. var pIndex = 0; var p = points[pIndex]; for (var qIndex = 1; qIndex < pointsCount; ++qIndex) { if (qIndex == currentIndex) continue; // By explicitly ignoring points that are already on the hull, we prevent the possibility of an infinite loop. // Without this check, a point could potentially be chosen again if a collinearity check results in a // false negative due to floating point error. if (k_HullIndices.Contains(qIndex) && qIndex != leftmostPointIndex) continue; var q = points[qIndex]; var currentToP = p - currentPoint; var currentToQ = q - currentPoint; // The y value of the cross product of (current -> p) and (current -> q) tells us where q is // in relation to the line (current -> p). // If y is zero, q is on the line. var crossY = currentToP.z * currentToQ.x - currentToP.x * currentToQ.z; // next few lines are an inlined ` MathUtility.ApproximatelyZero(crossY) `, // because we sometimes call this equality check many thousands of times in a frame var yIsNegative = crossY < 0f; var absY = yIsNegative ? -crossY : crossY; var approximatelyEqual = absY < MathUtility.EpsilonScaled; if (approximatelyEqual) { // If current, p, and q are collinear, then we want p to be the point that is furthest from current. if (Vector3.SqrMagnitude(currentPoint - p) < Vector3.SqrMagnitude(currentPoint - q)) { pIndex = qIndex; p = points[pIndex]; } } // If y is negative, q is to the left. else if (yIsNegative) { pIndex = qIndex; p = points[pIndex]; } } currentIndex = pIndex; } while (currentIndex != leftmostPointIndex); return true; } /// /// Given a list of vertices of a 2d convex polygon, find the centroid of the polygon. /// This implementation operates only on the X and Z axes. /// /// Vertices defining the outline of a 2D polygon. /// The centroid point for the polygon. public static Vector3 PolygonCentroid2D(List vertices) { var vertexCount = vertices.Count; double partialSignedArea, signedArea = 0; double centroidX = 0, centroidZ = 0; double currentX, currentZ; double nextX, nextZ; int i; for (i = 0; i < vertexCount - 1; i++) { var vertex = vertices[i]; currentX = vertex.x; currentZ = vertex.z; var nextVertex = vertices[i + 1]; nextX = nextVertex.x; nextZ = nextVertex.z; partialSignedArea = currentX * nextZ - nextX * currentZ; signedArea += partialSignedArea; centroidX += (currentX + nextX) * partialSignedArea; centroidZ += (currentZ + nextZ) * partialSignedArea; } // Do last vertex separately so we don't check indexes via modulo every iteration var vertexI = vertices[i]; currentX = vertexI.x; currentZ = vertexI.z; var vertex0 = vertices[0]; nextX = vertex0.x; nextZ = vertex0.z; partialSignedArea = currentX * nextZ - nextX * currentZ; signedArea += partialSignedArea; centroidX += (currentX + nextX) * partialSignedArea; centroidZ += (currentZ + nextZ) * partialSignedArea; signedArea *= 0.5; var signedAreaMultiple = 6.0 * signedArea; centroidX /= signedAreaMultiple; centroidZ /= signedAreaMultiple; return new Vector3((float)centroidX, 0f, (float)centroidZ); } /// /// Find the oriented minimum bounding box for a 2D convex hull. /// /// /// This implements the 'rotating calipers' algorithm and operates in linear time. /// Operates only on the X and Z axes of the input. /// /// The list of all points in a 2D convex hull on the X and Z axes, in a clockwise winding order. /// An array of length 4 to fill with the vertex positions of the bounding box, /// in the order `{ top left, bottom left, bottom right, top right }`. /// The size of the bounding box on each axis. Y here maps to the Z axis. public static Vector2 OrientedMinimumBoundingBox2D(List convexHull, Vector3[] boundingBox) { // Caliper lines start axis-aligned as shown before we orient // top // ^------> // | | right // left | | // <------V // bottom var caliperLeft = new Vector3(0f, 0f, 1f); var caliperRight = new Vector3(0f, 0f, -1f); var caliperTop = new Vector3(1f, 0f, 0f); var caliperBottom = new Vector3(-1f, 0f, 0f); float xMin = float.MaxValue, yMin = float.MaxValue; float xMax = float.MinValue, yMax = float.MinValue; int leftIndex = 0, rightIndex = 0, topIndex = 0, bottomIndex = 0; // find the indices of the 'extreme points' in the hull to use as starting edge indices var vertexCount = convexHull.Count; for (var i = 0; i < vertexCount; i++) { var vertex = convexHull[i]; var x = vertex.x; if (x < xMin) { xMin = x; leftIndex = i; } if (x > xMax) { xMax = x; rightIndex = i; } var z = vertex.z; if (z < yMin) { yMin = z; bottomIndex = i; } if (z > yMax) { yMax = z; topIndex = i; } } // compute & store the direction of every edge in the hull k_HullEdgeDirections.Clear(); var lastVertexIndex = vertexCount - 1; for (var i = 0; i < lastVertexIndex; i++) { var edgeDirection = convexHull[i + 1] - convexHull[i]; edgeDirection.Normalize(); k_HullEdgeDirections.Add(edgeDirection); } // by doing the last vertex on its own, we can skip checking indices while iterating above var lastEdgeDirection = convexHull[0] - convexHull[lastVertexIndex]; lastEdgeDirection.Normalize(); k_HullEdgeDirections.Add(lastEdgeDirection); var bestOrientedBoundingBoxArea = double.MaxValue; // for every vertex in the hull, try aligning a caliper edge with an edge the vertex lies on for (var i = 0; i < vertexCount; i++) { var leftEdge = k_HullEdgeDirections[leftIndex]; var rightEdge = k_HullEdgeDirections[rightIndex]; var topEdge = k_HullEdgeDirections[topIndex]; var bottomEdge = k_HullEdgeDirections[bottomIndex]; // find the angles between our caliper lines and the polygon edges, by doing // ` arccosine(caliperEdge ยท hullEdge) ` for each pair of caliper edge & polygon edge var leftAngle = Math.Acos(caliperLeft.x * leftEdge.x + caliperLeft.z * leftEdge.z); var rightAngle = Math.Acos(caliperRight.x * rightEdge.x + caliperRight.z * rightEdge.z); var topAngle = Math.Acos(caliperTop.x * topEdge.x + caliperTop.z * topEdge.z); var bottomAngle = Math.Acos(caliperBottom.x * bottomEdge.x + caliperBottom.z * bottomEdge.z); // find smallest angle among the lines var smallestAngleIndex = 0; var smallestAngle = leftAngle; if (rightAngle < smallestAngle) { smallestAngle = rightAngle; smallestAngleIndex = 1; } if (topAngle < smallestAngle) { smallestAngle = topAngle; smallestAngleIndex = 2; } if (bottomAngle < smallestAngle) smallestAngleIndex = 3; // based on which caliper edge had the smallest angle between it & the polygon, rotate our calipers // and recalculate corners Vector3 upperLeft, upperRight, bottomLeft, bottomRight; switch (smallestAngleIndex) { // left case 0: RotateCalipers(leftEdge, convexHull, ref leftIndex, out topIndex, out rightIndex, out bottomIndex, out caliperLeft, out caliperTop, out caliperRight, out caliperBottom, out upperLeft, out upperRight, out bottomRight, out bottomLeft); break; // right case 1: RotateCalipers(rightEdge, convexHull, ref rightIndex, out bottomIndex, out leftIndex, out topIndex, out caliperRight, out caliperBottom, out caliperLeft, out caliperTop, out bottomRight, out bottomLeft, out upperLeft, out upperRight); break; // top case 2: RotateCalipers(topEdge, convexHull, ref topIndex, out rightIndex, out bottomIndex, out leftIndex, out caliperTop, out caliperRight, out caliperBottom, out caliperLeft, out upperRight, out bottomRight, out bottomLeft, out upperLeft); break; // bottom default: RotateCalipers(bottomEdge, convexHull, ref bottomIndex, out leftIndex, out topIndex, out rightIndex, out caliperBottom, out caliperLeft, out caliperTop, out caliperRight, out bottomLeft, out upperLeft, out upperRight, out bottomRight); break; } // usually with rotating calipers, this comparison is talked about in terms of distance, // but since we just want to know which is bigger it works to use square magnitudes var sqrDistanceX = (upperLeft - upperRight).sqrMagnitude; var sqrDistanceZ = (upperLeft - bottomLeft).sqrMagnitude; var sqrDistanceProduct = sqrDistanceX * sqrDistanceZ; // if this is a smaller box than any we've found before, it's our new candidate if (sqrDistanceProduct < bestOrientedBoundingBoxArea) { bestOrientedBoundingBoxArea = sqrDistanceProduct; boundingBox[0] = bottomLeft; boundingBox[1] = bottomRight; boundingBox[2] = upperRight; boundingBox[3] = upperLeft; } } // compute the size of the 2d bounds var topLeft = boundingBox[0]; var leftRightDistance = Vector3.Distance(topLeft, boundingBox[3]); var topBottomDistance = Vector3.Distance(topLeft, boundingBox[1]); return new Vector2(leftRightDistance, topBottomDistance); } static void RotateCalipers(Vector3 alignEdge, List vertices, ref int indexA, out int indexB, out int indexC, out int indexD, out Vector3 caliperA, out Vector3 caliperB, out Vector3 caliperC, out Vector3 caliperD, out Vector3 caliperAEndCorner, out Vector3 caliperBEndCorner, out Vector3 caliperCEndCorner, out Vector3 caliperDEndCorner) { var vertexCount = vertices.Count; caliperA = alignEdge; caliperB = new Vector3(caliperA.z, 0f, -caliperA.x); // orthogonal caliperC = -caliperA; // opposite caliperD = -caliperB; // opposite orthogonal indexA = (indexA + 1) % vertexCount; // For each caliper, determine the polygon edge for the next caliper by testing intersection between the current caliper // and the opposite orthogonal from subsequent polygon vertices until we've found the maximum intersection point. var startA = vertices[indexA]; indexB = indexA; var maxS = 0f; while (true) { var nextIndex = (indexB + 1) % vertexCount; ClosestTimesOnTwoLinesXZ(startA, caliperA, vertices[nextIndex], caliperD, out var s, out _); if (s <= maxS) break; maxS = s; indexB = nextIndex; } caliperAEndCorner = startA + caliperA * maxS; var startB = vertices[indexB]; indexC = indexB; maxS = 0f; while (true) { var nextIndex = (indexC + 1) % vertexCount; ClosestTimesOnTwoLinesXZ(startB, caliperB, vertices[nextIndex], caliperA, out var s, out _); if (s <= maxS) break; maxS = s; indexC = nextIndex; } caliperBEndCorner = startB + caliperB * maxS; var startC = vertices[indexC]; indexD = indexC; maxS = 0f; while (true) { var nextIndex = (indexD + 1) % vertexCount; ClosestTimesOnTwoLinesXZ(startC, caliperC, vertices[nextIndex], caliperB, out var s, out _); if (s <= maxS) break; maxS = s; indexD = nextIndex; } caliperCEndCorner = startC + caliperC * maxS; // No need for any intersection tests for the last corner since we have all the other corners caliperDEndCorner = caliperCEndCorner + caliperAEndCorner - caliperBEndCorner; } /// /// Given a 2D bounding box's vertices, find the rotation of the box. /// /// The 4 vertices of the bounding box, in the order /// `{ top left, bottom left, bottom right, top right }`. /// The rotation of the box, with the horizontal side aligned to the x axis and the /// vertical side aligned to the z axis public static Quaternion RotationForBox(Vector3[] vertices) { var topLeft = vertices[0]; var topRight = vertices[3]; var leftToRight = topRight - topLeft; return Quaternion.FromToRotation(Vector3.right, leftToRight); } /// /// Finds the area of a convex polygon. /// /// Vertices defining the outline of a polygon. /// The polygon must be convex, but can be in either winding order. /// The area of the polygon. public static float ConvexPolygonArea(List vertices) { var count = vertices.Count; if (count < 3) return 0f; var firstVertex = vertices[0]; var lastIndex = count - 1; var lastVertex = vertices[lastIndex]; var area = lastVertex.x * firstVertex.z - firstVertex.x * lastVertex.z; for (var i = 0; i < lastIndex; i++) { var currentVertex = vertices[i]; var nextVertex = vertices[i + 1]; area += currentVertex.x * nextVertex.z - nextVertex.x * currentVertex.z; } // Take absolute value because area is negative if vertices are clockwise return Math.Abs(area * 0.5f); } /// /// Determines if one polygon lies completely inside another coplanar polygon. /// /// The polygon to test for lying inside . /// The polygon to test for containing . /// Must be convex and coplanar with . /// True if lies completely inside , false otherwise. public static bool PolygonInPolygon(List polygonA, List polygonB) { if (polygonA.Count < 1) return false; foreach (var vertex in polygonA) { if (!PointInPolygon3D(vertex, polygonB)) return false; } return true; } /// /// Determines if two convex coplanar polygons are within a certain distance from each other. /// This includes the polygon perimeters as well as their interiors. /// /// The first polygon to test. Must be convex and coplanar with . /// The second polygon to test. Must be convex and coplanar with . /// The maximum distance allowed between the two polygons. /// True if the polygons are within the specified distance from each other, false otherwise. public static bool PolygonsWithinRange(List polygonA, List polygonB, float maxDistance) { return PolygonsWithinSqRange(polygonA, polygonB, maxDistance * maxDistance); } /// /// Determines if two convex coplanar polygons are within a specified distance from each other. /// /// The first polygon to test. Must be convex and coplanar with . /// The second polygon to test. Must be convex and coplanar with . /// The square of the maximum distance allowed between the two polygons. /// True if the polygons are within the specified distance from each other, false otherwise. public static bool PolygonsWithinSqRange(List polygonA, List polygonB, float maxSqDistance) { ClosestPolygonApproach(polygonA, polygonB, out var pointA, out var pointB); return Vector3.SqrMagnitude(pointB - pointA) <= maxSqDistance || PolygonInPolygon(polygonA, polygonB) || PolygonInPolygon(polygonB, polygonA); } /// /// Determines if a point lies on the bounds of a polygon, ignoring the y components. /// /// The point to test. /// Vertices defining the outline of a polygon. /// Custom epsilon value used when testing if the point lies on an edge. /// True if the point lies on any edge of the polygon, false otherwise. public static bool PointOnPolygonBoundsXZ(Vector3 testPoint, List vertices, float epsilon = float.Epsilon) { var verticesCount = vertices.Count; // No edge for the point to lie on if (verticesCount < 2) return false; var lastVertex = vertices[verticesCount - 1]; foreach (var vertex in vertices) { if (PointOnLineSegmentXZ(testPoint, lastVertex, vertex, epsilon)) return true; lastVertex = vertex; } return false; } /// /// Determines if a point lies on a line segment, ignoring the y components. /// /// The point to test. /// Starting point of the line segment. /// Ending point of the line segment. /// Custom epsilon value used for comparison checks. /// True if the point lies on the line segment, false otherwise. public static bool PointOnLineSegmentXZ(Vector3 testPoint, Vector3 lineStart, Vector3 lineEnd, float epsilon = float.Epsilon) { var startToEnd = lineEnd - lineStart; var startToTestPoint = testPoint - lineStart; var cross = startToEnd.z * startToTestPoint.x - startToEnd.x * startToTestPoint.z; var absCross = cross >= 0f ? cross : -cross; if (absCross >= epsilon) return false; var dot = startToEnd.x * startToTestPoint.x + startToEnd.z * startToTestPoint.z; var lineSqrMagnitude = startToEnd.x * startToEnd.x + startToEnd.z * startToEnd.z; return dot >= -epsilon && dot <= lineSqrMagnitude + epsilon; } static Quaternion NormalizeRotationKeepingUp(Quaternion rot) { var srcUp = (rot * k_Up).normalized; var isMostlyVertical = Mathf.Abs(srcUp.y) > k_MostlyVertical; Vector3 modFwd; if (isMostlyVertical) { modFwd = Vector3.Cross(k_Forward, srcUp); } else { var side = Vector3.Cross(srcUp, k_Up); modFwd = Vector3.Cross(srcUp, side); } return Quaternion.LookRotation(modFwd, srcUp); } /// /// Gets a corrected polygon uv pose from a given plane pose. /// /// The source plane pose. /// The rotation-corrected pose for calculating UVs. public static Pose PolygonUVPoseFromPlanePose(Pose pose) { return new Pose(k_Zero, NormalizeRotationKeepingUp(pose.rotation)); } /// /// Takes a polygon UV coordinate, and produces a pose-corrected UV coordinate. /// /// Vertex to transform. /// Polygon pose. /// UV-correction Pose. /// The corrected UV coordinate. public static Vector2 PolygonVertexToUV(Vector3 vertexPos, Pose planePose, Pose uvPose) { var worldPos = planePose.position + planePose.rotation * vertexPos; var localUv = Quaternion.Inverse(uvPose.rotation) * (worldPos - uvPose.position); localUv = k_VerticalCorrection * localUv; return new Vector2(localUv.x, localUv.z); } } }