PhysicsTests.cpp 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961
  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(2.0f / 60.0f, 1, 2);
  181. TestPhysicsFreeFall(c1);
  182. PhysicsTestContext c2(4.0f / 60.0f, 1, 4);
  183. TestPhysicsFreeFall(c2);
  184. PhysicsTestContext c3(4.0f / 60.0f, 2, 2);
  185. TestPhysicsFreeFall(c3);
  186. PhysicsTestContext c4(2.0f / 60.0f, 2, 1);
  187. TestPhysicsFreeFall(c4);
  188. PhysicsTestContext c5(8.0f / 60.0f, 4, 2);
  189. TestPhysicsFreeFall(c5);
  190. PhysicsTestContext c6(4.0f / 60.0f, 4, 1);
  191. TestPhysicsFreeFall(c6);
  192. }
  193. // Test acceleration of a box with force applied
  194. static void TestPhysicsApplyForce(PhysicsTestContext &ioContext)
  195. {
  196. const Vec3 cInitialPos(0.0f, 10.0f, 0.0f);
  197. const Vec3 cAcceleration(2.0f, 0.0f, 0.0f);
  198. const float cSimulationTime = 2.0f;
  199. // Create box
  200. Body &body = ioContext.CreateBox(cInitialPos, Quat::sIdentity(), EMotionType::Dynamic, EMotionQuality::Discrete, Layers::MOVING, Vec3(1, 1, 1));
  201. CHECK_APPROX_EQUAL(cInitialPos, body.GetPosition());
  202. CHECK_APPROX_EQUAL(Vec3::sZero(), body.GetLinearVelocity());
  203. // Validate mass
  204. float mass = Cubed(2.0f) * 1000.0f; // Density * Volume
  205. CHECK_APPROX_EQUAL(1.0f / mass, body.GetMotionProperties()->GetInverseMass());
  206. // Simulate while applying force
  207. ioContext.Simulate(cSimulationTime, [&]() { body.AddForce(mass * cAcceleration); });
  208. // Test resulting velocity (due to gravity and applied force)
  209. CHECK_APPROX_EQUAL(cSimulationTime * (cGravity + cAcceleration), body.GetLinearVelocity(), 1.0e-4f);
  210. // Test resulting position
  211. Vec3 expected_pos = ioContext.PredictPosition(cInitialPos, Vec3::sZero(), cGravity + cAcceleration, cSimulationTime);
  212. CHECK_APPROX_EQUAL(expected_pos, body.GetPosition());
  213. }
  214. TEST_CASE("TestPhysicsApplyForce")
  215. {
  216. PhysicsTestContext c(1.0f / 60.0f, 1, 1);
  217. TestPhysicsApplyForce(c);
  218. }
  219. TEST_CASE("TestPhysicsApplyForceSubStep")
  220. {
  221. PhysicsTestContext c1(2.0f / 60.0f, 1, 2);
  222. TestPhysicsApplyForce(c1);
  223. PhysicsTestContext c2(4.0f / 60.0f, 1, 4);
  224. TestPhysicsApplyForce(c2);
  225. PhysicsTestContext c3(4.0f / 60.0f, 2, 2);
  226. TestPhysicsApplyForce(c3);
  227. PhysicsTestContext c4(2.0f / 60.0f, 2, 1);
  228. TestPhysicsApplyForce(c4);
  229. PhysicsTestContext c5(8.0f / 60.0f, 4, 2);
  230. TestPhysicsApplyForce(c5);
  231. PhysicsTestContext c6(4.0f / 60.0f, 4, 1);
  232. TestPhysicsApplyForce(c6);
  233. }
  234. // Test angular accelartion for a box by applying torque every frame
  235. static void TestPhysicsApplyTorque(PhysicsTestContext &ioContext)
  236. {
  237. const Vec3 cInitialPos(0.0f, 10.0f, 0.0f);
  238. const Vec3 cAngularAcceleration(0.0f, 2.0f, 0.0f);
  239. const float cSimulationTime = 2.0f;
  240. // Create box
  241. Body &body = ioContext.CreateBox(cInitialPos, Quat::sIdentity(), EMotionType::Dynamic, EMotionQuality::Discrete, Layers::MOVING, Vec3(1, 1, 1));
  242. CHECK_APPROX_EQUAL(Quat::sIdentity(), body.GetRotation());
  243. CHECK_APPROX_EQUAL(Vec3::sZero(), body.GetAngularVelocity());
  244. // Validate mass and inertia
  245. constexpr float mass = Cubed(2.0f) * 1000.0f; // Density * Volume
  246. CHECK_APPROX_EQUAL(1.0f / mass, body.GetMotionProperties()->GetInverseMass());
  247. constexpr float inertia = mass * 8.0f / 12.0f; // See: https://en.wikipedia.org/wiki/List_of_moments_of_inertia
  248. CHECK_APPROX_EQUAL(Mat44::sScale(1.0f / inertia), body.GetMotionProperties()->GetLocalSpaceInverseInertia());
  249. // Simulate while applying torque
  250. ioContext.Simulate(cSimulationTime, [&]() { body.AddTorque(inertia * cAngularAcceleration); });
  251. // Get resulting angular velocity
  252. CHECK_APPROX_EQUAL(cSimulationTime * cAngularAcceleration, body.GetAngularVelocity(), 1.0e-4f);
  253. // Test resulting rotation
  254. Quat expected_rot = ioContext.PredictOrientation(Quat::sIdentity(), Vec3::sZero(), cAngularAcceleration, cSimulationTime);
  255. CHECK_APPROX_EQUAL(expected_rot, body.GetRotation(), 1.0e-4f);
  256. }
  257. TEST_CASE("TestPhysicsApplyTorque")
  258. {
  259. PhysicsTestContext c(1.0f / 60.0f, 1, 1);
  260. TestPhysicsApplyTorque(c);
  261. }
  262. TEST_CASE("TestPhysicsApplyTorqueSubStep")
  263. {
  264. PhysicsTestContext c1(2.0f / 60.0f, 1, 2);
  265. TestPhysicsApplyTorque(c1);
  266. PhysicsTestContext c2(4.0f / 60.0f, 1, 4);
  267. TestPhysicsApplyTorque(c2);
  268. PhysicsTestContext c3(4.0f / 60.0f, 2, 2);
  269. TestPhysicsApplyTorque(c3);
  270. PhysicsTestContext c4(2.0f / 60.0f, 2, 1);
  271. TestPhysicsApplyTorque(c4);
  272. PhysicsTestContext c5(8.0f / 60.0f, 4, 2);
  273. TestPhysicsApplyTorque(c5);
  274. PhysicsTestContext c6(4.0f / 60.0f, 4, 1);
  275. TestPhysicsApplyTorque(c6);
  276. }
  277. // Let a sphere bounce on the floor with restition = 1
  278. static void TestPhysicsCollisionElastic(PhysicsTestContext &ioContext)
  279. {
  280. const float cSimulationTime = 1.0f;
  281. const Vec3 cDistanceTraveled = ioContext.PredictPosition(Vec3::sZero(), Vec3::sZero(), cGravity, cSimulationTime);
  282. const float cFloorHitEpsilon = 1.0e-4f; // Apply epsilon so that we're sure that the collision algorithm will find a collision
  283. const Vec3 cFloorHitPos(0.0f, 1.0f - cFloorHitEpsilon, 0.0f); // Sphere with radius 1 will hit floor when 1 above the floor
  284. const Vec3 cInitialPos = cFloorHitPos - cDistanceTraveled;
  285. // Create sphere
  286. ioContext.CreateFloor();
  287. Body &body = ioContext.CreateSphere(cInitialPos, 1.0f, EMotionType::Dynamic, EMotionQuality::Discrete, Layers::MOVING);
  288. body.SetRestitution(1.0f);
  289. // Simulate until at floor
  290. ioContext.Simulate(cSimulationTime);
  291. CHECK_APPROX_EQUAL(cFloorHitPos, body.GetPosition());
  292. // Assert collision not yet processed
  293. CHECK_APPROX_EQUAL(cSimulationTime * cGravity, body.GetLinearVelocity(), 1.0e-4f);
  294. // Simulate one more step to process the collision
  295. ioContext.Simulate(ioContext.GetDeltaTime());
  296. // Assert that collision is processed and velocity is reversed (which is required for a fully elastic collision).
  297. // Note that the physics engine will first apply gravity for the time step and then do collision detection,
  298. // hence the reflected velocity is actually 1 sub-step times gravity bigger than it would be in reality
  299. // For the remainder of cDeltaTime normal gravity will be applied
  300. float sub_step_delta_time = ioContext.GetSubStepDeltaTime();
  301. float remaining_step_time = ioContext.GetDeltaTime() - ioContext.GetSubStepDeltaTime();
  302. Vec3 reflected_velocity_after_sub_step = -(cSimulationTime + sub_step_delta_time) * cGravity;
  303. Vec3 reflected_velocity_after_full_step = reflected_velocity_after_sub_step + remaining_step_time * cGravity;
  304. CHECK_APPROX_EQUAL(reflected_velocity_after_full_step, body.GetLinearVelocity(), 1.0e-4f);
  305. // Body should have bounced back
  306. Vec3 pos_after_bounce_sub_step = cFloorHitPos + reflected_velocity_after_sub_step * sub_step_delta_time;
  307. Vec3 pos_after_bounce_full_step = ioContext.PredictPosition(pos_after_bounce_sub_step, reflected_velocity_after_sub_step, cGravity, remaining_step_time);
  308. CHECK_APPROX_EQUAL(pos_after_bounce_full_step, body.GetPosition());
  309. // Simulate same time, with a fully elastic body we should reach the initial position again
  310. // In our physics engine because of the velocity being too big we actually end up a bit higher than our initial position
  311. Vec3 expected_pos = ioContext.PredictPosition(pos_after_bounce_full_step, reflected_velocity_after_full_step, cGravity, cSimulationTime);
  312. ioContext.Simulate(cSimulationTime);
  313. CHECK_APPROX_EQUAL(expected_pos, body.GetPosition(), 1.0e-4f);
  314. CHECK(expected_pos.GetY() >= cInitialPos.GetY());
  315. }
  316. TEST_CASE("TestPhysicsCollisionElastic")
  317. {
  318. PhysicsTestContext c(1.0f / 60.0f, 1, 1);
  319. TestPhysicsCollisionElastic(c);
  320. }
  321. TEST_CASE("TestPhysicsCollisionElasticSubStep")
  322. {
  323. PhysicsTestContext c1(2.0f / 60.0f, 1, 2);
  324. TestPhysicsCollisionElastic(c1);
  325. PhysicsTestContext c2(4.0f / 60.0f, 1, 4);
  326. TestPhysicsCollisionElastic(c2);
  327. PhysicsTestContext c3(4.0f / 60.0f, 2, 2);
  328. TestPhysicsCollisionElastic(c3);
  329. PhysicsTestContext c4(2.0f / 60.0f, 2, 1);
  330. TestPhysicsCollisionElastic(c4);
  331. PhysicsTestContext c5(4.0f / 60.0f, 4, 1);
  332. TestPhysicsCollisionElastic(c5);
  333. }
  334. // Let a sphere bounce on the floor with restitution = 0
  335. static void TestPhysicsCollisionInelastic(PhysicsTestContext &ioContext)
  336. {
  337. const float cSimulationTime = 1.0f;
  338. const Vec3 cDistanceTraveled = ioContext.PredictPosition(Vec3::sZero(), Vec3::sZero(), cGravity, cSimulationTime);
  339. const float cFloorHitEpsilon = 1.0e-4f; // Apply epsilon so that we're sure that the collision algorithm will find a collision
  340. const Vec3 cFloorHitPos(0.0f, 1.0f - cFloorHitEpsilon, 0.0f); // Sphere with radius 1 will hit floor when 1 above the floor
  341. const Vec3 cInitialPos = cFloorHitPos - cDistanceTraveled;
  342. // Create sphere
  343. ioContext.CreateFloor();
  344. Body &body = ioContext.CreateSphere(cInitialPos, 1.0f, EMotionType::Dynamic, EMotionQuality::Discrete, Layers::MOVING);
  345. body.SetRestitution(0.0f);
  346. // Simulate until at floor
  347. ioContext.Simulate(cSimulationTime);
  348. CHECK_APPROX_EQUAL(cFloorHitPos, body.GetPosition());
  349. // Assert collision not yet processed
  350. CHECK_APPROX_EQUAL(cSimulationTime * cGravity, body.GetLinearVelocity(), 1.0e-4f);
  351. // Simulate one more step to process the collision
  352. ioContext.Simulate(ioContext.GetDeltaTime());
  353. // Assert that all velocity was lost in the collision
  354. CHECK_APPROX_EQUAL(Vec3::sZero(), body.GetLinearVelocity(), 1.0e-4f);
  355. // Assert that we're on the floor
  356. CHECK_APPROX_EQUAL(cFloorHitPos, body.GetPosition(), 1.0e-4f);
  357. // Simulate some more to validate that we remain on the floor
  358. ioContext.Simulate(cSimulationTime);
  359. CHECK_APPROX_EQUAL(Vec3::sZero(), body.GetLinearVelocity(), 1.0e-4f);
  360. CHECK_APPROX_EQUAL(cFloorHitPos, body.GetPosition(), 1.0e-4f);
  361. }
  362. TEST_CASE("TestPhysicsCollisionInelastic")
  363. {
  364. PhysicsTestContext c(1.0f / 60.0f, 1, 1);
  365. TestPhysicsCollisionInelastic(c);
  366. }
  367. TEST_CASE("TestPhysicsCollisionInelasticSubStep")
  368. {
  369. PhysicsTestContext c1(2.0f / 60.0f, 1, 2);
  370. TestPhysicsCollisionInelastic(c1);
  371. PhysicsTestContext c2(4.0f / 60.0f, 1, 4);
  372. TestPhysicsCollisionInelastic(c2);
  373. PhysicsTestContext c3(4.0f / 60.0f, 2, 2);
  374. TestPhysicsCollisionInelastic(c3);
  375. PhysicsTestContext c4(2.0f / 60.0f, 2, 1);
  376. TestPhysicsCollisionInelastic(c4);
  377. PhysicsTestContext c5(4.0f / 60.0f, 4, 1);
  378. TestPhysicsCollisionInelastic(c5);
  379. }
  380. // Let box intersect with floor by cPenetrationSlop. It should not move, this is the maximum penetration allowed.
  381. static void TestPhysicsPenetrationSlop1(PhysicsTestContext &ioContext)
  382. {
  383. const float cPenetrationSlop = ioContext.GetSystem()->GetPhysicsSettings().mPenetrationSlop;
  384. const float cSimulationTime = 1.0f;
  385. const Vec3 cInitialPos(0.0f, 1.0f - cPenetrationSlop, 0.0f);
  386. // Create box, penetrating with floor
  387. ioContext.CreateFloor();
  388. Body &body = ioContext.CreateBox(cInitialPos, Quat::sIdentity(), EMotionType::Dynamic, EMotionQuality::Discrete, Layers::MOVING, Vec3(1, 1, 1));
  389. // Simulate
  390. ioContext.Simulate(cSimulationTime);
  391. // Test slop not resolved
  392. CHECK_APPROX_EQUAL(cInitialPos, body.GetPosition(), 1.0e-5f);
  393. CHECK_APPROX_EQUAL(Vec3::sZero(), body.GetLinearVelocity());
  394. CHECK_APPROX_EQUAL(Vec3::sZero(), body.GetAngularVelocity());
  395. }
  396. TEST_CASE("TestPhysicsPenetrationSlop1")
  397. {
  398. PhysicsTestContext c(1.0f / 60.0f, 1, 1);
  399. TestPhysicsPenetrationSlop1(c);
  400. }
  401. TEST_CASE("TestPhysicsPenetrationSlop1SubStep")
  402. {
  403. PhysicsTestContext c(1.0f / 30.0f, 1, 2);
  404. TestPhysicsPenetrationSlop1(c);
  405. PhysicsTestContext c2(1.0f / 30.0f, 2, 1);
  406. TestPhysicsPenetrationSlop1(c2);
  407. }
  408. // Let box intersect with floor with more than cPenetrationSlop. It should be resolved by SolvePositionConstraint until interpenetration is cPenetrationSlop.
  409. static void TestPhysicsPenetrationSlop2(PhysicsTestContext &ioContext)
  410. {
  411. const float cPenetrationSlop = ioContext.GetSystem()->GetPhysicsSettings().mPenetrationSlop;
  412. const float cSimulationTime = 1.0f;
  413. const Vec3 cInitialPos(0.0f, 1.0f - 2.0f * cPenetrationSlop, 0.0f);
  414. const Vec3 cFinalPos(0.0f, 1.0f - cPenetrationSlop, 0.0f);
  415. // Create box, penetrating with floor
  416. ioContext.CreateFloor();
  417. Body &body = ioContext.CreateBox(cInitialPos, Quat::sIdentity(), EMotionType::Dynamic, EMotionQuality::Discrete, Layers::MOVING, Vec3(1, 1, 1));
  418. // Simulate
  419. ioContext.Simulate(cSimulationTime);
  420. // Test resolved until slop
  421. CHECK_APPROX_EQUAL(cFinalPos, body.GetPosition(), 1.0e-5f);
  422. CHECK_APPROX_EQUAL(Vec3::sZero(), body.GetLinearVelocity());
  423. CHECK_APPROX_EQUAL(Vec3::sZero(), body.GetAngularVelocity());
  424. }
  425. TEST_CASE("TestPhysicsPenetrationSlop2")
  426. {
  427. PhysicsTestContext c(1.0f / 60.0f, 1, 1);
  428. TestPhysicsPenetrationSlop2(c);
  429. }
  430. TEST_CASE("TestPhysicsPenetrationSlop2SubStep")
  431. {
  432. PhysicsTestContext c(1.0f / 30.0f, 1, 2);
  433. TestPhysicsPenetrationSlop2(c);
  434. PhysicsTestContext c2(1.0f / 30.0f, 2, 1);
  435. TestPhysicsPenetrationSlop2(c2);
  436. }
  437. // Let box intersect with floor with less than cPenetrationSlop. Body should not move because SolveVelocityConstraint should reset velocity.
  438. static void TestPhysicsPenetrationSlop3(PhysicsTestContext &ioContext)
  439. {
  440. const float cPenetrationSlop = ioContext.GetSystem()->GetPhysicsSettings().mPenetrationSlop;
  441. const float cSimulationTime = 1.0f;
  442. const Vec3 cInitialPos(0.0f, 1.0f - 0.1f * cPenetrationSlop, 0.0f);
  443. // Create box, penetrating with floor
  444. ioContext.CreateFloor();
  445. Body &body = ioContext.CreateBox(cInitialPos, Quat::sIdentity(), EMotionType::Dynamic, EMotionQuality::Discrete, Layers::MOVING, Vec3(1, 1, 1));
  446. // Simulate
  447. ioContext.Simulate(cSimulationTime);
  448. // Test body remained static
  449. CHECK_APPROX_EQUAL(cInitialPos, body.GetPosition(), 1.0e-5f);
  450. CHECK_APPROX_EQUAL(Vec3::sZero(), body.GetLinearVelocity());
  451. CHECK_APPROX_EQUAL(Vec3::sZero(), body.GetAngularVelocity());
  452. }
  453. TEST_CASE("TestPhysicsPenetrationSlop3")
  454. {
  455. PhysicsTestContext c(1.0f / 60.0f, 1, 1);
  456. TestPhysicsPenetrationSlop3(c);
  457. }
  458. TEST_CASE("TestPhysicsPenetrationSlop3SubStep")
  459. {
  460. PhysicsTestContext c(1.0f / 30.0f, 1, 2);
  461. TestPhysicsPenetrationSlop3(c);
  462. PhysicsTestContext c2(1.0f / 30.0f, 2, 1);
  463. TestPhysicsPenetrationSlop3(c2);
  464. }
  465. TEST_CASE("TestPhysicsOutsideOfSpeculativeContactDistance")
  466. {
  467. PhysicsTestContext c(1.0f / 60.0f, 1, 1);
  468. Body &floor = c.CreateFloor();
  469. c.ZeroGravity();
  470. LoggingContactListener contact_listener;
  471. c.GetSystem()->SetContactListener(&contact_listener);
  472. // Create a box and a sphere just outside the speculative contact distance
  473. const float cSpeculativeContactDistance = c.GetSystem()->GetPhysicsSettings().mSpeculativeContactDistance;
  474. const float cDistanceAboveFloor = 1.1f * cSpeculativeContactDistance;
  475. const Vec3 cInitialPosBox(0, 1.0f + cDistanceAboveFloor, 0.0f);
  476. const Vec3 cInitialPosSphere = cInitialPosBox + Vec3(5, 0, 0);
  477. // Make it move 1 m per step down
  478. const Vec3 cVelocity(0, -1.0f / c.GetDeltaTime(), 0);
  479. Body &box = c.CreateBox(cInitialPosBox, Quat::sIdentity(), EMotionType::Dynamic, EMotionQuality::Discrete, Layers::MOVING, Vec3(1, 1, 1));
  480. box.SetLinearVelocity(cVelocity);
  481. Body &sphere = c.CreateSphere(cInitialPosSphere, 1.0f, EMotionType::Dynamic, EMotionQuality::Discrete, Layers::MOVING);
  482. sphere.SetLinearVelocity(cVelocity);
  483. // Simulate a step
  484. c.SimulateSingleStep();
  485. // 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)
  486. CHECK(contact_listener.GetEntryCount() == 0);
  487. CHECK_APPROX_EQUAL(box.GetPosition(), cInitialPosBox + cVelocity * c.GetDeltaTime());
  488. CHECK_APPROX_EQUAL(sphere.GetPosition(), cInitialPosSphere + cVelocity * c.GetDeltaTime());
  489. // Simulate a step
  490. c.SimulateSingleStep();
  491. // Check that the contacts are detected now
  492. CHECK(contact_listener.GetEntryCount() == 4); // 2 validates and 2 contacts
  493. CHECK(contact_listener.Contains(LoggingContactListener::EType::Validate, box.GetID(), floor.GetID()));
  494. CHECK(contact_listener.Contains(LoggingContactListener::EType::Add, box.GetID(), floor.GetID()));
  495. CHECK(contact_listener.Contains(LoggingContactListener::EType::Validate, sphere.GetID(), floor.GetID()));
  496. CHECK(contact_listener.Contains(LoggingContactListener::EType::Add, sphere.GetID(), floor.GetID()));
  497. }
  498. TEST_CASE("TestPhysicsInsideSpeculativeContactDistanceNoRestitution")
  499. {
  500. PhysicsTestContext c(1.0f / 60.0f, 1, 1);
  501. Body &floor = c.CreateFloor();
  502. c.ZeroGravity();
  503. LoggingContactListener contact_listener;
  504. c.GetSystem()->SetContactListener(&contact_listener);
  505. // Create a box and a sphere just inside the speculative contact distance
  506. const float cSpeculativeContactDistance = c.GetSystem()->GetPhysicsSettings().mSpeculativeContactDistance;
  507. const float cDistanceAboveFloor = 0.9f * cSpeculativeContactDistance;
  508. const Vec3 cInitialPosBox(0, 1.0f + cDistanceAboveFloor, 0.0f);
  509. const Vec3 cInitialPosSphere = cInitialPosBox + Vec3(5, 0, 0);
  510. // Make it move 1 m per step down
  511. const Vec3 cVelocity(0, -1.0f / c.GetDeltaTime(), 0);
  512. Body &box = c.CreateBox(cInitialPosBox, Quat::sIdentity(), EMotionType::Dynamic, EMotionQuality::Discrete, Layers::MOVING, Vec3(1, 1, 1));
  513. box.SetLinearVelocity(cVelocity);
  514. Body &sphere = c.CreateSphere(cInitialPosSphere, 1.0f, EMotionType::Dynamic, EMotionQuality::Discrete, Layers::MOVING);
  515. sphere.SetLinearVelocity(cVelocity);
  516. // Simulate a step
  517. c.SimulateSingleStep();
  518. // Check that it is now on the floor and that 2 collisions have been detected
  519. CHECK(contact_listener.GetEntryCount() == 4); // 2 validates and 2 contacts
  520. CHECK(contact_listener.Contains(LoggingContactListener::EType::Validate, box.GetID(), floor.GetID()));
  521. CHECK(contact_listener.Contains(LoggingContactListener::EType::Add, box.GetID(), floor.GetID()));
  522. CHECK(contact_listener.Contains(LoggingContactListener::EType::Validate, sphere.GetID(), floor.GetID()));
  523. CHECK(contact_listener.Contains(LoggingContactListener::EType::Add, sphere.GetID(), floor.GetID()));
  524. contact_listener.Clear();
  525. // Velocity should have been reduced to exactly hit the floor in this step
  526. const Vec3 cExpectedVelocity(0, -cDistanceAboveFloor / c.GetDeltaTime(), 0);
  527. // Box collision is less accurate than sphere as it hits with 4 corners so there's some floating point precision loss in the calculation
  528. CHECK_APPROX_EQUAL(box.GetPosition(), Vec3(0, 1, 0), 1.0e-3f);
  529. CHECK_APPROX_EQUAL(box.GetLinearVelocity(), cExpectedVelocity, 0.05f);
  530. CHECK_APPROX_EQUAL(box.GetAngularVelocity(), Vec3::sZero(), 1.0e-2f);
  531. // Sphere has only 1 contact point so is much more accurate
  532. CHECK_APPROX_EQUAL(sphere.GetPosition(), Vec3(5, 1, 0));
  533. CHECK_APPROX_EQUAL(sphere.GetLinearVelocity(), cExpectedVelocity, 1.0e-4f);
  534. CHECK_APPROX_EQUAL(sphere.GetAngularVelocity(), Vec3::sZero(), 1.0e-4f);
  535. // Simulate a step
  536. c.SimulateSingleStep();
  537. // Check that the contacts persisted
  538. CHECK(contact_listener.GetEntryCount() >= 2); // 2 persist and possibly 2 validates depending on if the cache got reused
  539. CHECK(contact_listener.Contains(LoggingContactListener::EType::Persist, box.GetID(), floor.GetID()));
  540. CHECK(contact_listener.Contains(LoggingContactListener::EType::Persist, sphere.GetID(), floor.GetID()));
  541. // Box should have come to rest
  542. CHECK_APPROX_EQUAL(box.GetPosition(), Vec3(0, 1, 0), 1.0e-3f);
  543. CHECK_APPROX_EQUAL(box.GetLinearVelocity(), Vec3::sZero(), 0.05f);
  544. CHECK_APPROX_EQUAL(box.GetAngularVelocity(), Vec3::sZero(), 1.0e-2f);
  545. // Sphere should have come to rest
  546. CHECK_APPROX_EQUAL(sphere.GetPosition(), Vec3(5, 1, 0), 1.0e-4f);
  547. CHECK_APPROX_EQUAL(sphere.GetLinearVelocity(), Vec3::sZero(), 1.0e-4f);
  548. CHECK_APPROX_EQUAL(sphere.GetAngularVelocity(), Vec3::sZero(), 1.0e-4f);
  549. }
  550. TEST_CASE("TestPhysicsInsideSpeculativeContactDistanceWithRestitution")
  551. {
  552. PhysicsTestContext c(1.0f / 60.0f, 1, 1);
  553. Body &floor = c.CreateFloor();
  554. c.ZeroGravity();
  555. LoggingContactListener contact_listener;
  556. c.GetSystem()->SetContactListener(&contact_listener);
  557. // Create a box and a sphere just inside the speculative contact distance
  558. const float cSpeculativeContactDistance = c.GetSystem()->GetPhysicsSettings().mSpeculativeContactDistance;
  559. const float cDistanceAboveFloor = 0.9f * cSpeculativeContactDistance;
  560. const Vec3 cInitialPosBox(0, 1.0f + cDistanceAboveFloor, 0.0f);
  561. const Vec3 cInitialPosSphere = cInitialPosBox + Vec3(5, 0, 0);
  562. // Make it move 1 m per step down
  563. const Vec3 cVelocity(0, -1.0f / c.GetDeltaTime(), 0);
  564. Body &box = c.CreateBox(cInitialPosBox, Quat::sIdentity(), EMotionType::Dynamic, EMotionQuality::Discrete, Layers::MOVING, Vec3(1, 1, 1));
  565. box.SetLinearVelocity(cVelocity);
  566. box.SetRestitution(1.0f);
  567. Body &sphere = c.CreateSphere(cInitialPosSphere, 1.0f, EMotionType::Dynamic, EMotionQuality::Discrete, Layers::MOVING);
  568. sphere.SetLinearVelocity(cVelocity);
  569. sphere.SetRestitution(1.0f);
  570. // Simulate a step
  571. c.SimulateSingleStep();
  572. // 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)
  573. CHECK(contact_listener.GetEntryCount() == 4); // 2 validates and 2 contacts
  574. CHECK(contact_listener.Contains(LoggingContactListener::EType::Validate, box.GetID(), floor.GetID()));
  575. CHECK(contact_listener.Contains(LoggingContactListener::EType::Add, box.GetID(), floor.GetID()));
  576. CHECK(contact_listener.Contains(LoggingContactListener::EType::Validate, sphere.GetID(), floor.GetID()));
  577. CHECK(contact_listener.Contains(LoggingContactListener::EType::Add, sphere.GetID(), floor.GetID()));
  578. contact_listener.Clear();
  579. // Box collision is less accurate than sphere as it hits with 4 corners so there's some floating point precision loss in the calculation
  580. CHECK_APPROX_EQUAL(box.GetPosition(), cInitialPosBox - cVelocity * c.GetDeltaTime(), 0.01f);
  581. CHECK_APPROX_EQUAL(box.GetLinearVelocity(), -cVelocity, 0.1f);
  582. CHECK_APPROX_EQUAL(box.GetAngularVelocity(), Vec3::sZero(), 0.02f);
  583. // Sphere has only 1 contact point so is much more accurate
  584. CHECK_APPROX_EQUAL(sphere.GetPosition(), cInitialPosSphere - cVelocity * c.GetDeltaTime(), 1.0e-5f);
  585. CHECK_APPROX_EQUAL(sphere.GetLinearVelocity(), -cVelocity, 2.0e-4f);
  586. CHECK_APPROX_EQUAL(sphere.GetAngularVelocity(), Vec3::sZero(), 2.0e-4f);
  587. // Simulate a step
  588. c.SimulateSingleStep();
  589. // Check that all contact points are removed
  590. CHECK(contact_listener.GetEntryCount() == 2); // 2 removes
  591. CHECK(contact_listener.Contains(LoggingContactListener::EType::Remove, box.GetID(), floor.GetID()));
  592. CHECK(contact_listener.Contains(LoggingContactListener::EType::Remove, sphere.GetID(), floor.GetID()));
  593. }
  594. TEST_CASE("TestPhysicsInsideSpeculativeContactDistanceMovingAway")
  595. {
  596. PhysicsTestContext c(1.0f / 60.0f, 1, 1);
  597. Body &floor = c.CreateFloor();
  598. c.ZeroGravity();
  599. LoggingContactListener contact_listener;
  600. c.GetSystem()->SetContactListener(&contact_listener);
  601. // Create a box and a sphere just inside the speculative contact distance
  602. const float cSpeculativeContactDistance = c.GetSystem()->GetPhysicsSettings().mSpeculativeContactDistance;
  603. const float cDistanceAboveFloor = 0.9f * cSpeculativeContactDistance;
  604. const Vec3 cInitialPosBox(0, 1.0f + cDistanceAboveFloor, 0.0f);
  605. const Vec3 cInitialPosSphere = cInitialPosBox + Vec3(5, 0, 0);
  606. // Make it move 1 m per step up
  607. const Vec3 cVelocity(0, 1.0f / c.GetDeltaTime(), 0);
  608. Body &box = c.CreateBox(cInitialPosBox, Quat::sIdentity(), EMotionType::Dynamic, EMotionQuality::Discrete, Layers::MOVING, Vec3(1, 1, 1));
  609. box.SetLinearVelocity(cVelocity);
  610. box.SetRestitution(1.0f);
  611. Body &sphere = c.CreateSphere(cInitialPosSphere, 1.0f, EMotionType::Dynamic, EMotionQuality::Discrete, Layers::MOVING);
  612. sphere.SetLinearVelocity(cVelocity);
  613. sphere.SetRestitution(1.0f);
  614. // Simulate a step
  615. c.SimulateSingleStep();
  616. // 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)
  617. CHECK(contact_listener.GetEntryCount() == 4); // 2 validates and 2 contacts
  618. CHECK(contact_listener.Contains(LoggingContactListener::EType::Validate, box.GetID(), floor.GetID()));
  619. CHECK(contact_listener.Contains(LoggingContactListener::EType::Add, box.GetID(), floor.GetID()));
  620. CHECK(contact_listener.Contains(LoggingContactListener::EType::Validate, sphere.GetID(), floor.GetID()));
  621. CHECK(contact_listener.Contains(LoggingContactListener::EType::Add, sphere.GetID(), floor.GetID()));
  622. contact_listener.Clear();
  623. // Box should have moved unimpeded
  624. CHECK_APPROX_EQUAL(box.GetPosition(), cInitialPosBox + cVelocity * c.GetDeltaTime());
  625. CHECK_APPROX_EQUAL(box.GetLinearVelocity(), cVelocity);
  626. CHECK_APPROX_EQUAL(box.GetAngularVelocity(), Vec3::sZero());
  627. // Sphere should have moved unimpeded
  628. CHECK_APPROX_EQUAL(sphere.GetPosition(), cInitialPosSphere + cVelocity * c.GetDeltaTime());
  629. CHECK_APPROX_EQUAL(sphere.GetLinearVelocity(), cVelocity);
  630. CHECK_APPROX_EQUAL(sphere.GetAngularVelocity(), Vec3::sZero());
  631. // Simulate a step
  632. c.SimulateSingleStep();
  633. // Check that all contact points are removed
  634. CHECK(contact_listener.GetEntryCount() == 2); // 2 removes
  635. CHECK(contact_listener.Contains(LoggingContactListener::EType::Remove, box.GetID(), floor.GetID()));
  636. CHECK(contact_listener.Contains(LoggingContactListener::EType::Remove, sphere.GetID(), floor.GetID()));
  637. }
  638. static void TestPhysicsActivationDeactivation(PhysicsTestContext &ioContext)
  639. {
  640. const float cPenetrationSlop = ioContext.GetSystem()->GetPhysicsSettings().mPenetrationSlop;
  641. // Install activation listener
  642. LoggingBodyActivationListener activation_listener;
  643. ioContext.GetSystem()->SetBodyActivationListener(&activation_listener);
  644. // Create floor
  645. Body &floor = ioContext.CreateBox(Vec3(0, -1, 0), Quat::sIdentity(), EMotionType::Static, EMotionQuality::Discrete, Layers::NON_MOVING, Vec3(100, 1, 100));
  646. CHECK(!floor.IsActive());
  647. // Create inactive box
  648. Body &box = ioContext.CreateBox(Vec3(0, 5, 0), Quat::sIdentity(), EMotionType::Dynamic, EMotionQuality::Discrete, Layers::MOVING, Vec3::sReplicate(0.5f), EActivation::DontActivate);
  649. CHECK(!box.IsActive());
  650. CHECK(activation_listener.GetEntryCount() == 0);
  651. // Box should not activate by itself
  652. ioContext.Simulate(1.0f);
  653. CHECK(box.GetPosition() == Vec3(0, 5, 0));
  654. CHECK(!box.IsActive());
  655. CHECK(activation_listener.GetEntryCount() == 0);
  656. // Activate the body and validate it is active now
  657. ioContext.GetBodyInterface().ActivateBody(box.GetID());
  658. CHECK(box.IsActive());
  659. CHECK(box.GetLinearVelocity().IsNearZero());
  660. CHECK(activation_listener.GetEntryCount() == 1);
  661. CHECK(activation_listener.Contains(LoggingBodyActivationListener::EType::Activated, box.GetID()));
  662. activation_listener.Clear();
  663. // Do a single step and check that the body is still active and has gained some velocity
  664. ioContext.SimulateSingleStep();
  665. CHECK(box.IsActive());
  666. CHECK(activation_listener.GetEntryCount() == 0);
  667. CHECK(!box.GetLinearVelocity().IsNearZero());
  668. // Simulate 5 seconds and check it has settled on the floor and is no longer active
  669. ioContext.Simulate(5.0f);
  670. CHECK_APPROX_EQUAL(box.GetPosition(), Vec3(0, 0.5f, 0), 1.1f * cPenetrationSlop);
  671. CHECK_APPROX_EQUAL(box.GetLinearVelocity(), Vec3::sZero());
  672. CHECK_APPROX_EQUAL(box.GetAngularVelocity(), Vec3::sZero());
  673. CHECK(!box.IsActive());
  674. CHECK(activation_listener.GetEntryCount() == 1);
  675. CHECK(activation_listener.Contains(LoggingBodyActivationListener::EType::Deactivated, box.GetID()));
  676. }
  677. TEST_CASE("TestPhysicsActivationDeactivation")
  678. {
  679. PhysicsTestContext c1(1.0f / 60.0f, 1, 1);
  680. TestPhysicsActivationDeactivation(c1);
  681. PhysicsTestContext c2(2.0f / 60.0f, 1, 2);
  682. TestPhysicsActivationDeactivation(c2);
  683. PhysicsTestContext c3(2.0f / 60.0f, 2, 1);
  684. TestPhysicsActivationDeactivation(c3);
  685. PhysicsTestContext c4(4.0f / 60.0f, 4, 1);
  686. TestPhysicsActivationDeactivation(c4);
  687. PhysicsTestContext c5(8.0f / 60.0f, 4, 2);
  688. TestPhysicsActivationDeactivation(c5);
  689. }
  690. // 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
  691. static void TestPhysicsActivateDuringStep(PhysicsTestContext &ioContext, bool inReverseCreate)
  692. {
  693. const float cPenetrationSlop = ioContext.GetSystem()->GetPhysicsSettings().mPenetrationSlop;
  694. const int cNumBodies = 10;
  695. const float cBoxExtent = 0.5f;
  696. PhysicsSystem *system = ioContext.GetSystem();
  697. BodyInterface &bi = ioContext.GetBodyInterface();
  698. LoggingBodyActivationListener activation_listener;
  699. system->SetBodyActivationListener(&activation_listener);
  700. LoggingContactListener contact_listener;
  701. system->SetContactListener(&contact_listener);
  702. // 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
  703. BodyIDVector body_ids;
  704. if (inReverseCreate)
  705. for (int i = cNumBodies - 1; i >= 0; --i)
  706. 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());
  707. else
  708. for (int i = 0; i < cNumBodies; ++i)
  709. 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());
  710. // Test that nothing is active yet
  711. CHECK(activation_listener.GetEntryCount() == 0);
  712. CHECK(contact_listener.GetEntryCount() == 0);
  713. for (BodyID id : body_ids)
  714. CHECK(!bi.IsActive(id));
  715. // 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
  716. bi.SetLinearVelocity(body_ids.front(), Vec3(500, 0, 0));
  717. // Test that only the left most box is active
  718. CHECK(activation_listener.GetEntryCount() == 1);
  719. CHECK(contact_listener.GetEntryCount() == 0);
  720. CHECK(bi.IsActive(body_ids.front()));
  721. CHECK(activation_listener.Contains(LoggingBodyActivationListener::EType::Activated, body_ids.front()));
  722. for (int i = 1; i < cNumBodies; ++i)
  723. CHECK(!bi.IsActive(body_ids[i]));
  724. activation_listener.Clear();
  725. // Step the world
  726. ioContext.SimulateSingleStep();
  727. // Other bodies should now be awake and each body should only collide with its neighbour
  728. CHECK(activation_listener.GetEntryCount() == cNumBodies - 1);
  729. CHECK(contact_listener.GetEntryCount() == 2 * (cNumBodies - 1));
  730. for (int i = 0; i < cNumBodies; ++i)
  731. {
  732. BodyID id = body_ids[i];
  733. // Check body is active
  734. CHECK(bi.IsActive(id));
  735. // Check that body moved to the right
  736. CHECK(bi.GetPosition(id).GetX() > i * (2.0f * cBoxExtent - cPenetrationSlop));
  737. }
  738. for (int i = 1; i < cNumBodies; ++i)
  739. {
  740. BodyID id1 = body_ids[i - 1];
  741. BodyID id2 = body_ids[i];
  742. // Check that we received activation events for each body
  743. CHECK(activation_listener.Contains(LoggingBodyActivationListener::EType::Activated, id2));
  744. // Check that we received a validate and an add for each body pair
  745. int validate = contact_listener.Find(LoggingContactListener::EType::Validate, id1, id2);
  746. CHECK(validate >= 0);
  747. int add = contact_listener.Find(LoggingContactListener::EType::Add, id1, id2);
  748. CHECK(add >= 0);
  749. CHECK(add > validate);
  750. // Check that bodies did not tunnel through each other
  751. CHECK(bi.GetPosition(id1).GetX() < bi.GetPosition(id2).GetX());
  752. }
  753. }
  754. TEST_CASE("TestPhysicsActivateDuringStep")
  755. {
  756. PhysicsTestContext c(1.0f / 60.0f, 1, 1);
  757. TestPhysicsActivateDuringStep(c, false);
  758. PhysicsTestContext c2(1.0f / 60.0f, 1, 1);
  759. TestPhysicsActivateDuringStep(c2, true);
  760. }
  761. }