ContactListenerTests.cpp 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. // SPDX-FileCopyrightText: 2021 Jorrit Rouwe
  2. // SPDX-License-Identifier: MIT
  3. #include "UnitTestFramework.h"
  4. #include "PhysicsTestContext.h"
  5. #include "Layers.h"
  6. #include "LoggingContactListener.h"
  7. TEST_SUITE("ContactListenerTests")
  8. {
  9. // Gravity vector
  10. const Vec3 cGravity = Vec3(0.0f, -9.81f, 0.0f);
  11. using LogEntry = LoggingContactListener::LogEntry;
  12. using EType = LoggingContactListener::EType;
  13. // Let a sphere bounce on the floor with restition = 1
  14. TEST_CASE("TestContactListenerElastic")
  15. {
  16. PhysicsTestContext c(1.0f / 60.0f, 1, 1);
  17. const float cSimulationTime = 1.0f;
  18. const Vec3 cDistanceTraveled = c.PredictPosition(Vec3::sZero(), Vec3::sZero(), cGravity, cSimulationTime);
  19. const float cFloorHitEpsilon = 1.0e-4f; // Apply epsilon so that we're sure that the collision algorithm will find a collision
  20. const Vec3 cFloorHitPos(0.0f, 1.0f - cFloorHitEpsilon, 0.0f); // Sphere with radius 1 will hit floor when 1 above the floor
  21. const Vec3 cInitialPos = cFloorHitPos - cDistanceTraveled;
  22. const float cPenetrationSlop = c.GetSystem()->GetPhysicsSettings().mPenetrationSlop;
  23. // Register listener
  24. LoggingContactListener listener;
  25. c.GetSystem()->SetContactListener(&listener);
  26. // Create sphere
  27. Body &floor = c.CreateFloor();
  28. Body &body = c.CreateSphere(cInitialPos, 1.0f, EMotionType::Dynamic, EMotionQuality::Discrete, Layers::MOVING);
  29. body.SetRestitution(1.0f);
  30. CHECK(floor.GetID() < body.GetID());
  31. // Simulate until at floor
  32. c.Simulate(cSimulationTime);
  33. // Assert collision not yet processed
  34. CHECK(listener.GetEntryCount() == 0);
  35. // Simulate one more step to process the collision
  36. c.Simulate(c.GetDeltaTime());
  37. // We expect a validate and a contact point added message
  38. CHECK(listener.GetEntryCount() == 2);
  39. if (listener.GetEntryCount() == 2)
  40. {
  41. // Check validate callback
  42. const LogEntry &validate = listener.GetEntry(0);
  43. CHECK(validate.mType == EType::Validate);
  44. CHECK(validate.mBody1 == body.GetID()); // Dynamic body should always be the 1st
  45. CHECK(validate.mBody2 == floor.GetID());
  46. // Check add contact callback
  47. const LogEntry &add_contact = listener.GetEntry(1);
  48. CHECK(add_contact.mType == EType::Add);
  49. CHECK(add_contact.mBody1 == floor.GetID()); // Lowest ID should be first
  50. CHECK(add_contact.mManifold.mSubShapeID1.GetValue() == SubShapeID().GetValue()); // Floor doesn't have any sub shapes
  51. CHECK(add_contact.mBody2 == body.GetID()); // Highest ID should be second
  52. CHECK(add_contact.mManifold.mSubShapeID2.GetValue() == SubShapeID().GetValue()); // Sphere doesn't have any sub shapes
  53. CHECK_APPROX_EQUAL(add_contact.mManifold.mWorldSpaceNormal, Vec3::sAxisY()); // Normal should move body 2 out of collision
  54. CHECK(add_contact.mManifold.mWorldSpaceContactPointsOn1.size() == 1);
  55. CHECK(add_contact.mManifold.mWorldSpaceContactPointsOn2.size() == 1);
  56. CHECK(add_contact.mManifold.mWorldSpaceContactPointsOn1[0].IsClose(Vec3::sZero(), Square(cPenetrationSlop)));
  57. CHECK(add_contact.mManifold.mWorldSpaceContactPointsOn2[0].IsClose(Vec3::sZero(), Square(cPenetrationSlop)));
  58. }
  59. listener.Clear();
  60. // Simulate same time, with a fully elastic body we should reach the initial position again
  61. c.Simulate(cSimulationTime);
  62. // We should only have a remove contact point
  63. CHECK(listener.GetEntryCount() == 1);
  64. if (listener.GetEntryCount() == 1)
  65. {
  66. // Check remove contact callback
  67. const LogEntry &remove = listener.GetEntry(0);
  68. CHECK(remove.mType == EType::Remove);
  69. CHECK(remove.mBody1 == floor.GetID()); // Lowest ID should be first
  70. CHECK(remove.mBody2 == body.GetID()); // Highest ID should be second
  71. }
  72. }
  73. // Let a sphere fall on the floor with restition = 0, then give it horizontal velocity, then take it away from the floor
  74. TEST_CASE("TestContactListenerInelastic")
  75. {
  76. PhysicsTestContext c(1.0f / 60.0f, 1, 1);
  77. const float cSimulationTime = 1.0f;
  78. const Vec3 cDistanceTraveled = c.PredictPosition(Vec3::sZero(), Vec3::sZero(), cGravity, cSimulationTime);
  79. const float cFloorHitEpsilon = 1.0e-4f; // Apply epsilon so that we're sure that the collision algorithm will find a collision
  80. const Vec3 cFloorHitPos(0.0f, 1.0f - cFloorHitEpsilon, 0.0f); // Sphere with radius 1 will hit floor when 1 above the floor
  81. const Vec3 cInitialPos = cFloorHitPos - cDistanceTraveled;
  82. const float cPenetrationSlop = c.GetSystem()->GetPhysicsSettings().mPenetrationSlop;
  83. // Register listener
  84. LoggingContactListener listener;
  85. c.GetSystem()->SetContactListener(&listener);
  86. // Create sphere
  87. Body &floor = c.CreateFloor();
  88. Body &body = c.CreateSphere(cInitialPos, 1.0f, EMotionType::Dynamic, EMotionQuality::Discrete, Layers::MOVING);
  89. body.SetRestitution(0.0f);
  90. body.SetAllowSleeping(false);
  91. CHECK(floor.GetID() < body.GetID());
  92. // Simulate until at floor
  93. c.Simulate(cSimulationTime);
  94. // Assert collision not yet processed
  95. CHECK(listener.GetEntryCount() == 0);
  96. // Simulate one more step to process the collision
  97. c.Simulate(c.GetDeltaTime());
  98. // We expect a validate and a contact point added message
  99. CHECK(listener.GetEntryCount() == 2);
  100. if (listener.GetEntryCount() == 2)
  101. {
  102. // Check validate callback
  103. const LogEntry &validate = listener.GetEntry(0);
  104. CHECK(validate.mType == EType::Validate);
  105. CHECK(validate.mBody1 == body.GetID()); // Dynamic body should always be the 1st
  106. CHECK(validate.mBody2 == floor.GetID());
  107. // Check add contact callback
  108. const LogEntry &add_contact = listener.GetEntry(1);
  109. CHECK(add_contact.mType == EType::Add);
  110. CHECK(add_contact.mBody1 == floor.GetID()); // Lowest ID first
  111. CHECK(add_contact.mManifold.mSubShapeID1.GetValue() == SubShapeID().GetValue()); // Floor doesn't have any sub shapes
  112. CHECK(add_contact.mBody2 == body.GetID()); // Highest ID second
  113. CHECK(add_contact.mManifold.mSubShapeID2.GetValue() == SubShapeID().GetValue()); // Sphere doesn't have any sub shapes
  114. CHECK_APPROX_EQUAL(add_contact.mManifold.mWorldSpaceNormal, Vec3::sAxisY()); // Normal should move body 2 out of collision
  115. CHECK(add_contact.mManifold.mWorldSpaceContactPointsOn1.size() == 1);
  116. CHECK(add_contact.mManifold.mWorldSpaceContactPointsOn2.size() == 1);
  117. CHECK(add_contact.mManifold.mWorldSpaceContactPointsOn1[0].IsClose(Vec3::sZero(), Square(cPenetrationSlop)));
  118. CHECK(add_contact.mManifold.mWorldSpaceContactPointsOn2[0].IsClose(Vec3::sZero(), Square(cPenetrationSlop)));
  119. }
  120. listener.Clear();
  121. // Simulate 10 steps
  122. c.Simulate(10 * c.GetDeltaTime());
  123. // We're not moving, we should have persisted contacts only
  124. CHECK(listener.GetEntryCount() == 10);
  125. for (size_t i = 0; i < listener.GetEntryCount(); ++i)
  126. {
  127. // Check persist callback
  128. const LogEntry &persist_contact = listener.GetEntry(i);
  129. CHECK(persist_contact.mType == EType::Persist);
  130. CHECK(persist_contact.mBody1 == floor.GetID()); // Lowest ID first
  131. CHECK(persist_contact.mManifold.mSubShapeID1.GetValue() == SubShapeID().GetValue()); // Floor doesn't have any sub shapes
  132. CHECK(persist_contact.mBody2 == body.GetID()); // Highest ID second
  133. CHECK(persist_contact.mManifold.mSubShapeID2.GetValue() == SubShapeID().GetValue()); // Sphere doesn't have any sub shapes
  134. CHECK_APPROX_EQUAL(persist_contact.mManifold.mWorldSpaceNormal, Vec3::sAxisY()); // Normal should move body 2 out of collision
  135. CHECK(persist_contact.mManifold.mWorldSpaceContactPointsOn1.size() == 1);
  136. CHECK(persist_contact.mManifold.mWorldSpaceContactPointsOn2.size() == 1);
  137. CHECK(persist_contact.mManifold.mWorldSpaceContactPointsOn1[0].IsClose(Vec3::sZero(), Square(cPenetrationSlop)));
  138. CHECK(persist_contact.mManifold.mWorldSpaceContactPointsOn2[0].IsClose(Vec3::sZero(), Square(cPenetrationSlop)));
  139. }
  140. listener.Clear();
  141. // Make the sphere move horizontal
  142. body.SetLinearVelocity(Vec3::sAxisX());
  143. // Simulate 10 steps
  144. c.Simulate(10 * c.GetDeltaTime());
  145. // We should have 10 persisted contacts events
  146. int validate = 0;
  147. int persisted = 0;
  148. for (size_t i = 0; i < listener.GetEntryCount(); ++i)
  149. {
  150. const LogEntry &entry = listener.GetEntry(i);
  151. switch (entry.mType)
  152. {
  153. case EType::Validate:
  154. ++validate;
  155. break;
  156. case EType::Persist:
  157. // Check persist callback
  158. CHECK(entry.mBody1 == floor.GetID()); // Lowest ID first
  159. CHECK(entry.mManifold.mSubShapeID1.GetValue() == SubShapeID().GetValue()); // Floor doesn't have any sub shapes
  160. CHECK(entry.mBody2 == body.GetID()); // Highest ID second
  161. CHECK(entry.mManifold.mSubShapeID2.GetValue() == SubShapeID().GetValue()); // Sphere doesn't have any sub shapes
  162. CHECK_APPROX_EQUAL(entry.mManifold.mWorldSpaceNormal, Vec3::sAxisY()); // Normal should move body 2 out of collision
  163. CHECK(entry.mManifold.mWorldSpaceContactPointsOn1.size() == 1);
  164. CHECK(entry.mManifold.mWorldSpaceContactPointsOn2.size() == 1);
  165. CHECK(abs(entry.mManifold.mWorldSpaceContactPointsOn1[0].GetY()) < cPenetrationSlop);
  166. CHECK(abs(entry.mManifold.mWorldSpaceContactPointsOn2[0].GetY()) < cPenetrationSlop);
  167. ++persisted;
  168. break;
  169. case EType::Add:
  170. case EType::Remove:
  171. default:
  172. CHECK(false); // Unexpected event
  173. }
  174. }
  175. CHECK(validate <= 10); // We may receive extra validate callbacks when the object is moving
  176. CHECK(persisted == 10);
  177. listener.Clear();
  178. // Move the sphere away from the floor
  179. c.GetSystem()->GetBodyInterface().SetPosition(body.GetID(), cInitialPos, EActivation::Activate);
  180. // Simulate 10 steps
  181. c.Simulate(10 * c.GetDeltaTime());
  182. // We should only have a remove contact point
  183. CHECK(listener.GetEntryCount() == 1);
  184. if (listener.GetEntryCount() == 1)
  185. {
  186. // Check remove contact callback
  187. const LogEntry &remove = listener.GetEntry(0);
  188. CHECK(remove.mType == EType::Remove);
  189. CHECK(remove.mBody1 == floor.GetID()); // Lowest ID first
  190. CHECK(remove.mBody2 == body.GetID()); // Highest ID second
  191. }
  192. }
  193. }