CollideShapeTests.cpp 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  1. // SPDX-FileCopyrightText: 2021 Jorrit Rouwe
  2. // SPDX-License-Identifier: MIT
  3. #include "UnitTestFramework.h"
  4. #include "PhysicsTestContext.h"
  5. #include <Physics/Collision/Shape/SphereShape.h>
  6. #include <Physics/Collision/Shape/ScaledShape.h>
  7. #include <Physics/Collision/Shape/BoxShape.h>
  8. #include <Physics/Collision/Shape/RotatedTranslatedShape.h>
  9. #include <Physics/Collision/Shape/CapsuleShape.h>
  10. #include <Physics/Collision/Shape/ConvexHullShape.h>
  11. #include <Physics/Collision/CollideShape.h>
  12. #include <Physics/Collision/CollisionCollectorImpl.h>
  13. #include <Physics/Collision/CollisionDispatch.h>
  14. #include <Geometry/EPAPenetrationDepth.h>
  15. #include "Layers.h"
  16. TEST_SUITE("CollideShapeTests")
  17. {
  18. // Compares CollideShapeResult for two spheres with given positions and radii
  19. static void sCompareCollideShapeResultSphere(Vec3Arg inPosition1, float inRadius1, Vec3Arg inPosition2, float inRadius2, const CollideShapeResult &inResult)
  20. {
  21. // Test if spheres overlap
  22. Vec3 delta = inPosition2 - inPosition1;
  23. float len = delta.Length();
  24. CHECK(len > 0.0f);
  25. CHECK(len <= inRadius1 + inRadius2);
  26. // Calculate points on surface + vector that will push 2 out of collision
  27. Vec3 expected_point1 = inPosition1 + delta * (inRadius1 / len);
  28. Vec3 expected_point2 = inPosition2 - delta * (inRadius2 / len);
  29. Vec3 expected_penetration_axis = delta / len;
  30. // Get actual results
  31. Vec3 penetration_axis = inResult.mPenetrationAxis.Normalized();
  32. // Compare
  33. CHECK_APPROX_EQUAL(expected_point1, inResult.mContactPointOn1);
  34. CHECK_APPROX_EQUAL(expected_point2, inResult.mContactPointOn2);
  35. CHECK_APPROX_EQUAL(expected_penetration_axis, penetration_axis);
  36. }
  37. // Test CollideShape function for spheres
  38. TEST_CASE("TestCollideShapeSphere")
  39. {
  40. // Locations of test sphere
  41. static const Vec3 cPosition1A(10.0f, 11.0f, 12.0f);
  42. static const Vec3 cPosition1B(10.0f, 21.0f, 12.0f);
  43. static const float cRadius1 = 2.0f;
  44. // Locations of sphere in the physics system
  45. static const Vec3 cPosition2A(13.0f, 11.0f, 12.0f);
  46. static const Vec3 cPosition2B(13.0f, 22.0f, 12.0f);
  47. static const float cRadius2 = 1.5f;
  48. // Create sphere to test with (shape 1)
  49. Ref<Shape> shape1 = new SphereShape(cRadius1);
  50. Mat44 shape1_com = Mat44::sTranslation(shape1->GetCenterOfMass());
  51. Mat44 shape1_transform = Mat44::sTranslation(cPosition1A) * Mat44::sRotationX(0.1f * JPH_PI) * shape1_com;
  52. // Create sphere to collide against (shape 2)
  53. PhysicsTestContext c;
  54. Body &body2 = c.CreateSphere(cPosition2A, cRadius2, EMotionType::Static, EMotionQuality::Discrete, Layers::NON_MOVING);
  55. // Filters
  56. SpecifiedBroadPhaseLayerFilter broadphase_moving_filter(BroadPhaseLayers::MOVING);
  57. SpecifiedBroadPhaseLayerFilter broadphase_non_moving_filter(BroadPhaseLayers::NON_MOVING);
  58. SpecifiedObjectLayerFilter object_moving_filter(Layers::MOVING);
  59. SpecifiedObjectLayerFilter object_non_moving_filter(Layers::NON_MOVING);
  60. // Collector that fails the test
  61. class FailCollideShapeCollector : public CollideShapeCollector
  62. {
  63. public:
  64. virtual void AddHit(const CollideShapeResult &inResult) override
  65. {
  66. FAIL("Callback should not be called");
  67. }
  68. };
  69. FailCollideShapeCollector fail_collector;
  70. // Set settings
  71. CollideShapeSettings settings;
  72. settings.mActiveEdgeMode = EActiveEdgeMode::CollideWithAll;
  73. settings.mBackFaceMode = EBackFaceMode::CollideWithBackFaces;
  74. // Test against wrong layer
  75. c.GetSystem()->GetNarrowPhaseQuery().CollideShape(shape1, Vec3::sReplicate(1.0f), shape1_transform, settings, fail_collector, broadphase_moving_filter, object_moving_filter);
  76. // Collector that tests that collision happens at position A
  77. class PositionACollideShapeCollector : public CollideShapeCollector
  78. {
  79. public:
  80. PositionACollideShapeCollector(const Body &inBody2) :
  81. mBody2(inBody2)
  82. {
  83. }
  84. virtual void AddHit(const CollideShapeResult &inResult) override
  85. {
  86. CHECK(mBody2.GetID() == GetContext()->mBodyID);
  87. sCompareCollideShapeResultSphere(cPosition1A, cRadius1, cPosition2A, cRadius2, inResult);
  88. mWasHit = true;
  89. }
  90. bool mWasHit = false;
  91. private:
  92. const Body & mBody2;
  93. };
  94. PositionACollideShapeCollector position_a_collector(body2);
  95. // Test collision against correct layer
  96. CHECK(!position_a_collector.mWasHit);
  97. c.GetSystem()->GetNarrowPhaseQuery().CollideShape(shape1, Vec3::sReplicate(1.0f), shape1_transform, settings, position_a_collector, broadphase_non_moving_filter, object_non_moving_filter);
  98. CHECK(position_a_collector.mWasHit);
  99. // Now move body to position B
  100. c.GetSystem()->GetBodyInterface().SetPositionAndRotation(body2.GetID(), cPosition2B, Quat::sRotation(Vec3::sAxisY(), 0.2f * JPH_PI), EActivation::DontActivate);
  101. // Test that original position doesn't collide anymore
  102. c.GetSystem()->GetNarrowPhaseQuery().CollideShape(shape1, Vec3::sReplicate(1.0f), shape1_transform, settings, fail_collector, broadphase_non_moving_filter, object_non_moving_filter);
  103. // Move test shape to position B
  104. shape1_transform = Mat44::sTranslation(cPosition1B) * Mat44::sRotationZ(0.3f * JPH_PI) * shape1_com;
  105. // Test against wrong layer
  106. c.GetSystem()->GetNarrowPhaseQuery().CollideShape(shape1, Vec3::sReplicate(1.0f), shape1_transform, settings, fail_collector, broadphase_moving_filter, object_moving_filter);
  107. // Callback that tests that collision happens at position B
  108. class PositionBCollideShapeCollector : public CollideShapeCollector
  109. {
  110. public:
  111. PositionBCollideShapeCollector(const Body &inBody2) :
  112. mBody2(inBody2)
  113. {
  114. }
  115. virtual void Reset() override
  116. {
  117. CollideShapeCollector::Reset();
  118. mWasHit = false;
  119. }
  120. virtual void AddHit(const CollideShapeResult &inResult) override
  121. {
  122. CHECK(mBody2.GetID() == GetContext()->mBodyID);
  123. sCompareCollideShapeResultSphere(cPosition1B, cRadius1, cPosition2B, cRadius2, inResult);
  124. mWasHit = true;
  125. }
  126. bool mWasHit = false;
  127. private:
  128. const Body & mBody2;
  129. };
  130. PositionBCollideShapeCollector position_b_collector(body2);
  131. // Test collision
  132. CHECK(!position_b_collector.mWasHit);
  133. c.GetSystem()->GetNarrowPhaseQuery().CollideShape(shape1, Vec3::sReplicate(1.0f), shape1_transform, settings, position_b_collector, broadphase_non_moving_filter, object_non_moving_filter);
  134. CHECK(position_b_collector.mWasHit);
  135. // Update the physics system (optimizes the broadphase)
  136. c.Simulate(c.GetDeltaTime());
  137. // Test against wrong layer
  138. c.GetSystem()->GetNarrowPhaseQuery().CollideShape(shape1, Vec3::sReplicate(1.0f), shape1_transform, settings, fail_collector, broadphase_moving_filter, object_moving_filter);
  139. // Test collision again
  140. position_b_collector.Reset();
  141. CHECK(!position_b_collector.mWasHit);
  142. c.GetSystem()->GetNarrowPhaseQuery().CollideShape(shape1, Vec3::sReplicate(1.0f), shape1_transform, settings, position_b_collector, broadphase_non_moving_filter, object_non_moving_filter);
  143. CHECK(position_b_collector.mWasHit);
  144. }
  145. // Test CollideShape function for a (scaled) sphere vs box
  146. TEST_CASE("TestCollideShapeSphereVsBox")
  147. {
  148. PhysicsTestContext c;
  149. // Create box to collide against (shape 2)
  150. // The box is scaled up by a factor 10 in the X axis and then rotated so that the X axis is up
  151. BoxShapeSettings box(Vec3::sReplicate(1.0f));
  152. box.SetEmbedded();
  153. ScaledShapeSettings scaled_box(&box, Vec3(10, 1, 1));
  154. scaled_box.SetEmbedded();
  155. Body &body2 = c.CreateBody(&scaled_box, Vec3(0, 1, 0), Quat::sRotation(Vec3::sAxisZ(), 0.5f * JPH_PI), EMotionType::Static, EMotionQuality::Discrete, Layers::NON_MOVING, EActivation::DontActivate);
  156. // Set settings
  157. CollideShapeSettings settings;
  158. settings.mActiveEdgeMode = EActiveEdgeMode::CollideWithAll;
  159. settings.mBackFaceMode = EBackFaceMode::CollideWithBackFaces;
  160. {
  161. // Create sphere
  162. Ref<Shape> normal_sphere = new SphereShape(1.0f);
  163. // Collect hit with normal sphere
  164. AllHitCollisionCollector<CollideShapeCollector> collector;
  165. c.GetSystem()->GetNarrowPhaseQuery().CollideShape(normal_sphere, Vec3::sReplicate(1.0f), Mat44::sTranslation(Vec3(0, 11, 0)), settings, collector);
  166. CHECK(collector.mHits.size() == 1);
  167. const CollideShapeResult &result = collector.mHits.front();
  168. CHECK(result.mBodyID2 == body2.GetID());
  169. CHECK_APPROX_EQUAL(result.mContactPointOn1, Vec3(0, 10, 0), 1.0e-5f);
  170. CHECK_APPROX_EQUAL(result.mContactPointOn2, Vec3(0, 11, 0), 1.0e-5f);
  171. Vec3 pen_axis = result.mPenetrationAxis.Normalized();
  172. CHECK_APPROX_EQUAL(pen_axis, Vec3(0, -1, 0), 1.0e-5f);
  173. CHECK_APPROX_EQUAL(result.mPenetrationDepth, 1.0f, 1.0e-5f);
  174. }
  175. {
  176. // This repeats the same test as above but uses scaling at all levels
  177. Ref<Shape> scaled_sphere = new ScaledShape(new SphereShape(0.1f), Vec3::sReplicate(5.0f));
  178. // Collect hit with scaled sphere
  179. AllHitCollisionCollector<CollideShapeCollector> collector;
  180. c.GetSystem()->GetNarrowPhaseQuery().CollideShape(scaled_sphere, Vec3::sReplicate(2.0f), Mat44::sTranslation(Vec3(0, 11, 0)), settings, collector);
  181. CHECK(collector.mHits.size() == 1);
  182. const CollideShapeResult &result = collector.mHits.front();
  183. CHECK(result.mBodyID2 == body2.GetID());
  184. CHECK_APPROX_EQUAL(result.mContactPointOn1, Vec3(0, 10, 0), 1.0e-5f);
  185. CHECK_APPROX_EQUAL(result.mContactPointOn2, Vec3(0, 11, 0), 1.0e-5f);
  186. Vec3 pen_axis = result.mPenetrationAxis.Normalized();
  187. CHECK_APPROX_EQUAL(pen_axis, Vec3(0, -1, 0), 1.0e-5f);
  188. CHECK_APPROX_EQUAL(result.mPenetrationDepth, 1.0f, 1.0e-5f);
  189. }
  190. }
  191. // Test colliding a very long capsule vs a box that is intersecting with the linesegment inside the capsule
  192. // This particular config reported the wrong penetration due to accuarcy problems before
  193. TEST_CASE("TestCollideShapeLongCapsuleVsEmbeddedBox")
  194. {
  195. // Create box
  196. Vec3 box_min(-1.0f, -2.0f, 0.5f);
  197. Vec3 box_max(2.0f, -0.5f, 3.0f);
  198. Ref<RotatedTranslatedShapeSettings> box_settings = new RotatedTranslatedShapeSettings(0.5f * (box_min + box_max), Quat::sIdentity(), new BoxShapeSettings(0.5f * (box_max - box_min)));
  199. Ref<Shape> box_shape = box_settings->Create().Get();
  200. Mat44 box_transform(Vec4(0.516170502f, -0.803887904f, -0.295520246f, 0.0f), Vec4(0.815010250f, 0.354940295f, 0.458012700f, 0.0f), Vec4(-0.263298869f, -0.477264702f, 0.838386655f, 0.0f), Vec4(-10.2214508f, -18.6808319f, 40.7468987f, 1.0f));
  201. // Create capsule
  202. float capsule_half_height = 75.0f;
  203. float capsule_radius = 1.5f;
  204. Ref<RotatedTranslatedShapeSettings> capsule_settings = new RotatedTranslatedShapeSettings(Vec3(0, 0, 75), Quat(0.499999970f, -0.499999970f, -0.499999970f, 0.499999970f), new CapsuleShapeSettings(capsule_half_height, capsule_radius));
  205. Ref<Shape> capsule_shape = capsule_settings->Create().Get();
  206. Mat44 capsule_transform = Mat44::sTranslation(Vec3(-9.68538570f, -18.0328083f, 41.3212280f));
  207. // Collision settings
  208. CollideShapeSettings settings;
  209. settings.mActiveEdgeMode = EActiveEdgeMode::CollideWithAll;
  210. settings.mBackFaceMode = EBackFaceMode::CollideWithBackFaces;
  211. settings.mCollectFacesMode = ECollectFacesMode::NoFaces;
  212. // Collide the two shapes
  213. AllHitCollisionCollector<CollideShapeCollector> collector;
  214. CollisionDispatch::sCollideShapeVsShape(capsule_shape, box_shape, Vec3::sReplicate(1.0f), Vec3::sReplicate(1.0f), capsule_transform, box_transform, SubShapeIDCreator(), SubShapeIDCreator(), settings, collector);
  215. // Check that there was a hit
  216. CHECK(collector.mHits.size() == 1);
  217. const CollideShapeResult &result = collector.mHits.front();
  218. // Now move the box 1% further than the returned penetration depth and check that it is no longer in collision
  219. Vec3 distance_to_move_box = result.mPenetrationAxis.Normalized() * result.mPenetrationDepth;
  220. collector.Reset();
  221. CHECK(!collector.HadHit());
  222. CollisionDispatch::sCollideShapeVsShape(capsule_shape, box_shape, Vec3::sReplicate(1.0f), Vec3::sReplicate(1.0f), capsule_transform, Mat44::sTranslation(1.01f * distance_to_move_box) * box_transform, SubShapeIDCreator(), SubShapeIDCreator(), settings, collector);
  223. CHECK(!collector.HadHit());
  224. // Now check that moving 1% less than the penetration distance makes the shapes still overlap
  225. CollisionDispatch::sCollideShapeVsShape(capsule_shape, box_shape, Vec3::sReplicate(1.0f), Vec3::sReplicate(1.0f), capsule_transform, Mat44::sTranslation(0.99f * distance_to_move_box) * box_transform, SubShapeIDCreator(), SubShapeIDCreator(), settings, collector);
  226. CHECK(collector.mHits.size() == 1);
  227. }
  228. // Another test case found in practice of a very large oriented box (convex hull) vs a small triangle outside the hull. This should not report a collision
  229. TEST_CASE("TestCollideShapeSmallTriangleVsLargeBox")
  230. {
  231. // Triangle vertices
  232. Vec3 v0(-81.5637589f, -126.987244f, -146.771729f);
  233. Vec3 v1(-81.8749924f, -127.270691f, -146.544403f);
  234. Vec3 v2(-81.6972275f, -127.383545f, -146.773254f);
  235. // Oriented box vertices
  236. vector<Vec3> obox_points = {
  237. Vec3(125.932892f, -374.712250f, 364.192169f),
  238. Vec3(319.492218f, -73.2614441f, 475.009613f),
  239. Vec3(-122.277550f, -152.200287f, 192.441437f),
  240. Vec3(71.2817841f, 149.250519f, 303.258881f),
  241. Vec3(-77.8921967f, -359.410797f, 678.579712f),
  242. Vec3(115.667137f, -57.9600067f, 789.397095f),
  243. Vec3(-326.102631f, -136.898834f, 506.828949f),
  244. Vec3(-132.543304f, 164.551971f, 617.646362f)
  245. };
  246. ConvexHullShapeSettings hull_settings(obox_points, 0.0f);
  247. RefConst<ConvexShape> convex_hull = static_cast<const ConvexShape *>(hull_settings.Create().Get().GetPtr());
  248. // Create triangle support function
  249. TriangleConvexSupport triangle(v0, v1, v2);
  250. // Create the convex hull support function
  251. ConvexShape::SupportBuffer buffer;
  252. const ConvexShape::Support *support = convex_hull->GetSupportFunction(ConvexShape::ESupportMode::IncludeConvexRadius, buffer, Vec3::sReplicate(1.0f));
  253. // Triangle is close enough to make GJK report indeterminate
  254. Vec3 penetration_axis = Vec3::sAxisX(), point1, point2;
  255. EPAPenetrationDepth pen_depth;
  256. EPAPenetrationDepth::EStatus status = pen_depth.GetPenetrationDepthStepGJK(*support, support->GetConvexRadius(), triangle, 0.0f, cDefaultCollisionTolerance, penetration_axis, point1, point2);
  257. CHECK(status == EPAPenetrationDepth::EStatus::Indeterminate);
  258. // But there should not be an actual collision
  259. CHECK(!pen_depth.GetPenetrationDepthStepEPA(*support, triangle, cDefaultPenetrationTolerance, penetration_axis, point1, point2));
  260. }
  261. }