How to Load ammo.js in a Modern Web Application

Integrating the 3D physics engine ammo.js into modern web applications requires handling its Emscripten-compiled WebAssembly (Wasm) codebase. This article explains the most common and efficient methods for loading ammo.js today, including asynchronous initialization, module bundler integration, and managing WebAssembly binary compilation in modern JavaScript frameworks.

Understanding the ammo.js Build

Because ammo.js is a direct port of the C++ Bullet Physics library compiled via Emscripten, it does not behave like a standard JavaScript library. It consists of a large JavaScript wrapper file and a WebAssembly (.wasm) binary. Because WebAssembly must be compiled by the browser before it can run, ammo.js cannot be used immediately upon loading; it must be initialized asynchronously.

The Standard Asynchronous Initialization Pattern

The most reliable way to load ammo.js is by calling the global Ammo() function, which returns a Promise. This function compiles the WebAssembly binary and resolves once the physics engine is ready for use.

Here is the standard pattern for loading and initializing the library:

import Ammo from 'ammo.js';

async function initPhysics() {
    // Initialize the WebAssembly module
    const AmmoModule = await Ammo();
    
    // Create physics world components
    const collisionConfiguration = new AmmoModule.btDefaultCollisionConfiguration();
    const dispatcher = new AmmoModule.btCollisionDispatcher(collisionConfiguration);
    const overlappingPairCache = new AmmoModule.btDbvtBroadphase();
    const solver = new AmmoModule.btSequentialImpulseConstraintSolver();
    
    const physicsWorld = new AmmoModule.btDiscreteDynamicsWorld(
        dispatcher, 
        overlappingPairCache, 
        solver, 
        collisionConfiguration
    );
    
    physicsWorld.setGravity(new AmmoModule.btVector3(0, -9.81, 0));
    console.log("Physics world initialized successfully!", physicsWorld);
}

initPhysics();

Loading ammo.js with Modern Bundlers (Vite & Webpack)

Modern frontend bundlers like Vite, Webpack, and Rollup require specific configurations to handle the .wasm file that accompanies the ammo.js JavaScript wrapper.

1. Using Vite

In Vite, the easiest way to load ammo.js is by placing the compiled ammo.wasm.js and ammo.wasm.wasm files in the public directory and loading them via a script tag, or by using a WebAssembly plugin. Alternatively, you can use the npm package ammo-node or pre-packaged WebPack/Vite-friendly wrappers like mixed-ammo or loading through a CDN dynamically:

// Dynamically importing from a CDN or local public folder
const loadAmmo = () => {
  return new Promise((resolve) => {
    const script = document.createElement('script');
    script.src = '/js/ammo.wasm.js'; // Placed in public folder
    script.onload = () => {
      window.Ammo().then((AmmoLib) => {
        resolve(AmmoLib);
      });
    };
    document.head.appendChild(script);
  });
};

2. Using Webpack 5

Webpack 5 has built-in support for WebAssembly. If you are importing ammo.js directly from your node_modules, you must configure your webpack.config.js to handle asset modules for the .wasm file:

module.exports = {
  experiments: {
    asyncWebAssembly: true,
  },
  module: {
    rules: [
      {
        test: /\.wasm$/,
        type: "asset/resource",
      },
    ],
  },
};

Using Web Worker Threading

Because physics calculations are CPU-intensive, running ammo.js on the main browser thread can cause frame rate drops in 3D rendering engines like Three.js or Babylon.js. Modern web applications often load ammo.js inside a Web Worker.

By utilizing importScripts or ES module workers, you can initialize the physics engine off the main thread, passing collider positions back and forth using Transferable objects or high-speed postMessage buffers.