Originally reported: March 15, 2026 — BabylonJS/Babylon.js v7.30.0
What we cover:
Babylon.js has been steadily shifting its physics story toward Havok, and recent releases push HavokPlugin further into the default path while nudging users away from AmmoJSPlugin. The change is driven by Havok’s actively maintained WebAssembly build, a cleaner memory model, and the fact that the Ammo.js port has been unmaintained upstream for years. Existing projects keep working, but new code should standardize on @babylonjs/havok.
- Default plugin:
HavokPluginfrom@babylonjs/havok - Status of Ammo:
AmmoJSPluginis still present in the codebase but is treated as the legacy path - Minimum runtime: WebAssembly +
SharedArrayBuffer(cross-origin isolation required for threaded mode) - License note: See the BabylonJS/havok repository for the current runtime license terms before shipping
Why did the Babylon team change the default?
The short answer: Ammo.js is a 2013-era Emscripten port of Bullet that nobody is actively patching, while Havok ships a WebAssembly build maintained directly by Microsoft. The move toward Havok as the default is about cutting a dependency chain that the core team cannot fix without forking an upstream C++ engine. Determinism, memory footprint, and CCD quality all tilt the same direction.
The official Havok plugin documentation has tracked that position since version 6, when Havok first landed. The ergonomic improvement is that enablePhysics() increasingly assumes Havok — if @babylonjs/havok resolves in the module graph, the scene wires it up with minimal boilerplate. For teams that were still passing new AmmoJSPlugin() out of habit, the first hint something shifted shows up as a deprecation notice in the docs:
If you need more context, Babylon’s WebGPU renderer covers the same ground.
// Legacy — explicit Ammo
import { AmmoJSPlugin } from "@babylonjs/core";
scene.enablePhysics(new Vector3(0, -9.81, 0), new AmmoJSPlugin());
// Current recommended path — Havok
import HavokPhysics from "@babylonjs/havok";
const havok = await HavokPhysics();
scene.enablePhysics(new Vector3(0, -9.81, 0), new HavokPlugin(true, havok));
The resolver checks for a registered plugin first, then for @babylonjs/havok. If nothing is registered and Havok isn’t installed, the call throws instead of silently doing nothing — a deliberate break from older behavior that some Ammo users relied on.

Official documentation.
The screenshot above is the current Havok plugin page on doc.babylonjs.com. The part worth reading carefully is the “Initialization” block: the HavokPhysics() factory returns a promise because the WASM module has to be fetched and instantiated before the plugin exists. If you try to call enablePhysics synchronously inside a constructor, you race the WASM load and the scene starts without gravity. The docs page also lists the HP_* native handles the plugin exposes — useful when reading stack traces that dip into the WASM layer.
What actually differs between Havok and Ammo.js in practice?
On paper the API surface looks similar — both implement IPhysicsEnginePlugin. The differences show up under three conditions: high rigid-body counts, stacked constraint chains, and continuous collision detection against fast-moving dynamic bodies. Havok handles all three categories noticeably better than the current Ammo.js build, which is the benchmark story the Babylon team has used to justify the shift.

The chart plots physics step time in milliseconds against rigid body count for a stacked-boxes stress scene. Ammo.js climbs roughly linearly past 500 bodies and crosses the 16.6 ms frame budget near 1,100 bodies. Havok stays well under budget through several thousand bodies before curving upward. The gap is not marginal — it is the reason you can ship a physics-heavy scene to a mid-range laptop without dropping to 30 fps. Numbers like these come from the PhysicsV2 sample scenes published in the Babylon Playground; reproduce them locally before trusting a single chart.
If you need more context, recent Babylon tooling updates covers the same ground.
Three other comparison points matter for real projects:
- Determinism: Havok’s solver is markedly more consistent run-to-run than Ammo’s float accumulation, which drifts between browsers. If you run server-authoritative physics in a Node.js or Bun process, Havok is the option that gives you the best shot at replayable simulations — verify on your target platforms before committing.
- Memory: The Havok WASM module and Ammo.js are in a similar weight class on the wire, but Havok allocates fewer JS objects at runtime because bodies and shapes live entirely in linear memory.
- CCD: Havok has per-body continuous collision detection that actually works for thin walls. Ammo’s CCD requires careful threshold tuning and still tunnels under stress.
How does the migration path look for existing scenes?
For most scenes the migration is two files: package.json and the single spot where enablePhysics is called. The plugin registers itself with the same PhysicsImpostor interface the 5.x and 6.x builds used, so existing sphereImpostor, boxImpostor, and meshImpostor code keeps compiling. The break happens only if you were calling Ammo-specific extensions like setCollisionFilterGroup directly on the native body — those have Havok equivalents under different method names.

The diagram outlines the decision tree the resolver walks: scene calls enablePhysics(), engine checks for a registered plugin, then for @babylonjs/havok, then throws. On the migration side, the diagram maps each Ammo-specific API onto its Havok replacement — setLinearFactor becomes setMotionType plus axis flags, constraint types change from numeric enums to named imports, and the “world” object is no longer exposed because Havok manages memory internally. Keep the diagram open while you do a find-and-replace pass; the mapping is mechanical once you see it laid out.
A related write-up: older Babylon 4.2 scenes.
Here is the minimal package.json diff:
{
"dependencies": {
- "ammo.js": "^0.0.10",
+ "@babylonjs/havok": "latest",
"@babylonjs/core": "latest"
}
}
And the minimal source change, as a side-by-side:
// Old (Ammo)
import Ammo from "ammo.js";
const ammo = await Ammo();
scene.enablePhysics(
new BABYLON.Vector3(0, -9.81, 0),
new BABYLON.AmmoJSPlugin(true, ammo)
);
// New (Havok)
import HavokPhysics from "@babylonjs/havok";
const havok = await HavokPhysics();
scene.enablePhysics(
new BABYLON.Vector3(0, -9.81, 0),
new BABYLON.HavokPlugin(true, havok)
);
The true argument is the _useDeltaForWorldStep flag — leave it on unless you are driving the physics step manually from a fixed-timestep loop. If you host the app behind Vite or Webpack, also copy HavokPhysics.wasm into your public assets folder or use the locateFile callback so the runtime can find the binary; the @babylonjs/havok npm page documents the locateFile signature.
What can go wrong when you flip to the new default?
Three failure modes account for most of the migration tickets reported on the Babylon forum. Each one has a recognizable error string, a narrow root cause, and a fix that takes minutes once you know what you are looking at.
1. WASM fetch fails in a Vite dev server.
A related write-up: a comparable engine upgrade.
Uncaught (in promise) RuntimeError: Aborted(both async and sync fetching of the wasm failed)
Root cause: Vite does not serve .wasm from node_modules by default, and the Havok factory tries both fetch() and readFileSync. The fix is to copy the file into public/ or point the factory at the CDN:
cp node_modules/@babylonjs/havok/lib/esm/HavokPhysics.wasm public/
Then pass a locateFile callback: HavokPhysics({ locateFile: () => "/HavokPhysics.wasm" }).
2. SharedArrayBuffer blocked in production.
ReferenceError: SharedArrayBuffer is not defined
Root cause: Havok’s threaded mode requires cross-origin isolation, which means the page must be served with Cross-Origin-Opener-Policy: same-origin and Cross-Origin-Embedder-Policy: require-corp. Without both headers, modern Chromium refuses to expose SharedArrayBuffer. The fix, as an Express middleware:
app.use((req, res, next) => {
res.setHeader("Cross-Origin-Opener-Policy", "same-origin");
res.setHeader("Cross-Origin-Embedder-Policy", "require-corp");
next();
});
3. Impostors created before Havok finished loading.
TypeError: Cannot read properties of undefined (reading 'HP_World_Create')
Root cause: Code called new PhysicsImpostor(mesh, ...) before await HavokPhysics() resolved, so the plugin’s native handle table is empty. The fix is to await the factory before creating any impostors — the two-line diff is usually moving the enablePhysics call into an async bootstrap function and awaiting it from main().

The Reddit thread captured here — recent posts from r/babylonjs — is dominated by variants of those same three errors. The signal worth pulling from the thread: most teams hit the SharedArrayBuffer header issue on Netlify or Vercel first, because neither platform sets the COOP/COEP pair by default. A pinned comment from a Babylon core contributor points to the same headers-plus-locateFile combination described above.
A migration audit you can run in 20 minutes
Before you ship the upgrade, walk this list from top to bottom. Each item is a concrete check, not a principle — if one fails, fix it before the next deploy rather than batching.
- Run
npm ls ammo.js. If anything still depends on it transitively, either upgrade the dependent package or keep the Ammo plugin registered during the transition. - Grep the codebase for
new AmmoJSPlugin(andAmmoJSPlugin. Replace every call site with the Havok equivalent shown above so you stop carrying the legacy path. - Confirm
@babylonjs/coreand@babylonjs/havokresolve to compatible versions — mismatched versions surface asHP_*handle errors at impostor creation time. - Load the app under Chrome DevTools with the Network tab filtered to
wasm. VerifyHavokPhysics.wasmreturns 200 and itsContent-Typeisapplication/wasm. A 404 or a wrong MIME type is the root cause of most “physics just doesn’t work” bug reports. - In the browser console, run
typeof SharedArrayBuffer. If it prints"undefined"on your production origin, fix the COOP/COEP headers before anything else — single-threaded Havok still runs, but you lose the determinism and speed that made you migrate. - Open the Playground at BabylonJS/havok and verify the sample demo runs against the exact version you pinned. If the sample breaks, your bundler is stripping the WASM; audit the asset pipeline.
- Enable
scene.getPhysicsEngine().getPhysicsPluginName()in a debug panel and confirm it prints"HavokPlugin". If it prints"AmmoJSPlugin", a stale dynamic import is still winning the resolver race. - Run the automated test suite with
HEADLESS=true. Puppeteer and Playwright both need the--enable-features=SharedArrayBufferflag or they will fail atenablePhysics(). Add it to the CI launch options now instead of chasing flakes later.
Pin @babylonjs/core and @babylonjs/havok to exact minor versions in CI, treat any AmmoJSPlugin reference as a build-breaking lint rule, and keep the COOP/COEP headers in your edge config alongside your CSP — that triad prevents the three failure modes above from ever reaching production. The Ammo plugin is firmly on the legacy track, so the cleanup work you do now is the same work you will do when it is eventually removed, except without the safety net of the fallback.
I wrote about runtime portability checks if you want to dig deeper.
