Skip to content

Instantly share code, notes, and snippets.

@njelly
Last active June 1, 2023 03:54
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save njelly/e025d0aa8403c084dc1e78d467b5d2b9 to your computer and use it in GitHub Desktop.
Save njelly/e025d0aa8403c084dc1e78d467b5d2b9 to your computer and use it in GitHub Desktop.
ShapeMath2D - A collection of static functions for performing common (and some uncommon!) geometry tests in 2D.
// MIT License
//
// Copyright (c) 2022 Nathaniel Ellingson
// https://njellingson.com
//
// 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.
// This is a collection of static math functions for performing common geometry tests in 2D. Usually this sort of math
// comes bundled with a bulky physics engine, but I've found that I frequently want to test whether or not, for example, a
// circle and a triangle intersect without having to instantiate a world and tick through a simulation. I'm tired of
// reinventing the wheel each time I have what should be a simple geometry problem, so I wrote this. Maybe it'll be
// useful for you too?
// *** NOTE: POLYGONS MUST BE CONVEX AND VERTICES MUST GO CLOCKWISE AROUND THE SHAPE!!!! ***
// An example Unity project can be found here: https://github.com/njelly/ShapeMath2D_Unity
using System;
using Vector2 = System.Numerics.Vector2;
namespace Tofunaut
{
public static class ShapeMath2D
{
public const float DegToRad = MathF.PI / 180f;
public const float PI2 = MathF.PI * 2f;
# region AABB
public static bool AABBContainsPoint(Vector2 min, Vector2 max, Vector2 point) =>
point.X >= min.X && point.X <= max.X && point.Y >= min.Y && point.Y <= max.Y;
public static bool AABBIntersectsAABB(Vector2 minA, Vector2 maxA, Vector2 minB, Vector2 maxB) =>
maxA.X - minA.X + maxB.X - minB.X > MathF.Max(maxA.X, maxB.X) - MathF.Min(minA.X, minB.X)
&& maxA.Y - minA.Y + maxB.Y - minB.Y > MathF.Max(maxA.Y, maxB.Y) - MathF.Min(minA.Y, minB.Y);
public static unsafe void GetVerticesAABB(Vector2 min, Vector2 max, Vector2[] vertices)
{
fixed (Vector2* ptr = vertices)
GetVerticesAABBUnsafe(min, max, ptr);
}
public static unsafe void GetVerticesAABBUnsafe(Vector2 min, Vector2 max, Vector2* vertices)
{
vertices[0] = max;
vertices[1] = new Vector2(max.X, min.Y);
vertices[2] = min;
vertices[3] = new Vector2(min.X, max.Y);
}
public static unsafe void GetBoundingAABB(Vector2[] vertices, out Vector2 aabbMin, out Vector2 aabbMax)
{
fixed(Vector2* ptr = vertices)
GetBoundingAABBUnsafe(ptr, vertices.Length, out aabbMin, out aabbMax);
}
public static unsafe void GetBoundingAABBUnsafe(Vector2* vertices, int length, out Vector2 aabbMin, out Vector2 aabbMax)
{
aabbMin = default;
aabbMax = default;
for (var i = 0; i < length; i++)
{
aabbMin = new Vector2(MathF.Min(aabbMin.X, vertices[i].X), MathF.Min(aabbMin.Y, vertices[i].Y));
aabbMax = new Vector2(MathF.Max(aabbMax.X, vertices[i].X), MathF.Max(aabbMax.Y, vertices[i].Y));
}
}
#endregion AABB
# region Circle
public static bool CircleContainsPoint(Vector2 center, float radius, Vector2 point) =>
(point - center).LengthSquared() <= radius * radius;
public static unsafe bool CircleIntersectsAABB(Vector2 center, float radius, Vector2 aabbMin, Vector2 aabbMax)
{
if (AABBContainsPoint(aabbMin, aabbMax, center))
return true;
var aabbVertices = stackalloc Vector2[4];
GetVerticesAABBUnsafe(aabbMin, aabbMax, aabbVertices);
for (var i = 0; i < 4; i++)
{
var nextIndex = (i + 1) % 4;
var closestPointOnEdge = ClosestPointOnLineSegment(aabbVertices[i], aabbVertices[nextIndex], center);
if (CircleContainsPoint(center, radius, closestPointOnEdge))
return true;
}
return false;
}
public static bool CircleIntersectsCircle(Vector2 centerA, float radiusA, Vector2 centerB, float radiusB)
{
var radiusSum = radiusA + radiusB;
return (centerB - centerA).LengthSquared() <= radiusSum * radiusSum;
}
public static unsafe void GetBoundingCircle(Vector2[] points, out Vector2 circleCenter, out float circleRadius)
{
fixed(Vector2* ptr = points)
GetBoundingCircleUnsafe(ptr, points.Length, out circleCenter, out circleRadius);
}
/// <summary>
/// Implements Welzl's algorithm for finding the smallest bounding circle containing a set of points in O(n) time.
/// This website was very helpful: http://www.sunshine2k.de/coding/java/Welzl/Welzl.html
/// </summary>
public static unsafe void GetBoundingCircleUnsafe(Vector2* points, int length, out Vector2 circleCenter,
out float circleRadius)
{
var pointsOnCircle = stackalloc Vector2[3];
welzl(length, 0, out circleCenter, out circleRadius);
void welzl(int numUnchecked, int numPointsOnCircle, out Vector2 c, out float r)
{
if (numUnchecked <= 0 || numPointsOnCircle == 3)
{
calculateCircle(numPointsOnCircle, out c, out r);
return;
}
var p = points[numUnchecked - 1];
welzl(numUnchecked - 1, numPointsOnCircle, out c, out r);
if (CircleContainsPoint(c, r, p))
return;
pointsOnCircle[numPointsOnCircle] = p;
welzl(numUnchecked - 1, numPointsOnCircle + 1, out c, out r);
}
void calculateCircle(int numPointsOnCircle, out Vector2 c, out float r)
{
c = default;
r = default;
switch (numPointsOnCircle)
{
case 1:
c = pointsOnCircle[0];
r = 0f;
break;
case 2:
c = (pointsOnCircle[1] + pointsOnCircle[0]) / 2f;
r = (pointsOnCircle[1] - pointsOnCircle[0]).Length() / 2f;
break;
case 3:
GetCircleFromTriangleUnsafe(pointsOnCircle, out c, out r);
break;
}
}
}
public static unsafe void GetCircleFromTriangle(Vector2[] points, out Vector2 circleCenter,
out float circleRadius)
{
fixed(Vector2* ptr = points)
GetCircleFromTriangleUnsafe(ptr, out circleCenter, out circleRadius);
}
public static unsafe void GetCircleFromTriangleUnsafe(Vector2* points, out Vector2 circleCenter,
out float circleRadius)
{
GetLongestEdgeOfPolygonUnsafe(points, 3, out var longestEdge);
var a = (longestEdge + 1) % 3;
var b = (longestEdge + 2) % 3;
var aToBMiddle = (points[a] + points[b]) / 2f;
var bToCMiddle = (points[b] + points[longestEdge]) / 2f;
var perpA = (aToBMiddle - points[a]).RotatedByRadians(MathF.PI / 2f) + aToBMiddle;
var perpB = (bToCMiddle - points[b]).RotatedByRadians(MathF.PI / 2f) + bToCMiddle;
LineIntersectsLine(aToBMiddle, perpA, bToCMiddle, perpB, out circleCenter);
circleRadius = (points[0] - circleCenter).Length();
}
#endregion Circle
#region Line
public static Vector2 ClosestPointOnLine(Vector2 a, Vector2 b, Vector2 point)
{
var ap = point - a;
var ab = b - a;
var dist = Vector2.Dot(ap, ab) / ab.LengthSquared();
return a + ab * dist;
}
public static Vector2 ClosestPointOnLineSegment(Vector2 a, Vector2 b, Vector2 point)
{
var ap = point - a;
var ab = b - a;
var dist = Vector2.Dot(ap, ab) / ab.LengthSquared();
return dist switch
{
< 0 => a,
> 1 => b,
_ => a + ab * dist
};
}
public static bool PointIsOnLeftSideOfLine(Vector2 a, Vector2 b, Vector2 point) =>
(b.X - a.X) * (point.Y - a.Y) - (b.Y - a.Y) * (point.X - a.X) > 0;
public static bool LineIntersectsLine(Vector2 a1, Vector2 b1, Vector2 a2, Vector2 b2,
out Vector2 intersection)
{
intersection = default;
var x1 = a1.X;
var x2 = b1.X;
var x3 = a2.X;
var x4 = b2.X;
var y1 = a1.Y;
var y2 = b1.Y;
var y3 = a2.Y;
var y4 = b2.Y;
var d = (x1 - x2) * (y3 - y4) - (y1 - y2) * (x3 - x4);
if (Math.Abs(d) <= float.Epsilon)
return false;
var pre = x1 * y2 - y1 * x2;
var post = x3 * y4 - y3 * x4;
intersection.X = (pre * (x3 - x4) - (x1 - x2) * post) / d;
intersection.Y = (pre * (y3 - y4) - (y1 - y2) * post) / d;
return true;
}
public static bool LineIntersectsLineSegment(Vector2 a1, Vector2 b1, Vector2 a2, Vector2 b2,
out Vector2 intersection) => LineIntersectsLine(a1, b1, a2, b2, out intersection) &&
IsInsideLineSegment(a2, b2, intersection);
public static bool LineSegmentIntersectsLineSegment(Vector2 a1, Vector2 b1, Vector2 a2, Vector2 b2,
out Vector2 intersection) => LineIntersectsLineSegment(a1, b1, a2, b2, out intersection) &&
IsInsideLineSegment(a1, b1, intersection);
private static bool IsInsideLineSegment(Vector2 a, Vector2 b, Vector2 point) =>
(point.X >= a.X && point.X <= b.X || point.X >= b.X && point.X <= a.X) &&
(point.Y >= a.Y && point.Y <= b.Y || point.Y >= b.Y && point.Y <= a.Y);
#endregion Line
# region Polygon
public static unsafe bool PolygonContainsPoint(Vector2[] vertices, Vector2 point)
{
fixed (Vector2* ptr = vertices)
return PolygonContainsPointUnsafe(ptr, vertices.Length, point);
}
public static unsafe bool PolygonContainsPointUnsafe(Vector2* vertices, int length, Vector2 point)
{
var firstSide = PointIsOnLeftSideOfLine(vertices[0], vertices[1], point);
for (var i = 1; i < length; i++)
{
var nextIndex = (i + 1) % length;
if (firstSide != PointIsOnLeftSideOfLine(vertices[i], vertices[nextIndex], point))
return false;
}
return true;
}
public static unsafe bool PolygonIntersectsAABB(Vector2[] vertices, Vector2 aabbMin, Vector2 aabbMax)
{
fixed (Vector2* ptr = vertices)
return PolygonIntersectsAABBUnsafe(ptr, vertices.Length, aabbMin, aabbMax);
}
public static unsafe bool PolygonIntersectsAABBUnsafe(Vector2* vertices, int length, Vector2 aabbMin, Vector2 aabbMax)
{
for (var i = 0; i < length; i++)
{
if (AABBContainsPoint(aabbMin, aabbMax, vertices[i]))
return true;
}
var aabbVertices = stackalloc Vector2[4];
GetVerticesAABBUnsafe(aabbMin, aabbMax, aabbVertices);
for (var i = 0; i < 4; i++)
{
if (PolygonContainsPointUnsafe(vertices, length, aabbVertices[i]))
return true;
}
for (var i = 0; i < length; i++)
{
var a = vertices[i];
var b = vertices[(i + 1) % length];
for (var j = 0; j < 4; j++)
{
var otherA = aabbVertices[j];
var otherB = aabbVertices[(j + 1) % 4];
if (LineSegmentIntersectsLineSegment(a, b, otherA, otherB, out _))
return true;
}
}
return false;
}
public static unsafe bool PolygonIntersectsCircle(Vector2[] vertices, Vector2 circleCenter, float circleRadius)
{
fixed (Vector2* ptr = vertices)
return PolygonIntersectsCircleUnsafe(ptr, vertices.Length, circleCenter, circleRadius);
}
public static unsafe bool PolygonIntersectsCircleUnsafe(Vector2* vertices, int length, Vector2 circleCenter,
float circleRadius)
{
if (PolygonContainsPointUnsafe(vertices, length, circleCenter))
return true;
for (var i = 0; i < length; i++)
{
var nextIndex = (i + 1) % length;
var closestPoint = ClosestPointOnLineSegment(vertices[i], vertices[nextIndex], circleCenter);
if (CircleContainsPoint(circleCenter, circleRadius, closestPoint))
return true;
}
return false;
}
public static unsafe bool PolygonIntersectsPolygon(Vector2[] verticesA, Vector2[] verticesB)
{
fixed (Vector2* ptrA = verticesA)
{
fixed (Vector2* ptrB = verticesB)
return PolygonIntersectsPolygonUnsafe(ptrA, verticesA.Length, ptrB, verticesB.Length);
}
}
public static unsafe bool PolygonIntersectsPolygonUnsafe(Vector2* verticesA, int lengthA, Vector2* verticesB,
int lengthB)
{
for (var i = 0; i < lengthA; i++)
{
if (PolygonContainsPointUnsafe(verticesB, lengthB, verticesA[i]))
return true;
}
for (var i = 0; i < lengthB; i++)
{
if (PolygonContainsPointUnsafe(verticesA, lengthA, verticesB[i]))
return true;
}
for (var i = 0; i < lengthA; i++)
{
var nextIndexA = (i + 1) % lengthA;
for (var j = 0; j < lengthB; j++)
{
var nextIndexB = (j + 1) % lengthB;
if (LineSegmentIntersectsLineSegment(verticesA[i], verticesA[nextIndexA], verticesB[j],
verticesB[nextIndexB], out _))
return true;
}
}
return false;
}
public static unsafe void GetCenterOfPolygon(Vector2[] vertices, out Vector2 center)
{
fixed(Vector2* ptr = vertices)
GetCenterOfPolygonUnsafe(ptr, vertices.Length, out center);
}
public static unsafe void GetCenterOfPolygonUnsafe(Vector2* vertices, int length, out Vector2 center)
{
center = Vector2.Zero;
for (var i = 0; i < length; i++)
center += vertices[i];
center /= length;
}
public static unsafe void GetLongestEdgeOfPolygon(Vector2[] vertices, out int longestEdge)
{
fixed (Vector2* ptr = vertices)
GetLongestEdgeOfPolygonUnsafe(ptr, vertices.Length, out longestEdge);
}
public static unsafe void GetLongestEdgeOfPolygonUnsafe(Vector2* vertices, int length, out int longestEdge)
{
var longestLengthSquared = (vertices[1] - vertices[0]).LengthSquared();
longestEdge = 0;
for (var i = 1; i < length; i++)
{
var nextVertex = (i + 1) % length;
var lengthSquared = (vertices[nextVertex] - vertices[i]).LengthSquared();
if (!(lengthSquared > longestLengthSquared))
continue;
longestEdge = i;
longestLengthSquared = lengthSquared;
}
}
public static unsafe void GetPolygonFromCircle(Vector2 circleCenter, float circleRadius, Vector2[] vertices)
{
fixed (Vector2* ptr = vertices)
GetPolygonFromCircleUnsafe(circleCenter, circleRadius, ptr, vertices.Length);
}
public static unsafe void GetPolygonFromCircleUnsafe(Vector2 circleCenter, float circleRadius,
Vector2* vertices, int length)
{
var angleStep = MathF.PI * 2f / length;
for (var i = 0; i < length; i++)
vertices[i] = circleCenter + new Vector2(circleRadius, 0f).RotatedByRadians(angleStep * i);
}
public static unsafe void GetBoundingPolygon(Vector2[] points, Vector2[] boundingPolygonVertices, out int numBoundingPolygonVertices)
{
fixed (Vector2* ptrA = points)
{
fixed (Vector2* ptrB = boundingPolygonVertices)
GetBoundingPolygonUnsafe(ptrA, points.Length, ptrB, boundingPolygonVertices.Length, out numBoundingPolygonVertices);
}
}
public static unsafe void GetBoundingPolygonUnsafe(Vector2* points, int pointsLength,
Vector2* boundingPolygonVertices, int boundingPolygonVerticesLength, out int numBoundingPolygonVertices)
{
numBoundingPolygonVertices = 0;
var leftMostIndex = 0;
for (var i = 1; i < pointsLength; i++)
{
if (points[i].X < points[leftMostIndex].X)
leftMostIndex = i;
}
var p = leftMostIndex;
do
{
boundingPolygonVertices[numBoundingPolygonVertices++] = points[p];
var q = (p + 1) % pointsLength;
for (var i = 0; i < pointsLength; i++)
{
if (orientation(points[p], points[i], points[q]) == 2)
q = i;
}
p = q;
} while (p != leftMostIndex);
int orientation(Vector2 p, Vector2 q, Vector2 r)
{
var val = (q.Y - p.Y) * (r.X - q.X) - (q.X - p.X) * (r.Y - q.Y);
if (val == 0f) return 0;
return val > 0f ? 1 : 2;
}
}
/// <summary>
/// Writes to the a (start) and b (end) arrays based on the array of vertices passed in. The a and b arrays can
/// then be used for line intersection tests.
/// </summary>
public static unsafe void GetEdges(Vector2[] vertices, Vector2[] a, Vector2[] b)
{
fixed (Vector2* verticesPtr = vertices)
{
fixed (Vector2* aPtr = a)
{
fixed (Vector2* bPtr = b)
{
GetEdgesUnsafe(verticesPtr, vertices.Length, aPtr, bPtr);
}
}
}
}
public static unsafe void GetEdgesUnsafe(Vector2* vertices, int verticesLength, Vector2* a, Vector2* b)
{
for (var i = 0; i < verticesLength; i++)
{
var bIndex = (i + 1) % verticesLength;
a[i] = vertices[i];
b[i] = vertices[bIndex];
}
}
# endregion
# region Vector2
public static Vector2 RotatedByDegrees(this Vector2 v, float degrees) => v.RotatedByDegrees(degrees, Vector2.Zero);
public static Vector2 RotatedByDegrees(this Vector2 v, float degrees, Vector2 center) =>
v.RotatedByRadians(degrees * DegToRad, center);
public static Vector2 RotatedByRadians(this Vector2 v, float radians) => v.RotatedByRadians(radians, Vector2.Zero);
public static Vector2 RotatedByRadians(this Vector2 v, float radians, Vector2 center)
{
var cosTheta = MathF.Cos(radians);
var sinTheta = MathF.Sin(radians);
return new Vector2
{
X = cosTheta * (v.X - center.X) - sinTheta * (v.Y - center.Y) + center.X,
Y = sinTheta * (v.X - center.X) + cosTheta * (v.Y - center.Y) + center.Y,
};
}
public static float AngleBetween(this Vector2 v, Vector2 p) => MathF.Acos(Vector2.Dot(v, p) / (v.Length() * p.Length()));
public static float AngleBetweenSigned(this Vector2 v, Vector2 p) =>
v.AngleBetween(p) * (PointIsOnLeftSideOfLine(Vector2.Zero, v, p) ? 1f : -1f);
# endregion Vector2
# region Miscellaneous
private static unsafe void SortArrayUnsafe<T>(T* arr, int left, int right, Comparison<T> comp) where T : unmanaged
{
if (left >= right)
return;
var p = partition(left, right);
SortArrayUnsafe(arr, left, p - 1, comp);
SortArrayUnsafe(arr, p + 1, right, comp);
void swap(int a, int b) => (arr[a], arr[b]) = (arr[b], arr[a]);
int partition(int l, int r)
{
var pValue = arr[r];
var i = l - 1;
for (var j = l; j <= r - 1; j++)
{
if (comp(arr[j], pValue) >= 0)
continue;
i++;
swap(i, j);
}
swap(i + 1, r);
return i + 1;
}
}
public static float SmallestAngleDifferenceDegrees(float fromDeg, float toDeg) =>
SmallestAngleDifferenceRadians(fromDeg * DegToRad, toDeg * DegToRad);
public static float SmallestAngleDifferenceRadians(float fromRad, float toRad)
{
var diff = (toRad - fromRad + MathF.PI) % MathF.PI;
return diff < -MathF.PI ? diff + MathF.PI * 2f : diff;
}
# endregion Miscellaneous
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment