diff --git a/src/Collision/AABB2D.cs b/src/Collision/AABB2D.cs new file mode 100644 index 0000000..1822187 --- /dev/null +++ b/src/Collision/AABB2D.cs @@ -0,0 +1,144 @@ +using System.Collections.Generic; +using MoonWorks.Math; + +namespace MoonWorks.Collision +{ + /// + /// Axis-aligned bounding box. + /// + public struct AABB2D : System.IEquatable + { + /// + /// The top-left position of the AABB. + /// + /// + public Vector2 Min { get; private set; } + + /// + /// The bottom-right position of the AABB. + /// + /// + public Vector2 Max { get; private set; } + + public float Width { get { return Max.X - Min.X; } } + public float Height { get { return Max.Y - Min.Y; } } + + public float Right { get { return Max.X; } } + public float Left { get { return Min.X; } } + + /// + /// The top of the AABB. Assumes a downward-aligned Y axis, so this value will be smaller than Bottom. + /// + /// + public float Top { get { return Min.Y; } } + + /// + /// The bottom of the AABB. Assumes a downward-aligned Y axis, so this value will be larger than Top. + /// + /// + public float Bottom { get { return Max.Y; } } + + public AABB2D(float minX, float minY, float maxX, float maxY) + { + Min = new Vector2(minX, minY); + Max = new Vector2(maxX, maxY); + } + + public AABB2D(Vector2 min, Vector2 max) + { + Min = min; + Max = max; + } + + private static Matrix3x2 AbsoluteMatrix(Matrix3x2 matrix) + { + return new Matrix3x2 + ( + System.Math.Abs(matrix.M11), System.Math.Abs(matrix.M12), + System.Math.Abs(matrix.M21), System.Math.Abs(matrix.M22), + System.Math.Abs(matrix.M31), System.Math.Abs(matrix.M32) + ); + } + + /// + /// Efficiently transforms the AABB by a Transform2D. + /// + /// + /// + /// + public static AABB2D Transformed(AABB2D aabb, Transform2D transform) + { + return new AABB2D( + Vector2.Transform(aabb.Min, transform.TransformMatrix), + Vector2.Transform(aabb.Max, transform.TransformMatrix) + ); + } + + /// + /// Creates an AABB for an arbitrary collection of positions. + /// This is less efficient than defining a custom AABB method for most shapes, so avoid using this if possible. + /// + /// + /// + public static AABB2D FromVertices(IEnumerable vertices) + { + var minX = float.MaxValue; + var minY = float.MaxValue; + var maxX = float.MinValue; + var maxY = float.MinValue; + + foreach (var vertex in vertices) + { + if (vertex.X < minX) + { + minX = vertex.X; + } + if (vertex.Y < minY) + { + minY = vertex.Y; + } + if (vertex.X > maxX) + { + maxX = vertex.X; + } + if (vertex.Y > maxY) + { + maxY = vertex.Y; + } + } + + return new AABB2D(minX, minY, maxX, maxY); + } + + public static bool TestOverlap(AABB2D a, AABB2D b) + { + return a.Left <= b.Right && a.Right >= b.Left && a.Top <= b.Bottom && a.Bottom >= b.Top; + } + + public override bool Equals(object obj) + { + return obj is AABB2D aabb && Equals(aabb); + } + + public bool Equals(AABB2D other) + { + return Min == other.Min && + Max == other.Max; + } + + public override int GetHashCode() + { + return System.HashCode.Combine(Min, Max); + } + + public static bool operator ==(AABB2D left, AABB2D right) + { + return left.Equals(right); + } + + public static bool operator !=(AABB2D left, AABB2D right) + { + return !(left == right); + } + } +} diff --git a/src/Collision/IHasAABB2D.cs b/src/Collision/IHasAABB2D.cs new file mode 100644 index 0000000..31bb5b3 --- /dev/null +++ b/src/Collision/IHasAABB2D.cs @@ -0,0 +1,16 @@ +using MoonWorks.Math; + +namespace MoonWorks.Collision +{ + public interface IHasAABB2D + { + AABB2D AABB { get; } + + /// + /// Returns a bounding box based on the shape. + /// + /// A Transform for transforming the shape vertices. + /// Returns a bounding box based on the shape. + AABB2D TransformedAABB(Transform2D transform); + } +} diff --git a/src/Collision/IShape2D.cs b/src/Collision/IShape2D.cs new file mode 100644 index 0000000..91f224a --- /dev/null +++ b/src/Collision/IShape2D.cs @@ -0,0 +1,15 @@ +using MoonWorks.Math; + +namespace MoonWorks.Collision +{ + public interface IShape2D : IHasAABB2D, System.IEquatable + { + /// + /// A Minkowski support function. Gives the farthest point on the edge of a shape along the given direction. + /// + /// A normalized Vector2. + /// A Transform for transforming the shape vertices. + /// The farthest point on the edge of the shape along the given direction. + Vector2 Support(Vector2 direction, Transform2D transform); + } +} diff --git a/src/Collision/MinkowskiDifference.cs b/src/Collision/MinkowskiDifference.cs new file mode 100644 index 0000000..5f1aec2 --- /dev/null +++ b/src/Collision/MinkowskiDifference.cs @@ -0,0 +1,57 @@ +using MoonWorks.Math; + +namespace MoonWorks.Collision +{ + /// + /// A Minkowski difference between two shapes. + /// + public struct MinkowskiDifference : System.IEquatable + { + private IShape2D ShapeA { get; } + private Transform2D TransformA { get; } + private IShape2D ShapeB { get; } + private Transform2D TransformB { get; } + + public MinkowskiDifference(IShape2D shapeA, Transform2D transformA, IShape2D shapeB, Transform2D transformB) + { + ShapeA = shapeA; + TransformA = transformA; + ShapeB = shapeB; + TransformB = transformB; + } + + public Vector2 Support(Vector2 direction) + { + return ShapeA.Support(direction, TransformA) - ShapeB.Support(-direction, TransformB); + } + + public override bool Equals(object other) + { + return other is MinkowskiDifference minkowskiDifference && Equals(minkowskiDifference); + } + + public bool Equals(MinkowskiDifference other) + { + return + ShapeA == other.ShapeA && + TransformA == other.TransformA && + ShapeB == other.ShapeB && + TransformB == other.TransformB; + } + + public override int GetHashCode() + { + return System.HashCode.Combine(ShapeA, TransformA, ShapeB, TransformB); + } + + public static bool operator ==(MinkowskiDifference a, MinkowskiDifference b) + { + return a.Equals(b); + } + + public static bool operator !=(MinkowskiDifference a, MinkowskiDifference b) + { + return !(a == b); + } + } +} diff --git a/src/Collision/NarrowPhase.cs b/src/Collision/NarrowPhase.cs new file mode 100644 index 0000000..ddad6e9 --- /dev/null +++ b/src/Collision/NarrowPhase.cs @@ -0,0 +1,267 @@ +using MoonWorks.Math; + +namespace MoonWorks.Collision +{ + public static class NarrowPhase + { + private struct Edge + { + public float Distance; + public Vector2 Normal; + public int Index; + } + + public static bool TestCollision(IShape2D shapeA, Transform2D transformA, IShape2D shapeB, Transform2D transformB) + { + if (shapeA is Rectangle rectangleA && shapeB is Rectangle rectangleB && transformA.Rotation == 0 && transformB.Rotation == 0) + { + return TestRectangleOverlap(rectangleA, transformA, rectangleB, transformB); + } + else if (shapeA is Point && shapeB is Rectangle && transformB.Rotation == 0) + { + return TestPointRectangleOverlap((Point) shapeA, transformA, (Rectangle) shapeB, transformB); + } + else if (shapeA is Rectangle && shapeB is Point && transformA.Rotation == 0) + { + return TestPointRectangleOverlap((Point) shapeB, transformB, (Rectangle) shapeA, transformA); + } + else if (shapeA is Circle circleA && shapeB is Circle circleB && transformA.Scale.X == transformA.Scale.Y && transformB.Scale.X == transformB.Scale.Y) + { + return TestCircleOverlap(circleA, transformA, circleB, transformB); + } + + return FindCollisionSimplex(shapeA, transformA, shapeB, transformB).Item1; + } + + public static bool TestRectangleOverlap(Rectangle rectangleA, Transform2D transformA, Rectangle rectangleB, Transform2D transformB) + { + var firstAABB = rectangleA.TransformedAABB(transformA); + var secondAABB = rectangleB.TransformedAABB(transformB); + + return firstAABB.Left < secondAABB.Right && firstAABB.Right > secondAABB.Left && firstAABB.Top < secondAABB.Bottom && firstAABB.Bottom > secondAABB.Top; + } + + public static bool TestPointRectangleOverlap(Point point, Transform2D pointTransform, Rectangle rectangle, Transform2D rectangleTransform) + { + var transformedPoint = pointTransform.Position; + var AABB = rectangle.TransformedAABB(rectangleTransform); + + return transformedPoint.X > AABB.Left && transformedPoint.X < AABB.Right && transformedPoint.Y < AABB.Bottom && transformedPoint.Y > AABB.Top; + } + + public static bool TestCircleOverlap(Circle circleA, Transform2D transformA, Circle circleB, Transform2D transformB) + { + var radiusA = circleA.Radius * transformA.Scale.X; + var radiusB = circleB.Radius * transformB.Scale.Y; + + var centerA = transformA.Position; + var centerB = transformB.Position; + + var distanceSquared = (centerA - centerB).LengthSquared(); + var radiusSumSquared = (radiusA + radiusB) * (radiusA + radiusB); + + return distanceSquared < radiusSumSquared; + } + + public static (bool, Simplex2D) FindCollisionSimplex(IShape2D shapeA, Transform2D transformA, IShape2D shapeB, Transform2D transformB) + { + var minkowskiDifference = new MinkowskiDifference(shapeA, transformA, shapeB, transformB); + var c = minkowskiDifference.Support(Vector2.UnitX); + var b = minkowskiDifference.Support(-Vector2.UnitX); + return Check(minkowskiDifference, c, b); + } + + public unsafe static Vector2 Intersect(IShape2D shapeA, Transform2D Transform2DA, IShape2D shapeB, Transform2D Transform2DB, Simplex2D simplex) + { + if (shapeA == null) { throw new System.ArgumentNullException(nameof(shapeA)); } + if (shapeB == null) { throw new System.ArgumentNullException(nameof(shapeB)); } + if (!simplex.TwoSimplex) { throw new System.ArgumentException("Simplex must be a 2-Simplex.", nameof(simplex)); } + + var a = simplex.A; + var b = simplex.B.Value; + var c = simplex.C.Value; + + Vector2 intersection = default; + + for (var i = 0; i < 32; i++) + { + var edge = FindClosestEdge(simplex); + var support = CalculateSupport(shapeA, Transform2DA, shapeB, Transform2DB, edge.Normal); + var distance = Vector2.Dot(support, edge.Normal); + + intersection = edge.Normal; + intersection *= distance; + + if (System.Math.Abs(distance - edge.Distance) <= 0.00001f) + { + return intersection; + } + else + { + simplex.Insert(support, edge.Index); + } + } + + return intersection; // close enough + } + + private static unsafe Edge FindClosestEdge(Simplex2D simplex) + { + var closestDistance = float.PositiveInfinity; + var closestNormal = Vector2.Zero; + var closestIndex = 0; + + for (var i = 0; i < 4; i += 1) + { + var j = (i + 1 == 3) ? 0 : i + 1; + + var a = simplex[i]; + var b = simplex[j]; + + var e = b - a; + + var oa = a; + + var n = Vector2.Normalize(TripleProduct(e, oa, e)); + + var d = Vector2.Dot(n, a); + + if (d < closestDistance) + { + closestDistance = d; + closestNormal = n; + closestIndex = j; + } + } + + return new Edge + { + Distance = closestDistance, + Normal = closestNormal, + Index = closestIndex + }; + } + + private static Vector2 CalculateSupport(IShape2D shapeA, Transform2D Transform2DA, IShape2D shapeB, Transform2D Transform2DB, Vector2 direction) + { + return shapeA.Support(direction, Transform2DA) - shapeB.Support(-direction, Transform2DB); + } + + private static (bool, Simplex2D) Check(MinkowskiDifference minkowskiDifference, Vector2 c, Vector2 b) + { + var cb = c - b; + var c0 = -c; + var d = Direction(cb, c0); + return DoSimplex(minkowskiDifference, new Simplex2D(b, c), d); + } + + private static (bool, Simplex2D) DoSimplex(MinkowskiDifference minkowskiDifference, Simplex2D simplex, Vector2 direction) + { + var a = minkowskiDifference.Support(direction); + var notPastOrigin = Vector2.Dot(a, direction) < 0; + var (intersects, newSimplex, newDirection) = EnclosesOrigin(a, simplex); + + if (notPastOrigin) + { + return (false, default(Simplex2D)); + } + else if (intersects) + { + return (true, new Simplex2D(simplex.A, simplex.B.Value, a)); + } + else + { + return DoSimplex(minkowskiDifference, newSimplex, newDirection); + } + } + + private static (bool, Simplex2D, Vector2) EnclosesOrigin(Vector2 a, Simplex2D simplex) + { + if (simplex.ZeroSimplex) + { + return HandleZeroSimplex(a, simplex.A); + } + else if (simplex.OneSimplex) + { + return HandleOneSimplex(a, simplex.A, simplex.B.Value); + } + else + { + return (false, simplex, Vector2.Zero); + } + } + + private static (bool, Simplex2D, Vector2) HandleZeroSimplex(Vector2 a, Vector2 b) + { + var ab = b - a; + var a0 = -a; + var (newSimplex, newDirection) = SameDirection(ab, a0) ? (new Simplex2D(a, b), Perpendicular(ab, a0)) : (new Simplex2D(a), a0); + return (false, newSimplex, newDirection); + } + + private static (bool, Simplex2D, Vector2) HandleOneSimplex(Vector2 a, Vector2 b, Vector2 c) + { + var a0 = -a; + var ab = b - a; + var ac = c - a; + var abp = Perpendicular(ab, -ac); + var acp = Perpendicular(ac, -ab); + + if (SameDirection(abp, a0)) + { + if (SameDirection(ab, a0)) + { + return (false, new Simplex2D(a, b), abp); + } + else + { + return (false, new Simplex2D(a), a0); + } + } + else if (SameDirection(acp, a0)) + { + if (SameDirection(ac, a0)) + { + return (false, new Simplex2D(a, c), acp); + } + else + { + return (false, new Simplex2D(a), a0); + } + } + else + { + return (true, new Simplex2D(b, c), a0); + } + } + + private static Vector2 TripleProduct(Vector2 a, Vector2 b, Vector2 c) + { + var A = new Vector3(a.X, a.Y, 0); + var B = new Vector3(b.X, b.Y, 0); + var C = new Vector3(c.X, c.Y, 0); + + var first = Vector3.Cross(A, B); + var second = Vector3.Cross(first, C); + + return new Vector2(second.X, second.Y); + } + + private static Vector2 Direction(Vector2 a, Vector2 b) + { + var d = TripleProduct(a, b, a); + var collinear = d == Vector2.Zero; + return collinear ? new Vector2(a.Y, -a.X) : d; + } + + private static bool SameDirection(Vector2 a, Vector2 b) + { + return Vector2.Dot(a, b) > 0; + } + + private static Vector2 Perpendicular(Vector2 a, Vector2 b) + { + return TripleProduct(a, b, a); + } + } +} diff --git a/src/Collision/Shapes/Circle.cs b/src/Collision/Shapes/Circle.cs new file mode 100644 index 0000000..579e63f --- /dev/null +++ b/src/Collision/Shapes/Circle.cs @@ -0,0 +1,59 @@ +using MoonWorks.Math; + +namespace MoonWorks.Collision +{ + /// + /// A Circle is a shape defined by a radius. + /// + public struct Circle : IShape2D, System.IEquatable + { + public int Radius { get; } + public AABB2D AABB { get; } + + public Circle(int radius) + { + Radius = radius; + AABB = new AABB2D(-Radius, -Radius, Radius, Radius); + } + + public Vector2 Support(Vector2 direction, Transform2D transform) + { + return Vector2.Transform(Vector2.Normalize(direction) * Radius, transform.TransformMatrix); + } + + public AABB2D TransformedAABB(Transform2D transform2D) + { + return AABB2D.Transformed(AABB, transform2D); + } + + public override bool Equals(object obj) + { + return obj is IShape2D other && Equals(other); + } + + public bool Equals(IShape2D other) + { + return other is Circle circle && Equals(circle); + } + + public bool Equals(Circle other) + { + return Radius == other.Radius; + } + + public override int GetHashCode() + { + return System.HashCode.Combine(Radius); + } + + public static bool operator ==(Circle a, Circle b) + { + return a.Equals(b); + } + + public static bool operator !=(Circle a, Circle b) + { + return !(a == b); + } + } +} diff --git a/src/Collision/Shapes/Line.cs b/src/Collision/Shapes/Line.cs new file mode 100644 index 0000000..72efd67 --- /dev/null +++ b/src/Collision/Shapes/Line.cs @@ -0,0 +1,74 @@ +using MoonWorks.Math; + +namespace MoonWorks.Collision +{ + /// + /// A line is a shape defined by exactly two points in space. + /// + public struct Line : IShape2D, System.IEquatable + { + private Vector2 Start { get; } + private Vector2 End { get; } + + public AABB2D AABB { get; } + + public Line(Vector2 start, Vector2 end) + { + Start = start; + End = end; + + AABB = new AABB2D( + System.Math.Min(Start.X, End.X), + System.Math.Min(Start.Y, End.Y), + System.Math.Max(Start.X, End.X), + System.Math.Max(Start.Y, End.Y) + ); + } + + public Vector2 Support(Vector2 direction, Transform2D transform) + { + var transformedStart = Vector2.Transform(Start, transform.TransformMatrix); + var transformedEnd = Vector2.Transform(End, transform.TransformMatrix); + return Vector2.Dot(transformedStart, direction) > Vector2.Dot(transformedEnd, direction) ? + transformedStart : + transformedEnd; + } + + public AABB2D TransformedAABB(Transform2D transform) + { + return AABB2D.Transformed(AABB, transform); + } + + public override bool Equals(object obj) + { + return obj is IShape2D other && Equals(other); + } + + public bool Equals(IShape2D other) + { + return other is Line otherLine && Equals(otherLine); + } + + public bool Equals(Line other) + { + return + (Start == other.Start && End == other.End) || + (End == other.Start && Start == other.End); + } + + public override int GetHashCode() + { + return System.HashCode.Combine(Start, End); + } + + public static bool operator ==(Line a, Line b) + { + return a.Equals(b); + } + + public static bool operator !=(Line a, Line b) + { + return !(a == b); + } + } +} diff --git a/src/Collision/Shapes/Point.cs b/src/Collision/Shapes/Point.cs new file mode 100644 index 0000000..7bdc672 --- /dev/null +++ b/src/Collision/Shapes/Point.cs @@ -0,0 +1,53 @@ +using MoonWorks.Math; + +namespace MoonWorks.Collision +{ + /// + /// A Point is "that which has no part". + /// All points by themselves are identical. + /// + public struct Point : IShape2D, System.IEquatable + { + public AABB2D AABB { get; } + + public AABB2D TransformedAABB(Transform2D transform) + { + return AABB2D.Transformed(AABB, transform); + } + + public Vector2 Support(Vector2 direction, Transform2D transform) + { + return Vector2.Transform(Vector2.Zero, transform.TransformMatrix); + } + + public override bool Equals(object obj) + { + return obj is IShape2D other && Equals(other); + } + + public bool Equals(IShape2D other) + { + return other is Point otherPoint && Equals(otherPoint); + } + + public bool Equals(Point other) + { + return true; + } + + public override int GetHashCode() + { + return 0; + } + + public static bool operator ==(Point a, Point b) + { + return true; + } + + public static bool operator !=(Point a, Point b) + { + return false; + } + } +} diff --git a/src/Collision/Shapes/Rectangle.cs b/src/Collision/Shapes/Rectangle.cs new file mode 100644 index 0000000..2bd32df --- /dev/null +++ b/src/Collision/Shapes/Rectangle.cs @@ -0,0 +1,106 @@ +using MoonWorks.Math; + +namespace MoonWorks.Collision +{ + /// + /// A rectangle is a shape defined by a width and height. The origin is the center of the rectangle. + /// + public struct Rectangle : IShape2D, System.IEquatable + { + public AABB2D AABB { get; } + public int Width { get; } + public int Height { get; } + + public float Right { get; } + public float Left { get; } + public float Top { get; } + public float Bottom { get; } + public Vector2 BottomLeft { get; } + public Vector2 TopRight { get; } + + public Vector2 Min { get; } + public Vector2 Max { get; } + + public Rectangle(int left, int top, int width, int height) + { + Width = width; + Height = height; + Left = left; + Right = left + width; + Top = top; + Bottom = top + height; + AABB = new AABB2D(left, top, Right, Bottom); + BottomLeft = new Vector2(Left, Bottom); + TopRight = new Vector2(Top, Right); + Min = AABB.Min; + Max = AABB.Max; + } + + private Vector2 Support(Vector2 direction) + { + if (direction.X >= 0 && direction.Y >= 0) + { + return Max; + } + else if (direction.X >= 0 && direction.Y < 0) + { + return new Vector2(Max.X, Min.Y); + } + else if (direction.X < 0 && direction.Y >= 0) + { + return new Vector2(Min.X, Max.Y); + } + else if (direction.X < 0 && direction.Y < 0) + { + return new Vector2(Min.X, Min.Y); + } + else + { + throw new System.ArgumentException("Support vector direction cannot be zero."); + } + } + + public Vector2 Support(Vector2 direction, Transform2D transform) + { + Matrix3x2 inverseTransform; + Matrix3x2.Invert(transform.TransformMatrix, out inverseTransform); + var inverseDirection = Vector2.TransformNormal(direction, inverseTransform); + return Vector2.Transform(Support(inverseDirection), transform.TransformMatrix); + } + + public AABB2D TransformedAABB(Transform2D transform) + { + return AABB2D.Transformed(AABB, transform); + } + + public override bool Equals(object obj) + { + return obj is IShape2D other && Equals(other); + } + + public bool Equals(IShape2D other) + { + return (other is Rectangle rectangle && Equals(rectangle)); + } + + public bool Equals(Rectangle other) + { + return Min == other.Min && Max == other.Max; + } + + public override int GetHashCode() + { + return System.HashCode.Combine(Min, Max); + } + + public static bool operator ==(Rectangle a, Rectangle b) + { + return a.Equals(b); + } + + public static bool operator !=(Rectangle a, Rectangle b) + { + return !(a == b); + } + } +} diff --git a/src/Collision/Simplex2D.cs b/src/Collision/Simplex2D.cs new file mode 100644 index 0000000..f2bf7f6 --- /dev/null +++ b/src/Collision/Simplex2D.cs @@ -0,0 +1,136 @@ +using System.Collections.Generic; +using MoonWorks.Math; + +namespace MoonWorks.Collision +{ + /// + /// A simplex is a shape with up to n - 2 vertices in the nth dimension. + /// + public struct Simplex2D : System.IEquatable + { + private Vector2 a; + private Vector2? b; + private Vector2? c; + + public Vector2 A => a; + public Vector2? B => b; + public Vector2? C => c; + + public bool ZeroSimplex { get { return !b.HasValue && !c.HasValue; } } + public bool OneSimplex { get { return b.HasValue && !c.HasValue; } } + public bool TwoSimplex { get { return b.HasValue && c.HasValue; } } + + public int Count => TwoSimplex ? 3 : (OneSimplex ? 2 : 1); + + public Simplex2D(Vector2 a) + { + this.a = a; + b = null; + c = null; + } + + public Simplex2D(Vector2 a, Vector2 b) + { + this.a = a; + this.b = b; + c = null; + } + + public Simplex2D(Vector2 a, Vector2 b, Vector2 c) + { + this.a = a; + this.b = b; + this.c = c; + } + + public Vector2 this[int index] + { + get + { + if (index == 0) { return a; } + if (index == 1) { return b.Value; } + if (index == 2) { return c.Value; } + throw new System.IndexOutOfRangeException(); + } + } + + public IEnumerable Vertices + { + get + { + yield return (Vector2) a; + if (b.HasValue) { yield return (Vector2) b; } + if (c.HasValue) { yield return (Vector2) c; } + } + } + + public Vector2 Support(Vector2 direction, Transform2D transform) + { + var maxDotProduct = float.NegativeInfinity; + var maxVertex = a; + foreach (var vertex in Vertices) + { + var transformed = Vector2.Transform(vertex, transform.TransformMatrix); + var dot = Vector2.Dot(transformed, direction); + if (dot > maxDotProduct) + { + maxVertex = transformed; + maxDotProduct = dot; + } + } + return maxVertex; + } + + public void Insert(Vector2 point, int index) + { + if (index == 0) + { + c = b; + b = a; + a = point; + } + else if (index == 1) + { + c = b; + b = point; + } + else + { + c = point; + } + } + + public override bool Equals(object obj) + { + return obj is Simplex2D other && Equals(other); + } + + public bool Equals(Simplex2D other) + { + if (Count != other.Count) { return false; } + + return + (A == other.A && B == other.B && C == other.C) || + (A == other.A && B == other.C && C == other.B) || + (A == other.B && B == other.A && C == other.C) || + (A == other.B && B == other.C && C == other.A) || + (A == other.C && B == other.A && C == other.B) || + (A == other.C && B == other.B && C == other.A); + } + + public override int GetHashCode() + { + return System.HashCode.Combine(Vertices); + } + + public static bool operator ==(Simplex2D a, Simplex2D b) + { + return a.Equals(b); + } + + public static bool operator !=(Simplex2D a, Simplex2D b) + { + return !(a == b); + } + } +} diff --git a/src/Collision/SpatialHash2D.cs b/src/Collision/SpatialHash2D.cs new file mode 100644 index 0000000..4943387 --- /dev/null +++ b/src/Collision/SpatialHash2D.cs @@ -0,0 +1,187 @@ +using System.Collections.Generic; +using MoonWorks.Math; + +namespace MoonWorks.Collision +{ + /// + /// Used to quickly check if two shapes are potentially overlapping. + /// + /// The type that will be used to uniquely identify shape-transform pairs. + public class SpatialHash2D where T : System.IEquatable + { + private readonly int cellSize; + + private readonly Dictionary> hashDictionary = new Dictionary>(); + private readonly Dictionary IDLookup = new Dictionary(); + + public int MinX { get; private set; } = 0; + public int MaxX { get; private set; } = 0; + public int MinY { get; private set; } = 0; + public int MaxY { get; private set; } = 0; + + private Queue> hashSetPool = new Queue>(); + + public SpatialHash2D(int cellSize) + { + this.cellSize = cellSize; + } + + private (int, int) Hash(Vector2 position) + { + return ((int) System.Math.Floor(position.X / cellSize), (int) System.Math.Floor(position.Y / cellSize)); + } + + /// + /// Inserts an element into the SpatialHash. + /// + /// A unique ID for the shape-transform pair. + /// + /// + public void Insert(T id, IHasAABB2D shape, Transform2D transform2D) + { + var box = shape.TransformedAABB(transform2D); + var minHash = Hash(box.Min); + var maxHash = Hash(box.Max); + + for (var i = minHash.Item1; i <= maxHash.Item1; i++) + { + for (var j = minHash.Item2; j <= maxHash.Item2; j++) + { + var key = MakeLong(i, j); + if (!hashDictionary.ContainsKey(key)) + { + hashDictionary.Add(key, new HashSet()); + } + + hashDictionary[key].Add(id); + IDLookup[id] = (shape, transform2D); + } + } + + MinX = System.Math.Min(MinX, minHash.Item1); + MinY = System.Math.Min(MinY, minHash.Item2); + MaxX = System.Math.Max(MaxX, maxHash.Item1); + MaxY = System.Math.Max(MaxY, maxHash.Item2); + } + + /// + /// Retrieves all the potential collisions of a shape-transform pair. Excludes any shape-transforms with the given ID. + /// + public IEnumerable<(T, IHasAABB2D, Transform2D)> Retrieve(T id, IHasAABB2D shape, Transform2D transform2D) + { + var returned = AcquireHashSet(); + + var box = shape.TransformedAABB(transform2D); + var (minX, minY) = Hash(box.Min); + var (maxX, maxY) = Hash(box.Max); + + if (minX < MinX) { minX = MinX; } + if (maxX > MaxX) { maxX = MaxX; } + if (minY < MinY) { minY = MinY; } + if (maxY > MaxY) { maxY = MaxY; } + + for (var i = minX; i <= maxX; i++) + { + for (var j = minY; j <= maxY; j++) + { + var key = MakeLong(i, j); + if (hashDictionary.ContainsKey(key)) + { + foreach (var t in hashDictionary[key]) + { + if (!returned.Contains(t)) + { + var (otherShape, otherTransform) = IDLookup[t]; + if (!id.Equals(t) && AABB2D.TestOverlap(box, otherShape.TransformedAABB(otherTransform))) + { + returned.Add(t); + yield return (t, otherShape, otherTransform); + } + } + } + } + } + } + + FreeHashSet(returned); + } + + + /// + /// Retrieves objects based on a pre-transformed AABB. + /// + /// A transformed AABB. + /// + public IEnumerable<(T, IHasAABB2D, Transform2D)> Retrieve(AABB2D aabb) + { + var returned = AcquireHashSet(); + + var (minX, minY) = Hash(aabb.Min); + var (maxX, maxY) = Hash(aabb.Max); + + if (minX < MinX) { minX = MinX; } + if (maxX > MaxX) { maxX = MaxX; } + if (minY < MinY) { minY = MinY; } + if (maxY > MaxY) { maxY = MaxY; } + + for (var i = minX; i <= maxX; i++) + { + for (var j = minY; j <= maxY; j++) + { + var key = MakeLong(i, j); + if (hashDictionary.ContainsKey(key)) + { + foreach (var t in hashDictionary[key]) + { + if (!returned.Contains(t)) + { + var (otherShape, otherTransform) = IDLookup[t]; + if (AABB2D.TestOverlap(aabb, otherShape.TransformedAABB(otherTransform))) + { + yield return (t, otherShape, otherTransform); + } + } + } + } + } + } + + FreeHashSet(returned); + } + + /// + /// Removes everything that has been inserted into the SpatialHash. + /// + public void Clear() + { + foreach (var hash in hashDictionary.Values) + { + hash.Clear(); + } + + IDLookup.Clear(); + } + + private static long MakeLong(int left, int right) + { + return ((long) left << 32) | ((uint) right); + } + + private HashSet AcquireHashSet() + { + if (hashSetPool.Count == 0) + { + hashSetPool.Enqueue(new HashSet()); + } + + var hashSet = hashSetPool.Dequeue(); + hashSet.Clear(); + return hashSet; + } + + private void FreeHashSet(HashSet hashSet) + { + hashSetPool.Enqueue(hashSet); + } + } +} diff --git a/src/Graphics/Resources/Texture.cs b/src/Graphics/Resources/Texture.cs index 14b40da..e604a78 100644 --- a/src/Graphics/Resources/Texture.cs +++ b/src/Graphics/Resources/Texture.cs @@ -199,6 +199,8 @@ namespace MoonWorks.Graphics UsageFlags = textureCreateInfo.UsageFlags; } + public static implicit operator TextureSlice(Texture t) => new TextureSlice(t); + // Used by AcquireSwapchainTexture. // Should not be tracked, because swapchain textures are managed by Vulkan. internal Texture( diff --git a/src/Input/Gamepad.cs b/src/Input/Gamepad.cs index 8c82651..f025986 100644 --- a/src/Input/Gamepad.cs +++ b/src/Input/Gamepad.cs @@ -89,14 +89,14 @@ namespace MoonWorks.Input foreach (var (sdlEnum, axis) in EnumToAxis) { var sdlAxisValue = SDL.SDL_GameControllerGetAxis(Handle, sdlEnum); - var axisValue = Normalize(sdlAxisValue, short.MinValue, short.MaxValue, -1, 1); + var axisValue = MathHelper.Normalize(sdlAxisValue, short.MinValue, short.MaxValue, -1, 1); axis.Update(axisValue); } foreach (var (sdlEnum, trigger) in EnumToTrigger) { var sdlAxisValue = SDL.SDL_GameControllerGetAxis(Handle, sdlEnum); - var axisValue = Normalize(sdlAxisValue, 0, short.MaxValue, 0, 1); + var axisValue = MathHelper.Normalize(sdlAxisValue, 0, short.MaxValue, 0, 1); trigger.Update(axisValue); } } @@ -177,10 +177,5 @@ namespace MoonWorks.Input { return MoonWorks.Conversions.ByteToBool(SDL.SDL_GameControllerGetButton(Handle, button)); } - - private float Normalize(float value, short min, short max, short newMin, short newMax) - { - return ((value - min) * (newMax - newMin)) / (max - min) + newMin; - } } } diff --git a/src/Math/MathHelper.cs b/src/Math/MathHelper.cs index b98ef4c..da01c7d 100644 --- a/src/Math/MathHelper.cs +++ b/src/Math/MathHelper.cs @@ -331,6 +331,16 @@ namespace MoonWorks.Math return angle; } + public static float Normalize(float value, short min, short max, short newMin, short newMax) + { + return ((value - min) * (newMax - newMin)) / (max - min) + newMin; + } + + public static float Normalize(float value, float min, float max, float newMin, float newMax) + { + return ((value - min) * (newMax - newMin)) / (max - min) + newMin; + } + #endregion #region Internal Static Methods diff --git a/src/Math/Transform2D.cs b/src/Math/Transform2D.cs new file mode 100644 index 0000000..2a62c68 --- /dev/null +++ b/src/Math/Transform2D.cs @@ -0,0 +1,82 @@ +namespace MoonWorks.Math +{ + public struct Transform2D : System.IEquatable + { + public Vector2 Position { get; } + public float Rotation { get; } + public Vector2 Scale { get; } + + public Matrix3x2 TransformMatrix { get; } + + public Transform2D(Vector2 position) + { + Position = position; + Rotation = 0; + Scale = Vector2.One; + TransformMatrix = CreateTransformMatrix(Position, Rotation, Scale); + } + + public Transform2D(Vector2 position, float rotation) + { + Position = position; + Rotation = rotation; + Scale = Vector2.One; + TransformMatrix = CreateTransformMatrix(Position, Rotation, Scale); + } + + public Transform2D(Vector2 position, float rotation, Vector2 scale) + { + Position = position; + Rotation = rotation; + Scale = scale; + TransformMatrix = CreateTransformMatrix(Position, Rotation, Scale); + } + + public Transform2D Compose(Transform2D other) + { + return new Transform2D(Position + other.Position, Rotation + other.Rotation, Scale * other.Scale); + } + + private static Matrix3x2 CreateTransformMatrix(Vector2 position, float rotation, Vector2 scale) + { + return + Matrix3x2.CreateScale(scale) * + Matrix3x2.CreateRotation(rotation) * + Matrix3x2.CreateTranslation(position); + } + + public bool Equals(Transform2D other) + { + return + Position == other.Position && + Rotation == other.Rotation && + Scale == other.Scale; + } + + + public override bool Equals(System.Object other) + { + if (other is Transform2D otherTransform) + { + return Equals(otherTransform); + } + + return false; + } + + public override int GetHashCode() + { + return System.HashCode.Combine(Position, Rotation, Scale); + } + + public static bool operator ==(Transform2D a, Transform2D b) + { + return a.Equals(b); + } + + public static bool operator !=(Transform2D a, Transform2D b) + { + return !a.Equals(b); + } + } +} diff --git a/src/Math/Vector2.cs b/src/Math/Vector2.cs index 89c4a3c..3075166 100644 --- a/src/Math/Vector2.cs +++ b/src/Math/Vector2.cs @@ -878,10 +878,10 @@ namespace MoonWorks.Math } /// - /// Creates a new that contains a transformation of 2d-vector by the specified . + /// Creates a new that contains a transformation of 2d-vector by the specified . /// /// Source . - /// The transformation . + /// The transformation . /// Transformed . public static Vector2 Transform(Vector2 position, Matrix3x2 matrix) { @@ -999,6 +999,19 @@ namespace MoonWorks.Math ); } + /// + /// Creates a new that contains a transformation of the specified normal by the specified . + /// + /// Source which represents a normal vector. + /// The transformation . + /// Transformed normal. + public static Vector2 TransformNormal(Vector2 normal, Matrix3x2 matrix) + { + return new Vector2( + normal.X * matrix.M11 + normal.Y * matrix.M21, + normal.X * matrix.M12 + normal.Y * matrix.M22); + } + /// /// Creates a new that contains a transformation of the specified normal by the specified . ///