ContactListenerTests.cpp 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  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 "Layers.h"
  7. #include "LoggingContactListener.h"
  8. #include <Jolt/Physics/Collision/Shape/StaticCompoundShape.h>
  9. #include <Jolt/Physics/Collision/Shape/SphereShape.h>
  10. TEST_SUITE("ContactListenerTests")
  11. {
  12. // Gravity vector
  13. const Vec3 cGravity = Vec3(0.0f, -9.81f, 0.0f);
  14. using LogEntry = LoggingContactListener::LogEntry;
  15. using EType = LoggingContactListener::EType;
  16. // Let a sphere bounce on the floor with restition = 1
  17. TEST_CASE("TestContactListenerElastic")
  18. {
  19. PhysicsTestContext c(1.0f / 60.0f, 1, 1);
  20. const float cSimulationTime = 1.0f;
  21. const RVec3 cDistanceTraveled = c.PredictPosition(RVec3::sZero(), Vec3::sZero(), cGravity, cSimulationTime);
  22. const float cFloorHitEpsilon = 1.0e-4f; // Apply epsilon so that we're sure that the collision algorithm will find a collision
  23. const RVec3 cFloorHitPos(0.0f, 1.0f - cFloorHitEpsilon, 0.0f); // Sphere with radius 1 will hit floor when 1 above the floor
  24. const RVec3 cInitialPos = cFloorHitPos - cDistanceTraveled;
  25. const float cPenetrationSlop = c.GetSystem()->GetPhysicsSettings().mPenetrationSlop;
  26. // Register listener
  27. LoggingContactListener listener;
  28. c.GetSystem()->SetContactListener(&listener);
  29. // Create sphere
  30. Body &floor = c.CreateFloor();
  31. Body &body = c.CreateSphere(cInitialPos, 1.0f, EMotionType::Dynamic, EMotionQuality::Discrete, Layers::MOVING);
  32. body.SetRestitution(1.0f);
  33. CHECK(floor.GetID() < body.GetID());
  34. // Simulate until at floor
  35. c.Simulate(cSimulationTime);
  36. // Assert collision not yet processed
  37. CHECK(listener.GetEntryCount() == 0);
  38. // Simulate one more step to process the collision
  39. c.Simulate(c.GetDeltaTime());
  40. // We expect a validate and a contact point added message
  41. CHECK(listener.GetEntryCount() == 2);
  42. if (listener.GetEntryCount() == 2)
  43. {
  44. // Check validate callback
  45. const LogEntry &validate = listener.GetEntry(0);
  46. CHECK(validate.mType == EType::Validate);
  47. CHECK(validate.mBody1 == body.GetID()); // Dynamic body should always be the 1st
  48. CHECK(validate.mBody2 == floor.GetID());
  49. // Check add contact callback
  50. const LogEntry &add_contact = listener.GetEntry(1);
  51. CHECK(add_contact.mType == EType::Add);
  52. CHECK(add_contact.mBody1 == floor.GetID()); // Lowest ID should be first
  53. CHECK(add_contact.mManifold.mSubShapeID1.GetValue() == SubShapeID().GetValue()); // Floor doesn't have any sub shapes
  54. CHECK(add_contact.mBody2 == body.GetID()); // Highest ID should be second
  55. CHECK(add_contact.mManifold.mSubShapeID2.GetValue() == SubShapeID().GetValue()); // Sphere doesn't have any sub shapes
  56. CHECK_APPROX_EQUAL(add_contact.mManifold.mWorldSpaceNormal, Vec3::sAxisY()); // Normal should move body 2 out of collision
  57. CHECK(add_contact.mManifold.mRelativeContactPointsOn1.size() == 1);
  58. CHECK(add_contact.mManifold.mRelativeContactPointsOn2.size() == 1);
  59. CHECK(add_contact.mManifold.GetWorldSpaceContactPointOn1(0).IsClose(RVec3::sZero(), Square(cPenetrationSlop)));
  60. CHECK(add_contact.mManifold.GetWorldSpaceContactPointOn2(0).IsClose(RVec3::sZero(), Square(cPenetrationSlop)));
  61. }
  62. listener.Clear();
  63. // Simulate same time, with a fully elastic body we should reach the initial position again
  64. c.Simulate(cSimulationTime);
  65. // We should only have a remove contact point
  66. CHECK(listener.GetEntryCount() == 1);
  67. if (listener.GetEntryCount() == 1)
  68. {
  69. // Check remove contact callback
  70. const LogEntry &remove = listener.GetEntry(0);
  71. CHECK(remove.mType == EType::Remove);
  72. CHECK(remove.mBody1 == floor.GetID()); // Lowest ID should be first
  73. CHECK(remove.mBody2 == body.GetID()); // Highest ID should be second
  74. }
  75. }
  76. // Let a sphere fall on the floor with restition = 0, then give it horizontal velocity, then take it away from the floor
  77. TEST_CASE("TestContactListenerInelastic")
  78. {
  79. PhysicsTestContext c(1.0f / 60.0f, 1, 1);
  80. const float cSimulationTime = 1.0f;
  81. const RVec3 cDistanceTraveled = c.PredictPosition(RVec3::sZero(), Vec3::sZero(), cGravity, cSimulationTime);
  82. const float cFloorHitEpsilon = 1.0e-4f; // Apply epsilon so that we're sure that the collision algorithm will find a collision
  83. const RVec3 cFloorHitPos(0.0f, 1.0f - cFloorHitEpsilon, 0.0f); // Sphere with radius 1 will hit floor when 1 above the floor
  84. const RVec3 cInitialPos = cFloorHitPos - cDistanceTraveled;
  85. const float cPenetrationSlop = c.GetSystem()->GetPhysicsSettings().mPenetrationSlop;
  86. // Register listener
  87. LoggingContactListener listener;
  88. c.GetSystem()->SetContactListener(&listener);
  89. // Create sphere
  90. Body &floor = c.CreateFloor();
  91. Body &body = c.CreateSphere(cInitialPos, 1.0f, EMotionType::Dynamic, EMotionQuality::Discrete, Layers::MOVING);
  92. body.SetRestitution(0.0f);
  93. body.SetAllowSleeping(false);
  94. CHECK(floor.GetID() < body.GetID());
  95. // Simulate until at floor
  96. c.Simulate(cSimulationTime);
  97. // Assert collision not yet processed
  98. CHECK(listener.GetEntryCount() == 0);
  99. // Simulate one more step to process the collision
  100. c.Simulate(c.GetDeltaTime());
  101. CHECK_APPROX_EQUAL(body.GetPosition(), cFloorHitPos, cPenetrationSlop);
  102. // We expect a validate and a contact point added message
  103. CHECK(listener.GetEntryCount() == 2);
  104. if (listener.GetEntryCount() == 2)
  105. {
  106. // Check validate callback
  107. const LogEntry &validate = listener.GetEntry(0);
  108. CHECK(validate.mType == EType::Validate);
  109. CHECK(validate.mBody1 == body.GetID()); // Dynamic body should always be the 1st
  110. CHECK(validate.mBody2 == floor.GetID());
  111. // Check add contact callback
  112. const LogEntry &add_contact = listener.GetEntry(1);
  113. CHECK(add_contact.mType == EType::Add);
  114. CHECK(add_contact.mBody1 == floor.GetID()); // Lowest ID first
  115. CHECK(add_contact.mManifold.mSubShapeID1.GetValue() == SubShapeID().GetValue()); // Floor doesn't have any sub shapes
  116. CHECK(add_contact.mBody2 == body.GetID()); // Highest ID second
  117. CHECK(add_contact.mManifold.mSubShapeID2.GetValue() == SubShapeID().GetValue()); // Sphere doesn't have any sub shapes
  118. CHECK_APPROX_EQUAL(add_contact.mManifold.mWorldSpaceNormal, Vec3::sAxisY()); // Normal should move body 2 out of collision
  119. CHECK(add_contact.mManifold.mRelativeContactPointsOn1.size() == 1);
  120. CHECK(add_contact.mManifold.mRelativeContactPointsOn2.size() == 1);
  121. CHECK(add_contact.mManifold.GetWorldSpaceContactPointOn1(0).IsClose(RVec3::sZero(), Square(cPenetrationSlop)));
  122. CHECK(add_contact.mManifold.GetWorldSpaceContactPointOn2(0).IsClose(RVec3::sZero(), Square(cPenetrationSlop)));
  123. }
  124. listener.Clear();
  125. // Simulate 10 steps
  126. c.Simulate(10 * c.GetDeltaTime());
  127. CHECK_APPROX_EQUAL(body.GetPosition(), cFloorHitPos, cPenetrationSlop);
  128. // We're not moving, we should have persisted contacts only
  129. CHECK(listener.GetEntryCount() == 10);
  130. for (size_t i = 0; i < listener.GetEntryCount(); ++i)
  131. {
  132. // Check persist callback
  133. const LogEntry &persist_contact = listener.GetEntry(i);
  134. CHECK(persist_contact.mType == EType::Persist);
  135. CHECK(persist_contact.mBody1 == floor.GetID()); // Lowest ID first
  136. CHECK(persist_contact.mManifold.mSubShapeID1.GetValue() == SubShapeID().GetValue()); // Floor doesn't have any sub shapes
  137. CHECK(persist_contact.mBody2 == body.GetID()); // Highest ID second
  138. CHECK(persist_contact.mManifold.mSubShapeID2.GetValue() == SubShapeID().GetValue()); // Sphere doesn't have any sub shapes
  139. CHECK_APPROX_EQUAL(persist_contact.mManifold.mWorldSpaceNormal, Vec3::sAxisY()); // Normal should move body 2 out of collision
  140. CHECK(persist_contact.mManifold.mRelativeContactPointsOn1.size() == 1);
  141. CHECK(persist_contact.mManifold.mRelativeContactPointsOn2.size() == 1);
  142. CHECK(persist_contact.mManifold.GetWorldSpaceContactPointOn1(0).IsClose(RVec3::sZero(), Square(cPenetrationSlop)));
  143. CHECK(persist_contact.mManifold.GetWorldSpaceContactPointOn2(0).IsClose(RVec3::sZero(), Square(cPenetrationSlop)));
  144. }
  145. listener.Clear();
  146. // Make the body able to go to sleep
  147. body.SetAllowSleeping(true);
  148. // Let the body go to sleep
  149. c.Simulate(1.0f);
  150. CHECK_APPROX_EQUAL(body.GetPosition(), cFloorHitPos, cPenetrationSlop);
  151. // Check it went to sleep and that we received a contact removal callback
  152. CHECK(!body.IsActive());
  153. CHECK(listener.GetEntryCount() > 0);
  154. for (size_t i = 0; i < listener.GetEntryCount(); ++i)
  155. {
  156. // Check persist / removed callbacks
  157. const LogEntry &entry = listener.GetEntry(i);
  158. CHECK(entry.mBody1 == floor.GetID());
  159. CHECK(entry.mBody2 == body.GetID());
  160. CHECK(entry.mType == ((i == listener.GetEntryCount() - 1)? EType::Remove : EType::Persist)); // The last entry should remove the contact as the body went to sleep
  161. }
  162. listener.Clear();
  163. // Wake the body up again
  164. c.GetBodyInterface().ActivateBody(body.GetID());
  165. CHECK(body.IsActive());
  166. // Simulate 1 time step to detect the collision with the floor again
  167. c.SimulateSingleStep();
  168. // Check that the contact got readded
  169. CHECK(listener.GetEntryCount() == 2);
  170. CHECK(listener.Contains(EType::Validate, floor.GetID(), body.GetID()));
  171. CHECK(listener.Contains(EType::Add, floor.GetID(), body.GetID()));
  172. listener.Clear();
  173. // Prevent body from going to sleep again
  174. body.SetAllowSleeping(false);
  175. // Make the sphere move horizontal
  176. body.SetLinearVelocity(Vec3::sAxisX());
  177. // Simulate 10 steps
  178. c.Simulate(10 * c.GetDeltaTime());
  179. // We should have 10 persisted contacts events
  180. int validate = 0;
  181. int persisted = 0;
  182. for (size_t i = 0; i < listener.GetEntryCount(); ++i)
  183. {
  184. const LogEntry &entry = listener.GetEntry(i);
  185. switch (entry.mType)
  186. {
  187. case EType::Validate:
  188. ++validate;
  189. break;
  190. case EType::Persist:
  191. // Check persist callback
  192. CHECK(entry.mBody1 == floor.GetID()); // Lowest ID first
  193. CHECK(entry.mManifold.mSubShapeID1.GetValue() == SubShapeID().GetValue()); // Floor doesn't have any sub shapes
  194. CHECK(entry.mBody2 == body.GetID()); // Highest ID second
  195. CHECK(entry.mManifold.mSubShapeID2.GetValue() == SubShapeID().GetValue()); // Sphere doesn't have any sub shapes
  196. CHECK_APPROX_EQUAL(entry.mManifold.mWorldSpaceNormal, Vec3::sAxisY()); // Normal should move body 2 out of collision
  197. CHECK(entry.mManifold.mRelativeContactPointsOn1.size() == 1);
  198. CHECK(entry.mManifold.mRelativeContactPointsOn2.size() == 1);
  199. CHECK(abs(entry.mManifold.GetWorldSpaceContactPointOn1(0).GetY()) < cPenetrationSlop);
  200. CHECK(abs(entry.mManifold.GetWorldSpaceContactPointOn2(0).GetY()) < cPenetrationSlop);
  201. ++persisted;
  202. break;
  203. case EType::Add:
  204. case EType::Remove:
  205. default:
  206. CHECK(false); // Unexpected event
  207. }
  208. }
  209. CHECK(validate <= 10); // We may receive extra validate callbacks when the object is moving
  210. CHECK(persisted == 10);
  211. listener.Clear();
  212. // Move the sphere away from the floor
  213. c.GetBodyInterface().SetPosition(body.GetID(), cInitialPos, EActivation::Activate);
  214. // Simulate 10 steps
  215. c.Simulate(10 * c.GetDeltaTime());
  216. // We should only have a remove contact point
  217. CHECK(listener.GetEntryCount() == 1);
  218. if (listener.GetEntryCount() == 1)
  219. {
  220. // Check remove contact callback
  221. const LogEntry &remove = listener.GetEntry(0);
  222. CHECK(remove.mType == EType::Remove);
  223. CHECK(remove.mBody1 == floor.GetID()); // Lowest ID first
  224. CHECK(remove.mBody2 == body.GetID()); // Highest ID second
  225. }
  226. }
  227. TEST_CASE("TestWereBodiesInContact")
  228. {
  229. for (int sign = -1; sign <= 1; sign += 2)
  230. {
  231. PhysicsTestContext c(1.0f / 60.0f, 1, 1);
  232. PhysicsSystem *s = c.GetSystem();
  233. BodyInterface &bi = c.GetBodyInterface();
  234. Body &floor = c.CreateFloor();
  235. // Two spheres at a distance so that when one sphere leaves the floor the body can still be touching the floor with the other sphere
  236. Ref<StaticCompoundShapeSettings> compound_shape = new StaticCompoundShapeSettings;
  237. compound_shape->AddShape(Vec3(-2, 0, 0), Quat::sIdentity(), new SphereShape(1));
  238. compound_shape->AddShape(Vec3(2, 0, 0), Quat::sIdentity(), new SphereShape(1));
  239. Body &body = *bi.CreateBody(BodyCreationSettings(compound_shape, RVec3(0, 0.999f, 0), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING));
  240. bi.AddBody(body.GetID(), EActivation::Activate);
  241. class ContactListenerImpl : public ContactListener
  242. {
  243. public:
  244. ContactListenerImpl(PhysicsSystem *inSystem) : mSystem(inSystem) { }
  245. virtual void OnContactAdded(const Body &inBody1, const Body &inBody2, const ContactManifold &inManifold, ContactSettings &ioSettings) override
  246. {
  247. ++mAdded;
  248. }
  249. virtual void OnContactRemoved(const SubShapeIDPair &inSubShapePair) override
  250. {
  251. ++mRemoved;
  252. mWasInContact = mSystem->WereBodiesInContact(inSubShapePair.GetBody1ID(), inSubShapePair.GetBody2ID());
  253. CHECK(mWasInContact == mSystem->WereBodiesInContact(inSubShapePair.GetBody2ID(), inSubShapePair.GetBody1ID())); // Returned value should be the same regardless of order
  254. }
  255. int GetAddCount() const
  256. {
  257. return mAdded - mRemoved;
  258. }
  259. void Reset()
  260. {
  261. mAdded = 0;
  262. mRemoved = 0;
  263. mWasInContact = false;
  264. }
  265. PhysicsSystem * mSystem;
  266. int mAdded = 0;
  267. int mRemoved = 0;
  268. bool mWasInContact = false;
  269. };
  270. // Set listener
  271. ContactListenerImpl listener(s);
  272. s->SetContactListener(&listener);
  273. // If the simulation hasn't run yet, we can't be in contact
  274. CHECK(!s->WereBodiesInContact(floor.GetID(), body.GetID()));
  275. // Step the simulation to allow detecting the contact
  276. c.SimulateSingleStep();
  277. // Should be in contact now
  278. CHECK(s->WereBodiesInContact(floor.GetID(), body.GetID()));
  279. CHECK(s->WereBodiesInContact(body.GetID(), floor.GetID()));
  280. CHECK(listener.GetAddCount() == 1);
  281. listener.Reset();
  282. // Impulse on one side
  283. bi.AddImpulse(body.GetID(), Vec3(0, 10000, 0), RVec3(Real(-sign * 2), 0, 0));
  284. c.SimulateSingleStep(); // One step to detach from the ground (but starts penetrating so will not send a remove callback)
  285. CHECK(listener.GetAddCount() == 0);
  286. c.SimulateSingleStep(); // One step to get contact remove callback
  287. // Should still be in contact
  288. // Note that we may get a remove and an add callback because manifold reduction has combined the collision with both spheres into 1 contact manifold.
  289. // At that point it has to select one of the sub shapes for the contact and if that sub shape no longer collides we get a remove for this sub shape and then an add callback for the other sub shape.
  290. CHECK(s->WereBodiesInContact(floor.GetID(), body.GetID()));
  291. CHECK(s->WereBodiesInContact(body.GetID(), floor.GetID()));
  292. CHECK(listener.GetAddCount() == 0);
  293. CHECK((listener.mRemoved == 0 || listener.mWasInContact));
  294. listener.Reset();
  295. // Impulse on the other side
  296. bi.AddImpulse(body.GetID(), Vec3(0, 10000, 0), RVec3(Real(sign * 2), 0, 0));
  297. c.SimulateSingleStep(); // One step to detach from the ground (but starts penetrating so will not send a remove callback)
  298. CHECK(listener.GetAddCount() == 0);
  299. c.SimulateSingleStep(); // One step to get contact remove callback
  300. // Should no longer be in contact
  301. CHECK(!s->WereBodiesInContact(floor.GetID(), body.GetID()));
  302. CHECK(!s->WereBodiesInContact(body.GetID(), floor.GetID()));
  303. CHECK(listener.GetAddCount() == -1);
  304. CHECK((listener.mRemoved == 1 && !listener.mWasInContact));
  305. }
  306. }
  307. }