Ammo.js Swept Sphere Test for High-Speed Collision
In fast-paced 3D applications, fast-moving character controllers often pass right through thin walls or obstacles—a physics bug known as tunneling or clipping. This article provides a practical guide on how to implement a swept sphere test using Ammo.js (the Emscripten port of the Bullet Physics engine) to predict collisions along a character’s movement path. By calculating collisions continuously rather than discretely, you can entirely prevent character clipping during high-speed movement.
The Problem: Tunneling at High Speeds
Standard physics engines evaluate collisions at discrete time steps. If a character moves 5 units per frame and a wall is only 1 unit thick, the character’s position in Frame A might be in front of the wall, and in Frame B, it might be behind the wall. Because the engine never detects an overlap at either discrete step, the character passes through the wall.
To solve this, we use Continuous Collision Detection (CCD). Specifically, a swept sphere test projects a sphere shape from the character’s starting position to their destination, identifying any intersections with geometry along that trajectory.
How to Implement Swept Sphere in Ammo.js
Ammo.js provides this functionality through the
convexSweepTest method on the dynamics world. Below are the
steps to implement this test.
Step 1: Define Start and End Transforms
You must define where the sweep starts (the character’s current position) and where it is projected to end (the desired next position).
// Create transforms
const transformFrom = new Ammo.btTransform();
transformFrom.setIdentity();
transformFrom.setOrigin(new Ammo.btVector3(currentX, currentY, currentZ));
const transformTo = new Ammo.btTransform();
transformTo.setIdentity();
transformTo.setOrigin(new Ammo.btVector3(targetX, targetY, targetZ));Step 2: Create the Swept Shape
Use a btSphereShape to represent the character’s
collision volume. The radius should match your character’s physical
size.
const sphereRadius = 0.5; // Match your character's radius
const sweptShape = new Ammo.btSphereShape(sphereRadius);Step 3: Set Up the Callback
To capture the collision results, use
ClosestConvexResultCallback. This callback determines the
closest object hit along the sweep path.
const callback = new Ammo.btCollisionWorld_ClosestConvexResultCallback(
transformFrom.getOrigin(),
transformTo.getOrigin()
);
// Optional: Filter collision groups to avoid self-collision with the character's own rigid body
callback.m_collisionFilterGroup = collisionGroup;
callback.m_collisionFilterMask = collisionMask;Step 4: Perform the Sweep Test
Run the sweep test on your Ammo.js dynamics world instance.
dynamicsWorld.convexSweepTest(
sweptShape,
transformFrom,
transformTo,
callback,
0 // Allowed penetration depth (usually 0)
);Step 5: Process the Results
After running the test, check if the callback detected a hit. If a hit is registered, you must stop the character at the point of impact.
if (callback.hasHit()) {
// Get the fraction of the distance traveled before collision (0.0 to 1.0)
const hitFraction = callback.m_closestHitFraction;
// Calculate the safe stop position slightly before the hit point
const safeFraction = Math.max(0, hitFraction - 0.01);
const finalX = currentX + (targetX - currentX) * safeFraction;
const finalY = currentY + (targetY - currentY) * safeFraction;
const finalZ = currentZ + (targetZ - currentZ) * safeFraction;
// Position character at the safe stop position
character.setPosition(finalX, finalY, finalZ);
// Optional: Get collision normal to calculate sliding vector
const hitNormal = callback.m_hitNormalWorld;
} else {
// No collision: Move character to the target position safely
character.setPosition(targetX, targetY, targetZ);
}Step 6: Clean Up Memory
Ammo.js does not automatically garbage-collect C++ objects allocated in JavaScript. You must manually destroy the created objects to prevent memory leaks.
Ammo.destroy(transformFrom);
Ammo.destroy(transformTo);
Ammo.destroy(sweptShape);
Ammo.destroy(callback);Summary of Best Practices
- Slide Along Walls: When a collision is detected, project the remaining movement vector onto the plane perpendicular to the hit normal. This allows your character to slide smoothly along walls instead of sticking.
- Scale the Sweep: Only perform the swept sphere test when the character’s velocity exceeds a specific threshold to save CPU overhead. For slow-moving states, rely on standard discrete collision resolution.