ContactListenerTests.cpp 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677
  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 restitution = 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 restitution = 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. TEST_CASE("TestInfiniteMassOverride")
  437. {
  438. for (bool do_swap : { false, true })
  439. for (EMotionQuality quality : { EMotionQuality::Discrete, EMotionQuality::LinearCast })
  440. {
  441. // A contact listener that makes a body have infinite mass
  442. class ContactListenerImpl : public ContactListener
  443. {
  444. public:
  445. ContactListenerImpl(const BodyID &inBodyID) : mBodyID(inBodyID) { }
  446. virtual void OnContactAdded(const Body &inBody1, const Body &inBody2, const ContactManifold &inManifold, ContactSettings &ioSettings) override
  447. {
  448. if (mBodyID == inBody1.GetID())
  449. {
  450. ioSettings.mInvInertiaScale1 = 0.0f;
  451. ioSettings.mInvMassScale1 = 0.0f;
  452. }
  453. else if (mBodyID == inBody2.GetID())
  454. {
  455. ioSettings.mInvInertiaScale2 = 0.0f;
  456. ioSettings.mInvMassScale2 = 0.0f;
  457. }
  458. }
  459. virtual void OnContactPersisted(const Body &inBody1, const Body &inBody2, const ContactManifold &inManifold, ContactSettings &ioSettings) override
  460. {
  461. OnContactAdded(inBody1, inBody2, inManifold, ioSettings);
  462. }
  463. private:
  464. BodyID mBodyID;
  465. };
  466. PhysicsTestContext c;
  467. c.ZeroGravity();
  468. // Create a box
  469. const RVec3 cInitialBoxPos(0, 2, 0);
  470. BodyCreationSettings box_settings(new BoxShape(Vec3::sReplicate(2)), cInitialBoxPos, Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING);
  471. box_settings.mOverrideMassProperties = EOverrideMassProperties::CalculateInertia;
  472. box_settings.mMassPropertiesOverride.mMass = 1.0f;
  473. // Create a sphere
  474. BodyCreationSettings sphere_settings(new SphereShape(2), RVec3(30, 2, 0), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING);
  475. sphere_settings.mLinearVelocity = Vec3(-100, 0, 0);
  476. sphere_settings.mOverrideMassProperties = EOverrideMassProperties::CalculateInertia;
  477. sphere_settings.mMassPropertiesOverride.mMass = 10.0f;
  478. sphere_settings.mRestitution = 0.1f;
  479. sphere_settings.mLinearDamping = 0.0f;
  480. sphere_settings.mMotionQuality = quality;
  481. BodyID box_id, sphere_id;
  482. if (do_swap)
  483. {
  484. // Swap the bodies so that the contact listener will receive the bodies in the opposite order
  485. sphere_id = c.GetBodyInterface().CreateAndAddBody(sphere_settings, EActivation::Activate);
  486. box_id = c.GetBodyInterface().CreateAndAddBody(box_settings, EActivation::Activate);
  487. }
  488. else
  489. {
  490. box_id = c.GetBodyInterface().CreateAndAddBody(box_settings, EActivation::Activate);
  491. sphere_id = c.GetBodyInterface().CreateAndAddBody(sphere_settings, EActivation::Activate);
  492. }
  493. // Add listener that will make the box have infinite mass
  494. ContactListenerImpl listener(box_id);
  495. c.GetSystem()->SetContactListener(&listener);
  496. // Simulate
  497. const float cSimulationTime = 0.3f;
  498. c.Simulate(cSimulationTime);
  499. // Check that the box didn't move
  500. BodyInterface &bi = c.GetBodyInterface();
  501. CHECK(bi.GetPosition(box_id) == cInitialBoxPos);
  502. CHECK(bi.GetLinearVelocity(box_id) == Vec3::sZero());
  503. CHECK(bi.GetAngularVelocity(box_id) == Vec3::sZero());
  504. // Check that the sphere bounced off the box
  505. CHECK_APPROX_EQUAL(bi.GetLinearVelocity(sphere_id), -sphere_settings.mLinearVelocity * sphere_settings.mRestitution);
  506. }
  507. }
  508. TEST_CASE("TestCollideKinematicVsNonDynamic")
  509. {
  510. for (EMotionType m1 = EMotionType::Static; m1 <= EMotionType::Dynamic; m1 = EMotionType((int)m1 + 1))
  511. for (int allow1 = 0; allow1 < 2; ++allow1)
  512. for (int active1 = 0; active1 < 2; ++active1)
  513. for (EMotionType m2 = EMotionType::Static; m2 <= EMotionType::Dynamic; m2 = EMotionType((int)m2 + 1))
  514. for (int allow2 = 0; allow2 < 2; ++allow2)
  515. for (int active2 = 0; active2 < 2; ++active2)
  516. if ((m1 != EMotionType::Static && active1) || (m2 != EMotionType::Static && active2))
  517. {
  518. PhysicsTestContext c;
  519. c.ZeroGravity();
  520. const Vec3 cInitialVelocity1(m1 != EMotionType::Static && active1 != 0? 1.0f : 0.0f, 0, 0);
  521. const Vec3 cInitialVelocity2(m2 != EMotionType::Static && active2 != 0? -1.0f : 0.0f, 0, 0);
  522. // Create two spheres that are colliding initially
  523. BodyCreationSettings bcs(new SphereShape(1.0f), RVec3::sZero(), Quat::sIdentity(), m1, m1 != EMotionType::Static? Layers::MOVING : Layers::NON_MOVING);
  524. bcs.mPosition = RVec3(-0.5_r, 0, 0);
  525. bcs.mLinearVelocity = cInitialVelocity1;
  526. bcs.mCollideKinematicVsNonDynamic = allow1 != 0;
  527. Body &body1 = *c.GetBodyInterface().CreateBody(bcs);
  528. c.GetBodyInterface().AddBody(body1.GetID(), active1 != 0? EActivation::Activate : EActivation::DontActivate);
  529. bcs.mMotionType = m2;
  530. bcs.mObjectLayer = m2 != EMotionType::Static? Layers::MOVING : Layers::NON_MOVING;
  531. bcs.mPosition = RVec3(0.5_r, 0, 0);
  532. bcs.mLinearVelocity = cInitialVelocity2;
  533. bcs.mCollideKinematicVsNonDynamic = allow2 != 0;
  534. Body &body2 = *c.GetBodyInterface().CreateBody(bcs);
  535. c.GetBodyInterface().AddBody(body2.GetID(), active2 != 0? EActivation::Activate : EActivation::DontActivate);
  536. // Set listener
  537. LoggingContactListener listener;
  538. c.GetSystem()->SetContactListener(&listener);
  539. // Step
  540. c.SimulateSingleStep();
  541. if ((allow1 || allow2) // In this case we always get a callback
  542. || (m1 == EMotionType::Dynamic || m2 == EMotionType::Dynamic)) // Otherwise we only get a callback when one of the bodies is dynamic
  543. {
  544. // Check that we received a callback
  545. CHECK(listener.GetEntryCount() == 2);
  546. CHECK(listener.Contains(EType::Validate, body1.GetID(), body2.GetID()));
  547. CHECK(listener.Contains(EType::Add, body1.GetID(), body2.GetID()));
  548. }
  549. else
  550. {
  551. // No collision events should have been received
  552. CHECK(listener.GetEntryCount() == 0);
  553. }
  554. // Velocities should only change if the body is dynamic
  555. if (m1 == EMotionType::Dynamic)
  556. {
  557. CHECK(body1.GetLinearVelocity() != cInitialVelocity1);
  558. CHECK(body1.IsActive());
  559. }
  560. else
  561. CHECK(body1.GetLinearVelocity() == cInitialVelocity1);
  562. if (m2 == EMotionType::Dynamic)
  563. {
  564. CHECK(body2.GetLinearVelocity() != cInitialVelocity2);
  565. CHECK(body2.IsActive());
  566. }
  567. else
  568. CHECK(body2.GetLinearVelocity() == cInitialVelocity2);
  569. }
  570. }
  571. }