Continuous Raycasting Along a Spline with ammo.js

Ammo.js, the Emscripten port of the Bullet physics engine, cannot perform continuous curved raycasting along a spline path out of the box because its native raycasting API is strictly linear. However, you can easily achieve this functionality by discretizing your spline path into a series of short, linear raycast segments and executing them sequentially. This article explains the technical limitations of ammo.js raycasting and provides a step-by-step implementation guide to simulate continuous spline raycasting.

The Limitation of Native ammo.js Raycasting

In ammo.js, raycasting is handled by the btCollisionWorld.prototype.rayTest method. This function requires a starting 3D vector, an ending 3D vector, and a callback object (usually ClosestRayResultCallback). Because the physics engine calculates intersections mathematically using linear algebra, it only supports straight lines.

To cast a ray along a curved spline—such as a Bezier or Catmull-Rom curve—you must approximate the curve using linear segments.

How to Implement Spline Raycasting in ammo.js

To perform a continuous raycast along a spline, you must break the spline down into a sequence of points, cast a linear ray between each consecutive pair of points, and stop the process as soon as a collision is detected.

Step 1: Define and Discretize the Spline

First, define your spline path. If you are using ammo.js alongside a 3D library like Three.js, you can use the built-in curve classes to generate points along the path.

// Example using Three.js to generate spline points
const curve = new THREE.CatmullRomCurve3([
    new THREE.Vector3(-10, 0, 0),
    new THREE.Vector3(0, 5, 10),
    new THREE.Vector3(10, 0, 0)
]);

// Divide the spline into 50 segments (51 points)
const points = curve.getPoints(50);

Step 2: Set Up the Sequential Raycast Loop

Iterate through the points generated from your spline. For each segment, create an ammo.js raycast. If a segment registers a hit, terminate the loop immediately to simulate the first point of impact along the curved path.

function raycastAlongSpline(points, dynamicsWorld) {
    const startVec = new Ammo.btVector3();
    const endVec = new Ammo.btVector3();
    
    let hitResult = null;

    for (let i = 0; i < points.length - 1; i++) {
        const startPoint = points[i];
        const endPoint = points[i + 1];

        startVec.setValue(startPoint.x, startPoint.y, startPoint.z);
        endVec.setValue(endPoint.x, endPoint.y, endPoint.z);

        const rayCallback = new Ammo.ClosestRayResultCallback(startVec, endVec);

        // Perform the linear raycast for this segment
        dynamicsWorld.rayTest(startVec, endVec, rayCallback);

        if (rayCallback.hasHit()) {
            // Retrieve collision details
            hitResult = {
                body: Ammo.castObject(rayCallback.get_m_collisionObject(), Ammo.btRigidBody),
                point: rayCallback.get_m_hitPointWorld(),
                normal: rayCallback.get_m_hitNormalWorld(),
                segmentIndex: i
            };

            // Clean up memory for the callback before exiting
            Ammo.destroy(rayCallback);
            break; 
        }

        Ammo.destroy(rayCallback);
    }

    // Clean up temporary vectors
    Ammo.destroy(startVec);
    Ammo.destroy(endVec);

    return hitResult; // Returns null if no collision occurred along the spline
}

Performance Considerations

While this iterative approach successfully simulates spline raycasting, running multiple physics raycasts in a single frame can impact performance. To keep your application running smoothly, consider the following optimization strategies: