How to Bundle Ammo.js with Vite or Webpack

Bundling Ammo.js—the WebAssembly (WASM) port of the Bullet physics engine—often presents challenges for modern build tools due to its large file size and asynchronous WASM loading requirements. This article provides a clear, step-by-step guide to successfully configuring both Vite and Webpack to bundle Ammo.js, ensuring smooth integration and optimal runtime loading in your web applications.


Understanding the Ammo.js Challenge

Ammo.js consists of two main parts: a JavaScript wrapper file (ammo.js or ammo.wasm.js) and a binary WebAssembly file (ammo.wasm.wasm). Because the WASM file must be loaded asynchronously at runtime, standard module bundlers will fail if they try to bundle the binary directly into the JavaScript chunk without proper configuration.

To solve this, you must configure your bundler to serve the .wasm file as a static asset and tell the JavaScript wrapper where to find it.


Bundling Ammo.js with Vite

Vite is highly efficient but requires explicit instructions to handle Ammo’s WASM binary and dependency optimization.

Step 1: Install or Copy the Files

The most reliable way to use Ammo.js in Vite is to place the build files directly in your project’s public/ folder.

  1. Download ammo.wasm.js and ammo.wasm.wasm from the official Ammo.js repository.
  2. Place both files inside your project’s /public directory (e.g., /public/js/ammo.wasm.js and /public/js/ammo.wasm.wasm).

Step 2: Configure Vite

Since the files are in the public folder, they will be served as static assets. You need to prevent Vite from trying to optimize or bundle the wrapper. Add this to your vite.config.js:

import { defineConfig } from 'vite';

export default defineConfig({
  optimizeDeps: {
    exclude: ['ammo.js']
  }
});

Step 3: Initialize Ammo in Your Code

To load Ammo, dynamically inject the script or use a global loading helper. In your main JavaScript/TypeScript file:

async function initPhysics() {
  return new Promise((resolve) => {
    const script = document.createElement('script');
    script.src = '/js/ammo.wasm.js';
    script.onload = () => {
      // Ammo is loaded globally. Now initialize the WASM binary.
      window.Ammo().then((AmmoLib) => {
        resolve(AmmoLib);
      });
    };
    document.head.appendChild(script);
  });
}

initPhysics().then((Ammo) => {
  const collisionConfiguration = new Ammo.btDefaultCollisionConfiguration();
  console.log("Ammo.js initialized successfully!", collisionConfiguration);
});

Bundling Ammo.js with Webpack 5

Webpack 5 supports WebAssembly natively, but because Ammo.js uses a custom loading mechanism, you must configure asset modules and Webpack experiments.

Step 1: Install the Webpack Plugins

You need the copy-webpack-plugin to ensure the WebAssembly binary is copied to your output build folder.

npm install copy-webpack-plugin --save-dev

Step 2: Configure Webpack

Modify your webpack.config.js to enable WebAssembly experiments and copy the WASM file to your output directory.

const path = require('path');
const CopyPlugin = require('copy-webpack-plugin');

module.exports = {
  entry: './src/index.js',
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
    clean: true,
  },
  experiments: {
    asyncWebAssembly: true,
    topLevelAwait: true,
  },
  plugins: [
    new CopyPlugin({
      patterns: [
        { 
          from: 'node_modules/ammo.js/builds/ammo.wasm.wasm', 
          to: '[name][ext]' 
        },
        { 
          from: 'node_modules/ammo.js/builds/ammo.wasm.js', 
          to: '[name][ext]' 
        }
      ],
    }),
  ],
  resolve: {
    fallback: {
      fs: false,
      path: false,
    },
  },
};

Step 3: Initialize Ammo in Webpack

When loading Ammo in a Webpack environment, you must manually point the Ammo wrapper to the location of the copied .wasm file.

import AmmoFactory from '../dist/ammo.wasm.js';

async function init() {
  const Ammo = await AmmoFactory({
    locateFile: (path) => {
      if (path.endsWith('.wasm')) {
        return './ammo.wasm.wasm'; // Points to the copied file in your dist folder
      }
      return path;
    }
  });

  const gravity = new Ammo.btVector3(0, -9.81, 0);
  console.log("Ammo.js loaded in Webpack. Gravity vector created:", gravity.y());
}

init();