CollideShapeTests.cpp 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486
  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 "PhysicsTestContext.h"
  6. #include <Jolt/Physics/Collision/Shape/SphereShape.h>
  7. #include <Jolt/Physics/Collision/Shape/ScaledShape.h>
  8. #include <Jolt/Physics/Collision/Shape/BoxShape.h>
  9. #include <Jolt/Physics/Collision/Shape/RotatedTranslatedShape.h>
  10. #include <Jolt/Physics/Collision/Shape/CapsuleShape.h>
  11. #include <Jolt/Physics/Collision/Shape/ConvexHullShape.h>
  12. #include <Jolt/Physics/Collision/Shape/TriangleShape.h>
  13. #include <Jolt/Physics/Collision/Shape/CylinderShape.h>
  14. #include <Jolt/Physics/Collision/CollideShape.h>
  15. #include <Jolt/Physics/Collision/CollisionCollectorImpl.h>
  16. #include <Jolt/Physics/Collision/CollisionDispatch.h>
  17. #include <Jolt/Physics/Collision/CollideConvexVsTriangles.h>
  18. #include <Jolt/Geometry/EPAPenetrationDepth.h>
  19. #include "Layers.h"
  20. TEST_SUITE("CollideShapeTests")
  21. {
  22. // Compares CollideShapeResult for two spheres with given positions and radii
  23. static void sCompareCollideShapeResultSphere(Vec3Arg inPosition1, float inRadius1, Vec3Arg inPosition2, float inRadius2, const CollideShapeResult &inResult)
  24. {
  25. // Test if spheres overlap
  26. Vec3 delta = inPosition2 - inPosition1;
  27. float len = delta.Length();
  28. CHECK(len > 0.0f);
  29. CHECK(len <= inRadius1 + inRadius2);
  30. // Calculate points on surface + vector that will push 2 out of collision
  31. Vec3 expected_point1 = inPosition1 + delta * (inRadius1 / len);
  32. Vec3 expected_point2 = inPosition2 - delta * (inRadius2 / len);
  33. Vec3 expected_penetration_axis = delta / len;
  34. // Get actual results
  35. Vec3 penetration_axis = inResult.mPenetrationAxis.Normalized();
  36. // Compare
  37. CHECK_APPROX_EQUAL(expected_point1, inResult.mContactPointOn1);
  38. CHECK_APPROX_EQUAL(expected_point2, inResult.mContactPointOn2);
  39. CHECK_APPROX_EQUAL(expected_penetration_axis, penetration_axis);
  40. }
  41. // Test CollideShape function for spheres
  42. TEST_CASE("TestCollideShapeSphere")
  43. {
  44. // Locations of test sphere
  45. static const RVec3 cPosition1A(10.0f, 11.0f, 12.0f);
  46. static const RVec3 cPosition1B(10.0f, 21.0f, 12.0f);
  47. static const float cRadius1 = 2.0f;
  48. // Locations of sphere in the physics system
  49. static const RVec3 cPosition2A(13.0f, 11.0f, 12.0f);
  50. static const RVec3 cPosition2B(13.0f, 22.0f, 12.0f);
  51. static const float cRadius2 = 1.5f;
  52. // Create sphere to test with (shape 1)
  53. Ref<Shape> shape1 = new SphereShape(cRadius1);
  54. Mat44 shape1_com = Mat44::sTranslation(shape1->GetCenterOfMass());
  55. RMat44 shape1_transform = RMat44::sTranslation(cPosition1A) * Mat44::sRotationX(0.1f * JPH_PI) * shape1_com;
  56. // Create sphere to collide against (shape 2)
  57. PhysicsTestContext c;
  58. Body &body2 = c.CreateSphere(cPosition2A, cRadius2, EMotionType::Static, EMotionQuality::Discrete, Layers::NON_MOVING);
  59. // Filters
  60. SpecifiedBroadPhaseLayerFilter broadphase_moving_filter(BroadPhaseLayers::MOVING);
  61. SpecifiedBroadPhaseLayerFilter broadphase_non_moving_filter(BroadPhaseLayers::NON_MOVING);
  62. SpecifiedObjectLayerFilter object_moving_filter(Layers::MOVING);
  63. SpecifiedObjectLayerFilter object_non_moving_filter(Layers::NON_MOVING);
  64. // Collector that fails the test
  65. class FailCollideShapeCollector : public CollideShapeCollector
  66. {
  67. public:
  68. virtual void AddHit(const CollideShapeResult &inResult) override
  69. {
  70. FAIL("Callback should not be called");
  71. }
  72. };
  73. FailCollideShapeCollector fail_collector;
  74. // Set settings
  75. CollideShapeSettings settings;
  76. settings.mActiveEdgeMode = EActiveEdgeMode::CollideWithAll;
  77. settings.mBackFaceMode = EBackFaceMode::CollideWithBackFaces;
  78. // Test against wrong layer
  79. c.GetSystem()->GetNarrowPhaseQuery().CollideShape(shape1, Vec3::sReplicate(1.0f), shape1_transform, settings, RVec3::sZero(), fail_collector, broadphase_moving_filter, object_moving_filter);
  80. // Collector that tests that collision happens at position A
  81. class PositionACollideShapeCollector : public CollideShapeCollector
  82. {
  83. public:
  84. PositionACollideShapeCollector(const Body &inBody2) :
  85. mBody2(inBody2)
  86. {
  87. }
  88. virtual void AddHit(const CollideShapeResult &inResult) override
  89. {
  90. CHECK(mBody2.GetID() == GetContext()->mBodyID);
  91. sCompareCollideShapeResultSphere(Vec3(cPosition1A), cRadius1, Vec3(cPosition2A), cRadius2, inResult);
  92. mWasHit = true;
  93. }
  94. bool mWasHit = false;
  95. private:
  96. const Body & mBody2;
  97. };
  98. PositionACollideShapeCollector position_a_collector(body2);
  99. // Test collision against correct layer
  100. CHECK(!position_a_collector.mWasHit);
  101. c.GetSystem()->GetNarrowPhaseQuery().CollideShape(shape1, Vec3::sReplicate(1.0f), shape1_transform, settings, RVec3::sZero(), position_a_collector, broadphase_non_moving_filter, object_non_moving_filter);
  102. CHECK(position_a_collector.mWasHit);
  103. // Now move body to position B
  104. c.GetSystem()->GetBodyInterface().SetPositionAndRotation(body2.GetID(), cPosition2B, Quat::sRotation(Vec3::sAxisY(), 0.2f * JPH_PI), EActivation::DontActivate);
  105. // Test that original position doesn't collide anymore
  106. c.GetSystem()->GetNarrowPhaseQuery().CollideShape(shape1, Vec3::sReplicate(1.0f), shape1_transform, settings, RVec3::sZero(), fail_collector, broadphase_non_moving_filter, object_non_moving_filter);
  107. // Move test shape to position B
  108. shape1_transform = RMat44::sTranslation(cPosition1B) * Mat44::sRotationZ(0.3f * JPH_PI) * shape1_com;
  109. // Test against wrong layer
  110. c.GetSystem()->GetNarrowPhaseQuery().CollideShape(shape1, Vec3::sReplicate(1.0f), shape1_transform, settings, RVec3::sZero(), fail_collector, broadphase_moving_filter, object_moving_filter);
  111. // Callback that tests that collision happens at position B
  112. class PositionBCollideShapeCollector : public CollideShapeCollector
  113. {
  114. public:
  115. PositionBCollideShapeCollector(const Body &inBody2) :
  116. mBody2(inBody2)
  117. {
  118. }
  119. virtual void Reset() override
  120. {
  121. CollideShapeCollector::Reset();
  122. mWasHit = false;
  123. }
  124. virtual void AddHit(const CollideShapeResult &inResult) override
  125. {
  126. CHECK(mBody2.GetID() == GetContext()->mBodyID);
  127. sCompareCollideShapeResultSphere(Vec3(cPosition1B), cRadius1, Vec3(cPosition2B), cRadius2, inResult);
  128. mWasHit = true;
  129. }
  130. bool mWasHit = false;
  131. private:
  132. const Body & mBody2;
  133. };
  134. PositionBCollideShapeCollector position_b_collector(body2);
  135. // Test collision
  136. CHECK(!position_b_collector.mWasHit);
  137. c.GetSystem()->GetNarrowPhaseQuery().CollideShape(shape1, Vec3::sReplicate(1.0f), shape1_transform, settings, RVec3::sZero(), position_b_collector, broadphase_non_moving_filter, object_non_moving_filter);
  138. CHECK(position_b_collector.mWasHit);
  139. // Update the physics system (optimizes the broadphase)
  140. c.Simulate(c.GetDeltaTime());
  141. // Test against wrong layer
  142. c.GetSystem()->GetNarrowPhaseQuery().CollideShape(shape1, Vec3::sReplicate(1.0f), shape1_transform, settings, RVec3::sZero(), fail_collector, broadphase_moving_filter, object_moving_filter);
  143. // Test collision again
  144. position_b_collector.Reset();
  145. CHECK(!position_b_collector.mWasHit);
  146. c.GetSystem()->GetNarrowPhaseQuery().CollideShape(shape1, Vec3::sReplicate(1.0f), shape1_transform, settings, RVec3::sZero(), position_b_collector, broadphase_non_moving_filter, object_non_moving_filter);
  147. CHECK(position_b_collector.mWasHit);
  148. }
  149. // Test CollideShape function for a (scaled) sphere vs box
  150. TEST_CASE("TestCollideShapeSphereVsBox")
  151. {
  152. PhysicsTestContext c;
  153. // Create box to collide against (shape 2)
  154. // The box is scaled up by a factor 10 in the X axis and then rotated so that the X axis is up
  155. BoxShapeSettings box(Vec3::sReplicate(1.0f));
  156. box.SetEmbedded();
  157. ScaledShapeSettings scaled_box(&box, Vec3(10, 1, 1));
  158. scaled_box.SetEmbedded();
  159. Body &body2 = c.CreateBody(&scaled_box, RVec3(0, 1, 0), Quat::sRotation(Vec3::sAxisZ(), 0.5f * JPH_PI), EMotionType::Static, EMotionQuality::Discrete, Layers::NON_MOVING, EActivation::DontActivate);
  160. // Set settings
  161. CollideShapeSettings settings;
  162. settings.mActiveEdgeMode = EActiveEdgeMode::CollideWithAll;
  163. settings.mBackFaceMode = EBackFaceMode::CollideWithBackFaces;
  164. {
  165. // Create sphere
  166. Ref<Shape> normal_sphere = new SphereShape(1.0f);
  167. // Collect hit with normal sphere
  168. AllHitCollisionCollector<CollideShapeCollector> collector;
  169. c.GetSystem()->GetNarrowPhaseQuery().CollideShape(normal_sphere, Vec3::sReplicate(1.0f), RMat44::sTranslation(RVec3(0, 11, 0)), settings, RVec3::sZero(), collector);
  170. CHECK(collector.mHits.size() == 1);
  171. const CollideShapeResult &result = collector.mHits.front();
  172. CHECK(result.mBodyID2 == body2.GetID());
  173. CHECK_APPROX_EQUAL(result.mContactPointOn1, Vec3(0, 10, 0), 1.0e-4f);
  174. CHECK_APPROX_EQUAL(result.mContactPointOn2, Vec3(0, 11, 0), 1.0e-4f);
  175. Vec3 pen_axis = result.mPenetrationAxis.Normalized();
  176. CHECK_APPROX_EQUAL(pen_axis, Vec3(0, -1, 0), 1.0e-4f);
  177. CHECK_APPROX_EQUAL(result.mPenetrationDepth, 1.0f, 1.0e-5f);
  178. }
  179. {
  180. // This repeats the same test as above but uses scaling at all levels
  181. Ref<Shape> scaled_sphere = new ScaledShape(new SphereShape(0.1f), Vec3::sReplicate(5.0f));
  182. // Collect hit with scaled sphere
  183. AllHitCollisionCollector<CollideShapeCollector> collector;
  184. c.GetSystem()->GetNarrowPhaseQuery().CollideShape(scaled_sphere, Vec3::sReplicate(2.0f), RMat44::sTranslation(RVec3(0, 11, 0)), settings, RVec3::sZero(), collector);
  185. CHECK(collector.mHits.size() == 1);
  186. const CollideShapeResult &result = collector.mHits.front();
  187. CHECK(result.mBodyID2 == body2.GetID());
  188. CHECK_APPROX_EQUAL(result.mContactPointOn1, Vec3(0, 10, 0), 1.0e-4f);
  189. CHECK_APPROX_EQUAL(result.mContactPointOn2, Vec3(0, 11, 0), 1.0e-4f);
  190. Vec3 pen_axis = result.mPenetrationAxis.Normalized();
  191. CHECK_APPROX_EQUAL(pen_axis, Vec3(0, -1, 0), 1.0e-4f);
  192. CHECK_APPROX_EQUAL(result.mPenetrationDepth, 1.0f, 1.0e-5f);
  193. }
  194. }
  195. // Test colliding a very long capsule vs a box that is intersecting with the line segment inside the capsule
  196. // This particular config reported the wrong penetration due to accuracy problems before
  197. TEST_CASE("TestCollideShapeLongCapsuleVsEmbeddedBox")
  198. {
  199. // Create box
  200. Vec3 box_min(-1.0f, -2.0f, 0.5f);
  201. Vec3 box_max(2.0f, -0.5f, 3.0f);
  202. Ref<RotatedTranslatedShapeSettings> box_settings = new RotatedTranslatedShapeSettings(0.5f * (box_min + box_max), Quat::sIdentity(), new BoxShapeSettings(0.5f * (box_max - box_min)));
  203. Ref<Shape> box_shape = box_settings->Create().Get();
  204. 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));
  205. // Create capsule
  206. float capsule_half_height = 75.0f;
  207. float capsule_radius = 1.5f;
  208. 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));
  209. Ref<Shape> capsule_shape = capsule_settings->Create().Get();
  210. Mat44 capsule_transform = Mat44::sTranslation(Vec3(-9.68538570f, -18.0328083f, 41.3212280f));
  211. // Collision settings
  212. CollideShapeSettings settings;
  213. settings.mActiveEdgeMode = EActiveEdgeMode::CollideWithAll;
  214. settings.mBackFaceMode = EBackFaceMode::CollideWithBackFaces;
  215. settings.mCollectFacesMode = ECollectFacesMode::NoFaces;
  216. // Collide the two shapes
  217. AllHitCollisionCollector<CollideShapeCollector> collector;
  218. CollisionDispatch::sCollideShapeVsShape(capsule_shape, box_shape, Vec3::sReplicate(1.0f), Vec3::sReplicate(1.0f), capsule_transform, box_transform, SubShapeIDCreator(), SubShapeIDCreator(), settings, collector);
  219. // Check that there was a hit
  220. CHECK(collector.mHits.size() == 1);
  221. const CollideShapeResult &result = collector.mHits.front();
  222. // Now move the box 1% further than the returned penetration depth and check that it is no longer in collision
  223. Vec3 distance_to_move_box = result.mPenetrationAxis.Normalized() * result.mPenetrationDepth;
  224. collector.Reset();
  225. CHECK(!collector.HadHit());
  226. 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);
  227. CHECK(!collector.HadHit());
  228. // Now check that moving 1% less than the penetration distance makes the shapes still overlap
  229. 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);
  230. CHECK(collector.mHits.size() == 1);
  231. }
  232. // 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
  233. TEST_CASE("TestCollideShapeSmallTriangleVsLargeBox")
  234. {
  235. // Triangle vertices
  236. Vec3 v0(-81.5637589f, -126.987244f, -146.771729f);
  237. Vec3 v1(-81.8749924f, -127.270691f, -146.544403f);
  238. Vec3 v2(-81.6972275f, -127.383545f, -146.773254f);
  239. // Oriented box vertices
  240. Array<Vec3> obox_points = {
  241. Vec3(125.932892f, -374.712250f, 364.192169f),
  242. Vec3(319.492218f, -73.2614441f, 475.009613f),
  243. Vec3(-122.277550f, -152.200287f, 192.441437f),
  244. Vec3(71.2817841f, 149.250519f, 303.258881f),
  245. Vec3(-77.8921967f, -359.410797f, 678.579712f),
  246. Vec3(115.667137f, -57.9600067f, 789.397095f),
  247. Vec3(-326.102631f, -136.898834f, 506.828949f),
  248. Vec3(-132.543304f, 164.551971f, 617.646362f)
  249. };
  250. ConvexHullShapeSettings hull_settings(obox_points, 0.0f);
  251. RefConst<ConvexShape> convex_hull = StaticCast<ConvexShape>(hull_settings.Create().Get());
  252. // Create triangle support function
  253. TriangleConvexSupport triangle(v0, v1, v2);
  254. // Create the convex hull support function
  255. ConvexShape::SupportBuffer buffer;
  256. const ConvexShape::Support *support = convex_hull->GetSupportFunction(ConvexShape::ESupportMode::IncludeConvexRadius, buffer, Vec3::sReplicate(1.0f));
  257. // Triangle is close enough to make GJK report indeterminate
  258. Vec3 penetration_axis = Vec3::sAxisX(), point1, point2;
  259. EPAPenetrationDepth pen_depth;
  260. EPAPenetrationDepth::EStatus status = pen_depth.GetPenetrationDepthStepGJK(*support, support->GetConvexRadius(), triangle, 0.0f, cDefaultCollisionTolerance, penetration_axis, point1, point2);
  261. CHECK(status == EPAPenetrationDepth::EStatus::Indeterminate);
  262. // But there should not be an actual collision
  263. CHECK(!pen_depth.GetPenetrationDepthStepEPA(*support, triangle, cDefaultPenetrationTolerance, penetration_axis, point1, point2));
  264. }
  265. // A test case of a triangle that's nearly parallel to a capsule and penetrating it. This one was causing numerical issues.
  266. TEST_CASE("TestCollideParallelTriangleVsCapsule")
  267. {
  268. Vec3 v1(-0.479988575f, -1.36185002f, 0.269966960f);
  269. Vec3 v2(-0.104996204f, 0.388152480f, 0.269967079f);
  270. Vec3 v3(-0.104996204f, -1.36185002f, 0.269966960f);
  271. TriangleShape triangle(v1, v2, v3);
  272. triangle.SetEmbedded();
  273. float capsule_radius = 0.37f;
  274. float capsule_half_height = 0.5f;
  275. CapsuleShape capsule(capsule_half_height, capsule_radius);
  276. capsule.SetEmbedded();
  277. CollideShapeSettings settings;
  278. AllHitCollisionCollector<CollideShapeCollector> collector;
  279. CollisionDispatch::sCollideShapeVsShape(&triangle, &capsule, Vec3::sReplicate(1.0f), Vec3::sReplicate(1.0f), Mat44::sIdentity(), Mat44::sIdentity(), SubShapeIDCreator(), SubShapeIDCreator(), settings, collector);
  280. // The capsule's center is closest to the triangle's edge v2 v3
  281. Vec3 capsule_center_to_triangle_v2_v3 = v3;
  282. capsule_center_to_triangle_v2_v3.SetY(0); // The penetration axis will be in x, z only because the triangle is parallel to the capsule axis
  283. float capsule_center_to_triangle_v2_v3_len = capsule_center_to_triangle_v2_v3.Length();
  284. Vec3 expected_penetration_axis = -capsule_center_to_triangle_v2_v3 / capsule_center_to_triangle_v2_v3_len;
  285. float expected_penetration_depth = capsule_radius - capsule_center_to_triangle_v2_v3_len;
  286. CHECK(collector.mHits.size() == 1);
  287. const CollideShapeResult &hit = collector.mHits[0];
  288. Vec3 actual_penetration_axis = hit.mPenetrationAxis.Normalized();
  289. float actual_penetration_depth = hit.mPenetrationDepth;
  290. CHECK_APPROX_EQUAL(actual_penetration_axis, expected_penetration_axis);
  291. CHECK_APPROX_EQUAL(actual_penetration_depth, expected_penetration_depth);
  292. }
  293. // A test case of a triangle that's nearly parallel to a capsule and penetrating it. This one was causing numerical issues.
  294. TEST_CASE("TestCollideParallelTriangleVsCapsule2")
  295. {
  296. Vec3 v1(-0.0904417038f, -4.72410202f, 0.307858467f);
  297. Vec3 v2(-0.0904417038f, 5.27589798f, 0.307857513f);
  298. Vec3 v3(9.90955830f, 5.27589798f, 0.307864189f);
  299. TriangleShape triangle(v1, v2, v3);
  300. triangle.SetEmbedded();
  301. float capsule_radius = 0.42f;
  302. float capsule_half_height = 0.675f;
  303. CapsuleShape capsule(capsule_half_height, capsule_radius);
  304. capsule.SetEmbedded();
  305. CollideShapeSettings settings;
  306. AllHitCollisionCollector<CollideShapeCollector> collector;
  307. CollisionDispatch::sCollideShapeVsShape(&triangle, &capsule, Vec3::sReplicate(1.0f), Vec3::sReplicate(1.0f), Mat44::sIdentity(), Mat44::sIdentity(), SubShapeIDCreator(), SubShapeIDCreator(), settings, collector);
  308. // The capsule intersects with the triangle and the closest point is in the interior of the triangle
  309. Vec3 expected_penetration_axis = Vec3(0, 0, -1); // Triangle is in the XY plane so the normal is Z
  310. float expected_penetration_depth = capsule_radius - v1.GetZ();
  311. CHECK(collector.mHits.size() == 1);
  312. const CollideShapeResult &hit = collector.mHits[0];
  313. Vec3 actual_penetration_axis = hit.mPenetrationAxis.Normalized();
  314. float actual_penetration_depth = hit.mPenetrationDepth;
  315. CHECK_APPROX_EQUAL(actual_penetration_axis, expected_penetration_axis);
  316. CHECK_APPROX_EQUAL(actual_penetration_depth, expected_penetration_depth);
  317. }
  318. // A test case of a triangle that's nearly parallel to a capsule and almost penetrating it. This one was causing numerical issues.
  319. TEST_CASE("TestCollideParallelTriangleVsCapsule3")
  320. {
  321. Vec3 v1(-0.474807739f, 17.2921791f, 0.212532043f);
  322. Vec3 v2(-0.474807739f, -2.70782185f, 0.212535858f);
  323. Vec3 v3(-0.857490540f, -2.70782185f, -0.711341858f);
  324. TriangleShape triangle(v1, v2, v3);
  325. triangle.SetEmbedded();
  326. float capsule_radius = 0.5f;
  327. float capsule_half_height = 0.649999976f;
  328. CapsuleShape capsule(capsule_half_height, capsule_radius);
  329. capsule.SetEmbedded();
  330. CollideShapeSettings settings;
  331. settings.mMaxSeparationDistance = 0.120000005f;
  332. ClosestHitCollisionCollector<CollideShapeCollector> collector;
  333. CollisionDispatch::sCollideShapeVsShape(&capsule, &triangle, Vec3::sReplicate(1.0f), Vec3::sReplicate(1.0f), Mat44::sIdentity(), Mat44::sIdentity(), SubShapeIDCreator(), SubShapeIDCreator(), settings, collector);
  334. CHECK(collector.HadHit());
  335. Vec3 expected_normal = (v2 - v1).Cross(v3 - v1).Normalized();
  336. Vec3 actual_normal = -collector.mHit.mPenetrationAxis.Normalized();
  337. CHECK_APPROX_EQUAL(actual_normal, expected_normal, 1.0e-6f);
  338. float expected_penetration_depth = capsule.GetRadius() + v1.Dot(expected_normal);
  339. CHECK_APPROX_EQUAL(collector.mHit.mPenetrationDepth, expected_penetration_depth, 1.0e-6f);
  340. }
  341. // A test case of a triangle that's nearly parallel to a cylinder and is just penetrating it. This one was causing numerical issues. See issue #1008.
  342. TEST_CASE("TestCollideParallelTriangleVsCylinder")
  343. {
  344. CylinderShape cylinder(0.85f, 0.25f, 0.02f);
  345. cylinder.SetEmbedded();
  346. Mat44 cylinder_transform = Mat44::sTranslation(Vec3(-42.8155518f, -4.32299995f, 12.1734285f));
  347. CollideShapeSettings settings;
  348. settings.mMaxSeparationDistance = 0.001f;
  349. ClosestHitCollisionCollector<CollideShapeCollector> collector;
  350. CollideConvexVsTriangles c(&cylinder, Vec3::sReplicate(1.0f), Vec3::sReplicate(1.0f), cylinder_transform, Mat44::sIdentity(), SubShapeID(), settings, collector);
  351. Vec3 v0(-42.7954292f, -0.647318780f, 12.4227943f);
  352. Vec3 v1(-29.9111290f, -0.647318780f, 12.4227943f);
  353. Vec3 v2(-42.7954292f, -4.86970234f, 12.4227943f);
  354. c.Collide(v0, v1, v2, 0, SubShapeID());
  355. // Check there was a hit
  356. CHECK(collector.HadHit());
  357. CHECK(collector.mHit.mPenetrationDepth < 1.0e-4f);
  358. CHECK(collector.mHit.mPenetrationAxis.Normalized().IsClose(Vec3::sAxisZ()));
  359. }
  360. // A test case of a box and a convex hull that are nearly touching and that should return a contact with correct normal because the collision settings specify a max separation distance. This was producing the wrong normal.
  361. TEST_CASE("BoxVsConvexHullNoConvexRadius")
  362. {
  363. const float separation_distance = 0.001f;
  364. const float box_separation_from_hull = 0.5f * separation_distance;
  365. const float hull_height = 0.25f;
  366. // Box with no convex radius
  367. Ref<BoxShapeSettings> box_settings = new BoxShapeSettings(Vec3(0.25f, 0.75f, 0.375f), 0.0f);
  368. Ref<Shape> box_shape = box_settings->Create().Get();
  369. // Convex hull (also a box) with no convex radius
  370. Vec3 hull_points[] =
  371. {
  372. Vec3(-2.5f, -hull_height, -1.5f),
  373. Vec3(-2.5f, hull_height, -1.5f),
  374. Vec3(2.5f, -hull_height, -1.5f),
  375. Vec3(-2.5f, -hull_height, 1.5f),
  376. Vec3(-2.5f, hull_height, 1.5f),
  377. Vec3(2.5f, hull_height, -1.5f),
  378. Vec3(2.5f, -hull_height, 1.5f),
  379. Vec3(2.5f, hull_height, 1.5f)
  380. };
  381. Ref<ConvexHullShapeSettings> hull_settings = new ConvexHullShapeSettings(hull_points, 8, 0.0f);
  382. Ref<Shape> hull_shape = hull_settings->Create().Get();
  383. float angle = 0.0f;
  384. for (int i = 0; i < 481; ++i)
  385. {
  386. // Slowly rotate both box and convex hull
  387. angle += DegreesToRadians(45.0f) / 60.0f;
  388. Mat44 hull_transform = Mat44::sRotationY(angle);
  389. const Mat44 box_local_translation = Mat44::sTranslation(Vec3(0.1f, 1.0f + box_separation_from_hull, -0.5f));
  390. const Mat44 box_local_rotation = Mat44::sRotationY(DegreesToRadians(-45.0f));
  391. const Mat44 box_local_transform = box_local_translation * box_local_rotation;
  392. const Mat44 box_transform = hull_transform * box_local_transform;
  393. CollideShapeSettings settings;
  394. settings.mMaxSeparationDistance = separation_distance;
  395. ClosestHitCollisionCollector<CollideShapeCollector> collector;
  396. CollisionDispatch::sCollideShapeVsShape(box_shape, hull_shape, Vec3::sReplicate(1.0f), Vec3::sReplicate(1.0f), box_transform, hull_transform, SubShapeIDCreator(), SubShapeIDCreator(), settings, collector);
  397. // Check that there was a hit and that the contact normal is correct
  398. CHECK(collector.HadHit());
  399. const CollideShapeResult &hit = collector.mHit;
  400. CHECK_APPROX_EQUAL(hit.mContactPointOn1.GetY(), hull_height + box_separation_from_hull, 1.0e-3f);
  401. CHECK_APPROX_EQUAL(hit.mContactPointOn2.GetY(), hull_height);
  402. CHECK_APPROX_EQUAL(hit.mPenetrationAxis.NormalizedOr(Vec3::sZero()), -Vec3::sAxisY(), 1.0e-3f);
  403. }
  404. CHECK(angle >= 2.0f * JPH_PI);
  405. }
  406. }