2
0

EPATests.cpp 7.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. // Jolt Physics Library (https://github.com/jrouwe/JoltPhysics)
  2. // SPDX-FileCopyrightText: 2021 Jorrit Rouwe
  3. // SPDX-License-Identifier: MIT
  4. #include "UnitTestFramework.h"
  5. #include <Jolt/Geometry/ConvexSupport.h>
  6. #include <Jolt/Geometry/EPAPenetrationDepth.h>
  7. #include <Jolt/Geometry/AABox.h>
  8. #include <Jolt/Geometry/Sphere.h>
  9. #include <random>
  10. // Enable to trace accuracy of EPA algorithm
  11. #define EPA_TESTS_TRACE(...)
  12. //#define EPA_TESTS_TRACE(...) printf(__VA_ARGS__)
  13. TEST_SUITE("EPATests")
  14. {
  15. /// Helper function to return the angle between two vectors in degrees
  16. static float AngleBetweenVectors(Vec3Arg inV1, Vec3Arg inV2)
  17. {
  18. float dot = inV1.Dot(inV2);
  19. float len = inV1.Length() * inV2.Length();
  20. return RadiansToDegrees(ACos(dot / len));
  21. }
  22. /// Test box versus sphere and compare analytical solution with that of the EPA algorithm
  23. /// @return If a collision was detected
  24. static bool CollideBoxSphere(Mat44Arg inMatrix, const AABox &inBox, const Sphere &inSphere)
  25. {
  26. TransformedConvexObject<AABox> transformed_box(inMatrix, inBox);
  27. TransformedConvexObject<Sphere> transformed_sphere(inMatrix, inSphere);
  28. // Use EPA algorithm. Don't use convex radius to avoid EPA being skipped because the inner hulls are not touching.
  29. EPAPenetrationDepth epa;
  30. Vec3 v1 = Vec3::sAxisX(), pa1, pb1;
  31. bool intersect1 = epa.GetPenetrationDepth(transformed_box, transformed_box, 0.0f, transformed_sphere, transformed_sphere, 0.0f, 1.0e-2f, FLT_EPSILON, v1, pa1, pb1);
  32. // Analytical solution
  33. Vec3 pa2 = inBox.GetClosestPoint(inSphere.GetCenter());
  34. Vec3 v2 = inSphere.GetCenter() - pa2;
  35. bool intersect2 = v2.LengthSq() <= Square(inSphere.GetRadius());
  36. CHECK(intersect1 == intersect2);
  37. if (intersect1 && intersect2)
  38. {
  39. // Analytical solution of contact on B
  40. Vec3 pb2 = inSphere.GetCenter() - inSphere.GetRadius() * v2.NormalizedOr(Vec3::sZero());
  41. // Transform analytical solution
  42. v2 = inMatrix.Multiply3x3(v2);
  43. pa2 = inMatrix * pa2;
  44. pb2 = inMatrix * pb2;
  45. // Check angle between v1 and v2
  46. float angle = AngleBetweenVectors(v1, v2);
  47. CHECK(angle < 0.1f);
  48. EPA_TESTS_TRACE("Angle = %.9g\n", (double)angle);
  49. // Check delta between contact on A
  50. Vec3 dpa = pa2 - pa1;
  51. CHECK(dpa.Length() < 8.0e-4f);
  52. EPA_TESTS_TRACE("Delta A = %.9g\n", (double)dpa.Length());
  53. // Check delta between contact on B
  54. Vec3 dpb = pb2 - pb1;
  55. CHECK(dpb.Length() < 8.0e-4f);
  56. EPA_TESTS_TRACE("Delta B = %.9g\n", (double)dpb.Length());
  57. }
  58. return intersect1;
  59. }
  60. /// Test multiple boxes against spheres and transform both with inMatrix
  61. static void CollideBoxesWithSpheres(Mat44Arg inMatrix)
  62. {
  63. {
  64. // Sphere just missing face of box
  65. AABox box(Vec3(-2, -3, -4), Vec3(2, 3, 4));
  66. Sphere sphere(Vec3(4, 0, 0), 1.99f);
  67. CHECK(!CollideBoxSphere(inMatrix, box, sphere));
  68. }
  69. {
  70. // Sphere just touching face of box
  71. AABox box(Vec3(-2, -3, -4), Vec3(2, 3, 4));
  72. Sphere sphere(Vec3(4, 0, 0), 2.01f);
  73. CHECK(CollideBoxSphere(inMatrix, box, sphere));
  74. }
  75. {
  76. // Sphere deeply penetrating box on face
  77. AABox box(Vec3(-2, -3, -4), Vec3(2, 3, 4));
  78. Sphere sphere(Vec3(3, 0, 0), 2);
  79. CHECK(CollideBoxSphere(inMatrix, box, sphere));
  80. }
  81. {
  82. // Sphere just missing box on edge
  83. AABox box(Vec3(1, 1, -2), Vec3(2, 2, 2));
  84. Sphere sphere(Vec3(4, 4, 0), sqrt(8.0f) - 0.01f);
  85. CHECK(!CollideBoxSphere(inMatrix, box, sphere));
  86. }
  87. {
  88. // Sphere just penetrating box on edge
  89. AABox box(Vec3(1, 1, -2), Vec3(2, 2, 2));
  90. Sphere sphere(Vec3(4, 4, 0), sqrt(8.0f) + 0.01f);
  91. CHECK(CollideBoxSphere(inMatrix, box, sphere));
  92. }
  93. {
  94. // Sphere just missing box on vertex
  95. AABox box(Vec3(1, 1, 1), Vec3(2, 2, 2));
  96. Sphere sphere(Vec3(4, 4, 4), sqrt(12.0f) - 0.01f);
  97. CHECK(!CollideBoxSphere(inMatrix, box, sphere));
  98. }
  99. {
  100. // Sphere just penetrating box on vertex
  101. AABox box(Vec3(1, 1, 1), Vec3(2, 2, 2));
  102. Sphere sphere(Vec3(4, 4, 4), sqrt(12.0f) + 0.01f);
  103. CHECK(CollideBoxSphere(inMatrix, box, sphere));
  104. }
  105. }
  106. TEST_CASE("TestEPASphereBox")
  107. {
  108. // Test identity transform
  109. CollideBoxesWithSpheres(Mat44::sIdentity());
  110. // Test some random rotations/translations
  111. UnitTestRandom random;
  112. for (int i = 0; i < 10; ++i)
  113. CollideBoxesWithSpheres(Mat44::sRotationTranslation(Quat::sRandom(random), Vec3::sRandom(random)));
  114. }
  115. TEST_CASE("TestEPASphereSphereOverlapping")
  116. {
  117. // Worst case: Two spheres exactly overlapping
  118. // In this case the Minkowski sum is a sphere which means the EPA algorithm will be building a convex hull of a full sphere and run out of triangles resulting in a pretty bad approximation
  119. Sphere sphere(Vec3(1, 2, 3), 2.0f);
  120. EPAPenetrationDepth epa;
  121. Vec3 v = Vec3::sAxisX(), pa, pb;
  122. CHECK(epa.GetPenetrationDepth(sphere, sphere, 0.0f, sphere, sphere, 0.0f, 1.0e-4f, FLT_EPSILON, v, pa, pb));
  123. float delta_a = (pa - sphere.GetCenter()).Length() - sphere.GetRadius();
  124. CHECK(abs(delta_a) < 0.07f);
  125. float delta_b = (pb - sphere.GetCenter()).Length() - sphere.GetRadius();
  126. CHECK(abs(delta_b) < 0.07f);
  127. float delta_penetration = (pa - pb).Length() - 2.0f * sphere.GetRadius();
  128. CHECK(abs(delta_penetration) < 0.14f);
  129. float angle = AngleBetweenVectors(v, pa - pb);
  130. CHECK(angle < 1.0e-3f);
  131. }
  132. TEST_CASE("TestEPASphereSphereNearOverlapping")
  133. {
  134. // Near worst case: Two spheres almost exactly overlapping
  135. // Still limited by amount of triangles in the hull but more precise
  136. Sphere sphere1(Vec3(1, 2, 3), 2.0f);
  137. Sphere sphere2(Vec3(1.1f, 2, 3), 1.8f);
  138. EPAPenetrationDepth epa;
  139. Vec3 v = Vec3::sAxisX(), pa, pb;
  140. CHECK(epa.GetPenetrationDepth(sphere1, sphere1, 0.0f, sphere2, sphere2, 0.0f, 1.0e-4f, FLT_EPSILON, v, pa, pb));
  141. float delta_a = (pa - sphere1.GetCenter()).Length() - sphere1.GetRadius();
  142. CHECK(abs(delta_a) < 0.05f);
  143. float delta_b = (pb - sphere2.GetCenter()).Length() - sphere2.GetRadius();
  144. CHECK(abs(delta_b) < 0.05f);
  145. float delta_penetration = (pa - pb).Length() - (sphere1.GetRadius() + sphere2.GetRadius() - (sphere1.GetCenter() - sphere2.GetCenter()).Length());
  146. CHECK(abs(delta_penetration) < 0.06f);
  147. float angle = AngleBetweenVectors(v, pa - pb);
  148. CHECK(angle < 1.0e-3f);
  149. }
  150. TEST_CASE("TestEPACastSphereSphereMiss")
  151. {
  152. Sphere sphere(Vec3(0, 0, 0), 1.0f);
  153. EPAPenetrationDepth epa;
  154. float lambda = 1.0f + FLT_EPSILON;
  155. const Vec3 invalid(-999, -999, -999);
  156. Vec3 pa = invalid, pb = invalid, normal = invalid;
  157. CHECK(!epa.CastShape(Mat44::sTranslation(Vec3(-10, 2.1f, 0)), Vec3(20, 0, 0), 1.0e-4f, 1.0e-4f, sphere, sphere, 0.0f, 0.0f, true, lambda, pa, pb, normal));
  158. CHECK(lambda == 1.0f + FLT_EPSILON); // Check input values didn't change
  159. CHECK(pa == invalid);
  160. CHECK(pb == invalid);
  161. CHECK(normal == invalid);
  162. }
  163. TEST_CASE("TestEPACastSphereSphereInitialOverlap")
  164. {
  165. Sphere sphere(Vec3(0, 0, 0), 1.0f);
  166. EPAPenetrationDepth epa;
  167. float lambda = 1.0f + FLT_EPSILON;
  168. const Vec3 invalid(-999, -999, -999);
  169. Vec3 pa = invalid, pb = invalid, normal = invalid;
  170. CHECK(epa.CastShape(Mat44::sTranslation(Vec3(-1, 0, 0)), Vec3(10, 0, 0), 1.0e-4f, 1.0e-4f, sphere, sphere, 0.0f, 0.0f, true, lambda, pa, pb, normal));
  171. CHECK(lambda == 0.0f);
  172. CHECK_APPROX_EQUAL(pa, Vec3::sZero(), 5.0e-3f);
  173. CHECK_APPROX_EQUAL(pb, Vec3(-1, 0, 0), 5.0e-3f);
  174. CHECK_APPROX_EQUAL(normal.NormalizedOr(Vec3::sZero()), Vec3(1, 0, 0), 1.0e-2f);
  175. }
  176. TEST_CASE("TestEPACastSphereSphereHit")
  177. {
  178. Sphere sphere(Vec3(0, 0, 0), 1.0f);
  179. EPAPenetrationDepth epa;
  180. float lambda = 1.0f + FLT_EPSILON;
  181. const Vec3 invalid(-999, -999, -999);
  182. Vec3 pa = invalid, pb = invalid, normal = invalid;
  183. CHECK(epa.CastShape(Mat44::sTranslation(Vec3(-10, 0, 0)), Vec3(20, 0, 0), 1.0e-4f, 1.0e-4f, sphere, sphere, 0.0f, 0.0f, true, lambda, pa, pb, normal));
  184. CHECK_APPROX_EQUAL(lambda, 8.0f / 20.0f);
  185. CHECK_APPROX_EQUAL(pa, Vec3(-1, 0, 0));
  186. CHECK_APPROX_EQUAL(pb, Vec3(-1, 0, 0));
  187. CHECK_APPROX_EQUAL(normal.NormalizedOr(Vec3::sZero()), Vec3(1, 0, 0));
  188. }
  189. }