PhysicsTests.cpp 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931
  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 "LoggingBodyActivationListener.h"
  7. #include "LoggingContactListener.h"
  8. #include <Physics/Collision/Shape/BoxShape.h>
  9. TEST_SUITE("PhysicsTests")
  10. {
  11. // Gravity vector
  12. const Vec3 cGravity = Vec3(0.0f, -9.81f, 0.0f);
  13. // Test the test framework's helper functions
  14. TEST_CASE("TestPhysicsTestContext")
  15. {
  16. // Test that the Symplectic Euler integrator is close enough to the real value
  17. const float cSimulationTime = 2.0f;
  18. // For position: x = x0 + v0 * t + 1/2 * a * t^2
  19. const Vec3 cInitialPos(0.0f, 10.0f, 0.0f);
  20. PhysicsTestContext c;
  21. Vec3 simulated_pos = c.PredictPosition(cInitialPos, Vec3::sZero(), cGravity, cSimulationTime);
  22. Vec3 integrated_position = cInitialPos + 0.5f * cGravity * Square(cSimulationTime);
  23. CHECK_APPROX_EQUAL(integrated_position, simulated_pos, 0.2f);
  24. // For rotation
  25. const Quat cInitialRot(Quat::sRotation(Vec3::sAxisY(), 0.1f));
  26. const Vec3 cAngularAcceleration(0.0f, 2.0f, 0.0f);
  27. Quat simulated_rot = c.PredictOrientation(cInitialRot, Vec3::sZero(), cAngularAcceleration, cSimulationTime);
  28. Vec3 integrated_acceleration = 0.5f * cAngularAcceleration * Square(cSimulationTime);
  29. float integrated_acceleration_len = integrated_acceleration.Length();
  30. Quat integrated_rot = Quat::sRotation(integrated_acceleration / integrated_acceleration_len, integrated_acceleration_len) * cInitialRot;
  31. CHECK_APPROX_EQUAL(integrated_rot, simulated_rot, 0.02f);
  32. }
  33. TEST_CASE("TestPhysicsBodyLock")
  34. {
  35. PhysicsTestContext c;
  36. BodyID body1_id;
  37. {
  38. // Creata a box
  39. Body &body1 = c.CreateBox(Vec3::sZero(), Quat::sIdentity(), EMotionType::Static, EMotionQuality::Discrete, 0, Vec3::sReplicate(1.0f));
  40. body1_id = body1.GetID();
  41. CHECK(body1_id.GetIndex() == 0);
  42. CHECK(body1_id.GetSequenceNumber() == 1);
  43. // Create another box
  44. Body &body2 = c.CreateBox(Vec3::sZero(), Quat::sIdentity(), EMotionType::Static, EMotionQuality::Discrete, 0, Vec3::sReplicate(1.0f));
  45. BodyID body2_id = body2.GetID();
  46. CHECK(body2_id.GetIndex() == 1);
  47. CHECK(body2_id.GetSequenceNumber() == 1);
  48. // Check that we can lock the first box
  49. {
  50. BodyLockRead lock1(c.GetSystem()->GetBodyLockInterface(), body1_id);
  51. CHECK(lock1.Succeeded());
  52. CHECK(lock1.SucceededAndIsInBroadPhase());
  53. }
  54. // Remove the first box
  55. c.GetSystem()->GetBodyInterface().RemoveBody(body1_id);
  56. // Check that we can lock the first box
  57. {
  58. BodyLockWrite lock1(c.GetSystem()->GetBodyLockInterface(), body1_id);
  59. CHECK(lock1.Succeeded());
  60. CHECK_FALSE(lock1.SucceededAndIsInBroadPhase());
  61. }
  62. // Destroy the first box
  63. c.GetSystem()->GetBodyInterface().DestroyBody(body1_id);
  64. // Check that we can not lock the body anymore
  65. {
  66. BodyLockWrite lock1(c.GetSystem()->GetBodyLockInterface(), body1_id);
  67. CHECK_FALSE(lock1.Succeeded());
  68. CHECK_FALSE(lock1.SucceededAndIsInBroadPhase());
  69. }
  70. }
  71. // Create another box
  72. Body &body3 = c.CreateBox(Vec3::sZero(), Quat::sIdentity(), EMotionType::Static, EMotionQuality::Discrete, 0, Vec3::sReplicate(1.0f));
  73. BodyID body3_id = body3.GetID();
  74. CHECK(body3_id.GetIndex() == 0); // Check index reused
  75. CHECK(body3_id.GetSequenceNumber() == 2); // Check sequence number changed
  76. // Check that we can lock it
  77. {
  78. BodyLockRead lock3(c.GetSystem()->GetBodyLockInterface(), body3_id);
  79. CHECK(lock3.Succeeded());
  80. CHECK(lock3.SucceededAndIsInBroadPhase());
  81. }
  82. // Check that we can't lock the old body with the same body index anymore
  83. {
  84. BodyLockRead lock1(c.GetSystem()->GetBodyLockInterface(), body1_id);
  85. CHECK_FALSE(lock1.Succeeded());
  86. CHECK_FALSE(lock1.SucceededAndIsInBroadPhase());
  87. }
  88. }
  89. TEST_CASE("TestPhysicsBodyID")
  90. {
  91. {
  92. BodyID body_id(0);
  93. CHECK(body_id.GetIndex() == 0);
  94. CHECK(body_id.GetSequenceNumber() == 0);
  95. }
  96. {
  97. BodyID body_id(~BodyID::cBroadPhaseBit);
  98. CHECK(body_id.GetIndex() == BodyID::cMaxBodyIndex);
  99. CHECK(body_id.GetSequenceNumber() == BodyID::cMaxSequenceNumber);
  100. }
  101. }
  102. TEST_CASE("TestPhysicsBodyIDSequenceNumber")
  103. {
  104. PhysicsTestContext c(1.0f / 60.0f, 1, 1);
  105. BodyInterface &bi = c.GetBodyInterface();
  106. // Create a body and check it's id
  107. BodyID body0_id = c.CreateBox(Vec3::sZero(), Quat::sIdentity(), EMotionType::Dynamic, EMotionQuality::Discrete, Layers::MOVING, Vec3(1, 1, 1)).GetID();
  108. CHECK(body0_id == BodyID(0, 1)); // Body 0, sequence number 1
  109. // Check that the sequence numbers aren't reused until after 256 iterations
  110. for (int seq_no = 1; seq_no < 258; ++seq_no)
  111. {
  112. BodyID body1_id = c.CreateBox(Vec3::sZero(), Quat::sIdentity(), EMotionType::Dynamic, EMotionQuality::Discrete, Layers::MOVING, Vec3(1, 1, 1)).GetID();
  113. CHECK(body1_id == BodyID(1, uint8(seq_no))); // Body 1
  114. bi.RemoveBody(body1_id);
  115. bi.DestroyBody(body1_id);
  116. }
  117. bi.RemoveBody(body0_id);
  118. bi.DestroyBody(body0_id);
  119. }
  120. TEST_CASE("TestPhysicsOverrideMassAndInertia")
  121. {
  122. PhysicsTestContext c(1.0f / 60.0f, 1, 1);
  123. BodyInterface &bi = c.GetBodyInterface();
  124. const float cDensity = 1234.0f;
  125. const Vec3 cBoxExtent(2.0f, 4.0f, 6.0f);
  126. const float cExpectedMass = cBoxExtent.GetX() * cBoxExtent.GetY() * cBoxExtent.GetZ() * cDensity;
  127. // See: https://en.wikipedia.org/wiki/List_of_moments_of_inertia
  128. const Vec3 cSquaredExtents = Vec3(Square(cBoxExtent.GetY()) + Square(cBoxExtent.GetZ()), Square(cBoxExtent.GetX()) + Square(cBoxExtent.GetZ()), Square(cBoxExtent.GetX()) + Square(cBoxExtent.GetY()));
  129. const Vec3 cExpectedInertiaDiagonal = cExpectedMass / 12.0f * cSquaredExtents;
  130. Ref<BoxShapeSettings> shape_settings = new BoxShapeSettings(0.5f * cBoxExtent);
  131. shape_settings->SetDensity(cDensity);
  132. BodyCreationSettings body_settings(shape_settings, Vec3::sZero(), Quat::sIdentity(), EMotionType::Dynamic, Layers::MOVING);
  133. // Create body as is
  134. Body &b1 = *bi.CreateBody(body_settings);
  135. CHECK_APPROX_EQUAL(b1.GetMotionProperties()->GetInverseMass(), 1.0f / cExpectedMass);
  136. CHECK_APPROX_EQUAL(b1.GetMotionProperties()->GetInertiaRotation(), Quat::sIdentity());
  137. CHECK_APPROX_EQUAL(b1.GetMotionProperties()->GetInverseInertiaDiagonal(), cExpectedInertiaDiagonal.Reciprocal());
  138. // Override only the mass
  139. const float cOverriddenMass = 13.0f;
  140. const Vec3 cOverriddenMassInertiaDiagonal = cOverriddenMass / 12.0f * cSquaredExtents;
  141. body_settings.mOverrideMassProperties = EOverrideMassProperties::CalculateInertia;
  142. body_settings.mMassPropertiesOverride.mMass = cOverriddenMass;
  143. Body &b2 = *bi.CreateBody(body_settings);
  144. CHECK_APPROX_EQUAL(b2.GetMotionProperties()->GetInverseMass(), 1.0f / cOverriddenMass);
  145. CHECK_APPROX_EQUAL(b2.GetMotionProperties()->GetInertiaRotation(), Quat::sIdentity());
  146. CHECK_APPROX_EQUAL(b2.GetMotionProperties()->GetInverseInertiaDiagonal(), cOverriddenMassInertiaDiagonal.Reciprocal());
  147. // Override both the mass and inertia
  148. const Vec3 cOverriddenInertiaDiagonal(3.0f, 2.0f, 1.0f); // From big to small so that MassProperties::DecomposePrincipalMomentsOfInertia returns the same rotation as we put in
  149. const Quat cOverriddenInertiaRotation = Quat::sRotation(Vec3(1, 1, 1).Normalized(), 0.1f * JPH_PI);
  150. body_settings.mOverrideMassProperties = EOverrideMassProperties::MassAndInertiaProvided;
  151. body_settings.mMassPropertiesOverride.mInertia = Mat44::sRotation(cOverriddenInertiaRotation) * Mat44::sScale(cOverriddenInertiaDiagonal) * Mat44::sRotation(cOverriddenInertiaRotation.Inversed());
  152. Body &b3 = *bi.CreateBody(body_settings);
  153. CHECK_APPROX_EQUAL(b3.GetMotionProperties()->GetInverseMass(), 1.0f / cOverriddenMass);
  154. CHECK_APPROX_EQUAL(b3.GetMotionProperties()->GetInertiaRotation(), cOverriddenInertiaRotation);
  155. CHECK_APPROX_EQUAL(b3.GetMotionProperties()->GetInverseInertiaDiagonal(), cOverriddenInertiaDiagonal.Reciprocal());
  156. }
  157. // Test a box free falling under gravity
  158. static void TestPhysicsFreeFall(PhysicsTestContext &ioContext)
  159. {
  160. const Vec3 cInitialPos(0.0f, 10.0f, 0.0f);
  161. const float cSimulationTime = 2.0f;
  162. // Create box
  163. Body &body = ioContext.CreateBox(cInitialPos, Quat::sIdentity(), EMotionType::Dynamic, EMotionQuality::Discrete, Layers::MOVING, Vec3(1, 1, 1));
  164. CHECK_APPROX_EQUAL(cInitialPos, body.GetPosition());
  165. CHECK_APPROX_EQUAL(Vec3::sZero(), body.GetLinearVelocity());
  166. ioContext.Simulate(cSimulationTime);
  167. // Test resulting velocity (due to gravity)
  168. CHECK_APPROX_EQUAL(cSimulationTime * cGravity, body.GetLinearVelocity(), 1.0e-4f);
  169. // Test resulting position
  170. Vec3 expected_pos = ioContext.PredictPosition(cInitialPos, Vec3::sZero(), cGravity, cSimulationTime);
  171. CHECK_APPROX_EQUAL(expected_pos, body.GetPosition());
  172. }
  173. TEST_CASE("TestPhysicsFreeFall")
  174. {
  175. PhysicsTestContext c(1.0f / 60.0f, 1, 1);
  176. TestPhysicsFreeFall(c);
  177. }
  178. TEST_CASE("TestPhysicsFreeFallSubStep")
  179. {
  180. PhysicsTestContext c1(1.0f / 30.0f, 1, 2);
  181. TestPhysicsFreeFall(c1);
  182. PhysicsTestContext c2(1.0f / 15.0f, 1, 4);
  183. TestPhysicsFreeFall(c2);
  184. PhysicsTestContext c3(1.0f / 15.0f, 2, 2);
  185. TestPhysicsFreeFall(c3);
  186. PhysicsTestContext c4(1.0f / 30.0f, 2, 1);
  187. TestPhysicsFreeFall(c4);
  188. }
  189. // Test acceleration of a box with force applied
  190. static void TestPhysicsApplyForce(PhysicsTestContext &ioContext)
  191. {
  192. const Vec3 cInitialPos(0.0f, 10.0f, 0.0f);
  193. const Vec3 cAcceleration(2.0f, 0.0f, 0.0f);
  194. const float cSimulationTime = 2.0f;
  195. // Create box
  196. Body &body = ioContext.CreateBox(cInitialPos, Quat::sIdentity(), EMotionType::Dynamic, EMotionQuality::Discrete, Layers::MOVING, Vec3(1, 1, 1));
  197. CHECK_APPROX_EQUAL(cInitialPos, body.GetPosition());
  198. CHECK_APPROX_EQUAL(Vec3::sZero(), body.GetLinearVelocity());
  199. // Validate mass
  200. float mass = Cubed(2.0f) * 1000.0f; // Density * Volume
  201. CHECK_APPROX_EQUAL(1.0f / mass, body.GetMotionProperties()->GetInverseMass());
  202. // Simulate while applying force
  203. ioContext.Simulate(cSimulationTime, [&]() { body.AddForce(mass * cAcceleration); });
  204. // Test resulting velocity (due to gravity and applied force)
  205. CHECK_APPROX_EQUAL(cSimulationTime * (cGravity + cAcceleration), body.GetLinearVelocity(), 1.0e-4f);
  206. // Test resulting position
  207. Vec3 expected_pos = ioContext.PredictPosition(cInitialPos, Vec3::sZero(), cGravity + cAcceleration, cSimulationTime);
  208. CHECK_APPROX_EQUAL(expected_pos, body.GetPosition());
  209. }
  210. TEST_CASE("TestPhysicsApplyForce")
  211. {
  212. PhysicsTestContext c(1.0f / 60.0f, 1, 1);
  213. TestPhysicsApplyForce(c);
  214. }
  215. TEST_CASE("TestPhysicsApplyForceSubStep")
  216. {
  217. PhysicsTestContext c1(1.0f / 30.0f, 1, 2);
  218. TestPhysicsApplyForce(c1);
  219. PhysicsTestContext c2(1.0f / 15.0f, 1, 4);
  220. TestPhysicsApplyForce(c2);
  221. PhysicsTestContext c3(1.0f / 15.0f, 2, 2);
  222. TestPhysicsApplyForce(c3);
  223. PhysicsTestContext c4(1.0f / 30.0f, 2, 1);
  224. TestPhysicsApplyForce(c4);
  225. }
  226. // Test angular accelartion for a box by applying torque every frame
  227. static void TestPhysicsApplyTorque(PhysicsTestContext &ioContext)
  228. {
  229. const Vec3 cInitialPos(0.0f, 10.0f, 0.0f);
  230. const Vec3 cAngularAcceleration(0.0f, 2.0f, 0.0f);
  231. const float cSimulationTime = 2.0f;
  232. // Create box
  233. Body &body = ioContext.CreateBox(cInitialPos, Quat::sIdentity(), EMotionType::Dynamic, EMotionQuality::Discrete, Layers::MOVING, Vec3(1, 1, 1));
  234. CHECK_APPROX_EQUAL(Quat::sIdentity(), body.GetRotation());
  235. CHECK_APPROX_EQUAL(Vec3::sZero(), body.GetAngularVelocity());
  236. // Validate mass and inertia
  237. constexpr float mass = Cubed(2.0f) * 1000.0f; // Density * Volume
  238. CHECK_APPROX_EQUAL(1.0f / mass, body.GetMotionProperties()->GetInverseMass());
  239. constexpr float inertia = mass * 8.0f / 12.0f; // See: https://en.wikipedia.org/wiki/List_of_moments_of_inertia
  240. CHECK_APPROX_EQUAL(Mat44::sScale(1.0f / inertia), body.GetMotionProperties()->GetLocalSpaceInverseInertia());
  241. // Simulate while applying torque
  242. ioContext.Simulate(cSimulationTime, [&]() { body.AddTorque(inertia * cAngularAcceleration); });
  243. // Get resulting angular velocity
  244. CHECK_APPROX_EQUAL(cSimulationTime * cAngularAcceleration, body.GetAngularVelocity(), 1.0e-4f);
  245. // Test resulting rotation
  246. Quat expected_rot = ioContext.PredictOrientation(Quat::sIdentity(), Vec3::sZero(), cAngularAcceleration, cSimulationTime);
  247. CHECK_APPROX_EQUAL(expected_rot, body.GetRotation(), 1.0e-4f);
  248. }
  249. TEST_CASE("TestPhysicsApplyTorque")
  250. {
  251. PhysicsTestContext c(1.0f / 60.0f, 1, 1);
  252. TestPhysicsApplyTorque(c);
  253. }
  254. TEST_CASE("TestPhysicsApplyTorqueSubStep")
  255. {
  256. PhysicsTestContext c1(1.0f / 30.0f, 1, 2);
  257. TestPhysicsApplyTorque(c1);
  258. PhysicsTestContext c2(1.0f / 15.0f, 1, 4);
  259. TestPhysicsApplyTorque(c2);
  260. PhysicsTestContext c3(1.0f / 15.0f, 2, 2);
  261. TestPhysicsApplyTorque(c3);
  262. PhysicsTestContext c4(1.0f / 30.0f, 2, 1);
  263. TestPhysicsApplyTorque(c4);
  264. }
  265. // Let a sphere bounce on the floor with restition = 1
  266. static void TestPhysicsCollisionElastic(PhysicsTestContext &ioContext)
  267. {
  268. const float cSimulationTime = 1.0f;
  269. const Vec3 cDistanceTraveled = ioContext.PredictPosition(Vec3::sZero(), Vec3::sZero(), cGravity, cSimulationTime);
  270. const float cFloorHitEpsilon = 1.0e-4f; // Apply epsilon so that we're sure that the collision algorithm will find a collision
  271. const Vec3 cFloorHitPos(0.0f, 1.0f - cFloorHitEpsilon, 0.0f); // Sphere with radius 1 will hit floor when 1 above the floor
  272. const Vec3 cInitialPos = cFloorHitPos - cDistanceTraveled;
  273. // Create sphere
  274. ioContext.CreateFloor();
  275. Body &body = ioContext.CreateSphere(cInitialPos, 1.0f, EMotionType::Dynamic, EMotionQuality::Discrete, Layers::MOVING);
  276. body.SetRestitution(1.0f);
  277. // Simulate until at floor
  278. ioContext.Simulate(cSimulationTime);
  279. CHECK_APPROX_EQUAL(cFloorHitPos, body.GetPosition());
  280. // Assert collision not yet processed
  281. CHECK_APPROX_EQUAL(cSimulationTime * cGravity, body.GetLinearVelocity(), 1.0e-4f);
  282. // Simulate one more step to process the collision
  283. ioContext.Simulate(ioContext.GetDeltaTime());
  284. // Assert that collision is processed and velocity is reversed (which is required for a fully elastic collision).
  285. // Note that the physics engine will first apply gravity for the time step and then do collision detection,
  286. // hence the reflected velocity is actually 1 sub-step times gravity bigger than it would be in reality
  287. // For the remainder of cDeltaTime normal gravity will be applied
  288. float sub_step_delta_time = ioContext.GetSubStepDeltaTime();
  289. float remaining_step_time = ioContext.GetDeltaTime() - ioContext.GetSubStepDeltaTime();
  290. Vec3 reflected_velocity_after_sub_step = -(cSimulationTime + sub_step_delta_time) * cGravity;
  291. Vec3 reflected_velocity_after_full_step = reflected_velocity_after_sub_step + remaining_step_time * cGravity;
  292. CHECK_APPROX_EQUAL(reflected_velocity_after_full_step, body.GetLinearVelocity(), 1.0e-4f);
  293. // Body should have bounced back
  294. Vec3 pos_after_bounce_sub_step = cFloorHitPos + reflected_velocity_after_sub_step * sub_step_delta_time;
  295. Vec3 pos_after_bounce_full_step = ioContext.PredictPosition(pos_after_bounce_sub_step, reflected_velocity_after_sub_step, cGravity, remaining_step_time);
  296. CHECK_APPROX_EQUAL(pos_after_bounce_full_step, body.GetPosition());
  297. // Simulate same time, with a fully elastic body we should reach the initial position again
  298. // In our physics engine because of the velocity being too big we actually end up a bit higher than our initial position
  299. Vec3 expected_pos = ioContext.PredictPosition(pos_after_bounce_full_step, reflected_velocity_after_full_step, cGravity, cSimulationTime);
  300. ioContext.Simulate(cSimulationTime);
  301. CHECK_APPROX_EQUAL(expected_pos, body.GetPosition(), 1.0e-4f);
  302. CHECK(expected_pos.GetY() >= cInitialPos.GetY());
  303. }
  304. TEST_CASE("TestPhysicsCollisionElastic")
  305. {
  306. PhysicsTestContext c(1.0f / 60.0f, 1, 1);
  307. TestPhysicsCollisionElastic(c);
  308. }
  309. TEST_CASE("TestPhysicsCollisionElasticSubStep")
  310. {
  311. PhysicsTestContext c1(1.0f / 30.0f, 1, 2);
  312. TestPhysicsCollisionElastic(c1);
  313. PhysicsTestContext c2(1.0f / 15.0f, 1, 4);
  314. TestPhysicsCollisionElastic(c2);
  315. PhysicsTestContext c3(1.0f / 15.0f, 2, 2);
  316. TestPhysicsCollisionElastic(c3);
  317. PhysicsTestContext c4(1.0f / 30.0f, 2, 1);
  318. TestPhysicsCollisionElastic(c4);
  319. }
  320. // Let a sphere bounce on the floor with restitution = 0
  321. static void TestPhysicsCollisionInelastic(PhysicsTestContext &ioContext)
  322. {
  323. const float cSimulationTime = 1.0f;
  324. const Vec3 cDistanceTraveled = ioContext.PredictPosition(Vec3::sZero(), Vec3::sZero(), cGravity, cSimulationTime);
  325. const float cFloorHitEpsilon = 1.0e-4f; // Apply epsilon so that we're sure that the collision algorithm will find a collision
  326. const Vec3 cFloorHitPos(0.0f, 1.0f - cFloorHitEpsilon, 0.0f); // Sphere with radius 1 will hit floor when 1 above the floor
  327. const Vec3 cInitialPos = cFloorHitPos - cDistanceTraveled;
  328. // Create sphere
  329. ioContext.CreateFloor();
  330. Body &body = ioContext.CreateSphere(cInitialPos, 1.0f, EMotionType::Dynamic, EMotionQuality::Discrete, Layers::MOVING);
  331. body.SetRestitution(0.0f);
  332. // Simulate until at floor
  333. ioContext.Simulate(cSimulationTime);
  334. CHECK_APPROX_EQUAL(cFloorHitPos, body.GetPosition());
  335. // Assert collision not yet processed
  336. CHECK_APPROX_EQUAL(cSimulationTime * cGravity, body.GetLinearVelocity(), 1.0e-4f);
  337. // Simulate one more step to process the collision
  338. ioContext.Simulate(ioContext.GetDeltaTime());
  339. // Assert that all velocity was lost in the collision
  340. CHECK_APPROX_EQUAL(Vec3::sZero(), body.GetLinearVelocity(), 1.0e-4f);
  341. // Assert that we're on the floor
  342. CHECK_APPROX_EQUAL(cFloorHitPos, body.GetPosition(), 1.0e-4f);
  343. // Simulate some more to validate that we remain on the floor
  344. ioContext.Simulate(cSimulationTime);
  345. CHECK_APPROX_EQUAL(Vec3::sZero(), body.GetLinearVelocity(), 1.0e-4f);
  346. CHECK_APPROX_EQUAL(cFloorHitPos, body.GetPosition(), 1.0e-4f);
  347. }
  348. TEST_CASE("TestPhysicsCollisionInelastic")
  349. {
  350. PhysicsTestContext c(1.0f / 60.0f, 1, 1);
  351. TestPhysicsCollisionInelastic(c);
  352. }
  353. TEST_CASE("TestPhysicsCollisionInelasticSubStep")
  354. {
  355. PhysicsTestContext c1(1.0f / 30.0f, 1, 2);
  356. TestPhysicsCollisionInelastic(c1);
  357. PhysicsTestContext c2(1.0f / 15.0f, 1, 4);
  358. TestPhysicsCollisionInelastic(c2);
  359. PhysicsTestContext c3(1.0f / 15.0f, 2, 2);
  360. TestPhysicsCollisionInelastic(c3);
  361. PhysicsTestContext c4(1.0f / 30.0f, 2, 1);
  362. TestPhysicsCollisionInelastic(c4);
  363. }
  364. // Let box intersect with floor by cPenetrationSlop. It should not move, this is the maximum penetration allowed.
  365. static void TestPhysicsPenetrationSlop1(PhysicsTestContext &ioContext)
  366. {
  367. const float cPenetrationSlop = ioContext.GetSystem()->GetPhysicsSettings().mPenetrationSlop;
  368. const float cSimulationTime = 1.0f;
  369. const Vec3 cInitialPos(0.0f, 1.0f - cPenetrationSlop, 0.0f);
  370. // Create box, penetrating with floor
  371. ioContext.CreateFloor();
  372. Body &body = ioContext.CreateBox(cInitialPos, Quat::sIdentity(), EMotionType::Dynamic, EMotionQuality::Discrete, Layers::MOVING, Vec3(1, 1, 1));
  373. // Simulate
  374. ioContext.Simulate(cSimulationTime);
  375. // Test slop not resolved
  376. CHECK_APPROX_EQUAL(cInitialPos, body.GetPosition(), 1.0e-5f);
  377. CHECK_APPROX_EQUAL(Vec3::sZero(), body.GetLinearVelocity());
  378. CHECK_APPROX_EQUAL(Vec3::sZero(), body.GetAngularVelocity());
  379. }
  380. TEST_CASE("TestPhysicsPenetrationSlop1")
  381. {
  382. PhysicsTestContext c(1.0f / 60.0f, 1, 1);
  383. TestPhysicsPenetrationSlop1(c);
  384. }
  385. TEST_CASE("TestPhysicsPenetrationSlop1SubStep")
  386. {
  387. PhysicsTestContext c(1.0f / 30.0f, 1, 2);
  388. TestPhysicsPenetrationSlop1(c);
  389. PhysicsTestContext c2(1.0f / 30.0f, 2, 1);
  390. TestPhysicsPenetrationSlop1(c2);
  391. }
  392. // Let box intersect with floor with more than cPenetrationSlop. It should be resolved by SolvePositionConstraint until interpenetration is cPenetrationSlop.
  393. static void TestPhysicsPenetrationSlop2(PhysicsTestContext &ioContext)
  394. {
  395. const float cPenetrationSlop = ioContext.GetSystem()->GetPhysicsSettings().mPenetrationSlop;
  396. const float cSimulationTime = 1.0f;
  397. const Vec3 cInitialPos(0.0f, 1.0f - 2.0f * cPenetrationSlop, 0.0f);
  398. const Vec3 cFinalPos(0.0f, 1.0f - cPenetrationSlop, 0.0f);
  399. // Create box, penetrating with floor
  400. ioContext.CreateFloor();
  401. Body &body = ioContext.CreateBox(cInitialPos, Quat::sIdentity(), EMotionType::Dynamic, EMotionQuality::Discrete, Layers::MOVING, Vec3(1, 1, 1));
  402. // Simulate
  403. ioContext.Simulate(cSimulationTime);
  404. // Test resolved until slop
  405. CHECK_APPROX_EQUAL(cFinalPos, body.GetPosition(), 1.0e-5f);
  406. CHECK_APPROX_EQUAL(Vec3::sZero(), body.GetLinearVelocity());
  407. CHECK_APPROX_EQUAL(Vec3::sZero(), body.GetAngularVelocity());
  408. }
  409. TEST_CASE("TestPhysicsPenetrationSlop2")
  410. {
  411. PhysicsTestContext c(1.0f / 60.0f, 1, 1);
  412. TestPhysicsPenetrationSlop2(c);
  413. }
  414. TEST_CASE("TestPhysicsPenetrationSlop2SubStep")
  415. {
  416. PhysicsTestContext c(1.0f / 30.0f, 1, 2);
  417. TestPhysicsPenetrationSlop2(c);
  418. PhysicsTestContext c2(1.0f / 30.0f, 2, 1);
  419. TestPhysicsPenetrationSlop2(c2);
  420. }
  421. // Let box intersect with floor with less than cPenetrationSlop. Body should not move because SolveVelocityConstraint should reset velocity.
  422. static void TestPhysicsPenetrationSlop3(PhysicsTestContext &ioContext)
  423. {
  424. const float cPenetrationSlop = ioContext.GetSystem()->GetPhysicsSettings().mPenetrationSlop;
  425. const float cSimulationTime = 1.0f;
  426. const Vec3 cInitialPos(0.0f, 1.0f - 0.1f * cPenetrationSlop, 0.0f);
  427. // Create box, penetrating with floor
  428. ioContext.CreateFloor();
  429. Body &body = ioContext.CreateBox(cInitialPos, Quat::sIdentity(), EMotionType::Dynamic, EMotionQuality::Discrete, Layers::MOVING, Vec3(1, 1, 1));
  430. // Simulate
  431. ioContext.Simulate(cSimulationTime);
  432. // Test body remained static
  433. CHECK_APPROX_EQUAL(cInitialPos, body.GetPosition(), 1.0e-5f);
  434. CHECK_APPROX_EQUAL(Vec3::sZero(), body.GetLinearVelocity());
  435. CHECK_APPROX_EQUAL(Vec3::sZero(), body.GetAngularVelocity());
  436. }
  437. TEST_CASE("TestPhysicsPenetrationSlop3")
  438. {
  439. PhysicsTestContext c(1.0f / 60.0f, 1, 1);
  440. TestPhysicsPenetrationSlop3(c);
  441. }
  442. TEST_CASE("TestPhysicsPenetrationSlop3SubStep")
  443. {
  444. PhysicsTestContext c(1.0f / 30.0f, 1, 2);
  445. TestPhysicsPenetrationSlop3(c);
  446. PhysicsTestContext c2(1.0f / 30.0f, 2, 1);
  447. TestPhysicsPenetrationSlop3(c2);
  448. }
  449. TEST_CASE("TestPhysicsOutsideOfSpeculativeContactDistance")
  450. {
  451. PhysicsTestContext c(1.0f / 60.0f, 1, 1);
  452. Body &floor = c.CreateFloor();
  453. c.ZeroGravity();
  454. LoggingContactListener contact_listener;
  455. c.GetSystem()->SetContactListener(&contact_listener);
  456. // Create a box and a sphere just outside the speculative contact distance
  457. const float cSpeculativeContactDistance = c.GetSystem()->GetPhysicsSettings().mSpeculativeContactDistance;
  458. const float cDistanceAboveFloor = 1.1f * cSpeculativeContactDistance;
  459. const Vec3 cInitialPosBox(0, 1.0f + cDistanceAboveFloor, 0.0f);
  460. const Vec3 cInitialPosSphere = cInitialPosBox + Vec3(5, 0, 0);
  461. // Make it move 1 m per step down
  462. const Vec3 cVelocity(0, -1.0f / c.GetDeltaTime(), 0);
  463. Body &box = c.CreateBox(cInitialPosBox, Quat::sIdentity(), EMotionType::Dynamic, EMotionQuality::Discrete, Layers::MOVING, Vec3(1, 1, 1));
  464. box.SetLinearVelocity(cVelocity);
  465. Body &sphere = c.CreateSphere(cInitialPosSphere, 1.0f, EMotionType::Dynamic, EMotionQuality::Discrete, Layers::MOVING);
  466. sphere.SetLinearVelocity(cVelocity);
  467. // Simulate a step
  468. c.SimulateSingleStep();
  469. // Check that it is now penetrating the floor (collision should not have been detected as it is a discrete body and there was no collision initially)
  470. CHECK(contact_listener.GetEntryCount() == 0);
  471. CHECK_APPROX_EQUAL(box.GetPosition(), cInitialPosBox + cVelocity * c.GetDeltaTime());
  472. CHECK_APPROX_EQUAL(sphere.GetPosition(), cInitialPosSphere + cVelocity * c.GetDeltaTime());
  473. // Simulate a step
  474. c.SimulateSingleStep();
  475. // Check that the contacts are detected now
  476. CHECK(contact_listener.GetEntryCount() == 4); // 2 validates and 2 contacts
  477. CHECK(contact_listener.Contains(LoggingContactListener::EType::Validate, box.GetID(), floor.GetID()));
  478. CHECK(contact_listener.Contains(LoggingContactListener::EType::Add, box.GetID(), floor.GetID()));
  479. CHECK(contact_listener.Contains(LoggingContactListener::EType::Validate, sphere.GetID(), floor.GetID()));
  480. CHECK(contact_listener.Contains(LoggingContactListener::EType::Add, sphere.GetID(), floor.GetID()));
  481. }
  482. TEST_CASE("TestPhysicsInsideSpeculativeContactDistanceNoRestitution")
  483. {
  484. PhysicsTestContext c(1.0f / 60.0f, 1, 1);
  485. Body &floor = c.CreateFloor();
  486. c.ZeroGravity();
  487. LoggingContactListener contact_listener;
  488. c.GetSystem()->SetContactListener(&contact_listener);
  489. // Create a box and a sphere just inside the speculative contact distance
  490. const float cSpeculativeContactDistance = c.GetSystem()->GetPhysicsSettings().mSpeculativeContactDistance;
  491. const float cDistanceAboveFloor = 0.9f * cSpeculativeContactDistance;
  492. const Vec3 cInitialPosBox(0, 1.0f + cDistanceAboveFloor, 0.0f);
  493. const Vec3 cInitialPosSphere = cInitialPosBox + Vec3(5, 0, 0);
  494. // Make it move 1 m per step down
  495. const Vec3 cVelocity(0, -1.0f / c.GetDeltaTime(), 0);
  496. Body &box = c.CreateBox(cInitialPosBox, Quat::sIdentity(), EMotionType::Dynamic, EMotionQuality::Discrete, Layers::MOVING, Vec3(1, 1, 1));
  497. box.SetLinearVelocity(cVelocity);
  498. Body &sphere = c.CreateSphere(cInitialPosSphere, 1.0f, EMotionType::Dynamic, EMotionQuality::Discrete, Layers::MOVING);
  499. sphere.SetLinearVelocity(cVelocity);
  500. // Simulate a step
  501. c.SimulateSingleStep();
  502. // Check that it is now on the floor and that 2 collisions have been detected
  503. CHECK(contact_listener.GetEntryCount() == 4); // 2 validates and 2 contacts
  504. CHECK(contact_listener.Contains(LoggingContactListener::EType::Validate, box.GetID(), floor.GetID()));
  505. CHECK(contact_listener.Contains(LoggingContactListener::EType::Add, box.GetID(), floor.GetID()));
  506. CHECK(contact_listener.Contains(LoggingContactListener::EType::Validate, sphere.GetID(), floor.GetID()));
  507. CHECK(contact_listener.Contains(LoggingContactListener::EType::Add, sphere.GetID(), floor.GetID()));
  508. contact_listener.Clear();
  509. // Velocity should have been reduced to exactly hit the floor in this step
  510. const Vec3 cExpectedVelocity(0, -cDistanceAboveFloor / c.GetDeltaTime(), 0);
  511. // Box collision is less accurate than sphere as it hits with 4 corners so there's some floating point precision loss in the calculation
  512. CHECK_APPROX_EQUAL(box.GetPosition(), Vec3(0, 1, 0), 1.0e-3f);
  513. CHECK_APPROX_EQUAL(box.GetLinearVelocity(), cExpectedVelocity, 0.05f);
  514. CHECK_APPROX_EQUAL(box.GetAngularVelocity(), Vec3::sZero(), 1.0e-2f);
  515. // Sphere has only 1 contact point so is much more accurate
  516. CHECK_APPROX_EQUAL(sphere.GetPosition(), Vec3(5, 1, 0));
  517. CHECK_APPROX_EQUAL(sphere.GetLinearVelocity(), cExpectedVelocity, 1.0e-4f);
  518. CHECK_APPROX_EQUAL(sphere.GetAngularVelocity(), Vec3::sZero(), 1.0e-4f);
  519. // Simulate a step
  520. c.SimulateSingleStep();
  521. // Check that the contacts persisted
  522. CHECK(contact_listener.GetEntryCount() >= 2); // 2 persist and possibly 2 validates depending on if the cache got reused
  523. CHECK(contact_listener.Contains(LoggingContactListener::EType::Persist, box.GetID(), floor.GetID()));
  524. CHECK(contact_listener.Contains(LoggingContactListener::EType::Persist, sphere.GetID(), floor.GetID()));
  525. // Box should have come to rest
  526. CHECK_APPROX_EQUAL(box.GetPosition(), Vec3(0, 1, 0), 1.0e-3f);
  527. CHECK_APPROX_EQUAL(box.GetLinearVelocity(), Vec3::sZero(), 0.05f);
  528. CHECK_APPROX_EQUAL(box.GetAngularVelocity(), Vec3::sZero(), 1.0e-2f);
  529. // Sphere should have come to rest
  530. CHECK_APPROX_EQUAL(sphere.GetPosition(), Vec3(5, 1, 0), 1.0e-4f);
  531. CHECK_APPROX_EQUAL(sphere.GetLinearVelocity(), Vec3::sZero(), 1.0e-4f);
  532. CHECK_APPROX_EQUAL(sphere.GetAngularVelocity(), Vec3::sZero(), 1.0e-4f);
  533. }
  534. TEST_CASE("TestPhysicsInsideSpeculativeContactDistanceWithRestitution")
  535. {
  536. PhysicsTestContext c(1.0f / 60.0f, 1, 1);
  537. Body &floor = c.CreateFloor();
  538. c.ZeroGravity();
  539. LoggingContactListener contact_listener;
  540. c.GetSystem()->SetContactListener(&contact_listener);
  541. // Create a box and a sphere just inside the speculative contact distance
  542. const float cSpeculativeContactDistance = c.GetSystem()->GetPhysicsSettings().mSpeculativeContactDistance;
  543. const float cDistanceAboveFloor = 0.9f * cSpeculativeContactDistance;
  544. const Vec3 cInitialPosBox(0, 1.0f + cDistanceAboveFloor, 0.0f);
  545. const Vec3 cInitialPosSphere = cInitialPosBox + Vec3(5, 0, 0);
  546. // Make it move 1 m per step down
  547. const Vec3 cVelocity(0, -1.0f / c.GetDeltaTime(), 0);
  548. Body &box = c.CreateBox(cInitialPosBox, Quat::sIdentity(), EMotionType::Dynamic, EMotionQuality::Discrete, Layers::MOVING, Vec3(1, 1, 1));
  549. box.SetLinearVelocity(cVelocity);
  550. box.SetRestitution(1.0f);
  551. Body &sphere = c.CreateSphere(cInitialPosSphere, 1.0f, EMotionType::Dynamic, EMotionQuality::Discrete, Layers::MOVING);
  552. sphere.SetLinearVelocity(cVelocity);
  553. sphere.SetRestitution(1.0f);
  554. // Simulate a step
  555. c.SimulateSingleStep();
  556. // Check that it has triggered contact points and has bounced from it's initial position (effectively travelling the extra distance to the floor and back for free)
  557. CHECK(contact_listener.GetEntryCount() == 4); // 2 validates and 2 contacts
  558. CHECK(contact_listener.Contains(LoggingContactListener::EType::Validate, box.GetID(), floor.GetID()));
  559. CHECK(contact_listener.Contains(LoggingContactListener::EType::Add, box.GetID(), floor.GetID()));
  560. CHECK(contact_listener.Contains(LoggingContactListener::EType::Validate, sphere.GetID(), floor.GetID()));
  561. CHECK(contact_listener.Contains(LoggingContactListener::EType::Add, sphere.GetID(), floor.GetID()));
  562. contact_listener.Clear();
  563. // Box collision is less accurate than sphere as it hits with 4 corners so there's some floating point precision loss in the calculation
  564. CHECK_APPROX_EQUAL(box.GetPosition(), cInitialPosBox - cVelocity * c.GetDeltaTime(), 0.01f);
  565. CHECK_APPROX_EQUAL(box.GetLinearVelocity(), -cVelocity, 0.1f);
  566. CHECK_APPROX_EQUAL(box.GetAngularVelocity(), Vec3::sZero(), 0.02f);
  567. // Sphere has only 1 contact point so is much more accurate
  568. CHECK_APPROX_EQUAL(sphere.GetPosition(), cInitialPosSphere - cVelocity * c.GetDeltaTime(), 1.0e-5f);
  569. CHECK_APPROX_EQUAL(sphere.GetLinearVelocity(), -cVelocity, 2.0e-4f);
  570. CHECK_APPROX_EQUAL(sphere.GetAngularVelocity(), Vec3::sZero(), 2.0e-4f);
  571. // Simulate a step
  572. c.SimulateSingleStep();
  573. // Check that all contact points are removed
  574. CHECK(contact_listener.GetEntryCount() == 2); // 2 removes
  575. CHECK(contact_listener.Contains(LoggingContactListener::EType::Remove, box.GetID(), floor.GetID()));
  576. CHECK(contact_listener.Contains(LoggingContactListener::EType::Remove, sphere.GetID(), floor.GetID()));
  577. }
  578. TEST_CASE("TestPhysicsInsideSpeculativeContactDistanceMovingAway")
  579. {
  580. PhysicsTestContext c(1.0f / 60.0f, 1, 1);
  581. Body &floor = c.CreateFloor();
  582. c.ZeroGravity();
  583. LoggingContactListener contact_listener;
  584. c.GetSystem()->SetContactListener(&contact_listener);
  585. // Create a box and a sphere just inside the speculative contact distance
  586. const float cSpeculativeContactDistance = c.GetSystem()->GetPhysicsSettings().mSpeculativeContactDistance;
  587. const float cDistanceAboveFloor = 0.9f * cSpeculativeContactDistance;
  588. const Vec3 cInitialPosBox(0, 1.0f + cDistanceAboveFloor, 0.0f);
  589. const Vec3 cInitialPosSphere = cInitialPosBox + Vec3(5, 0, 0);
  590. // Make it move 1 m per step up
  591. const Vec3 cVelocity(0, 1.0f / c.GetDeltaTime(), 0);
  592. Body &box = c.CreateBox(cInitialPosBox, Quat::sIdentity(), EMotionType::Dynamic, EMotionQuality::Discrete, Layers::MOVING, Vec3(1, 1, 1));
  593. box.SetLinearVelocity(cVelocity);
  594. box.SetRestitution(1.0f);
  595. Body &sphere = c.CreateSphere(cInitialPosSphere, 1.0f, EMotionType::Dynamic, EMotionQuality::Discrete, Layers::MOVING);
  596. sphere.SetLinearVelocity(cVelocity);
  597. sphere.SetRestitution(1.0f);
  598. // Simulate a step
  599. c.SimulateSingleStep();
  600. // Check that it has triggered contact points (note that this is wrong since the object never touched the floor but that's the downside of the speculative contacts -> you'll get an incorrect collision callback)
  601. CHECK(contact_listener.GetEntryCount() == 4); // 2 validates and 2 contacts
  602. CHECK(contact_listener.Contains(LoggingContactListener::EType::Validate, box.GetID(), floor.GetID()));
  603. CHECK(contact_listener.Contains(LoggingContactListener::EType::Add, box.GetID(), floor.GetID()));
  604. CHECK(contact_listener.Contains(LoggingContactListener::EType::Validate, sphere.GetID(), floor.GetID()));
  605. CHECK(contact_listener.Contains(LoggingContactListener::EType::Add, sphere.GetID(), floor.GetID()));
  606. contact_listener.Clear();
  607. // Box should have moved unimpeded
  608. CHECK_APPROX_EQUAL(box.GetPosition(), cInitialPosBox + cVelocity * c.GetDeltaTime());
  609. CHECK_APPROX_EQUAL(box.GetLinearVelocity(), cVelocity);
  610. CHECK_APPROX_EQUAL(box.GetAngularVelocity(), Vec3::sZero());
  611. // Sphere should have moved unimpeded
  612. CHECK_APPROX_EQUAL(sphere.GetPosition(), cInitialPosSphere + cVelocity * c.GetDeltaTime());
  613. CHECK_APPROX_EQUAL(sphere.GetLinearVelocity(), cVelocity);
  614. CHECK_APPROX_EQUAL(sphere.GetAngularVelocity(), Vec3::sZero());
  615. // Simulate a step
  616. c.SimulateSingleStep();
  617. // Check that all contact points are removed
  618. CHECK(contact_listener.GetEntryCount() == 2); // 2 removes
  619. CHECK(contact_listener.Contains(LoggingContactListener::EType::Remove, box.GetID(), floor.GetID()));
  620. CHECK(contact_listener.Contains(LoggingContactListener::EType::Remove, sphere.GetID(), floor.GetID()));
  621. }
  622. static void TestPhysicsActivationDeactivation(PhysicsTestContext &ioContext)
  623. {
  624. const float cPenetrationSlop = ioContext.GetSystem()->GetPhysicsSettings().mPenetrationSlop;
  625. // Install activation listener
  626. LoggingBodyActivationListener activation_listener;
  627. ioContext.GetSystem()->SetBodyActivationListener(&activation_listener);
  628. // Create floor
  629. Body &floor = ioContext.CreateBox(Vec3(0, -1, 0), Quat::sIdentity(), EMotionType::Static, EMotionQuality::Discrete, Layers::NON_MOVING, Vec3(100, 1, 100));
  630. CHECK(!floor.IsActive());
  631. // Create inactive box
  632. Body &box = ioContext.CreateBox(Vec3(0, 5, 0), Quat::sIdentity(), EMotionType::Dynamic, EMotionQuality::Discrete, Layers::MOVING, Vec3::sReplicate(0.5f), EActivation::DontActivate);
  633. CHECK(!box.IsActive());
  634. CHECK(activation_listener.GetEntryCount() == 0);
  635. // Box should not activate by itself
  636. ioContext.Simulate(1.0f);
  637. CHECK(box.GetPosition() == Vec3(0, 5, 0));
  638. CHECK(!box.IsActive());
  639. CHECK(activation_listener.GetEntryCount() == 0);
  640. // Activate the body and validate it is active now
  641. ioContext.GetBodyInterface().ActivateBody(box.GetID());
  642. CHECK(box.IsActive());
  643. CHECK(box.GetLinearVelocity().IsNearZero());
  644. CHECK(activation_listener.GetEntryCount() == 1);
  645. CHECK(activation_listener.Contains(LoggingBodyActivationListener::EType::Activated, box.GetID()));
  646. activation_listener.Clear();
  647. // Do a single step and check that the body is still active and has gained some velocity
  648. ioContext.SimulateSingleStep();
  649. CHECK(box.IsActive());
  650. CHECK(activation_listener.GetEntryCount() == 0);
  651. CHECK(!box.GetLinearVelocity().IsNearZero());
  652. // Simulate 5 seconds and check it has settled on the floor and is no longer active
  653. ioContext.Simulate(5.0f);
  654. CHECK_APPROX_EQUAL(box.GetPosition(), Vec3(0, 0.5f, 0), 1.1f * cPenetrationSlop);
  655. CHECK_APPROX_EQUAL(box.GetLinearVelocity(), Vec3::sZero());
  656. CHECK_APPROX_EQUAL(box.GetAngularVelocity(), Vec3::sZero());
  657. CHECK(!box.IsActive());
  658. CHECK(activation_listener.GetEntryCount() == 1);
  659. CHECK(activation_listener.Contains(LoggingBodyActivationListener::EType::Deactivated, box.GetID()));
  660. }
  661. TEST_CASE("TestPhysicsActivationDeactivation")
  662. {
  663. PhysicsTestContext c(1.0f / 60.0f, 1, 1);
  664. TestPhysicsActivationDeactivation(c);
  665. PhysicsTestContext c2(1.0f / 30.0f, 1, 2);
  666. TestPhysicsActivationDeactivation(c2);
  667. PhysicsTestContext c3(1.0f / 30.0f, 2, 1);
  668. TestPhysicsActivationDeactivation(c3);
  669. }
  670. // A test that checks that a row of penetrating boxes will all activate and handle collision in 1 frame so that active bodies cannot tunnel through inactive bodies
  671. static void TestPhysicsActivateDuringStep(PhysicsTestContext &ioContext, bool inReverseCreate)
  672. {
  673. const float cPenetrationSlop = ioContext.GetSystem()->GetPhysicsSettings().mPenetrationSlop;
  674. const int cNumBodies = 10;
  675. const float cBoxExtent = 0.5f;
  676. PhysicsSystem *system = ioContext.GetSystem();
  677. BodyInterface &bi = ioContext.GetBodyInterface();
  678. LoggingBodyActivationListener activation_listener;
  679. system->SetBodyActivationListener(&activation_listener);
  680. LoggingContactListener contact_listener;
  681. system->SetContactListener(&contact_listener);
  682. // Create a row of penetrating boxes. Since some of the algorithms rely on body index, we create them normally and reversed to test both cases
  683. BodyIDVector body_ids;
  684. if (inReverseCreate)
  685. for (int i = cNumBodies - 1; i >= 0; --i)
  686. body_ids.insert(body_ids.begin(), ioContext.CreateBox(Vec3(i * (2.0f * cBoxExtent - cPenetrationSlop), 0, 0), Quat::sIdentity(), EMotionType::Dynamic, EMotionQuality::Discrete, Layers::MOVING, Vec3::sReplicate(cBoxExtent), EActivation::DontActivate).GetID());
  687. else
  688. for (int i = 0; i < cNumBodies; ++i)
  689. body_ids.push_back(ioContext.CreateBox(Vec3(i * (2.0f * cBoxExtent - cPenetrationSlop), 0, 0), Quat::sIdentity(), EMotionType::Dynamic, EMotionQuality::Discrete, Layers::MOVING, Vec3::sReplicate(0.5f), EActivation::DontActivate).GetID());
  690. // Test that nothing is active yet
  691. CHECK(activation_listener.GetEntryCount() == 0);
  692. CHECK(contact_listener.GetEntryCount() == 0);
  693. for (BodyID id : body_ids)
  694. CHECK(!bi.IsActive(id));
  695. // Activate the left most box and give it a velocity that is high enough to make it tunnel through the second box in a single step
  696. bi.SetLinearVelocity(body_ids.front(), Vec3(500, 0, 0));
  697. // Test that only the left most box is active
  698. CHECK(activation_listener.GetEntryCount() == 1);
  699. CHECK(contact_listener.GetEntryCount() == 0);
  700. CHECK(bi.IsActive(body_ids.front()));
  701. CHECK(activation_listener.Contains(LoggingBodyActivationListener::EType::Activated, body_ids.front()));
  702. for (int i = 1; i < cNumBodies; ++i)
  703. CHECK(!bi.IsActive(body_ids[i]));
  704. activation_listener.Clear();
  705. // Step the world
  706. ioContext.SimulateSingleStep();
  707. // Other bodies should now be awake and each body should only collide with its neighbour
  708. CHECK(activation_listener.GetEntryCount() == cNumBodies - 1);
  709. CHECK(contact_listener.GetEntryCount() == 2 * (cNumBodies - 1));
  710. for (int i = 0; i < cNumBodies; ++i)
  711. {
  712. BodyID id = body_ids[i];
  713. // Check body is active
  714. CHECK(bi.IsActive(id));
  715. // Check that body moved to the right
  716. CHECK(bi.GetPosition(id).GetX() > i * (2.0f * cBoxExtent - cPenetrationSlop));
  717. }
  718. for (int i = 1; i < cNumBodies; ++i)
  719. {
  720. BodyID id1 = body_ids[i - 1];
  721. BodyID id2 = body_ids[i];
  722. // Check that we received activation events for each body
  723. CHECK(activation_listener.Contains(LoggingBodyActivationListener::EType::Activated, id2));
  724. // Check that we received a validate and an add for each body pair
  725. int validate = contact_listener.Find(LoggingContactListener::EType::Validate, id1, id2);
  726. CHECK(validate >= 0);
  727. int add = contact_listener.Find(LoggingContactListener::EType::Add, id1, id2);
  728. CHECK(add >= 0);
  729. CHECK(add > validate);
  730. // Check that bodies did not tunnel through each other
  731. CHECK(bi.GetPosition(id1).GetX() < bi.GetPosition(id2).GetX());
  732. }
  733. }
  734. TEST_CASE("TestPhysicsActivateDuringStep")
  735. {
  736. PhysicsTestContext c(1.0f / 60.0f, 1, 1);
  737. TestPhysicsActivateDuringStep(c, false);
  738. PhysicsTestContext c2(1.0f / 60.0f, 1, 1);
  739. TestPhysicsActivateDuringStep(c2, true);
  740. }
  741. }