#include "zeroerr/assert.h" #include "zeroerr/unittest.h" #include "physics/Physics.h" #include "physics/World3D.h" #include "physics/Body3D.h" #include "physics/Shape3D.h" #include "physics/DistanceField3D.h" #include "physics/Joint3D.h" #include "physics/TerrainLoadGravity.h" #include #include #include #include #include using namespace eve::physics; TEST_CASE("box3d.world.createAndGravity") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, -9.8f, 0.f, true)); REQUIRE(world.get() != nullptr); CHECK(std::fabs(world->getGravityX()) < 0.01f); CHECK(std::fabs(world->getGravityY() + 9.8f) < 0.01f); CHECK(std::fabs(world->getGravityZ()) < 0.01f); world->setGravity(1.f, -10.f, 2.f); CHECK(std::fabs(world->getGravityX() - 1.f) < 0.01f); CHECK(std::fabs(world->getGravityY() + 10.f) < 0.01f); CHECK(std::fabs(world->getGravityZ() - 2.f) < 0.01f); } TEST_CASE("box3d.body.fallsUnderGravity") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, -10.f, 0.f, true)); Body3D *ground = world->newBody("static", 0.f, -1.f, 0.f); ground->newBoxShape(20.f, 2.f, 20.f, 0.f, 0.5f, 0.f); Body3D *box = world->newBody("dynamic", 0.f, 4.f, 0.f); box->newBoxShape(1.f, 1.f, 1.f, 1.f, 0.3f, 0.1f); float y0 = box->getY(); for (int i = 0; i < 90; ++i) world->update(1.f / 60.f); CHECK_LT(box->getY(), y0); CHECK_GT(box->getY(), 0.f); } TEST_CASE("box3d.terrainLoadGravity.waitsThenRestoresOriginalScale") { auto* mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, -10.f, 0.f, false)); Body3D* body = world->newBody("dynamic", 0.f, 10.f, 0.f); body->newSphereShape(0.5f, 1.f, 0.5f, 0.f); body->setGravityScale(0.35f); TerrainLoadGravityState state; REQUIRE(static_cast(beginTerrainLoadGravity(&state, body, true))); CHECK(state.isMonitoring()); CHECK_EQ(body->getGravityScale(), 0.f); auto waiting = advanceTerrainLoadGravity(&state, body, false, 0.5f, 2.f); REQUIRE(static_cast(waiting)); CHECK(!waiting.value()); CHECK(!state.isActivationScheduled()); auto scheduled = advanceTerrainLoadGravity(&state, body, true, 0.75f, 2.f); REQUIRE(static_cast(scheduled)); CHECK(!scheduled.value()); CHECK(state.isActivationScheduled()); CHECK_EQ(state.getDelayElapsed(), 0.75f); CHECK(!advanceTerrainLoadGravity(&state, body, false, NAN, 2.f)); CHECK_EQ(state.getDelayElapsed(), 0.75f); auto activated = advanceTerrainLoadGravity(&state, body, false, 1.25f, 2.f); REQUIRE(static_cast(activated)); CHECK(activated.value()); CHECK(state.isCompleted()); CHECK(!state.isMonitoring()); CHECK_EQ(body->getGravityScale(), 0.35f); auto alreadyDone = advanceTerrainLoadGravity(&state, body, true, 1.f, 2.f); REQUIRE(static_cast(alreadyDone)); CHECK(!alreadyDone.value()); Body3D* passthrough = world->newBody("dynamic", 0.f, 3.f, 0.f); TerrainLoadGravityState absent; REQUIRE(static_cast(beginTerrainLoadGravity(&absent, passthrough, false))); CHECK(absent.isCompleted()); CHECK_EQ(passthrough->getGravityScale(), 1.f); CHECK(!beginTerrainLoadGravity(&absent, passthrough, false)); } TEST_CASE("box3d.body.staticDoesNotMove") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, -10.f, 0.f, true)); Body3D *ground = world->newBody("static", 0.f, 0.f, 0.f); ground->newBoxShape(4.f, 1.f, 4.f, 0.f); float y0 = ground->getY(); for (int i = 0; i < 30; ++i) world->update(1.f / 60.f); CHECK(std::fabs(ground->getY() - y0) < 0.01f); CHECK_EQ(ground->getType(), std::string("static")); } TEST_CASE("box3d.body.sphereAndVelocity") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, 0.f, 0.f, true)); Body3D *ball = world->newBody("dynamic", 0.f, 0.f, 0.f); ball->newSphereShape(0.5f, 1.f, 0.2f, 0.8f); ball->setLinearVelocity(5.f, 0.f, 0.f); for (int i = 0; i < 30; ++i) world->update(1.f / 60.f); CHECK_GT(ball->getX(), 1.f); CHECK(std::fabs(ball->getY()) < 0.1f); CHECK(std::fabs(ball->getZ()) < 0.1f); } TEST_CASE("box3d.body.dampingGravitySleepMassAndCenters") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, -10.f, 0.f, false)); Body3D *body = world->newBody("dynamic", 3.f, 4.f, 5.f); body->newBoxShape(2.f, 2.f, 2.f, 2.f); CHECK(std::fabs(body->getMass() - 16.f) < 0.05f); CHECK(std::fabs(body->getLocalCenterX()) < 0.001f); CHECK(std::fabs(body->getLocalCenterY()) < 0.001f); CHECK(std::fabs(body->getLocalCenterZ()) < 0.001f); CHECK(std::fabs(body->getWorldCenterX() - 3.f) < 0.001f); CHECK(std::fabs(body->getWorldCenterY() - 4.f) < 0.001f); CHECK(std::fabs(body->getWorldCenterZ() - 5.f) < 0.001f); body->setLinearDamping(3.f); body->setAngularDamping(4.f); body->setGravityScale(-0.5f); body->setSleepEnabled(false); body->setSleepThreshold(0.25f); CHECK(std::fabs(body->getLinearDamping() - 3.f) < 0.001f); CHECK(std::fabs(body->getAngularDamping() - 4.f) < 0.001f); CHECK(std::fabs(body->getGravityScale() + 0.5f) < 0.001f); CHECK(!body->isSleepEnabled()); CHECK(std::fabs(body->getSleepThreshold() - 0.25f) < 0.001f); body->setLinearVelocity(6.f, 0.f, 0.f); world->updateFull(0.25f, 4); CHECK_LT(body->getLinearVelocityX(), 6.f); CHECK_GT(body->getLinearVelocityY(), 0.f); CHECK_THROWS((body->setLinearDamping(-1.f), false)); CHECK_THROWS((body->setAngularDamping(std::numeric_limits::infinity()), false)); CHECK_THROWS((body->setGravityScale(std::numeric_limits::quiet_NaN()), false)); CHECK_THROWS((body->setSleepThreshold(-0.1f), false)); } TEST_CASE("box3d.body.customMassCenterAndInertia") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, 0.f, 0.f, false)); Body3D *body = world->newBody("dynamic", 10.f, 0.f, 0.f); body->newBoxShape(2.f, 2.f, 2.f, 2.f); const float automaticMass = body->getMass(); body->setMassProperties(4.f, 0.25f, -0.5f, 0.75f, 2.f, 3.f, 4.f, 0.1f, 0.2f, 0.3f); CHECK(std::fabs(body->getMass() - 4.f) < 1e-6f); CHECK(std::fabs(body->getLocalCenterX() - 0.25f) < 1e-6f); CHECK(std::fabs(body->getLocalCenterY() + 0.5f) < 1e-6f); CHECK(std::fabs(body->getLocalCenterZ() - 0.75f) < 1e-6f); CHECK(std::fabs(body->getWorldCenterX() - 10.25f) < 1e-6f); CHECK(std::fabs(body->getInertiaXX() - 2.f) < 1e-6f); CHECK(std::fabs(body->getInertiaYY() - 3.f) < 1e-6f); CHECK(std::fabs(body->getInertiaZZ() - 4.f) < 1e-6f); CHECK(std::fabs(body->getInertiaXY() - 0.1f) < 1e-6f); CHECK(std::fabs(body->getInertiaXZ() - 0.2f) < 1e-6f); CHECK(std::fabs(body->getInertiaYZ() - 0.3f) < 1e-6f); body->applyLinearImpulse(8.f, 0.f, 0.f); CHECK(std::fabs(body->getLinearVelocityX() - 2.f) < 1e-6f); body->resetMassProperties(); CHECK(std::fabs(body->getMass() - automaticMass) < 0.05f); CHECK(std::fabs(body->getLocalCenterX()) < 1e-6f); CHECK(std::fabs(body->getLocalCenterY()) < 1e-6f); CHECK(std::fabs(body->getLocalCenterZ()) < 1e-6f); CHECK_THROWS((body->setMassProperties(0.f, 0.f, 0.f, 0.f, 1.f, 1.f, 1.f), false)); CHECK_THROWS((body->setMassProperties(1.f, 0.f, 0.f, 0.f, 1.f, 1.f, -1.f), false)); CHECK_THROWS((body->setMassProperties(1.f, 0.f, 0.f, 0.f, 1.f, 1.f, 1.f, 2.f), false)); CHECK_THROWS((body->setMassProperties( 1.f, std::numeric_limits::quiet_NaN(), 0.f, 0.f, 1.f, 1.f, 1.f), false)); Body3D *ground = world->newBody("static", 0.f, 0.f, 0.f); CHECK_THROWS((ground->setMassProperties(1.f, 0.f, 0.f, 0.f, 1.f, 1.f, 1.f), false)); } TEST_CASE("box3d.body.spaceTransformsPointVelocityAndLocalImpulses") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, 0.f, 0.f, false)); Body3D *body = world->newBody("dynamic", 3.f, 4.f, 5.f); body->newSphereShape(0.5f, 1.f); const float halfSqrt2 = std::sqrt(0.5f); body->setRotation(0.f, 0.f, halfSqrt2, halfSqrt2); // local +X becomes world +Y const std::vector worldPoint = body->localToWorldPoint(1.f, 2.f, 3.f); REQUIRE(worldPoint.size() == 3); CHECK(std::fabs(worldPoint[0] - 1.f) < 1e-5f); CHECK(std::fabs(worldPoint[1] - 5.f) < 1e-5f); CHECK(std::fabs(worldPoint[2] - 8.f) < 1e-5f); const std::vector localPoint = body->worldToLocalPoint(worldPoint[0], worldPoint[1], worldPoint[2]); CHECK(std::fabs(localPoint[0] - 1.f) < 1e-5f); CHECK(std::fabs(localPoint[1] - 2.f) < 1e-5f); CHECK(std::fabs(localPoint[2] - 3.f) < 1e-5f); const std::vector worldVector = body->localToWorldVector(2.f, 0.f, 0.f); CHECK(std::fabs(worldVector[0]) < 1e-5f); CHECK(std::fabs(worldVector[1] - 2.f) < 1e-5f); CHECK(std::fabs(worldVector[2]) < 1e-5f); const std::vector localVector = body->worldToLocalVector(worldVector[0], worldVector[1], worldVector[2]); CHECK(std::fabs(localVector[0] - 2.f) < 1e-5f); CHECK(std::fabs(localVector[1]) < 1e-5f); body->setLinearVelocity(1.f, 2.f, 3.f); body->setAngularVelocity(0.f, 0.f, 2.f); // Local (2,0,0) is world offset (0,2,0): v + omega x r = (-3,2,3). const std::vector localPointVelocity = body->getLocalPointVelocity(2.f, 0.f, 0.f); const std::vector worldPointVelocity = body->getWorldPointVelocity(3.f, 6.f, 5.f); for (const std::vector *velocity : {&localPointVelocity, &worldPointVelocity}) { CHECK(std::fabs((*velocity)[0] + 3.f) < 1e-5f); CHECK(std::fabs((*velocity)[1] - 2.f) < 1e-5f); CHECK(std::fabs((*velocity)[2] - 3.f) < 1e-5f); } body->setLinearVelocity(0.f, 0.f, 0.f); body->setAngularVelocity(0.f, 0.f, 0.f); body->setMassProperties(2.f, 0.f, 0.f, 0.f, 2.f, 2.f, 2.f); body->applyLocalLinearImpulseToCenter(4.f, 0.f, 0.f); CHECK(std::fabs(body->getLinearVelocityX()) < 1e-5f); CHECK(std::fabs(body->getLinearVelocityY() - 2.f) < 1e-5f); body->applyLocalAngularImpulse(4.f, 0.f, 0.f); CHECK(std::fabs(body->getAngularVelocityX()) < 1e-5f); CHECK(std::fabs(body->getAngularVelocityY() - 2.f) < 1e-5f); CHECK_THROWS((body->localToWorldPoint(std::numeric_limits::infinity(), 0.f, 0.f), false)); CHECK_THROWS((body->applyLocalForceToCenter( 0.f, std::numeric_limits::quiet_NaN(), 0.f), false)); } TEST_CASE("box3d.body.perAxisLocksAndAdvancedForces") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, 0.f, 0.f, false)); Body3D *body = world->newBody("dynamic", 0.f, 0.f, 0.f); body->newBoxShape(1.f, 1.f, 1.f, 1.f); body->setMotionLocks(true, false, true, true, true, false); CHECK(body->isLinearXLocked()); CHECK(!body->isLinearYLocked()); CHECK(body->isLinearZLocked()); CHECK(body->isAngularXLocked()); CHECK(body->isAngularYLocked()); CHECK(!body->isAngularZLocked()); body->setLinearVelocity(5.f, 2.f, 4.f); body->applyLinearImpulseAt(0.f, 1.f, 0.f, 0.5f, 0.f, 0.f); body->applyTorque(0.f, 0.f, 4.f); world->updateFull(0.25f, 4); CHECK(std::fabs(body->getX()) < 0.001f); CHECK_GT(body->getY(), 0.1f); CHECK(std::fabs(body->getZ()) < 0.001f); CHECK(std::fabs(body->getAngularVelocityX()) < 0.001f); CHECK(std::fabs(body->getAngularVelocityY()) < 0.001f); CHECK_GT(std::fabs(body->getAngularVelocityZ()), 0.1f); body->setFixedRotation(true); CHECK(body->isAngularXLocked()); CHECK(body->isAngularYLocked()); CHECK(body->isAngularZLocked()); CHECK(body->isFixedRotation()); } TEST_CASE("box3d.body.kinematicTargetTransform") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, 0.f, 0.f, false)); Body3D *body = world->newBody("kinematic", 0.f, 0.f, 0.f); body->newBoxShape(1.f, 1.f, 1.f); body->setTargetTransform(2.f, 1.f, -1.f, 0.f, 0.f, 0.f, 2.f, 0.5f); world->updateFull(0.5f, 4); CHECK(std::fabs(body->getX() - 2.f) < 0.05f); CHECK(std::fabs(body->getY() - 1.f) < 0.05f); CHECK(std::fabs(body->getZ() + 1.f) < 0.05f); CHECK(std::fabs(body->getRotW() - 1.f) < 0.001f); CHECK_THROWS((body->setTargetTransform(0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 1.f), false)); CHECK_THROWS((body->setTargetTransform(0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 1.f, 0.f), false)); } TEST_CASE("box3d.joint.distanceMaintainsLengthAndSupportsSpringMotorConfiguration") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, -10.f, 0.f, false)); Body3D *ground = world->newBody("static", 0.f, 0.f, 0.f); Body3D *bob = world->newBody("dynamic", 0.f, 1.f, 0.f); bob->newSphereShape(0.25f, 2.f); Joint3D *joint = world->newDistanceJoint(ground, bob, 0.f, 3.f, 0.f, 0.f, 1.f, 0.f, 2.f, false); REQUIRE(joint != nullptr); CHECK(joint->isValid()); CHECK_EQ(joint->getKind(), std::string("distance")); CHECK_EQ(joint->getBodyAId(), ground->getId()); CHECK_EQ(joint->getBodyBId(), bob->getId()); CHECK(std::fabs(joint->getDistanceLength() - 2.f) < 0.001f); CHECK(!joint->getCollideConnected()); bob->applyLinearImpulse(1.f, 0.f, 0.f); for (int i = 0; i < 120; ++i) world->updateFull(1.f / 60.f, 4); CHECK(std::fabs(joint->getDistanceCurrentLength() - 2.f) < 0.03f); CHECK_GT(std::fabs(joint->getConstraintForceY()), 0.1f); joint->setDistanceSpring(true, 4.f, 0.7f); joint->setDistanceLimits(true, 1.5f, 2.5f); joint->setDistanceMotor(true, 0.25f, 20.f); joint->setDistanceLength(2.1f); CHECK(std::fabs(joint->getDistanceLength() - 2.1f) < 0.001f); CHECK_THROWS((joint->setDistanceLimits(true, 3.f, 2.f), false)); CHECK_THROWS((joint->setRevoluteMotor(true, 1.f, 2.f), false)); } TEST_CASE("box3d.joint.revoluteMotorHonorsLimitAndBodyDestructionInvalidatesWrapper") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, 0.f, 0.f, false)); Body3D *frame = world->newBody("static", 0.f, 0.f, 0.f); Body3D *door = world->newBody("dynamic", 1.f, 0.f, 0.f); door->newBoxShape(2.f, 0.25f, 0.25f, 1.f); Joint3D *hinge = world->newRevoluteJoint(frame, door, 0.f, 0.f, 0.f, 0.f, 0.f, 1.f, false); REQUIRE(hinge != nullptr); CHECK_EQ(hinge->getKind(), std::string("revolute")); hinge->setRevoluteLimits(true, -0.5f, 0.5f); hinge->setRevoluteSpring(false, 0.f, 0.f, 0.f); hinge->setRevoluteMotor(true, 3.f, 100.f); for (int i = 0; i < 120; ++i) world->updateFull(1.f / 60.f, 4); CHECK_GT(hinge->getRevoluteAngle(), 0.35f); CHECK_LT(hinge->getRevoluteAngle(), 0.55f); CHECK_GT(std::fabs(hinge->getRevoluteMotorTorque()), 0.01f); CHECK_THROWS((hinge->setRevoluteLimits(true, 1.f, -1.f), false)); CHECK_THROWS((hinge->setDistanceLength(1.f), false)); door->destroy(); CHECK(!hinge->isValid()); CHECK_EQ(hinge->getBodyAId(), -1); CHECK_EQ(hinge->getBodyBId(), -1); hinge->destroy(); } TEST_CASE("box3d.joint.validatesBodiesAxisAndWorldOwnership") { auto *mod = Physics::create(); std::unique_ptr worldA(mod->newWorld3D(0.f, 0.f, 0.f, false)); std::unique_ptr worldB(mod->newWorld3D(0.f, 0.f, 0.f, false)); Body3D *a = worldA->newBody("static", 0.f, 0.f, 0.f); Body3D *b = worldA->newBody("dynamic", 1.f, 0.f, 0.f); b->newSphereShape(0.25f); Body3D *foreign = worldB->newBody("dynamic", 0.f, 0.f, 0.f); foreign->newSphereShape(0.25f); CHECK_THROWS((worldA->newDistanceJoint(a, foreign, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 1.f, false), false)); CHECK_THROWS((worldA->newRevoluteJoint(a, b, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, false), false)); CHECK_THROWS((worldA->newDistanceJoint(a, a, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 1.f, false), false)); } TEST_CASE("box3d.joint.prismaticMotorMovesOnlyAlongAxisAndHonorsLimit") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, 0.f, 0.f, false)); Body3D *rail = world->newBody("static", 0.f, 0.f, 0.f); Body3D *carriage = world->newBody("dynamic", 0.f, 0.f, 0.f); carriage->newBoxShape(0.5f, 0.5f, 0.5f); Joint3D *slider = world->newPrismaticJoint(rail, carriage, 0.f, 0.f, 0.f, 1.f, 0.f, 0.f, false); REQUIRE(slider != nullptr); CHECK_EQ(slider->getKind(), std::string("prismatic")); slider->setPrismaticLimits(true, 0.f, 1.f); slider->setPrismaticSpring(false, 0.f, 0.f, 0.f); slider->setPrismaticMotor(true, 3.f, 100.f); for (int i = 0; i < 120; ++i) world->updateFull(1.f / 60.f, 4); CHECK_GT(slider->getPrismaticTranslation(), 0.85f); CHECK_LT(slider->getPrismaticTranslation(), 1.05f); CHECK(std::fabs(carriage->getY()) < 0.01f); CHECK(std::fabs(carriage->getZ()) < 0.01f); CHECK_GT(std::fabs(slider->getPrismaticMotorForce()), 0.01f); CHECK_THROWS((slider->setPrismaticLimits(true, 2.f, 1.f), false)); CHECK_THROWS((slider->setSphericalConeLimit(true, 0.5f), false)); } TEST_CASE("box3d.joint.sphericalMotorRespectsConeAndKeepsSharedAnchor") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, -10.f, 0.f, false)); Body3D *socket = world->newBody("static", 0.f, 0.f, 0.f); Body3D *limb = world->newBody("dynamic", 0.f, -1.f, 0.f); limb->newBoxShape(0.25f, 2.f, 0.25f); Joint3D *ball = world->newSphericalJoint(socket, limb, 0.f, 0.f, 0.f, 0.f, 0.f, 1.f, false); REQUIRE(ball != nullptr); CHECK_EQ(ball->getKind(), std::string("spherical")); ball->setSphericalConeLimit(true, 0.4f); ball->setSphericalTwistLimits(true, -0.3f, 0.3f); ball->setSphericalMotor(true, 4.f, 0.f, 0.f, 100.f); for (int i = 0; i < 120; ++i) world->updateFull(1.f / 60.f, 4); CHECK_GT(ball->getSphericalConeAngle(), 0.25f); CHECK_LT(ball->getSphericalConeAngle(), 0.45f); CHECK_LT(std::fabs(ball->getSphericalTwistAngle()), 0.35f); const b3Pos anchor = b3Body_GetWorldPoint(limb->raw(), b3Vec3{0.f, 1.f, 0.f}); CHECK(std::fabs(anchor.x) < 0.03f); CHECK(std::fabs(anchor.y) < 0.03f); CHECK(std::fabs(anchor.z) < 0.03f); CHECK_THROWS((ball->setSphericalConeLimit(true, 4.f), false)); CHECK_THROWS((ball->setSphericalTwistLimits(true, 0.5f, -0.5f), false)); } TEST_CASE("box3d.joint.stressEventsExposeStableIdentityAndSolverFeedback") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, -10.f, 0.f, false)); Body3D *support = world->newBody("static", 0.f, 2.f, 0.f); Body3D *load = world->newBody("dynamic", 0.f, 0.f, 0.f); load->newSphereShape(0.5f, 20.f); Joint3D *joint = world->newDistanceJoint(support, load, 0.f, 2.f, 0.f, 0.f, 0.f, 0.f, 2.f, false); joint->setForceThreshold(0.1f); joint->setTorqueThreshold(1000000.f); CHECK(std::fabs(joint->getForceThreshold() - 0.1f) < 0.001f); CHECK(std::fabs(joint->getTorqueThreshold() - 1000000.f) < 1.f); world->updateFull(1.f / 60.f, 4); REQUIRE_EQ(world->getJointStressCount(), 1); CHECK_EQ(world->getJointStressJointId(0), joint->getId()); CHECK_EQ(world->getJointStressBodyAId(0), support->getId()); CHECK_EQ(world->getJointStressBodyBId(0), load->getId()); CHECK_EQ(world->getJointStressKind(0), 0); CHECK_GT(std::fabs(world->getJointStressForceY(0)), 1.f); CHECK(std::isfinite(world->getJointStressTorqueX(0))); CHECK_THROWS((world->getJointStressJointId(1), false)); joint->setForceThreshold(1000000.f); world->updateFull(1.f / 60.f, 4); CHECK_EQ(world->getJointStressCount(), 0); CHECK_THROWS((joint->setForceThreshold(-1.f), false)); CHECK_THROWS((joint->setTorqueThreshold(std::numeric_limits::quiet_NaN()), false)); } TEST_CASE("box3d.joint.wheelCombinesSuspensionSpinAndSteering") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, 0.f, 0.f, false)); Body3D *chassis = world->newBody("static", 0.f, 0.f, 0.f); Body3D *wheelBody = world->newBody("dynamic", 0.f, 0.f, 0.f); wheelBody->newSphereShape(0.5f, 1.f); Joint3D *wheel = world->newWheelJoint(chassis, wheelBody, 0.f, 0.f, 0.f, 0.f, 1.f, 0.f, 0.f, 0.f, 1.f, false); REQUIRE(wheel != nullptr); CHECK_EQ(wheel->getKind(), std::string("wheel")); wheel->setWheelSuspension(true, 5.f, 0.7f); wheel->setWheelSuspensionLimits(true, -0.1f, 0.1f); wheel->setWheelSpinMotor(true, 5.f, 100.f); wheel->setWheelSteering(true, 0.3f, 5.f, 0.8f, 100.f); wheel->setWheelSteeringLimits(true, -0.4f, 0.4f); for (int i = 0; i < 120; ++i) world->updateFull(1.f / 60.f, 4); CHECK_GT(wheel->getWheelSpinSpeed(), 4.f); CHECK_GT(std::fabs(wheel->getWheelSpinTorque()), 0.001f); CHECK_GT(wheel->getWheelSteeringAngle(), 0.2f); CHECK_LT(wheel->getWheelSteeringAngle(), 0.4f); CHECK(std::isfinite(wheel->getWheelSteeringTorque())); CHECK(std::fabs(wheelBody->getX()) < 0.02f); CHECK(std::fabs(wheelBody->getY()) < 0.12f); CHECK(std::fabs(wheelBody->getZ()) < 0.02f); CHECK_THROWS((wheel->setWheelSuspensionLimits(true, 1.f, -1.f), false)); CHECK_THROWS((wheel->setWheelSpinMotor(true, 1.f, -1.f), false)); CHECK_THROWS((world->newWheelJoint(chassis, wheelBody, 0.f, 0.f, 0.f, 0.f, 1.f, 0.f, 0.f, 2.f, 0.f, false), false)); } TEST_CASE("box3d.shape.sensorAndMaterial") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, 0.f, 0.f, true)); Body3D *b = world->newBody("dynamic", 0.f, 0.f, 0.f); Shape3D *shape = b->newBoxShape(1.f, 1.f, 1.f); CHECK(!shape->isSensor()); shape->setSensor(true); CHECK(shape->isSensor()); shape->setFriction(0.7f); CHECK(std::fabs(shape->getFriction() - 0.7f) < 0.001f); shape->setRestitution(0.35f); shape->setRollingResistance(0.12f); shape->setTangentVelocity(1.f, 2.f, 3.f); shape->setMaterialId(-559038737); // 0xDEADBEEF preserves all low bits. shape->setFrictionCombineMode("multiply"); shape->setRestitutionCombineMode("minimum"); shape->setDensity(2.f); CHECK(std::fabs(shape->getRestitution() - 0.35f) < 0.001f); CHECK(std::fabs(shape->getRollingResistance() - 0.12f) < 0.001f); CHECK(std::fabs(shape->getTangentVelocityX() - 1.f) < 0.001f); CHECK(std::fabs(shape->getTangentVelocityY() - 2.f) < 0.001f); CHECK(std::fabs(shape->getTangentVelocityZ() - 3.f) < 0.001f); CHECK_EQ(shape->getMaterialId(), -559038737); CHECK_EQ(shape->getFrictionCombineMode(), std::string("multiply")); CHECK_EQ(shape->getRestitutionCombineMode(), std::string("minimum")); shape->setSensor(false); CHECK(std::fabs(shape->getRollingResistance() - 0.12f) < 0.001f); CHECK(std::fabs(shape->getTangentVelocityZ() - 3.f) < 0.001f); CHECK_EQ(shape->getMaterialId(), -559038737); CHECK_EQ(shape->getFrictionCombineMode(), std::string("multiply")); CHECK_EQ(shape->getRestitutionCombineMode(), std::string("minimum")); shape->setMaterialId(1234); CHECK_EQ(shape->getMaterialId(), 1234); CHECK_EQ(shape->getFrictionCombineMode(), std::string("multiply")); CHECK_THROWS((shape->setFriction(-0.1f), false)); CHECK_THROWS((shape->setRestitution(std::numeric_limits::quiet_NaN()), false)); CHECK_THROWS((shape->setRollingResistance(-0.1f), false)); CHECK_THROWS((shape->setTangentVelocity( 0.f, std::numeric_limits::infinity(), 0.f), false)); CHECK_THROWS((shape->setDensity(-1.f), false)); CHECK_THROWS((shape->setFrictionCombineMode("median"), false)); CHECK_THROWS((shape->setRestitutionCombineMode("add"), false)); } TEST_CASE("box3d.material.tangentVelocityDrivesConveyorContact") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, -10.f, 0.f, false)); Shape3D *belt = world->newBody("static", 0.f, -0.25f, 0.f)->newBoxShape(20.f, 0.5f, 4.f); belt->setFriction(1.f); belt->setTangentVelocity(3.f, 0.f, 0.f); Body3D *cargo = world->newBody("dynamic", 0.f, 0.6f, 0.f); cargo->newBoxShape(0.8f, 0.8f, 0.8f, 1.f, 1.f); for (int i = 0; i < 180; ++i) world->updateFull(1.f / 60.f, 4); CHECK_GT(std::fabs(cargo->getX()), 1.f); CHECK_LT(std::fabs(cargo->getY() - 0.4f), 0.08f); } TEST_CASE("box3d.material.restitutionCombineModeChangesCollisionResponse") { auto reboundSpeed = [](const std::string &mode) { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, -10.f, 0.f, false)); Shape3D *floor = world->newBody("static", 0.f, -0.25f, 0.f)->newBoxShape(10.f, 0.5f, 10.f); floor->setRestitution(0.2f); floor->setRestitutionCombineMode(mode); Body3D *ball = world->newBody("dynamic", 0.f, 2.f, 0.f); Shape3D *ballShape = ball->newSphereShape(0.25f); ballShape->setRestitution(0.8f); float maximumUpwardSpeed = 0.f; for (int i = 0; i < 120; ++i) { world->updateFull(1.f / 120.f, 2); if (i > 30) maximumUpwardSpeed = std::max(maximumUpwardSpeed, ball->getLinearVelocityY()); } return maximumUpwardSpeed; }; const float minimumBounce = reboundSpeed("minimum"); const float maximumBounce = reboundSpeed("maximum"); CHECK_GT(minimumBounce, 0.5f); CHECK_GT(maximumBounce, minimumBounce + 2.f); } TEST_CASE("box3d.world.solverAndContinuousCollisionTuning") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, 0.f, 0.f, false)); CHECK(world->isContinuousCollisionEnabled()); world->setContinuousCollisionEnabled(false); CHECK(!world->isContinuousCollisionEnabled()); world->setContinuousCollisionEnabled(true); world->setRestitutionThreshold(0.75f); CHECK(std::fabs(world->getRestitutionThreshold() - 0.75f) < 0.001f); world->setContactTuning(42.f, 0.85f, 2.5f); CHECK(std::fabs(world->getContactHertz() - 42.f) < 0.001f); CHECK(std::fabs(world->getContactDampingRatio() - 0.85f) < 0.001f); CHECK(std::fabs(world->getContactPushOutSpeed() - 2.5f) < 0.001f); world->setContactRecycleDistance(0.02f); CHECK(std::fabs(world->getContactRecycleDistance() - 0.02f) < 0.001f); world->setMaximumLinearSpeed(12.f); CHECK(std::fabs(world->getMaximumLinearSpeed() - 12.f) < 0.001f); CHECK(world->isWarmStartingEnabled()); world->setWarmStartingEnabled(false); CHECK(!world->isWarmStartingEnabled()); world->setWarmStartingEnabled(true); Body3D *fast = world->newBody("dynamic", 0.f, 0.f, 0.f); fast->newSphereShape(0.1f); fast->setLinearVelocity(100.f, 0.f, 0.f); world->updateFull(1.f / 60.f, 1); CHECK_LE(fast->getLinearVelocityX(), 12.01f); CHECK_THROWS((world->setRestitutionThreshold(-0.01f), false)); CHECK_THROWS((world->setContactTuning(0.f, 1.f, 1.f), false)); CHECK_THROWS((world->setContactTuning(30.f, -0.1f, 1.f), false)); CHECK_THROWS((world->setContactTuning(30.f, 1.f, std::numeric_limits::infinity()), false)); CHECK_THROWS((world->setContactRecycleDistance(-0.01f), false)); CHECK_THROWS((world->setMaximumLinearSpeed(0.f), false)); } TEST_CASE("box3d.contact.runtimeBodyAndShapePairOverridesRefreshImmediately") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, 0.f, 0.f, false)); Body3D *wallBody = world->newBody("static", 0.f, 0.f, 0.f); Shape3D *nearWall = wallBody->newBoxShape(0.2f, 4.f, 4.f); Shape3D *farWall = wallBody->newBoxShape(0.2f, 4.f, 4.f); farWall->setLocalPosition(2.f, 0.f, 0.f); Body3D *mover = world->newBody("dynamic", -2.f, 0.f, 0.f); Shape3D *ball = mover->newSphereShape(0.25f); mover->setBullet(true); CHECK(world->isBodyPairCollisionEnabled(wallBody, mover)); world->setBodyPairCollisionEnabled(wallBody, mover, false); CHECK(!world->isBodyPairCollisionEnabled(mover, wallBody)); mover->setLinearVelocity(5.f, 0.f, 0.f); for (int i = 0; i < 60; ++i) world->updateFull(1.f / 60.f, 2); CHECK_GT(mover->getX(), 2.5f); mover->setPosition(-2.f, 0.f, 0.f); mover->setLinearVelocity(5.f, 0.f, 0.f); world->setBodyPairCollisionEnabled(wallBody, mover, true); CHECK(world->isBodyPairCollisionEnabled(wallBody, mover)); for (int i = 0; i < 60; ++i) world->updateFull(1.f / 60.f, 2); CHECK_LT(mover->getX(), -0.25f); mover->setPosition(-2.f, 0.f, 0.f); mover->setLinearVelocity(5.f, 0.f, 0.f); world->setShapePairCollisionEnabled(nearWall, ball, false); CHECK(!world->isShapePairCollisionEnabled(ball, nearWall)); CHECK(world->isShapePairCollisionEnabled(ball, farWall)); for (int i = 0; i < 90; ++i) world->updateFull(1.f / 60.f, 2); CHECK_GT(mover->getX(), 0.5f); CHECK_LT(mover->getX(), 1.75f); world->setShapePairCollisionEnabled(nearWall, ball, true); CHECK(world->isShapePairCollisionEnabled(nearWall, ball)); CHECK_THROWS((world->setBodyPairCollisionEnabled(mover, mover, false), false)); CHECK_THROWS((world->setShapePairCollisionEnabled(ball, ball, false), false)); } TEST_CASE("box3d.world.explosionUsesGeometryFalloffMaskAndReportsVelocityDelta") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, 0.f, 0.f, false)); Body3D *nearBody = world->newBody("dynamic", 1.f, 0.f, 0.f); nearBody->newSphereShape(0.25f)->setCategoryBits(1); Body3D *farBody = world->newBody("dynamic", 3.5f, 0.f, 0.f); farBody->newSphereShape(0.25f)->setCategoryBits(1); Body3D *filteredBody = world->newBody("dynamic", 1.f, 0.f, 2.f); filteredBody->newSphereShape(0.25f)->setCategoryBits(2); Body3D *staticBody = world->newBody("static", 1.f, 0.f, -2.f); staticBody->newSphereShape(0.25f)->setCategoryBits(1); CHECK_EQ(world->explode(0.f, 0.f, 0.f, 1.f, 4.f, 10.f, 1), 2); CHECK_EQ(world->getExplosionResultCount(), 2); CHECK_EQ(world->getExplosionResultBodyId(0), nearBody->getId()); CHECK_EQ(world->getExplosionResultBodyId(1), farBody->getId()); CHECK_GT(nearBody->getLinearVelocityX(), farBody->getLinearVelocityX()); CHECK_GT(farBody->getLinearVelocityX(), 0.f); CHECK(std::fabs(filteredBody->getLinearVelocityX()) < 1e-6f); CHECK(std::fabs(world->getExplosionResultDeltaVelocityX(0) - nearBody->getLinearVelocityX()) < 1e-5f); CHECK(std::isfinite(world->getExplosionResultDeltaAngularVelocityZ(0))); nearBody->setLinearVelocity(0.f, 0.f, 0.f); farBody->setLinearVelocity(0.f, 0.f, 0.f); CHECK_EQ(world->explode(0.f, 0.f, 0.f, 1.f, 4.f, -10.f, 1), 2); CHECK_LT(nearBody->getLinearVelocityX(), 0.f); CHECK_EQ(world->explode(0.f, 0.f, 0.f, 1.f, 4.f, 0.f, 1), 0); CHECK_EQ(world->getExplosionResultCount(), 0); CHECK_THROWS((world->explode(0.f, 0.f, 0.f, -1.f, 0.f, 1.f), false)); CHECK_THROWS((world->explode(0.f, 0.f, 0.f, 1.f, -1.f, 1.f), false)); CHECK_THROWS((world->explode(std::numeric_limits::quiet_NaN(), 0.f, 0.f, 1.f, 0.f, 1.f), false)); CHECK_THROWS((world->getExplosionResultBodyId(0), false)); } TEST_CASE("box3d.world.diagnosticsExposeBoundsCountersAndStepProfile") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, -10.f, 0.f, true)); Body3D *floor = world->newBody("static", 0.f, -0.5f, 0.f); floor->newBoxShape(10.f, 1.f, 10.f); Body3D *a = world->newBody("dynamic", -0.5f, 2.f, 0.f); a->newSphereShape(0.25f); Body3D *b = world->newBody("dynamic", 0.5f, 2.f, 0.f); b->newSphereShape(0.25f); world->newDistanceJoint(a, b, -0.5f, 2.f, 0.f, 0.5f, 2.f, 0.f, 1.f); CHECK_EQ(world->getBodyCount(), 3); CHECK_EQ(world->getShapeCount(), 3); CHECK_EQ(world->getJointCount(), 1); CHECK_LE(world->getBoundsMinX(), -5.f); CHECK_GE(world->getBoundsMaxX(), 5.f); CHECK_LE(world->getBoundsMinY(), -1.f); CHECK_GE(world->getBoundsMaxY(), 2.25f); CHECK_GE(world->getStaticTreeHeight(), 0); CHECK_GE(world->getDynamicTreeHeight(), 0); CHECK_GT(world->getMemoryByteCount(), 0); for (int i = 0; i < 120; ++i) world->updateFull(1.f / 60.f, 4); CHECK_GT(world->getContactCount(), 0); CHECK_GE(world->getIslandCount(), 1); CHECK_GE(world->getAwakeBodyCount(), 0); CHECK_GE(world->getAwakeContactCount(), 0); CHECK_GE(world->getRecycledContactCount(), 0); CHECK_GE(world->getProfileStepMs(), 0.f); CHECK_GE(world->getProfilePairsMs(), 0.f); CHECK_GE(world->getProfileCollideMs(), 0.f); CHECK_GE(world->getProfileSolveMs(), 0.f); CHECK_GE(world->getProfileBulletsMs(), 0.f); CHECK_GE(world->getProfileSensorsMs(), 0.f); } TEST_CASE("box3d.shape.explosionScaleDisablesAmplifiesAndSurvivesRecreation") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, 0.f, 0.f, false)); Body3D *normal = world->newBody("dynamic", 2.f, 0.f, 0.f); Shape3D *normalShape = normal->newSphereShape(0.25f); Body3D *amplified = world->newBody("dynamic", 0.f, 0.f, 2.f); Shape3D *amplifiedShape = amplified->newSphereShape(0.25f); Body3D *immune = world->newBody("dynamic", -2.f, 0.f, 0.f); Shape3D *immuneShape = immune->newSphereShape(0.25f); const int stableId = amplifiedShape->getId(); amplifiedShape->setExplosionScale(2.f); immuneShape->setExplosionScale(0.f); amplifiedShape->setSensor(true); amplifiedShape->setSensor(false); CHECK_EQ(amplifiedShape->getId(), stableId); CHECK(std::fabs(amplifiedShape->getExplosionScale() - 2.f) < 1e-6f); CHECK(std::fabs(normalShape->getExplosionScale() - 1.f) < 1e-6f); CHECK_EQ(world->explode(0.f, 0.f, 0.f, 3.f, 0.f, 10.f), 2); const float normalSpeed = std::sqrt( normal->getLinearVelocityX() * normal->getLinearVelocityX() + normal->getLinearVelocityY() * normal->getLinearVelocityY() + normal->getLinearVelocityZ() * normal->getLinearVelocityZ()); const float amplifiedSpeed = std::sqrt( amplified->getLinearVelocityX() * amplified->getLinearVelocityX() + amplified->getLinearVelocityY() * amplified->getLinearVelocityY() + amplified->getLinearVelocityZ() * amplified->getLinearVelocityZ()); CHECK_GT(normalSpeed, 0.f); CHECK(std::fabs(amplifiedSpeed / normalSpeed - 2.f) < 0.02f); CHECK(std::fabs(immune->getLinearVelocityX()) < 1e-6f); CHECK(std::fabs(immune->getLinearVelocityZ()) < 1e-6f); CHECK_THROWS((immuneShape->setExplosionScale(-0.1f), false)); CHECK_THROWS((immuneShape->setExplosionScale( std::numeric_limits::infinity()), false)); } TEST_CASE("box3d.oneway.configurationAndSensorRecreation") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, 0.f, 0.f, true)); Shape3D *shape = world->newBody("static", 0.f, 0.f, 0.f)->newBoxShape(4.f, 0.5f, 4.f); CHECK(!shape->isOneWay()); CHECK_THROWS((shape->setOneWay(0.f, 0.f, 0.f, 0.f, 0.05f, 0.1f), false)); CHECK_THROWS((shape->setOneWay(0.f, 1.f, 0.f, 0.f, -0.01f, 0.1f), false)); CHECK_THROWS((shape->setOneWay(0.f, 1.f, 0.f, 0.f, 0.05f, 1.01f), false)); shape->setOneWay(0.f, 2.f, 0.f, 0.25f, 0.05f, 0.2f); CHECK(shape->isOneWay()); CHECK(b3Shape_ArePreSolveEventsEnabled(shape->raw())); shape->setSensor(true); CHECK(shape->isOneWay()); CHECK(!b3Shape_ArePreSolveEventsEnabled(shape->raw())); shape->setSensor(false); CHECK(shape->isOneWay()); CHECK(b3Shape_ArePreSolveEventsEnabled(shape->raw())); shape->disableOneWay(); CHECK(!shape->isOneWay()); CHECK(!b3Shape_ArePreSolveEventsEnabled(shape->raw())); } TEST_CASE("box3d.oneway.platformPassesFromBelowAndBlocksFromAbove") { auto run = [](float startY, float velocityY, bool oneWay) { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, 0.f, 0.f, false)); Shape3D *platform = world->newBody("static", 0.f, 0.f, 0.f)->newBoxShape(10.f, 0.5f, 10.f); if (oneWay) platform->setOneWay(0.f, 1.f, 0.f, 0.25f, 0.06f, 0.2f); Body3D *ball = world->newBody("dynamic", 0.f, startY, 0.f); ball->newSphereShape(0.25f); ball->setLinearVelocity(0.f, velocityY, 0.f); for (int i = 0; i < 45; ++i) world->updateFull(1.f / 60.f, 4); return ball->getY(); }; CHECK_GT(run(-2.f, 5.f, true), 1.f); const float fromAbove = run(2.f, -5.f, true); CHECK_GT(fromAbove, 0.45f); CHECK_LT(fromAbove, 0.55f); CHECK_LT(run(-2.f, 5.f, false), -0.45f); } TEST_CASE("box3d.shape.filterSurvivesSensorRecreation") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, 0.f, 0.f, true)); Body3D *body = world->newBody("static", 0.f, 0.f, 0.f); Shape3D *shape = body->newSphereShape(0.5f); shape->setCategoryBits(8); shape->setMaskBits(3); shape->setGroupIndex(-7); shape->setTag(42); CHECK_EQ(shape->getCategoryBits(), 8); CHECK_EQ(shape->getMaskBits(), 3); CHECK_EQ(shape->getGroupIndex(), -7); CHECK_EQ(shape->getTag(), 42); const int stableShapeId = shape->getId(); CHECK_GT(stableShapeId, 0); shape->setSensor(true); CHECK_EQ(shape->getId(), stableShapeId); CHECK_EQ(shape->getTag(), 42); CHECK_EQ(shape->getCategoryBits(), 8); CHECK_EQ(shape->getMaskBits(), 3); CHECK_EQ(shape->getGroupIndex(), -7); shape->setSphereRadius(0.75f); CHECK(shape->isSensor()); CHECK_EQ(shape->getId(), stableShapeId); CHECK_EQ(shape->getTag(), 42); CHECK(std::fabs(shape->getRadius() - 0.75f) < 1e-5f); } TEST_CASE("box3d.shape.localTransformBuildsCompoundCollider") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, 0.f, 0.f, false)); Body3D *body = world->newBody("static", 0.f, 0.f, 0.f); Shape3D *box = body->newBoxShape(4.f, 1.f, 1.f); box->setTag(2101); box->setCategoryBits(8); box->setMaskBits(3); box->setGroupIndex(-4); box->setFriction(0.65f); box->setRestitution(0.25f); box->setDensity(2.5f); box->setHitEventsEnabled(true); const int stableBoxId = box->getId(); const float halfRoot = std::sqrt(0.5f); box->setLocalTransform(3.f, 0.f, 0.f, 0.f, 0.f, halfRoot, halfRoot); CHECK_EQ(box->getId(), stableBoxId); CHECK_EQ(box->getTag(), 2101); CHECK_EQ(box->getCategoryBits(), 8); CHECK_EQ(box->getMaskBits(), 3); CHECK_EQ(box->getGroupIndex(), -4); CHECK(std::fabs(box->getFriction() - 0.65f) < 1e-5f); CHECK(std::fabs(box->getRestitution() - 0.25f) < 1e-5f); CHECK(std::fabs(box->getDensity() - 2.5f) < 1e-5f); CHECK(box->areHitEventsEnabled()); CHECK(std::fabs(box->getLocalX() - 3.f) < 1e-5f); CHECK(std::fabs(box->getLocalRotZ() - halfRoot) < 1e-5f); CHECK(box->testPoint(3.f, 1.5f, 0.f)); CHECK(!box->testPoint(4.5f, 0.f, 0.f)); Shape3D *sphere = body->newSphereShape(0.75f); sphere->setLocalPosition(-3.f, 0.f, 0.f); CHECK(sphere->testPoint(-3.f, 0.f, 0.f)); CHECK(!sphere->testPoint(0.f, 0.f, 0.f)); Shape3D *capsule = body->newCapsuleShape(4.f, 0.5f); capsule->setLocalTransform(0.f, 0.f, 3.f, 0.f, 0.f, halfRoot, halfRoot); CHECK(capsule->testPoint(1.5f, 0.f, 3.f)); CHECK(!capsule->testPoint(0.f, 1.5f, 3.f)); CHECK_THROWS((box->setLocalRotation(0.f, 0.f, 0.f, 0.f), false)); } TEST_CASE("box3d.shape.localTransformParticipatesInWorldQueries") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, 0.f, 0.f, false)); Body3D *body = world->newBody("static", 0.f, 0.f, 0.f); Shape3D *shape = body->newSphereShape(1.f); shape->setTag(2201); shape->setLocalPosition(5.f, 0.f, 0.f); CHECK_EQ(world->rayCast(5.f, -4.f, 0.f, 5.f, 4.f, 0.f), body->getId()); CHECK_EQ(world->getRayHitShapeId(), shape->getId()); CHECK_EQ(world->getRayHitShapeTag(), 2201); CHECK_EQ(world->querySphere(5.f, 0.f, 0.f, 0.25f), 1); CHECK_EQ(world->getQueryShapeId(0), shape->getId()); CHECK_EQ(world->querySphere(0.f, 0.f, 0.f, 0.25f), 0); } TEST_CASE("box3d.shape.runtimePrimitiveResizePreservesIdentityAndUpdatesMass") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, 0.f, 0.f, false)); Body3D *body = world->newBody("dynamic", 0.f, 0.f, 0.f); Shape3D *box = body->newBoxShape(1.f, 1.f, 1.f, 2.f, 0.6f, 0.2f); box->setTag(2301); box->setCategoryBits(16); box->setMaskBits(7); box->setGroupIndex(-3); box->setHitEventsEnabled(true); box->setLocalPosition(1.f, 0.f, 0.f); const int stableId = box->getId(); const float initialMass = b3Body_GetMass(body->raw()); CHECK_EQ(box->getKind(), std::string("box")); box->setBoxSize(2.f, 2.f, 2.f); CHECK_EQ(box->getId(), stableId); CHECK_EQ(box->getTag(), 2301); CHECK_EQ(box->getCategoryBits(), 16); CHECK_EQ(box->getMaskBits(), 7); CHECK_EQ(box->getGroupIndex(), -3); CHECK(box->areHitEventsEnabled()); CHECK(std::fabs(box->getFriction() - 0.6f) < 1e-5f); CHECK(std::fabs(box->getRestitution() - 0.2f) < 1e-5f); CHECK(std::fabs(box->getDensity() - 2.f) < 1e-5f); CHECK(std::fabs(box->getBoxWidth() - 2.f) < 1e-5f); CHECK(std::fabs(box->getBoxHeight() - 2.f) < 1e-5f); CHECK(std::fabs(box->getBoxDepth() - 2.f) < 1e-5f); CHECK(box->testPoint(1.f, 0.9f, 0.f)); CHECK_GT(b3Body_GetMass(body->raw()), initialMass * 7.5f); CHECK_THROWS((box->setSphereRadius(1.f), false)); CHECK_THROWS((box->setBoxSize(0.f, 1.f, 1.f), false)); CHECK(std::fabs(box->getBoxWidth() - 2.f) < 1e-5f); } TEST_CASE("box3d.shape.runtimeSphereAndCapsuleResizeAffectsQueries") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, 0.f, 0.f, false)); Body3D *body = world->newBody("static", 0.f, 0.f, 0.f); Shape3D *sphere = body->newSphereShape(0.5f); sphere->setLocalPosition(-3.f, 0.f, 0.f); sphere->setSphereRadius(1.5f); CHECK_EQ(sphere->getKind(), std::string("sphere")); CHECK(std::fabs(sphere->getRadius() - 1.5f) < 1e-5f); CHECK(sphere->testPoint(-1.75f, 0.f, 0.f)); Shape3D *capsule = body->newCapsuleShape(4.f, 0.5f); CHECK_EQ(capsule->getKind(), std::string("capsule")); CHECK(capsule->testPoint(0.f, 2.f, 0.f)); capsule->setCapsuleSize(1.f, 0.25f); CHECK(std::fabs(capsule->getCapsuleHeight() - 1.f) < 1e-5f); CHECK(std::fabs(capsule->getRadius() - 0.25f) < 1e-5f); CHECK(!capsule->testPoint(0.f, 2.f, 0.f)); CHECK(capsule->testPoint(0.f, 0.6f, 0.f)); CHECK_THROWS((capsule->setCapsuleSize(-1.f, 0.25f), false)); CHECK_THROWS((sphere->setCapsuleSize(1.f, 0.25f), false)); } TEST_CASE("box3d.shape.convexHullCreatesQueriesAndRebuildsAtomically") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, 0.f, 0.f, true)); Body3D *body = world->newBody("static", 0.f, 0.f, 0.f); const std::vector smallCube{ -0.5f, -0.5f, -0.5f, 0.5f, -0.5f, -0.5f, -0.5f, 0.5f, -0.5f, 0.5f, 0.5f, -0.5f, -0.5f, -0.5f, 0.5f, 0.5f, -0.5f, 0.5f, -0.5f, 0.5f, 0.5f, 0.5f, 0.5f, 0.5f}; Shape3D *hull = body->newConvexHullShape(smallCube, 16); REQUIRE(hull != nullptr); CHECK_EQ(hull->getKind(), std::string("convexHull")); CHECK_EQ(hull->getConvexHullPointCount(), 8); CHECK_EQ(hull->getConvexHullMaxVertices(), 16); CHECK(hull->testPoint(0.f, 0.f, 0.f)); CHECK(!hull->testPoint(0.75f, 0.f, 0.f)); CHECK_EQ(world->querySphere(0.f, 0.f, 0.f, 0.1f), 1); hull->setTag(77); hull->setCategoryBits(8); hull->setSensor(true); const int stableId = hull->getId(); const std::vector largeCube{ -1.f, -1.f, -1.f, 1.f, -1.f, -1.f, -1.f, 1.f, -1.f, 1.f, 1.f, -1.f, -1.f, -1.f, 1.f, 1.f, -1.f, 1.f, -1.f, 1.f, 1.f, 1.f, 1.f, 1.f}; hull->setConvexHullVertices(largeCube, 32); CHECK_EQ(hull->getId(), stableId); CHECK_EQ(hull->getTag(), 77); CHECK_EQ(hull->getCategoryBits(), 8); CHECK(hull->isSensor()); CHECK_EQ(hull->getConvexHullMaxVertices(), 32); CHECK(hull->testPoint(0.75f, 0.f, 0.f)); const std::vector coplanar{0.f, 0.f, 0.f, 1.f, 0.f, 0.f, 0.f, 1.f, 0.f, 1.f, 1.f, 0.f}; CHECK_THROWS((hull->setConvexHullVertices(coplanar, 16), false)); CHECK_THROWS((hull->setConvexHullVertices(largeCube, 3), false)); CHECK_THROWS((body->newConvexHullShape({0.f, 0.f, 0.f}, 16), false)); CHECK_EQ(hull->getConvexHullPointCount(), 8); CHECK_EQ(hull->getConvexHullMaxVertices(), 32); CHECK(hull->testPoint(0.75f, 0.f, 0.f)); } TEST_CASE("box3d.shape.convexHullSupportsLocalTransformAndRigidCollision") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, -10.f, 0.f, false)); const std::vector wedge{ -2.f, -0.5f, -2.f, 2.f, -0.5f, -2.f, -2.f, -0.5f, 2.f, 2.f, -0.5f, 2.f, -2.f, 0.5f, -2.f, 2.f, 0.5f, -2.f}; Shape3D *ground = world->newBody("static", 0.f, 0.f, 0.f)->newConvexHullShape(wedge, 16); ground->setLocalPosition(1.f, 0.f, 0.f); CHECK(ground->testPoint(1.f, 0.f, -1.5f)); CHECK_EQ(world->querySphere(1.f, 0.f, -1.5f, 0.1f), 1); Body3D *ball = world->newBody("dynamic", 1.f, 3.f, -1.5f); ball->newSphereShape(0.25f); for (int i = 0; i < 120; ++i) world->update(1.f / 60.f); CHECK_GT(ball->getY(), 0.65f); } TEST_CASE("box3d.shape.triangleMeshValidatesRebuildsAndStaysStatic") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, 0.f, 0.f, true)); const std::vector vertices{-1.f, 0.f, -1.f, 1.f, 0.f, -1.f, 1.f, 0.f, 1.f, -1.f, 0.f, 1.f}; const std::vector indices{0, 2, 1, 0, 3, 2}; Body3D *body = world->newBody("static", 0.f, 0.f, 0.f); Shape3D *mesh = body->newTriangleMeshShape(vertices, indices); REQUIRE(mesh != nullptr); CHECK_EQ(mesh->getKind(), std::string("triangleMesh")); CHECK_EQ(mesh->getTriangleMeshVertexCount(), 4); CHECK_EQ(mesh->getTriangleMeshTriangleCount(), 2); CHECK_EQ(world->rayCast(0.f, 2.f, 0.f, 0.f, -2.f, 0.f), body->getId()); mesh->setTag(91); mesh->setCategoryBits(16); mesh->setSensor(true); mesh->setLocalPosition(0.f, 1.f, 0.f); const int stableId = mesh->getId(); CHECK_EQ(world->rayCast(0.f, 2.f, 0.f, 0.f, 0.f, 0.f), body->getId()); const std::vector larger{-2.f, 0.f, -2.f, 2.f, 0.f, -2.f, 2.f, 0.f, 2.f, -2.f, 0.f, 2.f}; mesh->setTriangleMeshData(larger, indices, false, 0.f, true, true); CHECK_EQ(mesh->getId(), stableId); CHECK_EQ(mesh->getTag(), 91); CHECK_EQ(mesh->getCategoryBits(), 16); CHECK(mesh->isSensor()); CHECK_EQ(mesh->getTriangleMeshVertexCount(), 4); CHECK_EQ(world->rayCast(1.5f, 2.f, 0.f, 1.5f, 0.f, 0.f), body->getId()); CHECK_THROWS((mesh->setTriangleMeshData(larger, {0, 1, 8}), false)); CHECK_THROWS((mesh->setTriangleMeshData(larger, {0, 0, 1}), false)); CHECK_THROWS((mesh->setTriangleMeshData(larger, indices, true, -0.1f), false)); CHECK_EQ(mesh->getTriangleMeshTriangleCount(), 2); CHECK_EQ(world->rayCast(1.5f, 2.f, 0.f, 1.5f, 0.f, 0.f), body->getId()); CHECK_THROWS((body->setType("dynamic"), false)); CHECK_EQ(body->getType(), std::string("static")); Body3D *dynamicBody = world->newBody("dynamic", 0.f, 0.f, 0.f); CHECK_THROWS((dynamicBody->newTriangleMeshShape(vertices, indices), false)); } TEST_CASE("box3d.shape.concaveTriangleMeshPreservesHoleAndSupportsRigidContact") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, -10.f, 0.f, false)); std::vector vertices; std::vector indices; auto addQuad = [&](float minX, float maxX, float minZ, float maxZ) { const int32_t base = static_cast(vertices.size() / 3); vertices.insert(vertices.end(), {minX, 0.f, minZ, maxX, 0.f, minZ, maxX, 0.f, maxZ, minX, 0.f, maxZ}); indices.insert(indices.end(), {base, base + 2, base + 1, base, base + 3, base + 2}); }; addQuad(-3.f, -1.f, -3.f, 3.f); addQuad(1.f, 3.f, -3.f, 3.f); addQuad(-1.f, 1.f, -3.f, -1.f); addQuad(-1.f, 1.f, 1.f, 3.f); Body3D *groundBody = world->newBody("static", 0.f, 0.f, 0.f); groundBody->newTriangleMeshShape(vertices, indices, true, 0.001f, true, false); CHECK_EQ(world->querySphere(0.f, 0.f, 0.f, 0.2f), 0); CHECK_EQ(world->querySphere(2.f, 0.f, 0.f, 0.2f), 1); CHECK_EQ(world->rayCast(0.f, 2.f, 0.f, 0.f, -2.f, 0.f), -1); CHECK_EQ(world->rayCast(2.f, 2.f, 0.f, 2.f, -2.f, 0.f), groundBody->getId()); Body3D *ball = world->newBody("dynamic", 2.f, 3.f, 0.f); ball->newSphereShape(0.25f); for (int i = 0; i < 120; ++i) world->update(1.f / 60.f); CHECK_GT(ball->getY(), 0.2f); CHECK_LT(ball->getY(), 0.35f); } TEST_CASE("box3d.shape.heightFieldUpdatesRegionAndPreservesConfiguration") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, 0.f, 0.f, true)); Body3D *body = world->newBody("static", -1.f, 0.f, -1.f); std::vector heights(9, 0.f); Shape3D *terrain = body->newHeightFieldShape(3, 3, 1.f, 1.f, heights, -4.f, 4.f); REQUIRE(terrain != nullptr); CHECK_EQ(terrain->getKind(), std::string("heightField")); CHECK_EQ(terrain->getHeightFieldCountX(), 3); CHECK_EQ(terrain->getHeightFieldCountZ(), 3); CHECK(std::fabs(terrain->getHeightFieldCellSizeX() - 1.f) < 1e-6f); CHECK(std::fabs(terrain->getHeightFieldCellSizeZ() - 1.f) < 1e-6f); CHECK_EQ(world->rayCast(0.f, 3.f, 0.f, 0.f, -3.f, 0.f), body->getId()); CHECK(std::fabs(world->getRayHitY()) < 0.001f); terrain->setTag(123); terrain->setCategoryBits(32); terrain->setSensor(true); const int stableId = terrain->getId(); terrain->setHeightFieldRegion(1, 1, 1, 1, {2.f}); CHECK_EQ(terrain->getId(), stableId); CHECK_EQ(terrain->getTag(), 123); CHECK_EQ(terrain->getCategoryBits(), 32); CHECK(terrain->isSensor()); CHECK(std::fabs(terrain->getHeightFieldHeight(1, 1) - 2.f) < 1e-6f); CHECK_EQ(world->rayCast(0.f, 3.f, 0.f, 0.f, -3.f, 0.f), body->getId()); CHECK(std::fabs(world->getRayHitY() - 2.f) < 0.001f); CHECK_THROWS((terrain->setHeightFieldRegion(3, 0, 1, 1, {0.f}), false)); CHECK_THROWS((terrain->setHeightFieldRegion(0, 0, 2, 2, {0.f}), false)); CHECK_THROWS((terrain->setHeightFieldHeights(std::vector(9, 5.f)), false)); CHECK_THROWS((terrain->getHeightFieldHeight(3, 0), false)); CHECK_THROWS((terrain->setLocalPosition(1.f, 0.f, 0.f), false)); CHECK(std::fabs(terrain->getHeightFieldHeight(1, 1) - 2.f) < 1e-6f); body->setPosition(4.f, 1.f, 5.f); CHECK_EQ(world->rayCast(5.f, 5.f, 6.f, 5.f, -2.f, 6.f), body->getId()); CHECK(std::fabs(world->getRayHitY() - 3.f) < 0.001f); CHECK_THROWS((body->setType("kinematic"), false)); } TEST_CASE("box3d.shape.heightFieldSupportsTerrainContactAndStaticValidation") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, -10.f, 0.f, false)); Body3D *ground = world->newBody("static", -1.f, 0.f, -1.f); const std::vector heights(9, 0.f); ground->newHeightFieldShape(3, 3, 1.f, 1.f, heights, -1.f, 1.f); Body3D *ball = world->newBody("dynamic", 0.f, 3.f, 0.f); ball->newSphereShape(0.25f); for (int i = 0; i < 120; ++i) world->update(1.f / 60.f); CHECK_GT(ball->getY(), 0.2f); CHECK_LT(ball->getY(), 0.35f); Body3D *dynamicBody = world->newBody("dynamic", 0.f, 0.f, 0.f); CHECK_THROWS((dynamicBody->newHeightFieldShape(3, 3, 1.f, 1.f, heights, -1.f, 1.f), false)); CHECK_THROWS((ground->newHeightFieldShape(1, 3, 1.f, 1.f, heights, -1.f, 1.f), false)); CHECK_THROWS((ground->newHeightFieldShape(3, 3, 0.f, 1.f, heights, -1.f, 1.f), false)); CHECK_THROWS((ground->newHeightFieldShape(3, 3, 1.f, 1.f, heights, 2.f, 1.f), false)); } TEST_CASE("box3d.query.rayAndShapeCastExposeMeshAndHeightFieldTriangleIndex") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, 0.f, 0.f, true)); const std::vector meshVertices{-1.f, 0.f, -1.f, 1.f, 0.f, -1.f, 1.f, 0.f, 1.f, -1.f, 0.f, 1.f}; const std::vector meshIndices{0, 2, 1, 0, 3, 2}; Body3D *meshBody = world->newBody("static", 0.f, 0.f, 0.f); Shape3D *meshShape = meshBody->newTriangleMeshShape(meshVertices, meshIndices); meshShape->setMaterialId(0x12345678); CHECK_EQ(world->rayCast(-0.5f, 2.f, -0.5f, -0.5f, -2.f, -0.5f), meshBody->getId()); CHECK_GE(world->getRayHitTriangleIndex(), 0); CHECK_LT(world->getRayHitTriangleIndex(), 2); CHECK_EQ(world->getRayResultTriangleIndex(0), world->getRayHitTriangleIndex()); CHECK_EQ(world->getRayHitMaterialId(), 0x12345678); CHECK_EQ(world->getRayResultMaterialId(0), 0x12345678); CHECK_EQ(world->castSphere(-0.5f, 2.f, -0.5f, 0.1f, 0.f, -4.f, 0.f), meshBody->getId()); CHECK_GE(world->getShapeCastTriangleIndex(), 0); CHECK_LT(world->getShapeCastTriangleIndex(), 2); CHECK_EQ(world->getShapeCastResultTriangleIndex(0), world->getShapeCastTriangleIndex()); CHECK_EQ(world->getShapeCastMaterialId(), 0x12345678); CHECK_EQ(world->getShapeCastResultMaterialId(0), 0x12345678); Body3D *terrainBody = world->newBody("static", 10.f, 0.f, 0.f); terrainBody->newHeightFieldShape(3, 3, 1.f, 1.f, std::vector(9, 0.f), -1.f, 1.f); CHECK_EQ(world->rayCast(10.25f, 2.f, 0.25f, 10.25f, -2.f, 0.25f), terrainBody->getId()); CHECK_GE(world->getRayHitTriangleIndex(), 0); CHECK_LT(world->getRayHitTriangleIndex(), 8); CHECK_EQ(world->castSphere(10.25f, 2.f, 0.25f, 0.1f, 0.f, -4.f, 0.f), terrainBody->getId()); CHECK_GE(world->getShapeCastTriangleIndex(), 0); CHECK_LT(world->getShapeCastTriangleIndex(), 8); Body3D *boxBody = world->newBody("static", 20.f, 0.f, 0.f); boxBody->newBoxShape(2.f, 0.5f, 2.f); CHECK_EQ(world->rayCast(20.f, 2.f, 0.f, 20.f, -2.f, 0.f), boxBody->getId()); CHECK_EQ(world->getRayHitTriangleIndex(), -1); CHECK_EQ(world->getRayHitMaterialId(), 0); CHECK_EQ(world->castSphere(20.f, 2.f, 0.f, 0.1f, 0.f, -4.f, 0.f), boxBody->getId()); CHECK_EQ(world->getShapeCastTriangleIndex(), -1); CHECK_EQ(world->getShapeCastMaterialId(), 0); CHECK_EQ(world->rayCast(30.f, 2.f, 0.f, 30.f, -2.f, 0.f), -1); CHECK_EQ(world->getRayHitTriangleIndex(), -1); CHECK_EQ(world->castSphere(30.f, 2.f, 0.f, 0.1f, 0.f, -4.f, 0.f), -1); CHECK_EQ(world->getShapeCastTriangleIndex(), -1); CHECK_THROWS((world->getRayResultTriangleIndex(0), false)); CHECK_THROWS((world->getShapeCastResultTriangleIndex(0), false)); } TEST_CASE("box3d.shape.triangleMeshPerTriangleMaterialsDriveQueriesAndSurviveRebuild") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, 0.f, 0.f, false)); const std::vector vertices{ -4.f, 0.f, -1.f, -2.f, 0.f, -1.f, -3.f, 0.f, 1.f, 2.f, 0.f, -1.f, 4.f, 0.f, -1.f, 3.f, 0.f, 1.f}; const std::vector indices{0, 2, 1, 3, 5, 4}; Shape3D *mesh = world->newBody("static", 0.f, 0.f, 0.f) ->newTriangleMeshShape(vertices, indices, false); mesh->setTriangleMeshMaterialIndices({0, 1}); CHECK_EQ(mesh->getTriangleMeshMaterialCount(), 2); CHECK_EQ(mesh->getTriangleMeshMaterialIndex(0), 0); CHECK_EQ(mesh->getTriangleMeshMaterialIndex(1), 1); mesh->setTriangleMeshMaterial(0, 0.1f, 0.2f, 0.01f, 0.f, 0.f, 0.f, 101, "minimum", "average"); mesh->setTriangleMeshMaterial(1, 0.9f, 0.8f, 0.03f, 1.f, 0.f, 0.f, 202, "maximum", "multiply"); CHECK(std::fabs(mesh->getTriangleMeshMaterialFriction(1) - 0.9f) < 0.001f); CHECK(std::fabs(mesh->getTriangleMeshMaterialRestitution(0) - 0.2f) < 0.001f); CHECK(std::fabs(mesh->getTriangleMeshMaterialRollingResistance(1) - 0.03f) < 0.001f); CHECK_EQ(mesh->getTriangleMeshMaterialId(0), 101); CHECK_EQ(mesh->getTriangleMeshMaterialId(1), 202); CHECK_NE(world->rayCast(-3.f, 2.f, 0.f, -3.f, -2.f, 0.f), -1); CHECK_EQ(world->getRayHitMaterialId(), 101); CHECK_NE(world->rayCast(3.f, 2.f, 0.f, 3.f, -2.f, 0.f), -1); CHECK_EQ(world->getRayHitMaterialId(), 202); CHECK_NE(world->castSphere(3.f, 2.f, 0.f, 0.1f, 0.f, -4.f, 0.f), -1); CHECK_EQ(world->getShapeCastMaterialId(), 202); mesh->setSensor(true); mesh->setSensor(false); CHECK_EQ(mesh->getTriangleMeshMaterialCount(), 2); CHECK_EQ(mesh->getTriangleMeshMaterialId(1), 202); CHECK_NE(world->rayCast(3.f, 2.f, 0.f, 3.f, -2.f, 0.f), -1); CHECK_EQ(world->getRayHitMaterialId(), 202); CHECK_THROWS((mesh->setTriangleMeshMaterialIndices({0}), false)); CHECK_THROWS((mesh->setTriangleMeshMaterialIndices({0, 255}), false)); CHECK_THROWS((mesh->setTriangleMeshMaterial(2, 0.f, 0.f, 0.f, 0.f, 0.f, 0.f, 0, "default", "default"), false)); CHECK_THROWS((mesh->getTriangleMeshMaterialIndex(2), false)); } TEST_CASE("box3d.query.ignoreBodyAndShapeAppliesAcrossQueryTypes") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, 0.f, 0.f, false)); Body3D *self = world->newBody("static", 0.f, 0.f, 0.f); Shape3D *selfFront = self->newSphereShape(1.f); Shape3D *selfRear = self->newSphereShape(0.75f); selfRear->setLocalPosition(2.f, 0.f, 0.f); Body3D *target = world->newBody("static", 6.f, 0.f, 0.f); Shape3D *targetShape = target->newSphereShape(1.f); CHECK_EQ(world->rayCast(-3.f, 0.f, 0.f, 10.f, 0.f, 0.f), self->getId()); CHECK_EQ(world->getRayHitShapeId(), selfFront->getId()); world->setQueryIgnoredShapeId(selfFront->getId()); CHECK_EQ(world->rayCast(-3.f, 0.f, 0.f, 10.f, 0.f, 0.f), self->getId()); CHECK_EQ(world->getRayHitShapeId(), selfRear->getId()); world->setQueryIgnoredBodyId(self->getId()); CHECK_EQ(world->rayCast(-3.f, 0.f, 0.f, 10.f, 0.f, 0.f), target->getId()); CHECK_EQ(world->getRayHitShapeId(), targetShape->getId()); CHECK_EQ(world->querySphere(0.f, 0.f, 0.f, 3.f), 0); CHECK_EQ(world->castSphere(-3.f, 0.f, 0.f, 0.25f, 12.f, 0.f, 0.f), target->getId()); CHECK_EQ(world->closestPoint(0.f, 0.f, 0.f, 10.f), target->getId()); CHECK(world->moveCapsule(-3.f, -0.5f, 0.f, -3.f, 0.5f, 0.f, 0.25f, 12.f, 0.f, 0.f)); CHECK_GT(world->getMoverDeltaX(), 1.f); CHECK_LT(world->getMoverDeltaX(), 12.f); CHECK_EQ(world->getQueryIgnoredBodyId(), self->getId()); CHECK_EQ(world->getQueryIgnoredShapeId(), selfFront->getId()); world->clearQueryIgnores(); CHECK_EQ(world->getQueryIgnoredBodyId(), -1); CHECK_EQ(world->getQueryIgnoredShapeId(), -1); CHECK_EQ(world->rayCast(-3.f, 0.f, 0.f, 10.f, 0.f, 0.f), self->getId()); CHECK_THROWS((world->setQueryIgnoredBodyId(0), false)); CHECK_THROWS((world->setQueryIgnoredShapeId(-2), false)); } TEST_CASE("box3d.events.contactCarriesStableShapeIdentity") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, 0.f, 0.f, false)); Body3D *wall = world->newBody("static", 0.f, 0.f, 0.f); Shape3D *wallShape = wall->newBoxShape(2.f, 2.f, 2.f); wallShape->setTag(3101); Body3D *actor = world->newBody("dynamic", 0.f, 0.f, 0.f); Shape3D *actorShape = actor->newSphereShape(0.5f); actorShape->setTag(3102); world->update(1.f / 60.f); REQUIRE_EQ(world->getBeginContactCount(), 1); CHECK_EQ(world->getBeginContactShapeAId(0), wallShape->getId()); CHECK_EQ(world->getBeginContactShapeBId(0), actorShape->getId()); CHECK_EQ(world->getBeginContactBodyAId(0), wall->getId()); CHECK_EQ(world->getBeginContactBodyBId(0), actor->getId()); CHECK_EQ(world->getBeginContactShapeATag(0), 3101); CHECK_EQ(world->getBeginContactShapeBTag(0), 3102); actor->setPosition(10.f, 0.f, 0.f); world->update(1.f / 60.f); REQUIRE_EQ(world->getEndContactCount(), 1); CHECK_EQ(world->getEndContactShapeAId(0), wallShape->getId()); CHECK_EQ(world->getEndContactShapeBId(0), actorShape->getId()); CHECK_EQ(world->getBeginContactCount(), 0); world->clearContactEvents(); CHECK_EQ(world->getEndContactCount(), 0); } TEST_CASE("box3d.events.sensorUsesDedicatedTriggerBuffer") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, 0.f, 0.f, false)); Body3D *zone = world->newBody("static", 0.f, 0.f, 0.f); Shape3D *sensor = zone->newBoxShape(4.f, 4.f, 4.f); sensor->setTag(3201); sensor->setSensor(true); Body3D *actor = world->newBody("dynamic", 0.f, 0.f, 0.f); Shape3D *visitor = actor->newSphereShape(0.5f); visitor->setTag(3202); world->update(1.f / 60.f); REQUIRE_EQ(world->getBeginTriggerCount(), 1); CHECK_EQ(world->getBeginContactCount(), 0); CHECK_EQ(world->getBeginTriggerSensorBodyId(0), zone->getId()); CHECK_EQ(world->getBeginTriggerVisitorBodyId(0), actor->getId()); CHECK_EQ(world->getBeginTriggerSensorShapeId(0), sensor->getId()); CHECK_EQ(world->getBeginTriggerVisitorShapeId(0), visitor->getId()); CHECK_EQ(world->getBeginTriggerSensorShapeTag(0), 3201); CHECK_EQ(world->getBeginTriggerVisitorShapeTag(0), 3202); actor->setPosition(10.f, 0.f, 0.f); world->update(1.f / 60.f); REQUIRE_EQ(world->getEndTriggerCount(), 1); CHECK_EQ(world->getEndTriggerSensorShapeId(0), sensor->getId()); CHECK_EQ(world->getEndTriggerVisitorShapeId(0), visitor->getId()); CHECK_EQ(world->getBeginTriggerCount(), 0); } TEST_CASE("box3d.events.endTriggerSurvivesSensorBackendRecreation") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, 0.f, 0.f, false)); Body3D *zone = world->newBody("static", 0.f, 0.f, 0.f); Shape3D *sensor = zone->newBoxShape(4.f, 4.f, 4.f); sensor->setTag(3301); sensor->setSensor(true); Body3D *actor = world->newBody("dynamic", 0.f, 0.f, 0.f); Shape3D *visitor = actor->newSphereShape(0.5f); visitor->setTag(3302); world->update(1.f / 60.f); REQUIRE_EQ(world->getBeginTriggerCount(), 1); const int stableSensorId = sensor->getId(); sensor->setSensor(false); world->update(1.f / 60.f); REQUIRE_EQ(world->getEndTriggerCount(), 1); CHECK_EQ(world->getEndTriggerSensorShapeId(0), stableSensorId); CHECK_EQ(world->getEndTriggerVisitorShapeId(0), visitor->getId()); CHECK_EQ(world->getEndTriggerSensorShapeTag(0), 3301); CHECK_EQ(world->getEndTriggerVisitorShapeTag(0), 3302); } TEST_CASE("box3d.events.hitReportsPointNormalSpeedAndImpulse") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, 0.f, 0.f, false)); world->setHitEventThreshold(1.f); CHECK(std::fabs(world->getHitEventThreshold() - 1.f) < 1e-5f); Body3D *ground = world->newBody("static", 0.f, -0.5f, 0.f); Shape3D *groundShape = ground->newBoxShape(10.f, 1.f, 10.f); groundShape->setTag(3401); Body3D *ball = world->newBody("dynamic", 0.f, 2.f, 0.f); Shape3D *ballShape = ball->newSphereShape(0.5f, 1.f, 0.2f, 0.f); ballShape->setTag(3402); ballShape->setHitEventsEnabled(true); CHECK(ballShape->areHitEventsEnabled()); ball->setLinearVelocity(0.f, -30.f, 0.f); bool captured = false; for (int step = 0; step < 30 && !captured; ++step) { world->update(1.f / 60.f); captured = world->getHitCount() > 0; } REQUIRE(captured); CHECK_EQ(world->getHitShapeAId(0), groundShape->getId()); CHECK_EQ(world->getHitShapeBId(0), ballShape->getId()); CHECK_EQ(world->getHitShapeATag(0), 3401); CHECK_EQ(world->getHitShapeBTag(0), 3402); CHECK_EQ(world->getHitBodyAId(0), ground->getId()); CHECK_EQ(world->getHitBodyBId(0), ball->getId()); CHECK(std::fabs(world->getHitPointX(0)) < 0.1f); CHECK(std::fabs(world->getHitPointY(0)) < 0.2f); CHECK(std::fabs(world->getHitPointZ(0)) < 0.1f); CHECK_GT(world->getHitNormalY(0), 0.9f); CHECK_GT(world->getHitApproachSpeed(0), 1.f); CHECK_GT(world->getHitNormalImpulse(0), 0.f); } TEST_CASE("box3d.events.hitPreferenceSurvivesSensorToggle") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, 0.f, 0.f, false)); Body3D *body = world->newBody("dynamic", 0.f, 0.f, 0.f); Shape3D *shape = body->newSphereShape(0.5f); shape->setHitEventsEnabled(true); shape->setSensor(true); CHECK(shape->areHitEventsEnabled()); shape->setSensor(false); CHECK(shape->areHitEventsEnabled()); CHECK(b3Shape_AreHitEventsEnabled(shape->raw())); } TEST_CASE("box3d.contacts.bodyManifoldIsPersistentAndBodyRelative") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, -10.f, 0.f, false)); Body3D *ground = world->newBody("static", 0.f, -0.5f, 0.f); Shape3D *groundShape = ground->newBoxShape(10.f, 1.f, 10.f); groundShape->setTag(3501); Body3D *ball = world->newBody("dynamic", 0.f, 2.f, 0.f); Shape3D *ballShape = ball->newSphereShape(0.5f, 1.f, 0.4f, 0.f); ballShape->setTag(3502); for (int step = 0; step < 120; ++step) world->update(1.f / 60.f); REQUIRE_GT(world->queryBodyContacts(ball->getId(), 16), 0); CHECK_EQ(world->getContactPointCount(), world->queryBodyContacts(ball->getId(), 16)); CHECK_EQ(world->getContactPointShapeId(0), ballShape->getId()); CHECK_EQ(world->getContactPointShapeTag(0), 3502); CHECK_EQ(world->getContactPointOtherBodyId(0), ground->getId()); CHECK_EQ(world->getContactPointOtherShapeId(0), groundShape->getId()); CHECK_EQ(world->getContactPointOtherShapeTag(0), 3501); CHECK_LT(world->getContactPointNormalY(0), -0.9f); CHECK(std::fabs(world->getContactPointNormalX(0)) < 0.1f); CHECK(std::fabs(world->getContactPointZ(0)) < 0.1f); CHECK(std::fabs(world->getContactPointY(0)) < 0.1f); CHECK_LT(world->getContactPointSeparation(0), 0.05f); CHECK_GE(world->getContactPointNormalImpulse(0), 0.f); CHECK_GT(world->getContactPointTotalNormalImpulse(0), 0.f); CHECK(world->isContactPointPersisted(0)); REQUIRE_GT(world->queryBodyContacts(ground->getId(), 1), 0); CHECK_EQ(world->getContactPointCount(), 1); CHECK_EQ(world->getContactPointShapeId(0), groundShape->getId()); CHECK_EQ(world->getContactPointOtherShapeId(0), ballShape->getId()); CHECK_GT(world->getContactPointNormalY(0), 0.9f); ball->setPosition(0.f, 5.f, 0.f); world->update(1.f / 60.f); CHECK_EQ(world->queryBodyContacts(ball->getId(), 16), 0); CHECK_EQ(world->getContactPointCount(), 0); CHECK_THROWS((world->queryBodyContacts(ball->getId(), 0), false)); CHECK_THROWS((world->getContactPointShapeId(0), false)); } TEST_CASE("box3d.body.destroy") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, 0.f, 0.f, true)); Body3D *b = world->newBody("dynamic", 1.f, 2.f, 3.f); b->newBoxShape(0.5f, 0.5f, 0.5f); int id = b->getId(); CHECK_GT(id, 0); b->destroy(); CHECK(std::fabs(b->getX()) < 0.001f); delete b; } TEST_CASE("box3d.query.rayCastAndAABB") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, 0.f, 0.f, true)); Body3D *a = world->newBody("static", 0.f, 0.f, 0.f); Shape3D *sa = a->newBoxShape(2.f, 2.f, 2.f); sa->setTag(101); Body3D *b = world->newBody("static", 5.f, 0.f, 0.f); b->newSphereShape(1.f); CHECK(sa->testPoint(0.f, 0.f, 0.f)); CHECK(!sa->testPoint(10.f, 10.f, 10.f)); int hit = world->rayCast(-5.f, 0.f, 0.f, 10.f, 0.f, 0.f); CHECK_EQ(hit, a->getId()); CHECK(world->hasRayHit()); CHECK_EQ(world->getRayHitShapeId(), sa->getId()); CHECK_EQ(world->getRayHitShapeTag(), 101); CHECK(std::fabs(world->getRayHitY()) < 0.5f); int count = world->queryAABB(-2.f, -2.f, -2.f, 2.f, 2.f, 2.f); CHECK_GE(count, 1); bool foundA = false; for (int i = 0; i < world->getQueryCount(); ++i) { if (world->getQueryBodyId(i) == a->getId()) foundA = true; } CHECK(foundA); CHECK_EQ(world->getQueryShapeCount(), 1); CHECK_EQ(world->getQueryShapeId(0), sa->getId()); } TEST_CASE("box3d.query.rayCastAllIsSortedBoundedAndCompatible") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, 0.f, 0.f, false)); Body3D *nearBody = world->newBody("static", 0.f, 0.f, 0.f); Shape3D *nearShape = nearBody->newSphereShape(0.5f); nearShape->setTag(4101); Body3D *middleBody = world->newBody("static", 3.f, 0.f, 0.f); Shape3D *middleShape = middleBody->newSphereShape(0.5f); middleShape->setTag(4102); Body3D *farBody = world->newBody("static", 6.f, 0.f, 0.f); Shape3D *farShape = farBody->newSphereShape(0.5f); farShape->setTag(4103); REQUIRE_EQ(world->rayCastAll(-3.f, 0.f, 0.f, 9.f, 0.f, 0.f, 8), 3); CHECK_EQ(world->getRayResultCount(), 3); CHECK_EQ(world->getRayResultBodyId(0), nearBody->getId()); CHECK_EQ(world->getRayResultBodyId(1), middleBody->getId()); CHECK_EQ(world->getRayResultBodyId(2), farBody->getId()); CHECK_EQ(world->getRayResultShapeId(0), nearShape->getId()); CHECK_EQ(world->getRayResultShapeTag(1), 4102); CHECK_LT(world->getRayResultFraction(0), world->getRayResultFraction(1)); CHECK_LT(world->getRayResultFraction(1), world->getRayResultFraction(2)); CHECK_LT(world->getRayResultNormalX(0), -0.9f); CHECK(std::fabs(world->getRayResultY(0)) < 1e-5f); REQUIRE_EQ(world->rayCastAll(-3.f, 0.f, 0.f, 9.f, 0.f, 0.f, 2), 2); CHECK_EQ(world->getRayResultBodyId(0), nearBody->getId()); CHECK_EQ(world->getRayResultBodyId(1), middleBody->getId()); CHECK_EQ(world->getRayHitBodyId(), nearBody->getId()); world->setQueryIgnoredBodyId(nearBody->getId()); REQUIRE_EQ(world->rayCastAll(-3.f, 0.f, 0.f, 9.f, 0.f, 0.f, 8), 2); CHECK_EQ(world->getRayResultBodyId(0), middleBody->getId()); CHECK_EQ(world->getRayResultShapeId(1), farShape->getId()); world->clearQueryIgnores(); CHECK_EQ(world->rayCast(-3.f, 0.f, 0.f, 9.f, 0.f, 0.f), nearBody->getId()); CHECK_EQ(world->getRayResultCount(), 1); CHECK_EQ(world->getRayResultShapeId(0), nearShape->getId()); CHECK_THROWS((world->rayCastAll(0.f, 0.f, 0.f, 1.f, 0.f, 0.f, 0), false)); CHECK_EQ(world->getRayResultCount(), 0); CHECK_THROWS((world->getRayResultBodyId(0), false)); } TEST_CASE("box3d.query.rayCastAllTieUsesStableShapeId") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, 0.f, 0.f, false)); Body3D *body = world->newBody("static", 0.f, 0.f, 0.f); Shape3D *first = body->newSphereShape(1.f); Shape3D *second = body->newSphereShape(1.f); REQUIRE_EQ(world->rayCastAll(-3.f, 0.f, 0.f, 3.f, 0.f, 0.f, 2), 2); CHECK_LT(first->getId(), second->getId()); CHECK_EQ(world->getRayResultShapeId(0), first->getId()); CHECK_EQ(world->getRayResultShapeId(1), second->getId()); CHECK(std::fabs(world->getRayResultFraction(0) - world->getRayResultFraction(1)) < 1e-6f); } TEST_CASE("box3d.query.reportsEveryShapeOnOneBody") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, 0.f, 0.f, true)); Body3D *body = world->newBody("static", 0.f, 0.f, 0.f); Shape3D *sphere = body->newSphereShape(0.5f); Shape3D *box = body->newBoxShape(2.f, 2.f, 2.f); sphere->setTag(11); box->setTag(22); CHECK_NE(sphere->getId(), box->getId()); CHECK_EQ(world->querySphere(0.f, 0.f, 0.f, 0.1f), 1); // Unique bodies. CHECK_EQ(world->getQueryCount(), 1); CHECK_EQ(world->getQueryShapeCount(), 2); CHECK_EQ(world->getQueryShapeId(0), std::min(sphere->getId(), box->getId())); CHECK_EQ(world->getQueryShapeId(1), std::max(sphere->getId(), box->getId())); const int firstTag = sphere->getId() < box->getId() ? 11 : 22; CHECK_EQ(world->getQueryShapeTag(0), firstTag); CHECK_EQ(world->getQueryShapeTag(1), firstTag == 11 ? 22 : 11); CHECK_THROWS((world->getQueryShapeId(2), false)); } TEST_CASE("box3d.body.capsuleAndRotation") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, 0.f, 0.f, true)); Body3D *cap = world->newBody("dynamic", 0.f, 1.f, 0.f); cap->newCapsuleShape(1.f, 0.25f, 1.f); cap->setRotation(0.f, 0.f, 0.f, 1.f); CHECK(std::fabs(cap->getRotW() - 1.f) < 0.001f); cap->setFixedRotation(true); CHECK(cap->isFixedRotation()); } TEST_CASE("box3d.query.sphereAndCapsuleOverlap") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, 0.f, 0.f, true)); Body3D *left = world->newBody("static", -2.f, 0.f, 0.f); left->newSphereShape(0.5f); Body3D *middle = world->newBody("static", 0.f, 0.f, 0.f); middle->newBoxShape(0.5f, 0.5f, 0.5f); Body3D *right = world->newBody("static", 2.f, 0.f, 0.f); right->newCapsuleShape(1.f, 0.25f); CHECK_EQ(world->querySphere(0.f, 0.f, 0.f, 0.1f), 1); CHECK_EQ(world->getQueryBodyId(0), middle->getId()); CHECK_EQ(world->queryCapsule(-2.f, 0.f, 0.f, 2.f, 0.f, 0.f, 0.1f), 3); CHECK_EQ(world->getQueryBodyId(0), left->getId()); CHECK_EQ(world->getQueryBodyId(1), middle->getId()); CHECK_EQ(world->getQueryBodyId(2), right->getId()); CHECK_EQ(world->queryCapsule(-2.f, 3.f, 0.f, 2.f, 3.f, 0.f, 0.1f), 0); } TEST_CASE("box3d.query.collisionLayerFilter") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, 0.f, 0.f, true)); Shape3D *layer1 = world->newBody("static", -1.f, 0.f, 0.f)->newSphereShape(0.5f); Shape3D *layer2 = world->newBody("static", 1.f, 0.f, 0.f)->newSphereShape(0.5f); layer1->setCategoryBits(1); layer2->setCategoryBits(2); world->setQueryFilter(4, 1); CHECK_EQ(world->queryCapsule(-2.f, 0.f, 0.f, 2.f, 0.f, 0.f, 0.1f), 1); CHECK_EQ(world->getQueryMaskBits(), 1); world->setQueryFilter(4, 2); CHECK_EQ(world->queryCapsule(-2.f, 0.f, 0.f, 2.f, 0.f, 0.f, 0.1f), 1); world->resetQueryFilter(); CHECK_EQ(world->queryCapsule(-2.f, 0.f, 0.f, 2.f, 0.f, 0.f, 0.1f), 2); } TEST_CASE("box3d.query.sphereAndCapsuleCast") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, 0.f, 0.f, true)); Body3D *wall = world->newBody("static", 0.f, 0.f, 0.f); Shape3D *wallShape = wall->newBoxShape(1.f, 4.f, 4.f); wallShape->setTag(77); CHECK_EQ(world->castSphere(-5.f, 0.f, 0.f, 0.5f, 10.f, 0.f, 0.f), wall->getId()); CHECK(world->hasShapeCastHit()); CHECK_EQ(world->getShapeCastShapeId(), wallShape->getId()); CHECK_EQ(world->getShapeCastShapeTag(), 77); CHECK(world->getShapeCastFraction() > 0.35f); CHECK(world->getShapeCastFraction() < 0.45f); CHECK(world->getShapeCastNormalX() < -0.9f); CHECK_EQ(world->castCapsule(-5.f, -1.f, 0.f, -5.f, 1.f, 0.f, 0.25f, 10.f, 0.f, 0.f), wall->getId()); CHECK(world->getShapeCastFraction() > 0.35f); CHECK(world->getShapeCastFraction() < 0.45f); CHECK_EQ(world->castSphere(-5.f, 5.f, 0.f, 0.5f, 10.f, 0.f, 0.f), -1); CHECK(!world->hasShapeCastHit()); CHECK_EQ(world->getShapeCastShapeId(), -1); CHECK_EQ(world->getShapeCastShapeTag(), 0); } TEST_CASE("box3d.query.shapeCastAllIsSortedBoundedAndSharedByPrimitives") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, 0.f, 0.f, false)); Body3D *nearBody = world->newBody("static", 0.f, 0.f, 0.f); Shape3D *nearShape = nearBody->newBoxShape(0.5f, 4.f, 4.f); nearShape->setTag(4201); Body3D *middleBody = world->newBody("static", 3.f, 0.f, 0.f); Shape3D *middleShape = middleBody->newBoxShape(0.5f, 4.f, 4.f); middleShape->setTag(4202); Body3D *farBody = world->newBody("static", 6.f, 0.f, 0.f); Shape3D *farShape = farBody->newBoxShape(0.5f, 4.f, 4.f); farShape->setTag(4203); REQUIRE_EQ(world->castSphereAll(-4.f, 0.f, 0.f, 0.25f, 12.f, 0.f, 0.f, 8), 3); CHECK_EQ(world->getShapeCastResultCount(), 3); CHECK_EQ(world->getShapeCastResultBodyId(0), nearBody->getId()); CHECK_EQ(world->getShapeCastResultBodyId(1), middleBody->getId()); CHECK_EQ(world->getShapeCastResultBodyId(2), farBody->getId()); CHECK_EQ(world->getShapeCastResultShapeId(0), nearShape->getId()); CHECK_EQ(world->getShapeCastResultShapeTag(1), 4202); CHECK_LT(world->getShapeCastResultFraction(0), world->getShapeCastResultFraction(1)); CHECK_LT(world->getShapeCastResultFraction(1), world->getShapeCastResultFraction(2)); CHECK_LT(world->getShapeCastResultNormalX(0), -0.9f); CHECK(std::fabs(world->getShapeCastResultY(0)) < 1e-5f); CHECK_EQ(world->getShapeCastBodyId(), nearBody->getId()); REQUIRE_EQ(world->castCapsuleAll(-4.f, -0.5f, 0.f, -4.f, 0.5f, 0.f, 0.2f, 12.f, 0.f, 0.f, 2), 2); CHECK_EQ(world->getShapeCastResultBodyId(0), nearBody->getId()); CHECK_EQ(world->getShapeCastResultBodyId(1), middleBody->getId()); world->setQueryIgnoredBodyId(nearBody->getId()); REQUIRE_EQ(world->castBoxAll(-4.f, 0.f, 0.f, 0.4f, 0.4f, 0.4f, 0.f, 0.f, 0.f, 1.f, 12.f, 0.f, 0.f, 8), 2); CHECK_EQ(world->getShapeCastResultBodyId(0), middleBody->getId()); CHECK_EQ(world->getShapeCastResultShapeId(1), farShape->getId()); world->clearQueryIgnores(); CHECK_EQ(world->castSphere(-4.f, 0.f, 0.f, 0.25f, 12.f, 0.f, 0.f), nearBody->getId()); CHECK_EQ(world->getShapeCastResultCount(), 1); CHECK_EQ(world->getShapeCastResultShapeId(0), nearShape->getId()); CHECK_THROWS((world->castSphereAll(-4.f, 0.f, 0.f, 0.25f, 12.f, 0.f, 0.f, 0), false)); CHECK_EQ(world->getShapeCastResultCount(), 0); CHECK_THROWS((world->getShapeCastResultBodyId(0), false)); } TEST_CASE("box3d.query.orientedBoxOverlap") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, 0.f, 0.f, true)); Body3D *target = world->newBody("static", 1.2f, 0.f, 0.f); target->newSphereShape(0.1f); CHECK_EQ(world->queryBox(0.f, 0.f, 0.f, 3.f, 0.4f, 0.4f, 0.f, 0.f, 0.f, 1.f), 1); CHECK_EQ(world->getQueryBodyId(0), target->getId()); constexpr float sin45 = 0.70710678f; // A 90-degree yaw turns the long local X axis onto world Z, so it no longer reaches x=1.2. CHECK_EQ(world->queryBox(0.f, 0.f, 0.f, 3.f, 0.4f, 0.4f, 0.f, sin45, 0.f, sin45), 0); CHECK_THROWS((world->queryBox(0.f, 0.f, 0.f, 1.f, 1.f, 1.f, 0.f, 0.f, 0.f, 0.f), false)); CHECK_THROWS((world->queryBox(0.f, 0.f, 0.f, 0.f, 1.f, 1.f, 0.f, 0.f, 0.f, 1.f), false)); } TEST_CASE("box3d.query.orientedBoxCast") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, 0.f, 0.f, true)); Body3D *wall = world->newBody("static", 2.f, 0.f, 0.f); wall->newBoxShape(0.5f, 4.f, 4.f); CHECK_EQ(world->castBox(-1.f, 0.f, 0.f, 2.f, 0.4f, 0.4f, 0.f, 0.f, 0.f, 1.f, 4.f, 0.f, 0.f), wall->getId()); const float identityFraction = world->getShapeCastFraction(); CHECK(std::fabs(identityFraction - 0.4375f) < 0.01f); CHECK(world->getShapeCastNormalX() < -0.99f); constexpr float sin45 = 0.70710678f; CHECK_EQ(world->castBox(-1.f, 0.f, 0.f, 2.f, 0.4f, 0.4f, 0.f, sin45, 0.f, sin45, 4.f, 0.f, 0.f), wall->getId()); CHECK(world->getShapeCastFraction() > identityFraction + 0.15f); CHECK(world->getShapeCastFraction() < 0.65f); } TEST_CASE("box3d.query.closestPointAndLayerFilter") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, 0.f, 0.f, true)); Body3D *sphere = world->newBody("static", 2.f, 0.f, 0.f); Shape3D *sphereShape = sphere->newSphereShape(0.5f); sphereShape->setCategoryBits(2); sphereShape->setTag(201); Body3D *box = world->newBody("static", -3.f, 0.f, 0.f); Shape3D *boxShape = box->newBoxShape(1.f, 2.f, 2.f); boxShape->setCategoryBits(4); boxShape->setTag(202); CHECK_EQ(world->closestPoint(0.f, 0.f, 0.f, 10.f), sphere->getId()); CHECK(world->hasClosestPoint()); CHECK_EQ(world->getClosestBodyId(), sphere->getId()); CHECK_EQ(world->getClosestShapeId(), sphereShape->getId()); CHECK_EQ(world->getClosestShapeTag(), 201); CHECK(std::fabs(world->getClosestX() - 1.5f) < 1e-5f); CHECK(std::fabs(world->getClosestDistance() - 1.5f) < 1e-5f); CHECK(world->getClosestNormalX() < -0.999f); CHECK(std::fabs(world->getClosestNormalY()) < 1e-5f); world->setQueryFilter(1, 4); CHECK_EQ(world->closestPoint(0.f, 0.f, 0.f, 10.f), box->getId()); CHECK_EQ(world->getClosestShapeId(), boxShape->getId()); CHECK_EQ(world->getClosestShapeTag(), 202); CHECK(std::fabs(world->getClosestX() + 2.5f) < 1e-5f); CHECK(std::fabs(world->getClosestDistance() - 2.5f) < 1e-5f); CHECK(world->getClosestNormalX() > 0.999f); world->resetQueryFilter(); CHECK_EQ(world->closestPoint(2.f, 0.f, 0.f, 1.f), sphere->getId()); CHECK(std::fabs(world->getClosestDistance()) < 1e-6f); CHECK(std::fabs(world->getClosestNormalX()) < 1e-6f); CHECK_EQ(world->closestPoint(0.f, 5.f, 0.f, 1.f), -1); CHECK(!world->hasClosestPoint()); CHECK_EQ(world->getClosestBodyId(), -1); CHECK_EQ(world->getClosestShapeId(), -1); CHECK_EQ(world->getClosestShapeTag(), 0); CHECK_THROWS((world->closestPoint(0.f, 0.f, 0.f, -1.f), false)); } TEST_CASE("box3d.mover.capsuleSlidesAlongWall") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, 0.f, 0.f, true)); Body3D *wall = world->newBody("static", 0.f, 0.f, 0.f); wall->newBoxShape(1.f, 6.f, 10.f); CHECK(world->moveCapsule(-2.f, -1.f, 0.f, -2.f, 1.f, 0.f, 0.25f, 4.f, 0.f, 2.f)); CHECK(world->getMoverDeltaX() > 1.15f); CHECK(world->getMoverDeltaX() < 1.3f); CHECK(world->getMoverDeltaZ() > 1.9f); CHECK(world->getMoverNormalX() < -0.9f); CHECK_GT(world->getMoverPlaneCount(), 0); CHECK_GE(world->getMoverIterations(), 2); } TEST_CASE("box3d.mover.capsuleStopsAtCornerAndMovesFreely") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, 0.f, 0.f, true)); world->newBody("static", 0.f, 0.f, -2.f)->newBoxShape(1.f, 6.f, 6.f); world->newBody("static", -2.f, 0.f, 0.f)->newBoxShape(6.f, 6.f, 1.f); CHECK(world->moveCapsule(-2.f, -1.f, -2.f, -2.f, 1.f, -2.f, 0.25f, 4.f, 0.f, 4.f)); CHECK(world->getMoverDeltaX() > 1.15f); CHECK(world->getMoverDeltaX() < 1.3f); CHECK(world->getMoverDeltaZ() > 1.15f); CHECK(world->getMoverDeltaZ() < 1.3f); CHECK(!world->moveCapsule(-5.f, -1.f, -5.f, -5.f, 1.f, -5.f, 0.25f, -1.f, 0.f, -2.f)); CHECK(std::fabs(world->getMoverDeltaX() + 1.f) < 1e-5f); CHECK(std::fabs(world->getMoverDeltaZ() + 2.f) < 1e-5f); } TEST_CASE("box3d.mover.recoversInitialPenetration") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, 0.f, 0.f, true)); world->newBody("static", 0.f, 0.f, 0.f)->newBoxShape(1.f, 6.f, 6.f); // The wall face is x=-0.5 and the capsule reaches to x=-0.35: 0.15m penetration. CHECK(world->moveCapsule(-0.6f, -1.f, 0.f, -0.6f, 1.f, 0.f, 0.25f, 0.f, 0.f, 0.f)); CHECK(world->getMoverDeltaX() < -0.14f); CHECK(world->getMoverDeltaX() > -0.17f); CHECK(world->getMoverNormalX() < -0.9f); CHECK_GT(world->getMoverPlaneCount(), 0); } TEST_CASE("box3d.mover.groundAndSlopeClassification") { auto *mod = Physics::create(); std::unique_ptr world(mod->newWorld3D(0.f, 0.f, 0.f, true)); world->newBody("static", 0.f, -0.5f, 0.f)->newBoxShape(10.f, 1.f, 10.f); world->setMoverUp(0.f, 1.f, 1.f); // normalized internally; floor dot is sqrt(1/2) world->setMoverSlopeLimit(40.f); CHECK(world->moveCapsule(0.f, 0.5f, 0.f, 0.f, 1.5f, 0.f, 0.25f, 0.f, -1.f, 0.f)); CHECK(!world->isMoverGrounded()); CHECK(world->getMoverGroundDot() > 0.70f); CHECK(world->getMoverGroundDot() < 0.72f); world->setMoverSlopeLimit(50.f); CHECK(world->moveCapsule(0.f, 0.5f, 0.f, 0.f, 1.5f, 0.f, 0.25f, 0.f, -1.f, 0.f)); CHECK(world->isMoverGrounded()); } TEST_CASE("box3d.sdf.interpolationNormalAndSphere") { DistanceField3D field(5, 5, 5, 1.f, -2.f, -2.f, -2.f, 100.f); // Exact SDF for the plane y=0. Trilinear interpolation reproduces it exactly. for (int z = 0; z < 5; ++z) for (int y = 0; y < 5; ++y) for (int x = 0; x < 5; ++x) field.setDistance(x, y, z, static_cast(y - 2)); CHECK(std::fabs(field.sample(0.25f, 0.5f, -0.25f) - 0.5f) < 1e-5f); field.sampleNormal(0.f, 0.f, 0.f); CHECK(std::fabs(field.getNormalX()) < 1e-5f); CHECK(std::fabs(field.getNormalY() - 1.f) < 1e-5f); CHECK(std::fabs(field.getNormalZ()) < 1e-5f); CHECK(field.checkSphere(0.f, 0.4f, 0.f, 0.5f)); CHECK_LT(field.getCollisionDistance(), 0.f); CHECK(std::fabs(field.getPenetrationDepth() - 0.1f) < 1e-5f); CHECK(std::fabs(field.getSurfaceY()) < 1e-5f); CHECK(std::fabs(field.getShapeContactY() + 0.1f) < 1e-5f); CHECK(!field.checkSphere(0.f, 1.f, 0.f, 0.25f)); } TEST_CASE("box3d.sdf.boundaryNormalsUseOneSidedGradient") { DistanceField3D field(5, 5, 5, 1.f, -2.f, -2.f, -2.f, 100.f); for (int z = 0; z < 5; ++z) for (int y = 0; y < 5; ++y) for (int x = 0; x < 5; ++x) field.setDistance(x, y, z, static_cast(y - 2)); field.sampleNormal(0.f, -2.f, 0.f); CHECK(std::fabs(field.getNormalX()) < 1e-5f); CHECK(field.getNormalY() > 0.999f); CHECK(std::fabs(field.getNormalZ()) < 1e-5f); field.sampleNormal(0.f, 2.f, 0.f); CHECK(field.getNormalY() > 0.999f); // Outside points have no meaningful field gradient and use the documented safe fallback. field.sampleNormal(0.f, 2.01f, 0.f); CHECK(std::fabs(field.getNormalX()) < 1e-5f); CHECK(field.getNormalY() > 0.999f); CHECK(std::fabs(field.getNormalZ()) < 1e-5f); } TEST_CASE("box3d.sdf.bulkDataMetadataAndAtomicValidation") { DistanceField3D field(3, 2, 2, 0.5f, -1.f, 2.f, 4.f, 99.f); CHECK_EQ(field.getRevision(), 0u); CHECK_EQ(field.getWidth(), 3); CHECK_EQ(field.getHeight(), 2); CHECK_EQ(field.getDepth(), 2); CHECK_EQ(field.getSampleCount(), 12); CHECK(std::fabs(field.getCellSize() - 0.5f) < 1e-6f); CHECK(std::fabs(field.getOriginX() + 1.f) < 1e-6f); CHECK(std::fabs(field.getOriginY() - 2.f) < 1e-6f); CHECK(std::fabs(field.getOriginZ() - 4.f) < 1e-6f); CHECK(std::fabs(field.getOutsideDistance() - 99.f) < 1e-6f); std::vector samples(12); for (size_t i = 0; i < samples.size(); ++i) samples[i] = static_cast(i); field.setDistances(samples); CHECK_EQ(field.getRevision(), 1u); CHECK(std::fabs(field.getDistance(2, 0, 0) - 2.f) < 1e-6f); CHECK(std::fabs(field.getDistance(0, 1, 0) - 3.f) < 1e-6f); CHECK(std::fabs(field.getDistance(0, 0, 1) - 6.f) < 1e-6f); auto invalid = samples; invalid[4] = std::numeric_limits::quiet_NaN(); CHECK_THROWS((field.setDistances(invalid), false)); CHECK_EQ(field.getRevision(), 1u); CHECK(std::fabs(field.getDistance(1, 1, 0) - 4.f) < 1e-6f); CHECK_THROWS((field.setDistances(std::vector(11, 0.f)), false)); field.fill(-2.f); CHECK_EQ(field.getRevision(), 2u); CHECK(std::fabs(field.getDistance(2, 1, 1) + 2.f) < 1e-6f); CHECK_THROWS((field.fill(std::numeric_limits::infinity()), false)); CHECK(std::fabs(field.getDistance(2, 1, 1) + 2.f) < 1e-6f); const std::vector region{10.f, 11.f, 12.f, 13.f, 14.f, 15.f, 16.f, 17.f}; field.setDistanceRegion(1, 0, 0, 2, 2, 2, region); CHECK_EQ(field.getRevision(), 3u); CHECK(std::fabs(field.getDistance(1, 0, 0) - 10.f) < 1e-6f); CHECK(std::fabs(field.getDistance(2, 0, 0) - 11.f) < 1e-6f); CHECK(std::fabs(field.getDistance(1, 1, 0) - 12.f) < 1e-6f); CHECK(std::fabs(field.getDistance(1, 0, 1) - 14.f) < 1e-6f); CHECK(std::fabs(field.getDistance(2, 1, 1) - 17.f) < 1e-6f); CHECK(std::fabs(field.getDistance(0, 0, 0) + 2.f) < 1e-6f); auto invalidRegion = region; invalidRegion[2] = std::numeric_limits::quiet_NaN(); CHECK_THROWS((field.setDistanceRegion(1, 0, 0, 2, 2, 2, invalidRegion), false)); CHECK_THROWS((field.setDistanceRegion(2, 0, 0, 2, 2, 2, region), false)); CHECK_EQ(field.getRevision(), 3u); CHECK(std::fabs(field.getDistance(1, 1, 0) - 12.f) < 1e-6f); field.fillRegion(0, 1, 0, 1, 1, 2, 7.f); CHECK_EQ(field.getRevision(), 4u); CHECK(std::fabs(field.getDistance(0, 1, 0) - 7.f) < 1e-6f); CHECK(std::fabs(field.getDistance(0, 1, 1) - 7.f) < 1e-6f); CHECK_THROWS((field.fillRegion(0, 0, 0, 0, 1, 1, 3.f), false)); CHECK_EQ(field.getRevision(), 4u); } TEST_CASE("box3d.sdf.capsuleFindsInteriorCollision") { DistanceField3D field(7, 7, 7, 1.f, -3.f, -3.f, -3.f, 100.f); // Sphere SDF centered at the origin. for (int z = 0; z < 7; ++z) { for (int y = 0; y < 7; ++y) { for (int x = 0; x < 7; ++x) { float px = static_cast(x - 3); float py = static_cast(y - 3); float pz = static_cast(z - 3); field.setDistance(x, y, z, std::sqrt(px * px + py * py + pz * pz) - 1.f); } } } // Endpoints are outside, but the capsule segment crosses the solid center. CHECK(field.checkCapsule(-2.f, 0.f, 0.f, 2.f, 0.f, 0.f, 0.1f)); CHECK(std::fabs(field.getCollisionX()) < 0.51f); CHECK_LT(field.getCollisionDistance(), 0.f); CHECK(!field.checkCapsule(-2.f, 2.f, 0.f, 2.f, 2.f, 0.f, 0.1f)); } TEST_CASE("box3d.sdf.sphereCastPreventsHighSpeedTunneling") { DistanceField3D field(5, 5, 5, 1.f, -2.f, -2.f, -2.f, 100.f); for (int z = 0; z < 5; ++z) for (int y = 0; y < 5; ++y) for (int x = 0; x < 5; ++x) field.setDistance(x, y, z, static_cast(y - 2)); CHECK(field.castSphere(0.f, 1.75f, 0.f, 0.25f, 0.f, -3.5f, 0.f)); CHECK(!field.didCastStartInside()); CHECK(std::fabs(field.getCastFraction() - (1.5f / 3.5f)) < 1e-4f); CHECK(std::fabs(field.getCastDistance() - 1.5f) < 1e-4f); CHECK(std::fabs(field.getCollisionY() - 0.25f) < 1e-4f); CHECK(field.getNormalY() > 0.999f); CHECK(!field.castSphere(0.f, 1.75f, 0.f, 0.25f, 3.f, 0.f, 0.f)); CHECK(std::fabs(field.getCastFraction() - 1.f) < 1e-6f); CHECK(std::fabs(field.getCastDistance() - 3.f) < 1e-6f); } TEST_CASE("box3d.sdf.capsuleCastAndInitialOverlap") { DistanceField3D field(7, 7, 7, 1.f, -3.f, -3.f, -3.f, 100.f); for (int z = 0; z < 7; ++z) for (int y = 0; y < 7; ++y) for (int x = 0; x < 7; ++x) field.setDistance(x, y, z, static_cast(y - 3)); CHECK(field.castCapsule(-1.f, 2.f, 0.f, 1.f, 2.f, 0.f, 0.5f, 0.f, -4.f, 0.f)); CHECK(!field.didCastStartInside()); CHECK(std::fabs(field.getCastFraction() - 0.375f) < 1e-4f); CHECK(std::fabs(field.getCollisionY() - 0.5f) < 1e-4f); CHECK(field.getNormalY() > 0.999f); CHECK(field.castCapsule(-1.f, 0.25f, 0.f, 1.f, 0.25f, 0.f, 0.5f, 0.f, 2.f, 0.f)); CHECK(field.didCastStartInside()); CHECK(std::fabs(field.getCastFraction()) < 1e-6f); CHECK_LT(field.getCollisionDistance(), 0.f); } TEST_CASE("box3d.sdf.moverSlidesAlongWall") { DistanceField3D field(7, 7, 7, 1.f, -3.f, -3.f, -3.f, 100.f); for (int z = 0; z < 7; ++z) for (int y = 0; y < 7; ++y) for (int x = 0; x < 7; ++x) field.setDistance(x, y, z, static_cast(x - 3)); CHECK(field.moveCapsule(1.5f, -1.f, 0.f, 1.5f, 1.f, 0.f, 0.25f, -2.f, 0.f, 1.f)); CHECK(field.getMoverDeltaX() < -1.24f); CHECK(field.getMoverDeltaX() > -1.26f); CHECK(field.getMoverDeltaZ() > 0.99f); CHECK(field.getMoverNormalX() > 0.999f); CHECK_GT(field.getMoverIterations(), 1); CHECK(!field.moveCapsule(1.5f, -1.f, 0.f, 1.5f, 1.f, 0.f, 0.25f, 0.f, 0.f, 1.f)); CHECK(std::fabs(field.getMoverDeltaZ() - 1.f) < 1e-5f); } TEST_CASE("box3d.sdf.moverRecoversInitialPenetration") { DistanceField3D field(7, 7, 7, 1.f, -3.f, -3.f, -3.f, 100.f); for (int z = 0; z < 7; ++z) for (int y = 0; y < 7; ++y) for (int x = 0; x < 7; ++x) field.setDistance(x, y, z, static_cast(x - 3)); CHECK(field.moveCapsule(0.1f, -1.f, 0.f, 0.1f, 1.f, 0.f, 0.25f, 0.f, 0.f, 0.f)); CHECK(field.getMoverDeltaX() > 0.15f); CHECK(field.getMoverDeltaX() < 0.152f); CHECK(field.getMoverNormalX() > 0.999f); } TEST_CASE("box3d.sdf.moverStopsAtTwoPlaneCorner") { DistanceField3D field(9, 7, 9, 0.5f, -2.f, -1.5f, -2.f, 100.f); for (int z = 0; z < 9; ++z) { for (int y = 0; y < 7; ++y) { for (int x = 0; x < 9; ++x) { const float worldX = -2.f + 0.5f * static_cast(x); const float worldZ = -2.f + 0.5f * static_cast(z); // Union of x<=0 and z<=0.5 half spaces. field.setDistance(x, y, z, std::min(worldX, worldZ - 0.5f)); } } } CHECK(field.moveCapsule(1.5f, -0.5f, 2.f, 1.5f, 0.5f, 2.f, 0.25f, -2.f, 0.f, -3.f)); CHECK(field.getMoverDeltaX() < -1.24f); CHECK(field.getMoverDeltaX() > -1.26f); CHECK(field.getMoverDeltaZ() < -1.24f); CHECK(field.getMoverDeltaZ() > -1.26f); CHECK_GT(field.getMoverIterations(), 2); } TEST_CASE("box3d.sdf.moverGroundSlopeAndSkinConfiguration") { DistanceField3D field(7, 7, 7, 1.f, -3.f, -3.f, -3.f, 100.f); for (int z = 0; z < 7; ++z) for (int y = 0; y < 7; ++y) for (int x = 0; x < 7; ++x) field.setDistance(x, y, z, static_cast(y - 3)); field.setMoverSkinWidth(0.02f); CHECK(std::fabs(field.getMoverSkinWidth() - 0.02f) < 1e-6f); CHECK_THROWS((field.setMoverSkinWidth(-0.01f), false)); CHECK_THROWS((field.setMoverUp(0.f, 0.f, 0.f), false)); field.setMoverUp(0.f, 1.f, 1.f); // Normalized internally; floor dot is sqrt(1/2). field.setMoverSlopeLimit(40.f); CHECK(field.moveCapsule(0.f, 0.5f, 0.f, 0.f, 1.5f, 0.f, 0.25f, 0.f, -1.f, 0.f)); CHECK(!field.isMoverGrounded()); CHECK(field.getMoverGroundDot() > 0.70f); CHECK(field.getMoverGroundDot() < 0.72f); CHECK(field.getMoverDeltaY() > -0.24f); CHECK(field.getMoverDeltaY() < -0.22f); // Stops at radius + configured skin. field.setMoverSlopeLimit(50.f); CHECK(field.moveCapsule(0.f, 0.5f, 0.f, 0.f, 1.5f, 0.f, 0.25f, 0.f, -1.f, 0.f)); CHECK(field.isMoverGrounded()); } TEST_CASE("box3d.sdf.moverGroundSnapAndLedge") { DistanceField3D field(9, 9, 9, 0.5f, -2.f, -2.f, -2.f, 100.f); for (int z = 0; z < 9; ++z) { for (int y = 0; y < 9; ++y) { for (int x = 0; x < 9; ++x) { const float worldX = -2.f + 0.5f * static_cast(x); const float worldY = -2.f + 0.5f * static_cast(y); // Ground exists only on x<=0, leaving a sharp ledge to the right. field.setDistance(x, y, z, worldX <= 0.f ? worldY : 100.f); } } } field.setMoverSkinWidth(0.02f); field.setMoverGroundSnap(0.5f); CHECK(std::fabs(field.getMoverGroundSnap() - 0.5f) < 1e-6f); CHECK_THROWS((field.setMoverGroundSnap(-0.1f), false)); // Starts 0.25m above the configured skin and is pulled down onto walkable ground. CHECK(field.moveCapsule(-1.f, 0.5f, 0.f, -1.f, 1.5f, 0.f, 0.25f, 0.2f, 0.f, 0.f)); CHECK(field.isMoverGrounded()); CHECK(field.getMoverDeltaY() < -0.22f); CHECK(field.getMoverDeltaY() > -0.24f); CHECK(std::fabs(field.getMoverDeltaX() - 0.2f) < 1e-5f); // Over the empty side of the ledge, snap must not invent a ground contact. CHECK(!field.moveCapsule(1.f, 0.5f, 0.f, 1.f, 1.5f, 0.f, 0.25f, 0.2f, 0.f, 0.f)); CHECK(!field.isMoverGrounded()); CHECK(std::fabs(field.getMoverDeltaY()) < 1e-6f); } TEST_CASE("box3d.sdf.moverStepOffsetClimbsOnlyLowLedges") { DistanceField3D field(41, 31, 5, 0.1f, -2.f, -1.f, -0.2f, 100.f); for (int z = 0; z < 5; ++z) { for (int y = 0; y < 31; ++y) { for (int x = 0; x < 41; ++x) { const float worldX = -2.f + 0.1f * static_cast(x); const float worldY = -1.f + 0.1f * static_cast(y); // Ground union a 0.4m-high semi-infinite step at x>=0. const float step = std::max(-worldX, worldY - 0.4f); field.setDistance(x, y, z, std::min(worldY, step)); } } } field.setMoverSkinWidth(0.01f); field.setMoverGroundSnap(0.1f); field.setMoverStepHeight(0.5f); CHECK(std::fabs(field.getMoverStepHeight() - 0.5f) < 1e-6f); CHECK_THROWS((field.setMoverStepHeight(-0.1f), false)); CHECK(field.moveCapsule(-1.f, 0.26f, 0.f, -1.f, 1.26f, 0.f, 0.25f, 1.75f, 0.f, 0.f)); CHECK(field.isMoverGrounded()); CHECK(field.getMoverDeltaX() > 1.74f); CHECK(field.getMoverDeltaY() > 0.39f); CHECK(field.getMoverDeltaY() < 0.41f); CHECK(!field.didMoverHitWall()); // A step allowance lower than the obstacle must fall back to the direct wall result. field.setMoverStepHeight(0.3f); CHECK(field.moveCapsule(-1.f, 0.26f, 0.f, -1.f, 1.26f, 0.f, 0.25f, 1.75f, 0.f, 0.f)); CHECK(field.didMoverHitWall()); CHECK(field.getMoverDeltaX() < 1.f); CHECK(field.getMoverDeltaY() < 0.02f); }