Using ammo.js with Raw WebGL

This article explains how to integrate ammo.js, a powerful JavaScript port of the Bullet physics engine, directly with raw WebGL without relying on external 3D rendering libraries like Three.js or Babylon.js. We will explore the feasibility of this approach, outline the core architecture required to connect physics with custom WebGL shaders, and detail the step-by-step process of synchronizing physical simulations with raw GPU rendering.

Is it Possible?

Yes, ammo.js can be used with raw WebGL. Ammo.js is strictly a physics engine; it has no built-in rendering capabilities and does not depend on any specific graphics library. It manages mathematical representations of physical objects, calculates collisions, and updates positions and rotations in a virtual 3D space.

Because WebGL is simply a rasterization API that draws pixels based on coordinates you provide, you can feed the coordinate and rotation data generated by ammo.js directly into your custom WebGL shaders.

The Integration Pipeline

To use ammo.js with raw WebGL, you must build a bridge between the CPU-side physics simulation and the GPU-side rendering pipeline. The process consists of three main phases: initialization, the simulation step, and the rendering step.

1. Initialization

First, you must set up both systems independently: * WebGL Setup: Initialize your WebGL context, compile your vertex and fragment shaders, and buffer your 3D mesh geometries (vertices, indices, and normals) into Vertex Buffer Objects (VBOs). * Ammo.js Setup: Initialize the Bullet physics world. This requires setting up a collision configuration, a collision dispatcher, a broadphase interface, a constraint solver, and finally, the discrete dynamics world itself.

Once both are initialized, you define your objects. For every object in your scene, you must create: * A WebGL representation (geometry buffers and material properties). * An Ammo.js representation (a rigid body with a collision shape, mass, and motion state).

2. Stepping the Physics World

In your application’s animation loop (typically powered by requestAnimationFrame), you must advance the physics simulation. This is done by calling the step simulation method on your Ammo world:

physicsWorld.stepSimulation(deltaTime, maxSubSteps);

During this step, ammo.js calculates gravity, forces, and collisions, updating the positions and orientations of all active rigid bodies.

3. Synchronizing and Rendering

After the physics step, you must retrieve the updated transform data from ammo.js and pass it to WebGL before drawing.

  1. Retrieve the Transform: For each active rigid body, query its motion state to get the current world transform.
  2. Convert to Matrices: Extract the position (origin) and rotation (quaternion) from the Ammo transform. You must convert these values into a standard \(4 \times 4\) model matrix. Since raw WebGL operates on flat arrays, you will need a matrix math utility (such as glMatrix) to construct this matrix from Ammo’s data types.
  3. Update Shaders: Bind your WebGL shader program and send the computed model matrix to the vertex shader as a uniform matrix4fv.
  4. Draw: Bind the corresponding vertex buffers and execute the draw call (e.g., gl.drawElements).

Key Challenges to Consider

While bypassing 3D engines gives you maximum control and performance, it introduces several development challenges: