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:
- Conditional Debugging: Only run the node extraction loop and WebGL draw calls when debugging is explicitly toggled on by the developer.
- Pre-allocate Arrays: Avoid instantiating a new
Float32Arrayevery frame. Pre-allocate a global typed array sized to the maximum expected number of nodes, and update its subsets usingsubarray()orgl.bufferSubData(). - Render Links: To visualize the structure more
clearly, you can also query
softBody.get_m_links()to draw lines (gl.LINES) connecting the nodes, utilizing the index offsets provided by each link’s node references.