Append Nodes to Ammo.js Soft Body Rope Dynamically
This article explains the technical process of dynamically appending a new node to an existing soft body rope in the Ammo.js physics engine. Since the WebIDL bindings of Ammo.js do not natively support resizing a soft body’s node array at runtime, achieving this requires specific workarounds. We will cover the two most viable methods: recreating the rope while preserving physical state, and utilizing a pre-allocation pool.
The Core Limitation of Ammo.js Soft Bodies
In Ammo.js (a port of Bullet Physics), a soft body rope is
represented by a btSoftBody object containing a fixed
buffer of nodes (vertices) and links (edges). These buffers are
allocated in WebAssembly memory when the rope is initialized, typically
via btSoftBodyHelpers.CreateRope. Because memory is
statically allocated for the simulation structure, you cannot simply
push a new node into the active rope’s memory array at runtime without
causing memory corruption or physics crashes.
To dynamically append a node, you must implement one of the two strategies below.
Method 1: State Copying and Reconstruction (Recommended for True Dynamic Appending)
This method involves destroying the existing rope and instantly replacing it with a new, longer rope. To prevent visual stuttering or physical jolts, you must copy the exact positions and velocities of the old nodes to the new rope.
Step 1: Capture the Current State
Before removing the old rope, query its nodes to extract their current positions, previous positions, and velocities.
const nodeCount = oldRope.get_m_nodes().size();
const states = [];
for (let i = 0; i < nodeCount; i++) {
const node = oldRope.get_m_nodes().at(i);
states.push({
position: [node.get_m_x().x(), node.get_m_x().y(), node.get_m_x().z()],
velocity: [node.get_m_v().x(), node.get_m_v().y(), node.get_m_v().z()]
});
}Step 2: Calculate the Position of the New Node
Determine where the \(N+1\) node should be placed. This is typically calculated based on the position of the last node (\(N\)) plus a directional offset vector multiplied by the desired link length.
Step 3: Recreate the Rope
Remove the old rope from the physics world and destroy it to free WebAssembly memory.
physicsWorld.removeSoftBody(oldRope);
Ammo.destroy(oldRope);Next, create a new rope with \(N+1\) nodes using the helper:
const softBodyHelpers = new Ammo.btSoftBodyHelpers();
const newRope = softBodyHelpers.CreateRope(
physicsWorld.getWorldInfo(),
startPoint, // Temporary start
endPoint, // Temporary end
nodeCount + 1,
0 // fixeds mask
);Step 4: Inject the Stored State and Apply the New Node
Iterate through the new rope’s nodes. Apply the saved positions and velocities to the first \(N\) nodes. Set the position of the \((N+1)\)th node to your calculated coordinates.
for (let i = 0; i < nodeCount; i++) {
const node = newRope.get_m_nodes().at(i);
// Set Position
node.get_m_x().setValue(states[i].position[0], states[i].position[1], states[i].position[2]);
node.get_m_q().setValue(states[i].position[0], states[i].position[1], states[i].position[2]); // Previous position
// Set Velocity
node.get_m_v().setValue(states[i].velocity[0], states[i].velocity[1], states[i].velocity[2]);
}
// Set position for the newly appended node (index nodeCount)
const newNode = newRope.get_m_nodes().at(nodeCount);
newNode.get_m_x().setValue(newX, newY, newZ);
newNode.get_m_q().setValue(newX, newY, newZ);
newNode.get_m_v().setValue(0, 0, 0); // Start the new node at rest relative to the ropeStep 5: Re-add to the Physics World
Configure the physical properties of the new rope (such as total mass, stiffness, and collision flags) to match the old rope, and add it back to the physics world.
Method 2: Node Pre-allocation (Recommended for Performance)
If you need to append nodes frequently (e.g., a growing vine or a lasso), reconstructing the rope repeatedly is computationally expensive. Instead, pre-allocate a rope with the maximum number of nodes you anticipate needing.
Step 1: Initialize with Inactive Nodes
Create a rope with \(M\) nodes (where \(M\) is the maximum length). Place the “unused” nodes at the exact same spatial coordinate as the current terminal node of your active rope.
Step 2: Zero Out Mass of Inactive Nodes
Set the mass of all inactive nodes to zero. In Bullet Physics, a node with zero mass acts as an infinite mass (static) node, effectively freezing it in place and preventing it from responding to gravity or spring forces.
Step 3: Activate Nodes Dynamically
When you need to append a node: 1. Locate the first inactive node in
the sequence. 2. Position it at the desired offset relative to the
active end. 3. Assign it a positive mass value (e.g.,
1.0 / totalActiveNodes). 4. Update the soft body’s mass
distribution using internal Ammo.js updates so the solver registers the
change.