using UnityEngine; using System.Collections.Generic; using UnityEngine.Assertions; namespace ProceduralToolkit { /// /// Utility class for computational geometry algorithms /// public static class Geometry { /// /// A tiny floating point value used in comparisons /// public const float Epsilon = 0.00001f; #region Point samplers 2D /// /// Returns a point on a segment at the given normalized position /// /// Start of the segment /// End of the segment /// Normalized position public static Vector2 PointOnSegment2(Vector2 segmentA, Vector2 segmentB, float position) { return Vector2.Lerp(segmentA, segmentB, position); } /// /// Returns a list of evenly distributed points on a segment /// /// Start of the segment /// End of the segment /// Number of points public static List PointsOnSegment2(Vector2 segmentA, Vector2 segmentB, int count) { var points = new List(count); if (count <= 0) { return points; } if (count == 1) { points.Add(segmentA); return points; } for (int i = 0; i < count; i++) { points.Add(PointOnSegment2(segmentA, segmentB, i/(float) (count - 1))); } return points; } #region PointOnCircle2 /// /// Returns a point on a circle in the XY plane /// /// Circle radius /// Angle in degrees public static Vector2 PointOnCircle2(float radius, float angle) { float angleInRadians = angle*Mathf.Deg2Rad; return new Vector2(radius*Mathf.Sin(angleInRadians), radius*Mathf.Cos(angleInRadians)); } /// /// Returns a point on a circle in the XY plane /// /// Center of the circle /// Circle radius /// Angle in degrees public static Vector2 PointOnCircle2(Vector2 center, float radius, float angle) { return center + PointOnCircle2(radius, angle); } #endregion PointOnCircle2 #region PointsOnCircle2 /// /// Returns a list of evenly distributed points on a circle in the XY plane /// /// Circle radius /// Number of points public static List PointsOnCircle2(float radius, int count) { float segmentAngle = 360f/count; float currentAngle = 0; var points = new List(count); for (var i = 0; i < count; i++) { points.Add(PointOnCircle2(radius, currentAngle)); currentAngle += segmentAngle; } return points; } /// /// Returns a list of evenly distributed points on a circle in the XY plane /// /// Center of the circle /// Circle radius /// Number of points public static List PointsOnCircle2(Vector2 center, float radius, int count) { float segmentAngle = 360f/count; float currentAngle = 0; var points = new List(count); for (var i = 0; i < count; i++) { points.Add(PointOnCircle2(center, radius, currentAngle)); currentAngle += segmentAngle; } return points; } #endregion PointsOnCircle2 #region PointsInCircle2 /// /// Returns a list of evenly distributed points inside a circle in the XY plane /// /// Circle radius /// Number of points public static List PointsInCircle2(float radius, int count) { float currentAngle = 0; var points = new List(count); for (int i = 0; i < count; i++) { // The 0.5 offset improves the position of the first point float r = Mathf.Sqrt((i + 0.5f)/count); points.Add(new Vector2(radius*Mathf.Sin(currentAngle)*r, radius*Mathf.Cos(currentAngle)*r)); currentAngle += PTUtils.GoldenAngle; } return points; } /// /// Returns a list of evenly distributed points inside a circle in the XY plane /// /// Center of the circle /// Circle radius /// Number of points public static List PointsInCircle2(Vector2 center, float radius, int count) { float currentAngle = 0; var points = new List(count); for (int i = 0; i < count; i++) { // The 0.5 offset improves the position of the first point float r = Mathf.Sqrt((i + 0.5f)/count); points.Add(center + new Vector2(radius*Mathf.Sin(currentAngle)*r, radius*Mathf.Cos(currentAngle)*r)); currentAngle += PTUtils.GoldenAngle; } return points; } #endregion PointsInCircle2 #endregion Point samplers 2D #region Point samplers 3D /// /// Returns a point on a segment at the given normalized position /// /// Start of the segment /// End of the segment /// Normalized position public static Vector3 PointOnSegment3(Vector3 segmentA, Vector3 segmentB, float position) { return Vector3.Lerp(segmentA, segmentB, position); } /// /// Returns a list of evenly distributed points on a segment /// /// Start of the segment /// End of the segment /// Number of points public static List PointsOnSegment3(Vector3 segmentA, Vector3 segmentB, int count) { var points = new List(count); if (count <= 0) { return points; } if (count == 1) { points.Add(segmentA); return points; } for (int i = 0; i < count; i++) { points.Add(PointOnSegment3(segmentA, segmentB, i/(float) (count - 1))); } return points; } #region PointOnCircle3 /// /// Returns a point on a circle in the XY plane /// /// Circle radius /// Angle in degrees public static Vector3 PointOnCircle3XY(float radius, float angle) { return PointOnCircle3(0, 1, radius, angle); } /// /// Returns a point on a circle in the XY plane /// /// Center of the circle /// Circle radius /// Angle in degrees public static Vector3 PointOnCircle3XY(Vector3 center, float radius, float angle) { return PointOnCircle3(0, 1, center, radius, angle); } /// /// Returns a point on a circle in the XZ plane /// /// Circle radius /// Angle in degrees public static Vector3 PointOnCircle3XZ(float radius, float angle) { return PointOnCircle3(0, 2, radius, angle); } /// /// Returns a point on a circle in the XZ plane /// /// Center of the circle /// Circle radius /// Angle in degrees public static Vector3 PointOnCircle3XZ(Vector3 center, float radius, float angle) { return PointOnCircle3(0, 2, center, radius, angle); } /// /// Returns a point on a circle in the YZ plane /// /// Circle radius /// Angle in degrees public static Vector3 PointOnCircle3YZ(float radius, float angle) { return PointOnCircle3(1, 2, radius, angle); } /// /// Returns a point on a circle in the YZ plane /// /// Center of the circle /// Circle radius /// Angle in degrees public static Vector3 PointOnCircle3YZ(Vector3 center, float radius, float angle) { return PointOnCircle3(1, 2, center, radius, angle); } private static Vector3 PointOnCircle3(int xIndex, int yIndex, float radius, float angle) { float angleInRadians = angle*Mathf.Deg2Rad; var point = new Vector3(); point[xIndex] = radius*Mathf.Sin(angleInRadians); point[yIndex] = radius*Mathf.Cos(angleInRadians); return point; } private static Vector3 PointOnCircle3(int xIndex, int yIndex, Vector3 center, float radius, float angle) { return center + PointOnCircle3(xIndex, yIndex, radius, angle); } #endregion PointOnCircle3 #region PointsOnCircle3 /// /// Returns a list of evenly distributed points on a circle in the XY plane /// /// Circle radius /// Number of points public static List PointsOnCircle3XY(float radius, int count) { return PointsOnCircle3(0, 1, radius, count); } /// /// Returns a list of evenly distributed points on a circle in the XY plane /// /// Center of the circle /// Circle radius /// Number of points public static List PointsOnCircle3XY(Vector3 center, float radius, int count) { return PointsOnCircle3(0, 1, center, radius, count); } /// /// Returns a list of evenly distributed points on a circle in the XZ plane /// /// Circle radius /// Number of points public static List PointsOnCircle3XZ(float radius, int count) { return PointsOnCircle3(0, 2, radius, count); } /// /// Returns a list of evenly distributed points on a circle in the XZ plane /// /// Center of the circle /// Circle radius /// Number of points public static List PointsOnCircle3XZ(Vector3 center, float radius, int count) { return PointsOnCircle3(0, 2, center, radius, count); } /// /// Returns a list of evenly distributed points on a circle in the YZ plane /// /// Circle radius /// Number of points public static List PointsOnCircle3YZ(float radius, int count) { return PointsOnCircle3(1, 2, radius, count); } /// /// Returns a list of evenly distributed points on a circle in the YZ plane /// /// Center of the circle /// Circle radius /// Number of points public static List PointsOnCircle3YZ(Vector3 center, float radius, int count) { return PointsOnCircle3(1, 2, center, radius, count); } private static List PointsOnCircle3(int xIndex, int yIndex, float radius, int count) { float segmentAngle = 360f/count; float currentAngle = 0; var points = new List(count); for (var i = 0; i < count; i++) { points.Add(PointOnCircle3(xIndex, yIndex, radius, currentAngle)); currentAngle += segmentAngle; } return points; } private static List PointsOnCircle3(int xIndex, int yIndex, Vector3 center, float radius, int count) { float segmentAngle = 360f/count; float currentAngle = 0; var points = new List(count); for (var i = 0; i < count; i++) { points.Add(PointOnCircle3(xIndex, yIndex, center, radius, currentAngle)); currentAngle += segmentAngle; } return points; } #endregion PointsOnCircle3 #region PointsInCircle3 /// /// Returns a list of evenly distributed points inside a circle in the XY plane /// /// Circle radius /// Number of points public static List PointsInCircle3XY(float radius, int count) { return PointsInCircle3(0, 1, radius, count); } /// /// Returns a list of evenly distributed points inside a circle in the XY plane /// /// Circle radius /// Number of points public static List PointsInCircle3XY(Vector3 center, float radius, int count) { return PointsInCircle3(0, 1, center, radius, count); } /// /// Returns a list of evenly distributed points inside a circle in the XZ plane /// /// Circle radius /// Number of points public static List PointsInCircle3XZ(float radius, int count) { return PointsInCircle3(0, 2, radius, count); } /// /// Returns a list of evenly distributed points inside a circle in the XZ plane /// /// Circle radius /// Number of points public static List PointsInCircle3XZ(Vector3 center, float radius, int count) { return PointsInCircle3(0, 2, center, radius, count); } /// /// Returns a list of evenly distributed points inside a circle in the YZ plane /// /// Circle radius /// Number of points public static List PointsInCircle3YZ(float radius, int count) { return PointsInCircle3(1, 2, radius, count); } /// /// Returns a list of evenly distributed points inside a circle in the YZ plane /// /// Circle radius /// Number of points public static List PointsInCircle3YZ(Vector3 center, float radius, int count) { return PointsInCircle3(1, 2, center, radius, count); } private static List PointsInCircle3(int xIndex, int yIndex, float radius, int count) { float currentAngle = 0; var points = new List(count); for (int i = 0; i < count; i++) { // The 0.5 offset improves the position of the first point float r = Mathf.Sqrt((i + 0.5f)/count); var point = new Vector3(); point[xIndex] = radius*Mathf.Sin(currentAngle)*r; point[yIndex] = radius*Mathf.Cos(currentAngle)*r; points.Add(point); currentAngle += PTUtils.GoldenAngle; } return points; } private static List PointsInCircle3(int xIndex, int yIndex, Vector3 center, float radius, int count) { float currentAngle = 0; var points = new List(count); for (int i = 0; i < count; i++) { // The 0.5 offset improves the position of the first point float r = Mathf.Sqrt((i + 0.5f)/count); var point = new Vector3(); point[xIndex] = radius*Mathf.Sin(currentAngle)*r; point[yIndex] = radius*Mathf.Cos(currentAngle)*r; points.Add(center + point); currentAngle += PTUtils.GoldenAngle; } return points; } #endregion PointsInCircle3 /// /// Returns a point on a sphere in geographic coordinate system /// /// Sphere radius /// Horizontal angle in degrees [0, 360] /// Vertical angle in degrees [-90, 90] public static Vector3 PointOnSphere(float radius, float horizontalAngle, float verticalAngle) { return PointOnSpheroid(radius, radius, horizontalAngle, verticalAngle); } /// /// Returns a point on a spheroid in geographic coordinate system /// /// Spheroid radius /// Spheroid height /// Horizontal angle in degrees [0, 360] /// Vertical angle in degrees [-90, 90] public static Vector3 PointOnSpheroid(float radius, float height, float horizontalAngle, float verticalAngle) { float horizontalRadians = horizontalAngle*Mathf.Deg2Rad; float verticalRadians = verticalAngle*Mathf.Deg2Rad; float cosVertical = Mathf.Cos(verticalRadians); return new Vector3( x: radius*Mathf.Sin(horizontalRadians)*cosVertical, y: height*Mathf.Sin(verticalRadians), z: radius*Mathf.Cos(horizontalRadians)*cosVertical); } /// /// Returns a point on a teardrop surface in geographic coordinate system /// /// Teardrop radius /// Teardrop height /// Horizontal angle in degrees [0, 360] /// Vertical angle in degrees [-90, 90] public static Vector3 PointOnTeardrop(float radius, float height, float horizontalAngle, float verticalAngle) { float horizontalRadians = horizontalAngle*Mathf.Deg2Rad; float verticalRadians = verticalAngle*Mathf.Deg2Rad; float sinVertical = Mathf.Sin(verticalRadians); float teardrop = (1 - sinVertical)*Mathf.Cos(verticalRadians)/2; return new Vector3( x: radius*Mathf.Sin(horizontalRadians)*teardrop, y: height*sinVertical, z: radius*Mathf.Cos(horizontalRadians)*teardrop); } /// /// Returns a list of evenly distributed points on a sphere /// /// Sphere radius /// Number of points public static List PointsOnSphere(float radius, int count) { var points = new List(count); float deltaY = -2f/count; float y = 1 + deltaY/2; float currentAngle = 0; for (int i = 0; i < count; i++) { float r = Mathf.Sqrt(1 - y*y); points.Add(new Vector3( x: radius*Mathf.Sin(currentAngle)*r, y: radius*y, z: radius*Mathf.Cos(currentAngle)*r)); y += deltaY; currentAngle += PTUtils.GoldenAngle; } return points; } #endregion Point samplers 3D /// /// Returns a list of points representing a polygon in the XY plane /// /// Radius of the circle passing through the vertices /// Number of polygon vertices public static List Polygon2(int vertices, float radius) { return PointsOnCircle2(radius, vertices); } /// /// Returns a list of points representing a star polygon in the XY plane /// /// Radius of the circle passing through the outer vertices /// Radius of the circle passing through the inner vertices /// Number of polygon vertices public static List StarPolygon2(int vertices, float innerRadius, float outerRadius) { float segmentAngle = 360f/vertices; float halfSegmentAngle = segmentAngle/2; float currentAngle = 0; var polygon = new List(vertices); for (var i = 0; i < vertices; i++) { polygon.Add(PointOnCircle2(outerRadius, currentAngle)); polygon.Add(PointOnCircle2(innerRadius, currentAngle + halfSegmentAngle)); currentAngle += segmentAngle; } return polygon; } /// /// Returns the value of an angle. Assumes clockwise order of the polygon. /// /// Previous vertex /// Current vertex /// Next vertex public static float GetAngle(Vector2 previous, Vector2 current, Vector2 next) { Vector2 toPrevious = (previous - current).normalized; Vector2 toNext = (next - current).normalized; return VectorE.Angle360(toNext, toPrevious); } /// /// Returns the bisector of an angle. Assumes clockwise order of the polygon. /// /// Previous vertex /// Current vertex /// Next vertex /// Value of the angle in degrees. Always positive. public static Vector2 GetAngleBisector(Vector2 previous, Vector2 current, Vector2 next, out float degrees) { Vector2 toPrevious = (previous - current).normalized; Vector2 toNext = (next - current).normalized; degrees = VectorE.Angle360(toNext, toPrevious); Assert.IsFalse(float.IsNaN(degrees)); return toNext.RotateCW(degrees/2); } /// /// Creates a new offset polygon from the input polygon. Assumes clockwise order of the polygon. /// Does not handle intersections. /// /// Vertices of the polygon in clockwise order. /// Offset distance. Positive values offset outside, negative inside. public static List OffsetPolygon(IReadOnlyList polygon, float distance) { var newPolygon = new List(polygon.Count); for (int i = 0; i < polygon.Count; i++) { Vector2 previous = polygon.GetLooped(i - 1); Vector2 current = polygon[i]; Vector2 next = polygon.GetLooped(i + 1); Vector2 bisector = GetAngleBisector(previous, current, next, out float angle); float angleOffset = GetAngleOffset(distance, angle); newPolygon.Add(current - bisector*angleOffset); } return newPolygon; } /// /// Offsets the input polygon. Assumes clockwise order of the polygon. /// Does not handle intersections. /// /// Vertices of the polygon in clockwise order. /// Offset distance. Positive values offset outside, negative inside. public static void OffsetPolygon(ref List polygon, float distance) { var offsets = new Vector2[polygon.Count]; for (int i = 0; i < polygon.Count; i++) { Vector2 previous = polygon.GetLooped(i - 1); Vector2 current = polygon[i]; Vector2 next = polygon.GetLooped(i + 1); Vector2 bisector = GetAngleBisector(previous, current, next, out float angle); float angleOffset = GetAngleOffset(distance, angle); offsets[i] = -bisector*angleOffset; } for (int i = 0; i < polygon.Count; i++) { polygon[i] += offsets[i]; } } /// /// Offsets the input polygon. Assumes clockwise order of the polygon. /// Does not handle intersections. /// /// Vertices of the polygon in clockwise order. /// Offset distance. Positive values offset outside, negative inside. public static void OffsetPolygon(ref Vector2[] polygon, float distance) { var offsets = new Vector2[polygon.Length]; for (int i = 0; i < polygon.Length; i++) { Vector2 previous = polygon.GetLooped(i - 1); Vector2 current = polygon[i]; Vector2 next = polygon.GetLooped(i + 1); Vector2 bisector = GetAngleBisector(previous, current, next, out float angle); float angleOffset = GetAngleOffset(distance, angle); offsets[i] = -bisector*angleOffset; } for (int i = 0; i < polygon.Length; i++) { polygon[i] += offsets[i]; } } public static float GetAngleOffset(float edgeOffset, float angle) { return edgeOffset/GetAngleBisectorSin(angle); } public static float GetAngleBisectorSin(float angle) { return Mathf.Sin(angle*Mathf.Deg2Rad/2); } /// /// Calculates the area of the input polygon. /// /// Vertices of the polygon. public static float GetArea(IReadOnlyList polygon) { return Mathf.Abs(GetSignedArea(polygon)); } /// /// Calculates the signed area of the input polygon. /// /// Vertices of the polygon. public static float GetSignedArea(IReadOnlyList polygon) { if (polygon.Count < 3) return 0; float a = 0; for (int i = 0; i < polygon.Count; i++) { a += VectorE.PerpDot(polygon.GetLooped(i - 1), polygon[i]); } return a/2; } /// /// Calculates the orientation of the vertices of the input polygon. /// /// Vertices of the polygon. public static Orientation GetOrientation(IReadOnlyList polygon) { if (polygon.Count < 3) return Orientation.NonOrientable; float signedArea = GetSignedArea(polygon); if (signedArea < -Epsilon) return Orientation.Clockwise; if (signedArea > Epsilon) return Orientation.CounterClockwise; return Orientation.NonOrientable; } /// /// Calculates the perimeter of the input polygon. /// /// Vertices of the polygon. public static float GetPerimeter(IReadOnlyList polygon) { if (polygon.Count < 2) return 0; float perimeter = 0; for (var i = 0; i < polygon.Count; i++) { perimeter += Vector2.Distance(polygon.GetLooped(i - 1), polygon[i]); } return perimeter; } /// /// Calculates a bounding rect for a set of vertices. /// public static Rect GetRect(IReadOnlyList vertices) { Vector2 min = vertices[0]; Vector2 max = vertices[0]; for (var i = 1; i < vertices.Count; i++) { var vertex = vertices[i]; min = Vector2.Min(min, vertex); max = Vector2.Max(max, vertex); } return Rect.MinMaxRect(min.x, min.y, max.x, max.y); } /// /// Calculates a circumradius for a rectangle. /// public static float GetCircumradius(Rect rect) { return GetCircumradius(rect.width, rect.height); } /// /// Calculates a circumradius for a rectangle. /// public static float GetCircumradius(float width, float height) { return Mathf.Sqrt(width/2*width/2 + height/2*height/2); } } }