Create Realistic Cloth Simulation with Ammo.js

This article provides a step-by-step technical guide on how to create a highly realistic cloth simulation using Ammo.js, the WebAssembly port of the Bullet physics engine. It covers setting up a soft body physics world, generating the cloth geometry, configuring physical properties such as stiffness and damping, anchoring the cloth to static objects, and syncing the physics simulation with a 3D renderer like Three.js.


1. Initialize the Soft Body Physics World

To simulate cloth, you must use a physics world that supports soft bodies. In Ammo.js, this requires initializing a btSoftRigidDynamicsWorld instead of the standard rigid-only world.

// Configure collision and solver setup
const collisionConfiguration = new Ammo.btSoftBodyRigidBodyCollisionConfiguration();
const dispatcher = new Ammo.btCollisionDispatcher(collisionConfiguration);
const broadphase = new Ammo.btDbvtBroadphase();
const solver = new Ammo.btSequentialImpulseConstraintSolver();
const softBodySolver = new Ammo.btDefaultSoftBodySolver();

// Create the soft body dynamics world
const physicsWorld = new Ammo.btSoftRigidDynamicsWorld(
    dispatcher, 
    broadphase, 
    solver, 
    collisionConfiguration, 
    softBodySolver
);

// Set gravity (Y-axis downward force)
physicsWorld.setGravity(new Ammo.btVector3(0, -9.81, 0));

2. Generate the Graphical Cloth Mesh

Before creating the physics representation, set up the visual mesh using Three.js (or your chosen 3D library). A simple flat plane geometry acts as the foundation for the cloth.

const clothWidth = 4;
const clothHeight = 3;
const segmentsX = 40;
const segmentsY = 30;

const clothGeometry = new THREE.PlaneGeometry(clothWidth, clothHeight, segmentsX, segmentsY);
clothGeometry.rotateX(Math.PI / 2); // Orient horizontally

const clothMaterial = new THREE.MeshPhongMaterial({ color: 0xff0000, side: THREE.DoubleSide });
const clothMesh = new THREE.Mesh(clothGeometry, clothMaterial);
scene.add(clothMesh);

3. Create the Physics Soft Body

Ammo.js provides a helper class, btSoftBodyHelpers, to generate soft body patches directly from corner coordinates. This matches the dimensions and subdivision resolution of your graphical mesh.

const softBodyHelpers = new Ammo.btSoftBodyHelpers();

// Define the four corners of the cloth patch
const c00 = new Ammo.btVector3(-clothWidth / 2, 0, -clothHeight / 2);
const c10 = new Ammo.btVector3(clothWidth / 2, 0, -clothHeight / 2);
const c01 = new Ammo.btVector3(-clothWidth / 2, 0, clothHeight / 2);
const c11 = new Ammo.btVector3(clothWidth / 2, 0, clothHeight / 2);

// Create the soft body patch
const clothSoftBody = softBodyHelpers.CreatePatch(
    physicsWorld.getWorldInfo(),
    c00, c10, c01, c11,
    segmentsX + 1, segmentsY + 1,
    0, true
);

// Configure general soft body physics parameters
const sbConfig = clothSoftBody.get_m_cfg();
sbConfig.set_viterations(10); // Velocity iterations for constraint solving
sbConfig.set_piterations(10); // Position iterations for accuracy
sbConfig.set_collisions(0x11); // Enable self-collision and rigid body collision

// Set mass properties
const totalMass = 1.2; // Mass in kilograms
clothSoftBody.setTotalMass(totalMass, false);

// Prevent the body from sleeping
clothSoftBody.setActivationState(4); // DISABLE_DEACTIVATION

// Add the soft body to the physics world
physicsWorld.addSoftBody(clothSoftBody, 1, -1);

4. Fine-Tune Material properties for Realism

To make the cloth behave like fabric (e.g., silk, canvas, or heavy denim) rather than rubber, you must adjust the stiffness and damping parameters of the soft body’s material.

const material = clothSoftBody.get_m_materials().at(0);

// Linear stiffness (kLST): Dictates stretching resistance (range 0.0 to 1.0)
material.set_m_kLST(0.9); 

// Angular stiffness (kAST): Dictates bending resistance (range 0.0 to 1.0)
material.set_m_kAST(0.9);

// Volume stiffness (kVST): Keeps volume if simulating closed 3D soft bodies
material.set_m_kVST(0.9);

5. Anchor the Cloth in Space

Without anchors, the cloth will immediately drop due to gravity. You can anchor specific nodes (vertices) of the soft body to keep them suspended or attached to rigid bodies.

To pin the two top corners of the cloth:

// Anchor index formula based on resolution
const topLeftNodeIndex = 0;
const topRightNodeIndex = segmentsX;

// Append anchors (params: nodeIndex, rigidBodyToAnchorTo, disableCollisionBetweenThem, influence)
const influence = 1;
clothSoftBody.appendAnchor(topLeftNodeIndex, rigidBodyPole, false, influence);
clothSoftBody.appendAnchor(topRightNodeIndex, rigidBodyPole, false, influence);

If you just want the nodes to float statically in space without a rigid body, use a null/empty rigid body reference:

clothSoftBody.appendAnchor(topLeftNodeIndex, null, false, 1);
clothSoftBody.appendAnchor(topRightNodeIndex, null, false, 1);

6. Synchronize Physics and Graphics in the Render Loop

Every frame, you must step the physics world forward and map the physical positions of the soft body nodes back onto the vertices of the Three.js mesh.

function updateSimulation(deltaTime) {
    // Step the physics world
    physicsWorld.stepSimulation(deltaTime, 10);

    // Get the graphical mesh vertices and the physics nodes
    const geometry = clothMesh.geometry;
    const positions = geometry.attributes.position.array;
    const numVertices = positions.length / 3;

    const nodes = clothSoftBody.get_m_nodes();

    // Loop through vertices and update positions
    for (let i = 0; i < numVertices; i++) {
        const node = nodes.at(i);
        const nodePos = node.get_m_x(); // Get current simulated position

        const i3 = i * 3;
        positions[i3] = nodePos.x();
        positions[i3 + 1] = nodePos.y();
        positions[i3 + 2] = nodePos.z();
    }

    // Flag geometry updates for the renderer
    geometry.attributes.position.needsUpdate = true;
    geometry.computeVertexNormals(); // Recompute lighting normals
}