Visualize Ammo.js Soft Body Nodes with WebGL Debug Drawer

This article explains how to extract, process, and render soft body simulation nodes from Ammo.js using a custom WebGL debug drawer. You will learn how to access the underlying physics node data in JavaScript, buffer these coordinates into WebGL, and draw them dynamically as points or wireframes to diagnose and debug your 3D physics simulations in real time.

Accessing Soft Body Nodes in Ammo.js

Unlike rigid bodies, which are represented by a single transform matrix, soft bodies in Ammo.js (btSoftBody) consist of a dynamic network of nodes (vertices) and links (edges). To visualize these nodes, you must iterate through the soft body’s node array at each frame of your render loop.

You can access the nodes of a btSoftBody object using the following approach:

// Assume 'softBody' is an initialized Ammo.btSoftBody instance
const nodes = softBody.get_m_nodes();
const numNodes = nodes.size();

for (let i = 0; i < numNodes; i++) {
    const node = nodes.at(i);
    const position = node.get_m_x(); // Returns a btVector3
    
    const x = position.x();
    const y = position.y();
    const z = position.z();
    
    // Use x, y, z coordinates for visualization
}

Setting Up the WebGL Debug Drawer

To render these coordinates without the overhead of a full 3D engine, you can create a lightweight WebGL debug drawer. This requires a dedicated shader program designed to render point clouds or wireframes.

1. Vertex and Fragment Shaders

The vertex shader receives the dynamic coordinates of the nodes, while the fragment shader outputs a solid color (e.g., bright green) for high visibility during debugging.

const vertexShaderSource = `
    attribute vec3 a_position;
    uniform mat4 u_projectionView;
    void main() {
        gl_Position = u_projectionView * vec4(a_position, 1.0);
        gl_PointSize = 5.0; // Size of the debug node points
    }
`;

const fragmentShaderSource = `
    precision mediump float;
    void main() {
        gl_FragColor = vec4(0.0, 1.0, 0.0, 1.0); // Bright green
    }
`;

2. Initializing WebGL Buffers

Create a dynamic buffer to hold the vertex positions. Because soft body nodes move every frame, the buffer must be updated during every render cycle using gl.DYNAMIC_DRAW.

// Initialize WebGL context and program
const gl = canvas.getContext('webgl');
const shaderProgram = createShaderProgram(gl, vertexShaderSource, fragmentShaderSource);

// Retrieve attribute and uniform locations
const positionAttributeLocation = gl.getAttribLocation(shaderProgram, 'a_position');
const projectionViewLocation = gl.getUniformLocation(shaderProgram, 'u_projectionView');

// Create a buffer for the node positions
const positionBuffer = gl.createBuffer();

Updating and Drawing Nodes in the Render Loop

During each frame of the simulation, you must collect the current positions of the soft body nodes, upload them to the GPU, and execute a draw call.

function drawSoftBodyNodes(gl, softBody, projectionViewMatrix) {
    const nodes = softBody.get_m_nodes();
    const numNodes = nodes.size();
    
    // Create a float array to hold coordinates [x1, y1, z1, x2, y2, z2, ...]
    const positions = new Float32Array(numNodes * 3);
    
    for (let i = 0; i < numNodes; i++) {
        const node = nodes.at(i);
        const pos = node.get_m_x();
        
        positions[i * 3] = pos.x();
        positions[i * 3 + 1] = pos.y();
        positions[i * 3 + 2] = pos.z();
    }
    
    // Bind the shader program
    gl.useProgram(shaderProgram);
    
    // Set the camera matrix uniform
    gl.uniformMatrix4fv(projectionViewLocation, false, projectionViewMatrix);
    
    // Bind and upload the node position data
    gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
    gl.bufferData(gl.ARRAY_BUFFER, positions, gl.DYNAMIC_DRAW);
    
    // Enable the attribute and point to the buffer
    gl.enableVertexAttribArray(positionAttributeLocation);
    gl.vertexAttribPointer(positionAttributeLocation, 3, gl.FLOAT, false, 0, 0);
    
    // Draw the nodes as WebGL points
    gl.drawArrays(gl.POINTS, 0, numNodes);
}

Optimizing Debug Rendering

Repeatedly querying the memory layout of Ammo.js (which runs inside WebAssembly) can introduce performance bottlenecks. To keep your debug drawer running smoothly, implement the following optimizations: