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.