Implement a Car with Ammo.js btRaycastVehicle

This guide explains how to implement a robust, physics-driven car in JavaScript using the btRaycastVehicle class from ammo.js, the Emscripten port of the Bullet Physics engine. You will learn how to configure the rigid body chassis, set up raycast wheels, adjust suspension parameters for stability, apply driving forces, and synchronize the physics simulation with your 3D visual meshes.

The Core Architecture

Instead of simulating wheels with individual physical rigid bodies connected by joints—which is computationally expensive and prone to physical instability—btRaycastVehicle uses a simplified raycasting approach. It utilizes a single rigid body for the car chassis. For each wheel, the engine casts a ray downward to detect the ground. It then calculates the suspension compression, spring forces, tire friction, and contact points mathematically, applying the resulting forces directly to the chassis.

Step 1: Creating the Chassis Rigid Body

The foundation of the vehicle is its chassis. To make the car stable and prevent it from tipping over easily, use a compound shape. This allows you to offset the physical collision box relative to the center of mass, keeping the gravity center low.

// Define chassis dimensions
const chassisWidth = 1.8;
const chassisHeight = 0.6;
const chassisLength = 4.0;
const mass = 800; // in kg

// Create compound shape to offset center of mass
const compoundShape = new Ammo.btCompoundShape();
const localTrans = new Ammo.btTransform();
localTrans.setIdentity();
localTrans.setOrigin(new Ammo.btVector3(0, 0.3, 0)); // Shift collision box upward

const chassisShape = new Ammo.btBoxShape(
    new Ammo.btVector3(chassisWidth * 0.5, chassisHeight * 0.5, chassisLength * 0.5)
);
compoundShape.addChildShape(localTrans, chassisShape);

// Calculate local inertia
const localInertia = new Ammo.btVector3(0, 0, 0);
compoundShape.calculateLocalInertia(mass, localInertia);

// Create motion state and rigid body
const startTransform = new Ammo.btTransform();
startTransform.setIdentity();
startTransform.setOrigin(new Ammo.btVector3(0, 2, 0)); // Start above ground

const motionState = new Ammo.btDefaultMotionState(startTransform);
const rbInfo = new Ammo.btRigidBodyConstructionInfo(mass, motionState, compoundShape, localInertia);
const chassisBody = new Ammo.btRigidBody(rbInfo);

// Disable deactivation so physics stay active when stationary
chassisBody.setActivationState(4); 
physicsWorld.addRigidBody(chassisBody);

Step 2: Initializing the Raycast Vehicle

With the chassis rigid body active in the physics world, instantiate the vehicle raycaster and tuning parameters.

const tuning = new Ammo.btVehicleTuning();
const raycaster = new Ammo.btDefaultVehicleRaycaster(physicsWorld);
const vehicle = new Ammo.btRaycastVehicle(tuning, chassisBody, raycaster);

// Set coordinate system: 0 = X (right), 1 = Y (up), 2 = Z (forward)
vehicle.setCoordinateSystem(0, 1, 2);
physicsWorld.addAction(vehicle);

Step 3: Configuring and Adding Wheels

Define the suspension, friction, and positioning of each wheel. You must configure the attachment points relative to the chassis center.

const wheelDirectionCS0 = new Ammo.btVector3(0, -1, 0); // Raycast points straight down
const wheelAxleCS = new Ammo.btVector3(-1, 0, 0);       // Axle points to the left
const suspensionRestLength = 0.6;
const wheelRadius = 0.4;

function addWheel(isFront, connectionPoint) {
    const wheelInfo = vehicle.addWheel(
        connectionPoint,
        wheelDirectionCS0,
        wheelAxleCS,
        suspensionRestLength,
        wheelRadius,
        tuning,
        isFront
    );

    // Suspension and grip configuration
    wheelInfo.set_m_suspensionStiffness(20.0);       // Hardness of the spring
    wheelInfo.set_m_wheelsDampingRelaxation(2.3);    // Suspension damping during rebound
    wheelInfo.set_m_wheelsDampingCompression(4.4);   // Suspension damping during compression
    wheelInfo.set_m_frictionSlip(1000.0);            // Tire grip coefficient
    wheelInfo.set_m_rollInfluence(0.1);              // Reduce body roll (prevents flipping)
}

// Wheel offsets relative to chassis center
const wheelHalfWidth = 0.9;
const wheelBackOffset = -1.2;
const wheelFrontOffset = 1.2;
const wheelHeight = 0.0;

// Add four wheels (Front Left, Front Right, Rear Left, Rear Right)
addWheel(true, new Ammo.btVector3(wheelHalfWidth, wheelHeight, wheelFrontOffset));
addWheel(true, new Ammo.btVector3(-wheelHalfWidth, wheelHeight, wheelFrontOffset));
addWheel(false, new Ammo.btVector3(wheelHalfWidth, wheelHeight, wheelBackOffset));
addWheel(false, new Ammo.btVector3(-wheelHalfWidth, wheelHeight, wheelBackOffset));

Step 4: Applying Input Controls

Control the vehicle by modifying steering angles, engine forces, and braking values across the wheel indices in your update loop.

function updateVehicleControls(engineForce, steeringValue, brakeForce) {
    // Apply steering to front wheels
    vehicle.setSteeringValue(steeringValue, 0);
    vehicle.setSteeringValue(steeringValue, 1);

    // Apply engine acceleration to rear wheels
    vehicle.applyEngineForce(engineForce, 2);
    vehicle.applyEngineForce(engineForce, 3);

    // Apply brakes to all wheels
    vehicle.setBrake(brakeForce, 0);
    vehicle.setBrake(brakeForce, 1);
    vehicle.setBrake(brakeForce, 2);
    vehicle.setBrake(brakeForce, 3);
}

Step 5: Synchronizing Visuals with Physics

During your application render loop, retrieve the updated physical transforms of the chassis and wheels to position your 3D meshes.

function updateVisuals() {
    // 1. Update Chassis Mesh
    const chassisTransform = vehicle.getChassisWorldTransform();
    const origin = chassisTransform.getOrigin();
    const rotation = chassisTransform.getRotation();
    
    chassisMesh.position.set(origin.x(), origin.y(), origin.z());
    chassisMesh.quaternion.set(rotation.x(), rotation.y(), rotation.z(), rotation.w());

    // 2. Update Wheel Meshes
    const numWheels = vehicle.getNumWheels();
    for (let i = 0; i < numWheels; i++) {
        // Force the engine to update wheel transform matrix
        vehicle.updateWheelTransform(i, true);
        
        const wheelTransform = vehicle.getWheelInfo(i).get_m_worldTransform();
        const wheelOrigin = wheelTransform.getOrigin();
        const wheelRotation = wheelTransform.getRotation();

        wheelMeshes[i].position.set(wheelOrigin.x(), wheelOrigin.y(), wheelOrigin.z());
        wheelMeshes[i].quaternion.set(wheelRotation.x(), wheelRotation.y(), wheelRotation.z(), wheelRotation.w());
    }
}