ContactListenerTests.cpp 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521
  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. #include <Jolt/Physics/Collision/Shape/BoxShape.h>
  11. TEST_SUITE("ContactListenerTests")
  12. {
  13. // Gravity vector
  14. const Vec3 cGravity = Vec3(0.0f, -9.81f, 0.0f);
  15. using LogEntry = LoggingContactListener::LogEntry;
  16. using EType = LoggingContactListener::EType;
  17. // Let a sphere bounce on the floor with restition = 1
  18. TEST_CASE("TestContactListenerElastic")
  19. {
  20. PhysicsTestContext c;
  21. const float cSimulationTime = 1.0f;
  22. const RVec3 cDistanceTraveled = c.PredictPosition(RVec3::sZero(), Vec3::sZero(), cGravity, cSimulationTime);
  23. const float cFloorHitEpsilon = 1.0e-4f; // Apply epsilon so that we're sure that the collision algorithm will find a collision
  24. const RVec3 cFloorHitPos(0.0f, 1.0f - cFloorHitEpsilon, 0.0f); // Sphere with radius 1 will hit floor when 1 above the floor
  25. const RVec3 cInitialPos = cFloorHitPos - cDistanceTraveled;
  26. const float cPenetrationSlop = c.GetSystem()->GetPhysicsSettings().mPenetrationSlop;
  27. // Register listener
  28. LoggingContactListener listener;
  29. c.GetSystem()->SetContactListener(&listener);
  30. // Create sphere
  31. Body &floor = c.CreateFloor();
  32. Body &body = c.CreateSphere(cInitialPos, 1.0f, EMotionType::Dynamic, EMotionQuality::Discrete, Layers::MOVING);
  33. body.SetRestitution(1.0f);
  34. CHECK(floor.GetID() < body.GetID());
  35. // Simulate until at floor
  36. c.Simulate(cSimulationTime);
  37. // Assert collision not yet processed
  38. CHECK(listener.GetEntryCount() == 0);
  39. // Simulate one more step to process the collision
  40. c.Simulate(c.GetDeltaTime());
  41. // We expect a validate and a contact point added message
  42. CHECK(listener.GetEntryCount() == 2);
  43. if (listener.GetEntryCount() == 2)
  44. {
  45. // Check validate callback
  46. const LogEntry &validate = listener.GetEntry(0);
  47. CHECK(validate.mType == EType::Validate);
  48. CHECK(validate.mBody1 == body.GetID()); // Dynamic body should always be the 1st
  49. CHECK(validate.mBody2 == floor.GetID());
  50. // Check add contact callback
  51. const LogEntry &add_contact = listener.GetEntry(1);
  52. CHECK(add_contact.mType == EType::Add);
  53. CHECK(add_contact.mBody1 == floor.GetID()); // Lowest ID should be first
  54. CHECK(add_contact.mManifold.mSubShapeID1.GetValue() == SubShapeID().GetValue()); // Floor doesn't have any sub shapes
  55. CHECK(add_contact.mBody2 == body.GetID()); // Highest ID should be second
  56. CHECK(add_contact.mManifold.mSubShapeID2.GetValue() == SubShapeID().GetValue()); // Sphere doesn't have any sub shapes
  57. CHECK_APPROX_EQUAL(add_contact.mManifold.mWorldSpaceNormal, Vec3::sAxisY()); // Normal should move body 2 out of collision
  58. CHECK(add_contact.mManifold.mRelativeContactPointsOn1.size() == 1);
  59. CHECK(add_contact.mManifold.mRelativeContactPointsOn2.size() == 1);
  60. CHECK(add_contact.mManifold.GetWorldSpaceContactPointOn1(0).IsClose(RVec3::sZero(), Square(cPenetrationSlop)));
  61. CHECK(add_contact.mManifold.GetWorldSpaceContactPointOn2(0).IsClose(RVec3::sZero(), Square(cPenetrationSlop)));
  62. }
  63. listener.Clear();
  64. // Simulate same time, with a fully elastic body we should reach the initial position again
  65. c.Simulate(cSimulationTime);
  66. // We should only have a remove contact point
  67. CHECK(listener.GetEntryCount() == 1);
  68. if (listener.GetEntryCount() == 1)
  69. {
  70. // Check remove contact callback
  71. const LogEntry &remove = listener.GetEntry(0);
  72. CHECK(remove.mType == EType::Remove);
  73. CHECK(remove.mBody1 == floor.GetID()); // Lowest ID should be first
  74. CHECK(remove.mBody2 == body.GetID()); // Highest ID should be second
  75. }
  76. }
  77. // Let a sphere fall on the floor with restition = 0, then give it horizontal velocity, then take it away from the floor
  78. TEST_CASE("TestContactListenerInelastic")
  79. {
  80. PhysicsTestContext c;
  81. const float cSimulationTime = 1.0f;
  82. const RVec3 cDistanceTraveled = c.PredictPosition(RVec3::sZero(), Vec3::sZero(), cGravity, cSimulationTime);
  83. const float cFloorHitEpsilon = 1.0e-4f; // Apply epsilon so that we're sure that the collision algorithm will find a collision
  84. const RVec3 cFloorHitPos(0.0f, 1.0f - cFloorHitEpsilon, 0.0f); // Sphere with radius 1 will hit floor when 1 above the floor
  85. const RVec3 cInitialPos = cFloorHitPos - cDistanceTraveled;
  86. const float cPenetrationSlop = c.GetSystem()->GetPhysicsSettings().mPenetrationSlop;
  87. // Register listener
  88. LoggingContactListener listener;
  89. c.GetSystem()->SetContactListener(&listener);
  90. // Create sphere
  91. Body &floor = c.CreateFloor();
  92. Body &body = c.CreateSphere(cInitialPos, 1.0f, EMotionType::Dynamic, EMotionQuality::Discrete, Layers::MOVING);
  93. body.SetRestitution(0.0f);
  94. body.SetAllowSleeping(false);
  95. CHECK(floor.GetID() < body.GetID());
  96. // Simulate until at floor
  97. c.Simulate(cSimulationTime);
  98. // Assert collision not yet processed
  99. CHECK(listener.GetEntryCount() == 0);
  100. // Simulate one more step to process the collision
  101. c.Simulate(c.GetDeltaTime());
  102. CHECK_APPROX_EQUAL(body.GetPosition(), cFloorHitPos, cPenetrationSlop);
  103. // We expect a validate and a contact point added message
  104. CHECK(listener.GetEntryCount() == 2);
  105. if (listener.GetEntryCount() == 2)
  106. {
  107. // Check validate callback
  108. const LogEntry &validate = listener.GetEntry(0);
  109. CHECK(validate.mType == EType::Validate);
  110. CHECK(validate.mBody1 == body.GetID()); // Dynamic body should always be the 1st
  111. CHECK(validate.mBody2 == floor.GetID());
  112. // Check add contact callback
  113. const LogEntry &add_contact = listener.GetEntry(1);
  114. CHECK(add_contact.mType == EType::Add);
  115. CHECK(add_contact.mBody1 == floor.GetID()); // Lowest ID first
  116. CHECK(add_contact.mManifold.mSubShapeID1.GetValue() == SubShapeID().GetValue()); // Floor doesn't have any sub shapes
  117. CHECK(add_contact.mBody2 == body.GetID()); // Highest ID second
  118. CHECK(add_contact.mManifold.mSubShapeID2.GetValue() == SubShapeID().GetValue()); // Sphere doesn't have any sub shapes
  119. CHECK_APPROX_EQUAL(add_contact.mManifold.mWorldSpaceNormal, Vec3::sAxisY()); // Normal should move body 2 out of collision
  120. CHECK(add_contact.mManifold.mRelativeContactPointsOn1.size() == 1);
  121. CHECK(add_contact.mManifold.mRelativeContactPointsOn2.size() == 1);
  122. CHECK(add_contact.mManifold.GetWorldSpaceContactPointOn1(0).IsClose(RVec3::sZero(), Square(cPenetrationSlop)));
  123. CHECK(add_contact.mManifold.GetWorldSpaceContactPointOn2(0).IsClose(RVec3::sZero(), Square(cPenetrationSlop)));
  124. }
  125. listener.Clear();
  126. // Simulate 10 steps
  127. c.Simulate(10 * c.GetDeltaTime());
  128. CHECK_APPROX_EQUAL(body.GetPosition(), cFloorHitPos, cPenetrationSlop);
  129. // We're not moving, we should have persisted contacts only
  130. CHECK(listener.GetEntryCount() == 10);
  131. for (size_t i = 0; i < listener.GetEntryCount(); ++i)
  132. {
  133. // Check persist callback
  134. const LogEntry &persist_contact = listener.GetEntry(i);
  135. CHECK(persist_contact.mType == EType::Persist);
  136. CHECK(persist_contact.mBody1 == floor.GetID()); // Lowest ID first
  137. CHECK(persist_contact.mManifold.mSubShapeID1.GetValue() == SubShapeID().GetValue()); // Floor doesn't have any sub shapes
  138. CHECK(persist_contact.mBody2 == body.GetID()); // Highest ID second
  139. CHECK(persist_contact.mManifold.mSubShapeID2.GetValue() == SubShapeID().GetValue()); // Sphere doesn't have any sub shapes
  140. CHECK_APPROX_EQUAL(persist_contact.mManifold.mWorldSpaceNormal, Vec3::sAxisY()); // Normal should move body 2 out of collision
  141. CHECK(persist_contact.mManifold.mRelativeContactPointsOn1.size() == 1);
  142. CHECK(persist_contact.mManifold.mRelativeContactPointsOn2.size() == 1);
  143. CHECK(persist_contact.mManifold.GetWorldSpaceContactPointOn1(0).IsClose(RVec3::sZero(), Square(cPenetrationSlop)));
  144. CHECK(persist_contact.mManifold.GetWorldSpaceContactPointOn2(0).IsClose(RVec3::sZero(), Square(cPenetrationSlop)));
  145. }
  146. listener.Clear();
  147. // Make the body able to go to sleep
  148. body.SetAllowSleeping(true);
  149. // Let the body go to sleep
  150. c.Simulate(1.0f);
  151. CHECK_APPROX_EQUAL(body.GetPosition(), cFloorHitPos, cPenetrationSlop);
  152. // Check it went to sleep and that we received a contact removal callback
  153. CHECK(!body.IsActive());
  154. CHECK(listener.GetEntryCount() > 0);
  155. for (size_t i = 0; i < listener.GetEntryCount(); ++i)
  156. {
  157. // Check persist / removed callbacks
  158. const LogEntry &entry = listener.GetEntry(i);
  159. CHECK(entry.mBody1 == floor.GetID());
  160. CHECK(entry.mBody2 == body.GetID());
  161. CHECK(entry.mType == ((i == listener.GetEntryCount() - 1)? EType::Remove : EType::Persist)); // The last entry should remove the contact as the body went to sleep
  162. }
  163. listener.Clear();
  164. // Wake the body up again
  165. c.GetBodyInterface().ActivateBody(body.GetID());
  166. CHECK(body.IsActive());
  167. // Simulate 1 time step to detect the collision with the floor again
  168. c.SimulateSingleStep();
  169. // Check that the contact got readded
  170. CHECK(listener.GetEntryCount() == 2);
  171. CHECK(listener.Contains(EType::Validate, floor.GetID(), body.GetID()));
  172. CHECK(listener.Contains(EType::Add, floor.GetID(), body.GetID()));
  173. listener.Clear();
  174. // Prevent body from going to sleep again
  175. body.SetAllowSleeping(false);
  176. // Make the sphere move horizontal
  177. body.SetLinearVelocity(Vec3::sAxisX());
  178. // Simulate 10 steps
  179. c.Simulate(10 * c.GetDeltaTime());
  180. // We should have 10 persisted contacts events
  181. int validate = 0;
  182. int persisted = 0;
  183. for (size_t i = 0; i < listener.GetEntryCount(); ++i)
  184. {
  185. const LogEntry &entry = listener.GetEntry(i);
  186. switch (entry.mType)
  187. {
  188. case EType::Validate:
  189. ++validate;
  190. break;
  191. case EType::Persist:
  192. // Check persist callback
  193. CHECK(entry.mBody1 == floor.GetID()); // Lowest ID first
  194. CHECK(entry.mManifold.mSubShapeID1.GetValue() == SubShapeID().GetValue()); // Floor doesn't have any sub shapes
  195. CHECK(entry.mBody2 == body.GetID()); // Highest ID second
  196. CHECK(entry.mManifold.mSubShapeID2.GetValue() == SubShapeID().GetValue()); // Sphere doesn't have any sub shapes
  197. CHECK_APPROX_EQUAL(entry.mManifold.mWorldSpaceNormal, Vec3::sAxisY()); // Normal should move body 2 out of collision
  198. CHECK(entry.mManifold.mRelativeContactPointsOn1.size() == 1);
  199. CHECK(entry.mManifold.mRelativeContactPointsOn2.size() == 1);
  200. CHECK(abs(entry.mManifold.GetWorldSpaceContactPointOn1(0).GetY()) < cPenetrationSlop);
  201. CHECK(abs(entry.mManifold.GetWorldSpaceContactPointOn2(0).GetY()) < cPenetrationSlop);
  202. ++persisted;
  203. break;
  204. case EType::Add:
  205. case EType::Remove:
  206. default:
  207. CHECK(false); // Unexpected event
  208. }
  209. }
  210. CHECK(validate <= 10); // We may receive extra validate callbacks when the object is moving
  211. CHECK(persisted == 10);
  212. listener.Clear();
  213. // Move the sphere away from the floor
  214. c.GetBodyInterface().SetPosition(body.GetID(), cInitialPos, EActivation::Activate);
  215. // Simulate 10 steps
  216. c.Simulate(10 * c.GetDeltaTime());
  217. // We should only have a remove contact point
  218. CHECK(listener.GetEntryCount() == 1);
  219. if (listener.GetEntryCount() == 1)
  220. {
  221. // Check remove contact callback
  222. const LogEntry &remove = listener.GetEntry(0);
  223. CHECK(remove.mType == EType::Remove);
  224. CHECK(remove.mBody1 == floor.GetID()); // Lowest ID first
  225. CHECK(remove.mBody2 == body.GetID()); // Highest ID second
  226. }
  227. }
  228. TEST_CASE("TestWereBodiesInContact")
  229. {
  230. for (int sign = -1; sign <= 1; sign += 2)
  231. {
  232. PhysicsTestContext c;
  233. PhysicsSystem *s = c.GetSystem();
  234. BodyInterface &bi = c.GetBodyInterface();
  235. Body &floor = c.CreateFloor();
  236. // 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
  237. Ref<StaticCompoundShapeSettings> compound_shape = new StaticCompoundShapeSettings;
  238. compound_shape->AddShape(Vec3(-2, 0, 0), Quat::sIdentity(), new SphereShape(1));
  239. compound_shape->AddShape(Vec3(2, 0, 0), Quat::sIdentity(), new SphereShape(1));
  240. Body &body = *bi.CreateBody(BodyCreationSettings(compound_shape, RVec3(0, 0.999f, 0), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING));
  241. bi.AddBody(body.GetID(), EActivation::Activate);
  242. class ContactListenerImpl : public ContactListener
  243. {
  244. public:
  245. ContactListenerImpl(PhysicsSystem *inSystem) : mSystem(inSystem) { }
  246. virtual void OnContactAdded(const Body &inBody1, const Body &inBody2, const ContactManifold &inManifold, ContactSettings &ioSettings) override
  247. {
  248. ++mAdded;
  249. }
  250. virtual void OnContactRemoved(const SubShapeIDPair &inSubShapePair) override
  251. {
  252. ++mRemoved;
  253. mWasInContact = mSystem->WereBodiesInContact(inSubShapePair.GetBody1ID(), inSubShapePair.GetBody2ID());
  254. CHECK(mWasInContact == mSystem->WereBodiesInContact(inSubShapePair.GetBody2ID(), inSubShapePair.GetBody1ID())); // Returned value should be the same regardless of order
  255. }
  256. int GetAddCount() const
  257. {
  258. return mAdded - mRemoved;
  259. }
  260. void Reset()
  261. {
  262. mAdded = 0;
  263. mRemoved = 0;
  264. mWasInContact = false;
  265. }
  266. PhysicsSystem * mSystem;
  267. int mAdded = 0;
  268. int mRemoved = 0;
  269. bool mWasInContact = false;
  270. };
  271. // Set listener
  272. ContactListenerImpl listener(s);
  273. s->SetContactListener(&listener);
  274. // If the simulation hasn't run yet, we can't be in contact
  275. CHECK(!s->WereBodiesInContact(floor.GetID(), body.GetID()));
  276. // Step the simulation to allow detecting the contact
  277. c.SimulateSingleStep();
  278. // Should be in contact now
  279. CHECK(s->WereBodiesInContact(floor.GetID(), body.GetID()));
  280. CHECK(s->WereBodiesInContact(body.GetID(), floor.GetID()));
  281. CHECK(listener.GetAddCount() == 1);
  282. listener.Reset();
  283. // Impulse on one side
  284. bi.AddImpulse(body.GetID(), Vec3(0, 10000, 0), RVec3(Real(-sign * 2), 0, 0));
  285. c.SimulateSingleStep(); // One step to detach from the ground (but starts penetrating so will not send a remove callback)
  286. CHECK(listener.GetAddCount() == 0);
  287. c.SimulateSingleStep(); // One step to get contact remove callback
  288. // Should still be in contact
  289. // 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.
  290. // 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.
  291. CHECK(s->WereBodiesInContact(floor.GetID(), body.GetID()));
  292. CHECK(s->WereBodiesInContact(body.GetID(), floor.GetID()));
  293. CHECK(listener.GetAddCount() == 0);
  294. CHECK((listener.mRemoved == 0 || listener.mWasInContact));
  295. listener.Reset();
  296. // Impulse on the other side
  297. bi.AddImpulse(body.GetID(), Vec3(0, 10000, 0), RVec3(Real(sign * 2), 0, 0));
  298. c.SimulateSingleStep(); // One step to detach from the ground (but starts penetrating so will not send a remove callback)
  299. CHECK(listener.GetAddCount() == 0);
  300. c.SimulateSingleStep(); // One step to get contact remove callback
  301. // Should no longer be in contact
  302. CHECK(!s->WereBodiesInContact(floor.GetID(), body.GetID()));
  303. CHECK(!s->WereBodiesInContact(body.GetID(), floor.GetID()));
  304. CHECK(listener.GetAddCount() == -1);
  305. CHECK((listener.mRemoved == 1 && !listener.mWasInContact));
  306. }
  307. }
  308. TEST_CASE("TestSurfaceVelocity")
  309. {
  310. PhysicsTestContext c;
  311. Body &floor = c.CreateBox(RVec3(0, -1, 0), Quat::sRotation(Vec3::sAxisY(), DegreesToRadians(10.0f)), EMotionType::Static, EMotionQuality::Discrete, Layers::NON_MOVING, Vec3(100.0f, 1.0f, 100.0f));
  312. floor.SetFriction(1.0f);
  313. for (int iteration = 0; iteration < 2; ++iteration)
  314. {
  315. Body &box = c.CreateBox(RVec3(0, 0.999f, 0), Quat::sRotation(Vec3::sAxisY(), DegreesToRadians(30.0f)), EMotionType::Dynamic, EMotionQuality::Discrete, Layers::MOVING, Vec3::sReplicate(1.0f));
  316. box.SetFriction(1.0f);
  317. // Contact listener sets a constant surface velocity
  318. class ContactListenerImpl : public ContactListener
  319. {
  320. public:
  321. ContactListenerImpl(Body &inFloor, Body &inBox) : mFloor(inFloor), mBox(inBox) { }
  322. virtual void OnContactAdded(const Body &inBody1, const Body &inBody2, const ContactManifold &inManifold, ContactSettings &ioSettings) override
  323. {
  324. // Ensure that the body order is as expected
  325. JPH_ASSERT(inBody1.GetID() == mFloor.GetID() || inBody2.GetID() == mBox.GetID());
  326. // Calculate the relative surface velocity
  327. ioSettings.mRelativeLinearSurfaceVelocity = -(inBody1.GetRotation() * mLocalSpaceLinearVelocity);
  328. ioSettings.mRelativeAngularSurfaceVelocity = -(inBody1.GetRotation() * mLocalSpaceAngularVelocity);
  329. }
  330. virtual void OnContactPersisted(const Body &inBody1, const Body &inBody2, const ContactManifold &inManifold, ContactSettings &ioSettings) override
  331. {
  332. OnContactAdded(inBody1, inBody2, inManifold, ioSettings);
  333. }
  334. Body & mFloor;
  335. Body & mBox;
  336. Vec3 mLocalSpaceLinearVelocity;
  337. Vec3 mLocalSpaceAngularVelocity;
  338. };
  339. // Set listener
  340. ContactListenerImpl listener(floor, box);
  341. c.GetSystem()->SetContactListener(&listener);
  342. // Set linear velocity or angular velocity depending on the iteration
  343. listener.mLocalSpaceLinearVelocity = iteration == 0? Vec3(0, 0, -2.0f) : Vec3::sZero();
  344. listener.mLocalSpaceAngularVelocity = iteration == 0? Vec3::sZero() : Vec3(0, DegreesToRadians(30.0f), 0);
  345. // Simulate
  346. c.Simulate(5.0f);
  347. // Check that the box is moving with the correct linear/angular velocity
  348. CHECK_APPROX_EQUAL(box.GetLinearVelocity(), floor.GetRotation() * listener.mLocalSpaceLinearVelocity, 0.005f);
  349. CHECK_APPROX_EQUAL(box.GetAngularVelocity(), floor.GetRotation() * listener.mLocalSpaceAngularVelocity, 1.0e-4f);
  350. }
  351. }
  352. static float sGetInvMassScale(const Body &inBody)
  353. {
  354. uint64 ud = inBody.GetUserData();
  355. int index = ((ud & 1) != 0? (ud >> 1) : (ud >> 3)) & 0b11;
  356. float mass_overrides[] = { 1.0f, 0.0f, 0.5f, 2.0f };
  357. return mass_overrides[index];
  358. }
  359. TEST_CASE("TestMassOverride")
  360. {
  361. for (EMotionType m1 = EMotionType::Static; m1 <= EMotionType::Dynamic; m1 = EMotionType((int)m1 + 1))
  362. for (EMotionType m2 = EMotionType::Static; m2 <= EMotionType::Dynamic; m2 = EMotionType((int)m2 + 1))
  363. if (m1 != EMotionType::Static || m2 != EMotionType::Static)
  364. for (int i = 0; i < 16; ++i)
  365. {
  366. PhysicsTestContext c;
  367. c.ZeroGravity();
  368. const float cInitialVelocity1 = m1 != EMotionType::Static? 3.0f : 0.0f;
  369. const float cInitialVelocity2 = m2 != EMotionType::Static? -4.0f : 0.0f;
  370. // Create two spheres on a collision course
  371. BodyCreationSettings bcs(new SphereShape(1.0f), RVec3::sZero(), Quat::sIdentity(), m1, m1 != EMotionType::Static? Layers::MOVING : Layers::NON_MOVING);
  372. bcs.mOverrideMassProperties = EOverrideMassProperties::CalculateInertia;
  373. bcs.mMassPropertiesOverride.mMass = 1.0f;
  374. bcs.mRestitution = 1.0f;
  375. bcs.mLinearDamping = 0.0f;
  376. bcs.mPosition = RVec3(-2, 0, 0);
  377. bcs.mLinearVelocity = Vec3(cInitialVelocity1, 0, 0);
  378. bcs.mUserData = i << 1;
  379. Body &body1 = *c.GetBodyInterface().CreateBody(bcs);
  380. c.GetBodyInterface().AddBody(body1.GetID(), EActivation::Activate);
  381. bcs.mMotionType = m2;
  382. bcs.mObjectLayer = m2 != EMotionType::Static? Layers::MOVING : Layers::NON_MOVING;
  383. bcs.mMassPropertiesOverride.mMass = 2.0f;
  384. bcs.mPosition = RVec3(2, 0, 0);
  385. bcs.mLinearVelocity = Vec3(cInitialVelocity2, 0, 0);
  386. bcs.mUserData++;
  387. Body &body2 = *c.GetBodyInterface().CreateBody(bcs);
  388. c.GetBodyInterface().AddBody(body2.GetID(), EActivation::Activate);
  389. // Contact listener that modifies mass
  390. class ContactListenerImpl : public ContactListener
  391. {
  392. public:
  393. virtual void OnContactAdded(const Body &inBody1, const Body &inBody2, const ContactManifold &inManifold, ContactSettings &ioSettings) override
  394. {
  395. // Override the mass of body 1
  396. float scale1 = sGetInvMassScale(inBody1);
  397. ioSettings.mInvMassScale1 = scale1;
  398. ioSettings.mInvInertiaScale1 = scale1;
  399. // Override the mass of body 2
  400. float scale2 = sGetInvMassScale(inBody2);
  401. ioSettings.mInvMassScale2 = scale2;
  402. ioSettings.mInvInertiaScale2 = scale2;
  403. }
  404. virtual void OnContactPersisted(const Body &inBody1, const Body &inBody2, const ContactManifold &inManifold, ContactSettings &ioSettings) override
  405. {
  406. OnContactAdded(inBody1, inBody2, inManifold, ioSettings);
  407. }
  408. };
  409. // Set listener
  410. ContactListenerImpl listener;
  411. c.GetSystem()->SetContactListener(&listener);
  412. // Simulate
  413. c.Simulate(1.0f);
  414. // Calculate resulting inverse mass
  415. float inv_m1 = body1.GetMotionType() == EMotionType::Dynamic? sGetInvMassScale(body1) * body1.GetMotionProperties()->GetInverseMass() : 0.0f;
  416. float inv_m2 = body2.GetMotionType() == EMotionType::Dynamic? sGetInvMassScale(body2) * body2.GetMotionProperties()->GetInverseMass() : 0.0f;
  417. float v1, v2;
  418. if (inv_m1 == 0.0f && inv_m2 == 0.0f)
  419. {
  420. // If both bodies became kinematic they will pass through each other
  421. v1 = cInitialVelocity1;
  422. v2 = cInitialVelocity2;
  423. }
  424. else
  425. {
  426. // Calculate resulting velocity using conservation of momentum and energy
  427. // See: https://en.wikipedia.org/wiki/Elastic_collision where m1 = 1 / inv_m1 and m2 = 1 / inv_m2
  428. v1 = (2.0f * inv_m1 * cInitialVelocity2 + (inv_m2 - inv_m1) * cInitialVelocity1) / (inv_m1 + inv_m2);
  429. v2 = (2.0f * inv_m2 * cInitialVelocity1 + (inv_m1 - inv_m2) * cInitialVelocity2) / (inv_m1 + inv_m2);
  430. }
  431. // Check that the spheres move according to their overridden masses
  432. CHECK_APPROX_EQUAL(body1.GetLinearVelocity(), Vec3(v1, 0, 0));
  433. CHECK_APPROX_EQUAL(body2.GetLinearVelocity(), Vec3(v2, 0, 0));
  434. }
  435. }
  436. }