Implementing Ammo.js Debug Drawer in Custom Renderer
Visualizing physics colliders is crucial for debugging 3D
simulations. This article provides a step-by-step guide on how to
implement a visual physics debug drawer for the Ammo.js physics engine
inside a custom WebGL or Three.js renderer. You will learn how to
interface with Ammo’s btIDebugDraw interface, extract line
drawing data, and render it using your custom graphics pipeline.
1. Understand the Ammo.js Debug Interface
Ammo.js (a port of the Bullet Physics engine) features a built-in debug drawer system. It works by traversing the physics world, calculating the wireframes of all active collision shapes, and passing the start and end coordinates of each line segment to a user-defined helper class.
To bridge Ammo.js with your custom renderer, you must implement a
JavaScript object that mimics the btIDebugDraw interface.
The key methods Ammo.js expects are:
drawLine(from, to, color): Called for every wireframe line segment.drawContactPoint(pointOnB, normalOnB, distance, lifeTime, color): Called to display contact points.reportErrorWarning(warningString): Prints physics warnings.draw3dText(location, textString): Renders 3D debug text (optional).setDebugMode(debugMode): Sets which debug elements to draw (e.g., wireframe, bounding boxes).getDebugMode(): Returns the current debug mode mask.
2. Implementing the JS Debug Drawer Wrapper
In Ammo.js, you can instantiate a btIDebugDraw subclass
by wrapping a plain JavaScript object. Because physics debug rendering
generates thousands of lines per frame, you should collect all vertices
into a dynamic array during the draw phase, and then upload them to your
GPU in a single batch.
Below is the implementation of the debug drawer class:
class AmmoDebugDrawer {
constructor(ammoInstance, world) {
this.ammo = ammoInstance;
this.world = world;
this.debugMode = 1; // 1 corresponds to DBG_DrawWireframe
this.vertices = [];
this.colors = [];
// Create the Ammo.js C++ wrapper
this.drawer = new this.ammo.DebugDrawer();
// Bind the required C++ interface methods
this.drawer.drawLine = this.drawLine.bind(this);
this.drawer.drawContactPoint = this.drawContactPoint.bind(this);
this.drawer.reportErrorWarning = this.reportErrorWarning.bind(this);
this.drawer.draw3dText = this.draw3dText.bind(this);
this.drawer.setDebugMode = this.setDebugMode.bind(this);
this.drawer.getDebugMode = this.getDebugMode.bind(this);
// Register the drawer with the physics world
this.world.setDebugDrawer(this.drawer);
}
drawLine(from, to, color) {
// Wrap pointers in btVector3 if they are not already object instances
const pFrom = this.ammo.wrapPointer(from, this.ammo.btVector3);
const pTo = this.ammo.wrapPointer(to, this.ammo.btVector3);
const pColor = this.ammo.wrapPointer(color, this.ammo.btVector3);
// Push positions
this.vertices.push(pFrom.x(), pFrom.y(), pFrom.z());
this.vertices.push(pTo.x(), pTo.y(), pTo.z());
// Push colors (RGB) for both vertices
const r = pColor.x();
const g = pColor.y();
const b = pColor.z();
this.colors.push(r, g, b, r, g, b);
}
drawContactPoint(pointOnB, normalOnB, distance, lifeTime, color) {
// Optional: Implement visual markers for collision contacts
}
reportErrorWarning(warningString) {
console.warn("Ammo.js Warning: ", warningString);
}
draw3dText(location, textString) {
// Optional: Render text in 3D space
}
setDebugMode(debugMode) {
this.debugMode = debugMode;
}
getDebugMode() {
return this.debugMode;
}
clear() {
this.vertices.length = 0;
this.colors.length = 0;
}
}3. Integrating with Your Custom WebGL Renderer
Once you have gathered the lines, you need to draw them. In a custom WebGL renderer, you should create a dedicated shader program for lines and a dynamic WebGL Buffer.
The Shader Program
Use a simple vertex shader that passes positions and colors, and a fragment shader that renders the vertex colors:
// Vertex Shader
attribute vec3 position;
attribute vec3 color;
varying vec3 vColor;
uniform mat4 uViewProjection;
void main() {
vColor = color;
gl_Position = uViewProjection * vec4(position, 1.0);
}
// Fragment Shader
precision mediump float;
varying vec3 vColor;
void main() {
gl_FragColor = vec4(vColor, 1.0);
}The WebGL Rendering Loop
In your application loop, clear the line arrays, force Ammo.js to
populate them by calling debugDrawWorld(), upload the
arrays to your WebGL buffers, and issue a draw call using
gl.LINES.
class PhysicsDebugger {
constructor(gl, ammoInstance, world) {
this.gl = gl;
this.drawer = new AmmoDebugDrawer(ammoInstance, world);
// Create WebGL Buffers
this.positionBuffer = gl.createBuffer();
this.colorBuffer = gl.createBuffer();
// Set up your shader program references here (program, uniforms, attributes)
this.shaderProgram = this.initShaderProgram();
}
updateAndRender(viewProjectionMatrix) {
const gl = this.gl;
// 1. Clear previous frame's geometry
this.drawer.clear();
// 2. Ask Ammo.js to populate the drawer with current physics state
this.drawer.world.debugDrawWorld();
if (this.drawer.vertices.length === 0) return;
// 3. Bind shader program
gl.useProgram(this.shaderProgram);
gl.uniformMatrix4fv(this.uViewProjectionLocation, false, viewProjectionMatrix);
// 4. Upload Positions
gl.bindBuffer(gl.ARRAY_BUFFER, this.positionBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(this.drawer.vertices), gl.DYNAMIC_DRAW);
gl.enableVertexAttribArray(this.positionAttribLocation);
gl.vertexAttribPointer(this.positionAttribLocation, 3, gl.FLOAT, false, 0, 0);
// 5. Upload Colors
gl.bindBuffer(gl.ARRAY_BUFFER, this.colorBuffer);
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(this.drawer.colors), gl.DYNAMIC_DRAW);
gl.enableVertexAttribArray(this.colorAttribLocation);
gl.vertexAttribPointer(this.colorAttribLocation, 3, gl.FLOAT, false, 0, 0);
// 6. Draw the wireframes
const vertexCount = this.drawer.vertices.length / 3;
gl.drawArrays(gl.LINES, 0, vertexCount);
}
}By separating the data-gathering step (drawLine) from
the rendering pipeline, you keep performance high. This design ensures
that you only make one draw call per frame to render the entire physics
world wireframe, keeping the overhead of your debugging tools
minimal.