Loading Ammo.js Asynchronously in Web Browsers
This article explains how the 3D physics library ammo.js
handles asynchronous loading within modern web browser environments.
Because ammo.js is a WebAssembly (Wasm) port of the C++
Bullet Physics engine, it cannot be run synchronously upon script
injection; instead, it requires an asynchronous compilation and
instantiation process to prevent blocking the browser’s main thread.
Below, we explore the mechanics of this initialization process and how
to implement it in your projects.
Why Asynchronous Loading is Required
ammo.js is compiled from C++ using Emscripten. The
output consists of two parts: a large WebAssembly binary
(.wasm file) containing the compiled physics engine code,
and a JavaScript wrapper (.js file) that manages memory and
provides the API glue.
Browsers restrict the synchronous compilation of WebAssembly modules over a certain size to keep the user interface responsive. Consequently, the browser must fetch, compile, and instantiate the WebAssembly binary asynchronously.
The Promise-Based Initialization Pattern
In modern builds of ammo.js, the library is initialized
by calling the global Ammo() function. This function
returns a Promise-like object (a “thenable”) that resolves once the
WebAssembly module has been fully compiled, memory has been allocated,
and the runtime is ready.
The standard pattern for loading and initializing
ammo.js is as follows:
// Ensure the ammo.js script has been loaded via a script tag or bundler import
Ammo().then(function(AmmoLib) {
// The physics engine is now fully loaded and initialized.
// Use 'AmmoLib' to access Bullet Physics classes.
const collisionConfiguration = new AmmoLib.btDefaultCollisionConfiguration();
const dispatcher = new AmmoLib.btCollisionDispatcher(collisionConfiguration);
const overlappingPairCache = new AmmoLib.btDbvtBroadphase();
const solver = new AmmoLib.btSequentialImpulseConstraintSolver();
const physicsWorld = new AmmoLib.btDiscreteDynamicsWorld(
dispatcher,
overlappingPairCache,
solver,
collisionConfiguration
);
console.log("Ammo.js successfully loaded and physics world initialized.");
});Using modern async/await syntax, the initialization can
be written more cleanly:
async function initPhysics() {
const AmmoLib = await Ammo();
const collisionConfiguration = new AmmoLib.btDefaultCollisionConfiguration();
// Additional physics setup goes here
}The Legacy Callback Approach
In older builds of ammo.js, Emscripten relied on a
global configuration object rather than a returned Promise. In these
versions, you pass a configuration object containing an
onRuntimeInitialized callback to the Ammo
function:
Ammo({
locateFile: (path) => `/path/to/assets/${path}`, // Optional helper to locate the .wasm file
}).then(function(AmmoLib) {
// Module is ready
});If you are using an older build that does not support the Promise
API, the onRuntimeInitialized callback is used to detect
readiness:
window.Ammo = {
onRuntimeInitialized: function() {
// Legacy initialization logic goes here
console.log("Legacy Ammo.js runtime initialized.");
}
};Managing the Wasm Binary Path
When loading ammo.js in complex directory structures,
the JavaScript wrapper may not automatically find the accompanying
.wasm file. You can guide the asynchronous loader by
providing a configuration object to the Ammo initializer
with a locateFile function:
Ammo({
locateFile: function(filename) {
return '/path/to/wasm/folder/' + filename;
}
}).then(function(AmmoLib) {
// Module ready
});By utilizing these asynchronous loading patterns,
ammo.js ensures that the heavy WebAssembly payload is
loaded efficiently and safely without disrupting the browser’s rendering
performance.