BoundingVolumeIntersection.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. using System;
  2. using System.Runtime.CompilerServices;
  3. using Microsoft.Xna.Framework;
  4. namespace MonoGame.Extended.Collisions;
  5. /// <summary>
  6. /// Provides intersection tests between bounding volume types.
  7. /// </summary>
  8. public static class BoundingVolumeIntersection
  9. {
  10. #region Rectangle (AABB) Tests
  11. /// <summary>
  12. /// Tests if two axis-aligned bounding rectangles intersect.
  13. /// </summary>
  14. /// <param name="a">The first rectangle.</param>
  15. /// <param name="b">The second rectangle.</param>
  16. /// <returns><see langword="true"/> if the rectangle overlap; otherwise, <see langword="false"/>.</returns>
  17. /// <remarks>
  18. /// Tests for overlap on both X and Y axes independently.
  19. /// Exits early on first separating axis found.
  20. /// Based on section 4.2.1 (min-max representation).
  21. /// </remarks>
  22. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  23. public static bool Intersects(in BoundingRectangle a, in BoundingRectangle b)
  24. {
  25. // Exit with no intersection if separated along axis
  26. if (a.Max.X < b.Min.X || a.Min.X > b.Max.X) return false;
  27. if (a.Max.Y < b.Min.Y || a.Min.Y > b.Max.Y) return false;
  28. // Overlapping on all axes means AABBs are intersecting
  29. return true;
  30. }
  31. #endregion
  32. #region Circle Tests
  33. /// <summary>
  34. /// Tests if two circles intersect.
  35. /// </summary>
  36. /// <param name="a">The first circle.</param>
  37. /// <param name="b">The second circle.</param>
  38. /// <returns><see langword="true"/> if the circles overlap; otherwise, <see langword="false"/>.</returns>
  39. /// <remarks>
  40. /// Uses squared distance comparison to avoid expensive square root operation.
  41. /// Based on section 4.3.1
  42. /// </remarks>
  43. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  44. public static bool Intersects(in BoundingCircle a, in BoundingCircle b)
  45. {
  46. // Calculate squared distance between centers
  47. Vector2 d = a.Center - b.Center;
  48. float distSquared = d.X * d.X + d.Y * d.Y;
  49. // Sphere intersect if squared distance is less than squared sum of radii
  50. float radiusSum = a.Radius + b.Radius;
  51. return distSquared <= radiusSum * radiusSum;
  52. }
  53. #endregion
  54. #region Circle-Rectangle Tests
  55. /// <summary>
  56. /// Tests if a circle intersects an axis-aligned bounding rectangle.
  57. /// </summary>
  58. /// <param name="circle">The circle to test.</param>
  59. /// <param name="rect">The rectangle to test.</param>
  60. /// <returns><see langword="true"/> if the circle and rectangle overlap; otherwise, <see langword="false"/>.</returns>
  61. /// <remarks>
  62. /// Finds closest point on rectangle to circle center, then performs distance test.
  63. /// </remarks>
  64. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  65. public static bool Intersects(in BoundingCircle circle, in BoundingRectangle rect)
  66. {
  67. // Find the closest point on the rectangle to the circle center
  68. float closestX = Math.Clamp(circle.Center.X, rect.Min.X, rect.Max.X);
  69. float closestY = Math.Clamp(circle.Center.Y, rect.Min.Y, rect.Max.Y);
  70. // Calculate squared distance from the circle center to closest point
  71. float dx = circle.Center.X - closestX;
  72. float dy = circle.Center.Y - closestY;
  73. float distSquared = dx * dx + dy * dy;
  74. // Circle intersects if distance to closest point is less than radius
  75. return distSquared <= circle.Radius * circle.Radius;
  76. }
  77. /// <summary>
  78. /// Tests if a rectangle intersects a circle.
  79. /// </summary>
  80. /// <param name="rect">The rectangle to test.</param>
  81. /// <param name="circle">The circle to test.</param>
  82. /// <returns><see langword="true"/> if the rectangle and circle overlap; otherwise, <see langword="false"/>.</returns>
  83. /// <remarks>
  84. /// Finds closest point on rectangle to circle center, then performs distance test.
  85. /// </remarks>
  86. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  87. public static bool Intersects(in BoundingRectangle rect, in BoundingCircle circle)
  88. {
  89. return Intersects(circle, rect);
  90. }
  91. #endregion
  92. #region Oriented Bounding Rectangle (OBB) Tests
  93. /// <summary>
  94. /// Tests if two oriented bounding rectangles intersect using Separating Axis Theorem.
  95. /// </summary>
  96. /// <param name="a">The first oriented bounding rectangle.</param>
  97. /// <param name="b">The second oriented bounding rectangle.</param>
  98. /// <returns><see langword="true"/> if the rectangles overlap; otherwise, <see langword="false"/>.</returns>
  99. /// <remarks>
  100. /// Uses Separating Axis Theorem (SAT). <br/>
  101. /// Tests 4 potential separating axes: the 2 local exes from each OBB. <br/>
  102. /// If volumes fail to overlap on any axis, they are not intersecting.
  103. /// </remarks>
  104. public static bool Intersects(in OrientedBoundingRectangle a, in OrientedBoundingRectangle b)
  105. {
  106. // Compute rotation matrix expressing b in a's coordinate frame.
  107. float r00 = Vector2.Dot(a.AxisX, b.AxisX);
  108. float r01 = Vector2.Dot(a.AxisX, b.AxisY);
  109. float r10 = Vector2.Dot(a.AxisY, b.AxisX);
  110. float r11 = Vector2.Dot(a.AxisY, b.AxisY);
  111. // Compute translation vector from a to b in a's coordinate frame.
  112. Vector2 t = b.Center - a.Center;
  113. float tx = Vector2.Dot(t, a.AxisX);
  114. float ty = Vector2.Dot(t, a.AxisY);
  115. // Compute common subexpressions
  116. // This handles near-parallel edges that produce near-zero cross products
  117. float ar00 = MathF.Abs(r00) + MathExtended.Epsilon;
  118. float ar01 = MathF.Abs(r01) + MathExtended.Epsilon;
  119. float ar10 = MathF.Abs(r10) + MathExtended.Epsilon;
  120. float ar11 = MathF.Abs(r11) + MathExtended.Epsilon;
  121. // Test separating axis L = A.AxisX
  122. float ra = a.HalfExtents.X;
  123. float rb = b.HalfExtents.X * ar00 + b.HalfExtents.Y * ar01;
  124. if (MathF.Abs(tx) > ra + rb)
  125. {
  126. return false;
  127. }
  128. // Test separating axis L = A.AxisY
  129. ra = a.HalfExtents.Y;
  130. rb = b.HalfExtents.X * ar10 + b.HalfExtents.Y * ar11;
  131. if (MathF.Abs(ty) > ra + rb)
  132. {
  133. return false;
  134. }
  135. // Test separating axis L = B.AxisX
  136. ra = a.HalfExtents.X * ar00 + a.HalfExtents.Y * ar10;
  137. rb = b.HalfExtents.X;
  138. if (MathF.Abs(tx * r00 + ty * r10) > ra + rb)
  139. {
  140. return false;
  141. }
  142. // Test separating axis L = B.AxisY
  143. ra = a.HalfExtents.X * ar01 + a.HalfExtents.Y * ar11;
  144. rb = b.HalfExtents.Y;
  145. if (MathF.Abs(tx * r01 + ty * r11) > ra + rb)
  146. {
  147. return false;
  148. }
  149. // No separating axis found, must be intersecting
  150. return true;
  151. }
  152. /// <summary>
  153. /// Tests if an oriented bounding rectangle intersects an axis-aligned rectangle.
  154. /// </summary>
  155. /// <param name="obb">The oriented bounding rectangle.</param>
  156. /// <param name="aabb">The axis-aligned rectangle.</param>
  157. /// <returns><see langword="true"/> if the rectangle overlap; otherwise, <see langword="false"/>.</returns>
  158. public static bool Intersects(in OrientedBoundingRectangle obb, in BoundingRectangle aabb)
  159. {
  160. OrientedBoundingRectangle aabbAsObb = OrientedBoundingRectangle.FromAxisAligned(aabb.Center, aabb.HalfExtents);
  161. return Intersects(obb, aabbAsObb);
  162. }
  163. /// <summary>
  164. /// Tests if an axis-aligned rectangle intersects an oriented bounding rectangle.
  165. /// </summary>
  166. /// <param name="aabb">The axis-aligned rectangle.</param>
  167. /// <param name="obb">The oriented bounding rectangle.</param>
  168. /// <returns><see langword="true"/> if the rectangles overlap; otherwise, <see langword="false"/>.</returns>
  169. public static bool Intersects(in BoundingRectangle aabb, in OrientedBoundingRectangle obb)
  170. {
  171. return Intersects(obb, aabb);
  172. }
  173. /// <summary>
  174. /// Tests if a circle intersects an oriented bounding rectangle.
  175. /// </summary>
  176. /// <param name="circle">The circle to test.</param>
  177. /// <param name="obb">The oriented bounding rectangle to test.</param>
  178. /// <returns><see langword="true"/> if the circle and rectangle overlap; otherwise, <see langword="false"/>.</returns>
  179. public static bool Intersects(in BoundingCircle circle, in OrientedBoundingRectangle obb)
  180. {
  181. // Find closest point on OBB to circle center
  182. Vector2 closest = GeometricPrimitives.ClosestPointOnOBB(obb, circle.Center);
  183. // Test if distance from circle center to closest point is less than radus
  184. float distSquared = GeometricPrimitives.DistanceSquared(circle.Center, closest);
  185. return distSquared <= circle.Radius * circle.Radius;
  186. }
  187. /// <summary>
  188. /// Tests if an oriented bounding rectangle intersects a circle.
  189. /// </summary>
  190. /// <param name="obb">The oriented bounding rectangle.</param>
  191. /// <param name="circle">The circle to test.</param>
  192. /// <returns><see langword="true"/> if the rectangle and circle overlap; otherwise, <see langword="false"/>.</returns>
  193. [MethodImpl(MethodImplOptions.AggressiveInlining)]
  194. public static bool Intersects(in OrientedBoundingRectangle obb, in BoundingCircle circle)
  195. {
  196. return Intersects(circle, obb);
  197. }
  198. #endregion
  199. #region Point Containment Tests
  200. /// <summary>
  201. /// Tests if a point is contained within an axis-aligned rectangle.
  202. /// </summary>
  203. /// <param name="rect">The rectangle to test.</param>
  204. /// <param name="point">The point to test.</param>
  205. /// <returns><see langword="true"/> if the point is inside or on the boundary, otherwise, <see langword="false"/>.</returns>
  206. public static bool Contains(in BoundingRectangle rect, Vector2 point)
  207. {
  208. return point.X >= rect.Min.X && point.X <= rect.Max.X &&
  209. point.Y >= rect.Min.Y && point.Y <= rect.Max.Y;
  210. }
  211. /// <summary>
  212. /// Tests if a point is contained within a circle.
  213. /// </summary>
  214. /// <param name="circle">The circle to test.</param>
  215. /// <param name="point">The point to test.</param>
  216. /// <returns><see langword="true"/> if the point is inside or on the boundary; otherwise, <see langword="false"/>.</returns>
  217. /// <remarks>
  218. /// uses squared distance to avoid square root operation.
  219. /// </remarks>
  220. public static bool Contains(in BoundingCircle circle, Vector2 point)
  221. {
  222. Vector2 d = point - circle.Center;
  223. float distSquared = d.X * d.X + d.Y * d.Y;
  224. return distSquared <= circle.Radius * circle.Radius;
  225. }
  226. /// <summary>
  227. /// Tests if a point is contained within an oriented bounding rectangle.
  228. /// </summary>
  229. /// <param name="obb">The oriented bounding rectangle.</param>
  230. /// <param name="point">The point to test.</param>
  231. /// <returns><see langword="true"/> if the point is inside or on the boundary; otherwise, <see langword="false"/>.</returns>
  232. public static bool Contains(in OrientedBoundingRectangle obb, Vector2 point)
  233. {
  234. // Transform point into OBB's local coordinate system
  235. Vector2 d = point - obb.Center;
  236. float distX = Vector2.Dot(d, obb.AxisX);
  237. float distY = Vector2.Dot(d, obb.AxisY);
  238. // Test against half-extents
  239. return MathF.Abs(distX) <= obb.HalfExtents.X + MathExtended.Epsilon
  240. && MathF.Abs(distY) <= obb.HalfExtents.Y + MathExtended.Epsilon;
  241. }
  242. #endregion
  243. }