Crafting Deformable Volumes in Ammo.js: Using btSoftBodyHelpers and btTransform
The Ammo.btSoftBodyHelpers constructor creates an auxiliary object for generating soft body shapes from meshes. It accepts no arguments and exposes the method CreateFromTriMesh(worldInfo, vertexArray, indexArray, indexCount, randomizeConstraints) which builds a soft body from triangle data.
- Parameters:
worldInfo– the world information object (btSoftBodyWorldInfo).vertexArray– a flatFloat32Arrayof vertex coordinatse.indexArray– a flatUint32Array(orUint16Array) of triangle indices.indexCount– number of indices (total array length).randomizeConstraints– boolean that controls whether constraints are randomized for stability.
- Returns: a
btSoftBodyinstance ready for configuration.
The Ammo.btTransform class represents a rigid body transformation (position and rotation). Its most used properties are origin (a btVector3) and rotation (a btQuaternion). Useful methods include setIdentity(), setOrigin(vector), and setRotation(quaternion). In soft body workflows it often serves as an intermediate transform for reading node positions.
Creating a Soft Volume
The typical pipeline involves preparing geometry, calling CreateFromTriMesh, configuring physical properties, and adding the body to the world. Below is a refactored function that creates a pressurized soft volume:
function makeSoftVolume(geometry, mass, pressure, world, helper) {
// Merge duplicate vertices to get a clean indexed mesh
const simpleGeo = new THREE.BufferGeometry();
simpleGeo.setAttribute('position', geometry.getAttribute('position'));
simpleGeo.setIndex(geometry.getIndex());
const indexed = BufferGeometryUtils.mergeVertices(simpleGeo);
// Build mapping from indexed vertices back to original vertices
const origPos = geometry.attributes.position.array;
const idxPos = indexed.attributes.position.array;
const associations = [];
for (let i = 0; i < idxPos.length; i += 3) {
const matches = [];
for (let j = 0; j < origPos.length; j += 3) {
if (Math.abs(idxPos[i] - origPos[j]) < 1e-6 &&
Math.abs(idxPos[i+1] - origPos[j+1]) < 1e-6 &&
Math.abs(idxPos[i+2] - origPos[j+2]) < 1e-6) {
matches.push(j);
}
}
associations.push(matches);
}
// Ammo soft body from the indexed mesh
const softBody = helper.CreateFromTriMesh(
world.getWorldInfo(),
idxPos,
indexed.index.array,
indexed.index.array.length / 3,
true
);
// Configuration
const cfg = softBody.get_m_cfg();
cfg.set_viterations(40);
cfg.set_piterations(40);
cfg.set_collisions(0x11); // soft-soft & soft-rigid
cfg.set_kDF(0.1); // dynamic friction
cfg.set_kDP(0.01); // damping
cfg.set_kPR(pressure); // pressure constant
const material = softBody.get_m_materials().at(0);
material.set_m_kLST(0.9); // linear stiffness
material.set_m_kAST(0.9); // angular stiffness
softBody.setTotalMass(mass, false);
Ammo.castObject(softBody, Ammo.btCollisionObject)
.getCollisionShape()
.setMargin(0.05);
world.addSoftBody(softBody, 1, -1);
softBody.setActivationState(4); // disable deactivation
// Three.js mesh for rendering
const mesh = new THREE.Mesh(geometry, new THREE.MeshPhongMaterial({color: 0xffffff}));
mesh.castShadow = true;
mesh.receiveShadow = true;
mesh.frustumCulled = false;
mesh.userData.physicsBody = softBody;
mesh.userData.associations = associations;
return mesh;
}
After each simulation step the mesh vertices must be updated from the soft body nodes:
function updateSoftMeshes(meshes) {
const transform = new Ammo.btTransform();
for (const mesh of meshes) {
const soft = mesh.userData.physicsBody;
const posAttr = mesh.geometry.attributes.position;
const nrmAttr = mesh.geometry.attributes.normal;
const assoc = mesh.userData.associations;
const nodes = soft.get_m_nodes();
for (let i = 0; i < assoc.length; i++) {
const node = nodes.at(i);
const nx = node.get_m_x().x();
const ny = node.get_m_x().y();
const nz = node.get_m_x().z();
const nnx = node.get_m_n().x();
const nny = node.get_m_n().y();
const nnz = node.get_m_n().z();
for (const idx of assoc[i]) {
posAttr.array[idx] = nx;
posAttr.array[idx+1] = ny;
posAttr.array[idx+2] = nz;
nrmAttr.array[idx] = -nnx;
nrmAttr.array[idx+1] = -nny;
nrmAttr.array[idx+2] = -nnz;
}
}
posAttr.needsUpdate = true;
nrmAttr.needsUpdate = true;
}
}
Setting up the world and the helper is straightforward:
const collisionConfig = new Ammo.btSoftBodyRigidBodyCollisionConfiguration();
const dispatcher = new Ammo.btCollisionDispatcher(collisionConfig);
const broadphase = new Ammo.btDbvtBroadphase();
const solver = new Ammo.btSequentialImpulseConstraintSolver();
const softSolver = new Ammo.btDefaultSoftBodySolver();
const dynamicsWorld = new Ammo.btSoftRigidDynamicsWorld(
dispatcher, broadphase, solver, collisionConfig, softSolver
);
dynamicsWorld.setGravity(new Ammo.btVector3(0, -9.8, 0));
dynamicsWorld.getWorldInfo().set_m_gravity(new Ammo.btVector3(0, -9.8, 0));
const btHelper = new Ammo.btSoftBodyHelpers();
You can now call makeSoftVolume with a THREE.BoxGeometry, THREE.SphereGeometry, or any triangulated geometry. Remebmer to call dynamicsWorld.stepSimulation(deltaTime) and then updateSoftMeshes to synchronize the visuals.