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.
- Steering: Typically applied to front wheels
(indices
0and1). - Engine Force: Applied to the drive wheels (indices
2and3for rear-wheel drive). - Brakes: Applied to all wheels for maximum stopping power.
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());
}
}