using System; using System.Collections.Generic; using System.Diagnostics; using Microsoft.Xna.Framework; namespace MonoGame.Extended { // Real-Time Collision Detection, Christer Ericson, 2005. Chapter 4.4; Bounding Volumes - Oriented Bounding Boxes (OBBs), pg 101. /// /// An oriented bounding rectangle is a rectangular block, much like a bounding rectangle /// but with an arbitrary orientation . /// /// [DebuggerDisplay($"{{{nameof(DebugDisplayString)},nq}}")] public struct OrientedRectangle : IEquatable, IShapeF { /// /// The centre position of this . /// public Vector2 Center; /// /// The distance from the point along both axes to any point on the boundary of this /// . /// /// public Vector2 Radii; /// /// The rotation matrix of the bounding rectangle . /// public Matrix3x2 Orientation; /// /// Initializes a new instance of the structure from the specified centre /// and the radii . /// /// The centre . /// The radii . /// The orientation . public OrientedRectangle(Vector2 center, SizeF radii, Matrix3x2 orientation) { Center = center; Radii = radii; Orientation = orientation; } /// /// Gets a list of points defining the corner points of the oriented rectangle. /// public IReadOnlyList Points { get { var topLeft = -Radii; var bottomLeft = -new Vector2(Radii.X, -Radii.Y); var topRight = (Vector2)new Vector2(Radii.X, -Radii.Y); var bottomRight = Radii; return new List { Vector2.Transform(topRight, Orientation) + Center, Vector2.Transform(topLeft, Orientation) + Center, Vector2.Transform(bottomLeft, Orientation) + Center, Vector2.Transform(bottomRight, Orientation) + Center }; } } public Vector2 Position { get => Vector2.Transform(-Radii, Orientation) + Center; set => throw new NotImplementedException(); } public RectangleF BoundingRectangle => (RectangleF)this; /// /// Computes the from the specified /// transformed by . /// /// The to transform. /// The transformation. /// A new . public static OrientedRectangle Transform(OrientedRectangle rectangle, ref Matrix3x2 transformMatrix) { Transform(ref rectangle, ref transformMatrix, out var result); return result; } private static void Transform(ref OrientedRectangle rectangle, ref Matrix3x2 transformMatrix, out OrientedRectangle result) { PrimitivesHelper.TransformOrientedRectangle( ref rectangle.Center, ref rectangle.Orientation, ref transformMatrix); result = new OrientedRectangle(); result.Center = rectangle.Center; result.Radii = rectangle.Radii; result.Orientation = rectangle.Orientation; } /// /// Compares to two structures. The result specifies whether the /// the values of the , and are /// equal. /// /// The left . /// The right . /// true if left and right argument are equal; otherwise, false. public static bool operator ==(OrientedRectangle left, OrientedRectangle right) { return left.Equals(right); } /// /// Compares to two structures. The result specifies whether the /// the values of the , or are /// unequal. /// /// The left . /// The right . /// true if left and right argument are unequal; otherwise, false. public static bool operator !=(OrientedRectangle left, OrientedRectangle right) { return !left.Equals(right); } /// /// Determines whether two instances of are equal. /// /// The other . /// true if the specified is equal /// to the current ; otherwise, false. public bool Equals(OrientedRectangle other) { return Center.Equals(other.Center) && Radii.Equals(other.Radii) && Orientation.Equals(other.Orientation); } /// /// Determines whether two instances of are equal. /// /// The to compare to. /// true if the specified is equal /// to the current ; otherwise, false. public override bool Equals(object obj) { return obj is OrientedRectangle other && Equals(other); } /// /// Returns a hash code for this object instance. /// /// public override int GetHashCode() { return HashCode.Combine(Center, Radii, Orientation); } /// /// Performs an implicit conversion from a to . /// /// The rectangle to convert. /// The resulting . public static explicit operator OrientedRectangle(RectangleF rectangle) { var radii = new SizeF(rectangle.Width * 0.5f, rectangle.Height * 0.5f); var centre = new Vector2(rectangle.X + radii.Width, rectangle.Y + radii.Height); return new OrientedRectangle(centre, radii, Matrix3x2.Identity); } public static explicit operator RectangleF(OrientedRectangle orientedRectangle) { var topLeft = -orientedRectangle.Radii; var rectangle = new RectangleF(topLeft, orientedRectangle.Radii * 2); var orientation = orientedRectangle.Orientation * Matrix3x2.CreateTranslation(orientedRectangle.Center); return RectangleF.Transform(rectangle, ref orientation); } /// /// See: /// https://www.flipcode.com/archives/2D_OBB_Intersection.shtml /// https://dyn4j.org/2010/01/sat /// /// /// /// /// public static (bool Intersects, Vector2 MinimumTranslationVector) Intersects( OrientedRectangle rectangle, OrientedRectangle other) { var corners = rectangle.Points; var otherCorners = other.Points; var allAxis = new[] { corners[1] - corners[0], corners[3] - corners[0], otherCorners[1] - otherCorners[0], otherCorners[3] - otherCorners[0], }; var normalizedAxis = new[] { allAxis[0], allAxis[1], allAxis[2], allAxis[3] }; var overlap = 0f; var minimumTranslationVector = Vector2.Zero; // Make the length of each axis 1/edge length, so we know any // dot product must be less than 1 to fall within the edge. for (var a = 0; a < normalizedAxis.Length; a++) { normalizedAxis[a] /= normalizedAxis[a].LengthSquared(); } for (var a = 0; a < normalizedAxis.Length; a++) { var axisProjectedOnto = normalizedAxis[a]; var originalAxis = allAxis[a]; var p1 = Project(corners, axisProjectedOnto); var p2 = Project(otherCorners, axisProjectedOnto); if (!IsOverlapping(p1, p2)) { // There was no intersection along this dimension; // the boxes cannot possibly overlap. return (false, Vector2.Zero); } var o = GetOverlap(p1, p2); if (o < overlap || overlap == 0f) { overlap = o; minimumTranslationVector = originalAxis * overlap; if (p1.Min > p2.Min) { minimumTranslationVector = -minimumTranslationVector; } } } // There was no dimension along which there is no intersection. // Therefore, the boxes overlap. return (true, minimumTranslationVector); (float Min, float Max) Project(IReadOnlyList vertices, Vector2 axis) { var t = vertices[0].Dot(axis); // Find the extent of box 2 on axis a var min = t; var max = t; for (var c = 1; c < 4; c++) { t = vertices[c].Dot(axis); if (t < min) { min = t; } else if (t > max) { max = t; } } return (min, max); } bool IsOverlapping((float Min, float Max) p1, (float Min, float Max) p2) { return p1.Min <= p2.Max && p1.Max >= p2.Min; } float GetOverlap((float Min, float Max) p1, (float Min, float Max) p2) { return Math.Min(p1.Max, p2.Max) - Math.Max(p1.Min, p2.Min); } } /// /// Returns a that represents this . /// /// /// A that represents this . /// public override string ToString() { return $"Centre: {Center}, Radii: {Radii}, Orientation: {Orientation}"; } internal string DebugDisplayString => ToString(); } }