Why Ammo.js Raycast Fails and Returns No Hits
In WebGL 3D physics development, developers frequently encounter issues where an ammo.js raycast silently fails, returning zero hits despite visually intersecting with objects in the scene. This article diagnoses the most common causes of this issue—ranging from incorrect collision filtering and unupdated world transforms to coordinate space mismatches and memory management errors in the Emscripten wrapper—and provides actionable solutions to restore proper raycast functionality.
Incorrect Collision Filtering (Groups and Masks)
The most common reason an ammo.js raycast silently returns no hits is
a mismatch in collision groups and masks. When performing a raycast
using ClosestRayResultCallback, Bullet Physics filters hits
based on bitwise operations.
By default, the raycast callback has its own collision group and mask. If you have assigned custom collision groups and masks to your rigid bodies when adding them to the physics world, the raycast will ignore them unless the ray’s mask matches the bodies’ groups.
To fix this, explicitly set the collision group and mask on your callback object:
var rayCallback = new Ammo.ClosestRayResultCallback(rayFrom, rayTo);
rayCallback.set_m_collisionFilterGroup(Ammo.CollisionFilterGroups.DefaultFilter);
rayCallback.set_m_collisionFilterTo(Ammo.CollisionFilterGroups.AllFilter); // Or your custom maskCoordinate Space Mismatches
Ammo.js operates entirely in world coordinates. If you pass local
coordinates or untransformed vectors to the raycast start
(rayFrom) and end (rayTo) positions, the ray
will be cast in the wrong area of the physics world.
- Ensure World Coordinates: Always convert your camera or origin vectors into world space before passing them to Ammo.js.
- Scale Mismatch: If your visual mesh (e.g., in Three.js) is scaled, but you did not scale the corresponding Ammo.js collision shape, the physics representation will not match the visual representation, causing the ray to miss.
Out-of-Sync Physics Transforms
If you move kinematic bodies or update the position of rigid bodies directly via their world transforms without letting the physics engine step, the collision AABBs (Axis-Aligned Bounding Boxes) will not update. Ammo.js uses a broadphase collision phase that relies on these bounding boxes to quickly filter ray intersections.
If the broadphase AABB is out of date, the raycast will skip the narrowphase collision check entirely. Ensure you call:
physicsWorld.stepSimulation(deltaTime, 10);Or force an update of the single object’s AABB if you manually mutated its position:
physicsWorld.updateSingleAabb(rigidBody);Using the Wrong Collision Flags
If you are trying to raycast against static or kinematic bodies, ensure they are correctly registered in the dynamics world. If a body’s collision flags are set incorrectly (for example, if it is set to a trigger state but the raycast callback is configured to ignore triggers), the raycast will return no results.
You can allow raycasts to hit triggers or specific body types by
configuring your callback options, or by ensuring your rigid bodies are
added using the correct collisionFlags:
body.setCollisionFlags(body.getCollisionFlags() | Ammo.CollisionFlags.CF_CUSTOM_MATERIAL_CALLBACK);Memory Allocation and Pointer Corruption
Because ammo.js is a direct port of C++ code via Emscripten, memory
must be handled manually. Failing to instantiate btVector3
objects correctly can lead to garbage values being sent to the physics
engine.
If you pass native JavaScript objects instead of instantiated Ammo.js objects, the engine will fail silently:
// WRONG: Will fail silently
var rayFrom = { x: 0, y: 10, z: 0 };
// CORRECT: Must use Ammo.js heap-allocated objects
var rayFrom = new Ammo.btVector3(0, 10, 0);
var rayTo = new Ammo.btVector3(0, -10, 0);Always remember to destroy these vectors using
Ammo.destroy(vector) once the raycast is complete to avoid
memory leaks.