Ammo.js Raycast Tutorial: Detect Physics Objects

Raycasting is a fundamental technique in 3D physics engines used to detect collisions, query the environment, and handle user interactions like clicking on objects. This article provides a step-by-step guide on how to construct and perform a spatial raycast within an ammo.js physics world, retrieve hit information, and identify intersected rigid bodies.


Step 1: Define the Start and End Points

A raycast requires a starting point and an ending point in 3D space. In ammo.js, these points must be represented as btVector3 objects.

// Define the start and end coordinates of the ray
const startX = 0, startY = 10, startZ = 0;
const endX = 0, endY = -10, endZ = 0;

// Create ammo.js vector objects
const rayFrom = new Ammo.btVector3(startX, startY, startZ);
const rayTo = new Ammo.btVector3(endX, endY, endZ);

Step 2: Create the Raycast Callback

Ammo.js uses callback objects to determine how the raycast behaves and how results are collected. To find only the first (closest) rigid body that the ray intersects, use ClosestRayResultCallback.

// Initialize the callback with the start and end vectors
const rayCallback = new Ammo.ClosestRayResultCallback(rayFrom, rayTo);

Step 3: Perform the Ray Test

To execute the raycast, call the rayTest method on your active btDynamicsWorld instance, passing the start vector, end vector, and the callback object.

// Assuming 'physicsWorld' is your instantiated Ammo.btDiscreteDynamicsWorld
physicsWorld.rayTest(rayFrom, rayTo, rayCallback);

Step 4: Retrieve and Process the Results

After rayTest is executed, the callback object is populated with collision data. You can check if a hit occurred using hasHit(), and then extract the collision object, the exact hit point, and the hit normal vector.

if (rayCallback.hasHit()) {
    // Retrieve the raw collision object
    const collisionObject = rayCallback.get_m_collisionObject();
    
    // Cast the generic collision object to a rigid body
    const rigidBody = Ammo.castObject(collisionObject, Ammo.btRigidBody);
    
    // Retrieve intersection details
    const hitPoint = rayCallback.get_m_hitPointWorld();
    const hitNormal = rayCallback.get_m_hitNormalWorld();
    
    console.log(`Hit Object:`, rigidBody);
    console.log(`Hit Point: X: ${hitPoint.x()}, Y: ${hitPoint.y()}, Z: ${hitPoint.z()}`);
    console.log(`Hit Normal: X: ${hitNormal.x()}, Y: ${hitNormal.y()}, Z: ${hitNormal.z()}`);
} else {
    console.log("The ray did not intersect any objects.");
}

Step 5: Clean Up Memory

Ammo.js is a WebAssembly/asm.js port of C++, meaning it does not benefit from automatic JavaScript garbage collection for its internal objects. To prevent memory leaks, manually destroy the vectors and callback objects once the raycast is complete.

// Free up memory allocated in the WebAssembly heap
Ammo.destroy(rayFrom);
Ammo.destroy(rayTo);
Ammo.destroy(rayCallback);