tumble.js
v0.3.0
Published
3D rigid body physics for the web. A TypeScript port of Erin Catto's Box3D.
Maintainers
Readme
tumble
Web physics engine — a from-scratch TypeScript port of Box3D by Erin Catto, bit-exact against the C reference. Multithreaded by default. Zero dependencies, tree-shakeable, ESM only.
0.x — the API tracks Box3D until its 1.0. Pinned to box3d
29bf523(v0.1.0+ fixes).
Install
npm install tumble.jsimport { init, World, BodyType, makeBoxHull } from "tumble.js";
await init(); // compiles the inlined wasm kernel + resolves threading
const world = new World();
const ground = world.createBody({ position: { x: 0, y: -1, z: 0 } });
ground.createHull({}, makeBoxHull(50, 1, 50));
const box = world.createBody({ type: BodyType.Dynamic, position: { x: 0, y: 10, z: 0 } });
box.createHull({ density: 1 }, makeBoxHull(0.5, 0.5, 0.5));
for (let i = 0; i < 120; i++) world.step(1 / 60, 4);
console.log(box.getPosition()); // rests at y ≈ 1.5Shapes (sphere, capsule, convex hull, mesh, heightfield, compound), eight joint types, continuous collision, ray/shape/overlap queries, contact + sensor events, kinematic character mover. Live samples on real WebGPU: dylanebert.github.io/tumble.js.
Threading
init() resolves threading per host. Results are bit-identical at any thread count.
| host | kernel |
| ------------------------------ | ---------------------------------- |
| bun / node | multithreaded, 4 threads |
| browser, cross-origin isolated | multithreaded, 4 threads |
| browser without COOP/COEP | single-threaded, one console note |
| init({ threads: 0 \| n }) | force single-thread / set count |
Browser multithreading needs two headers: Cross-Origin-Opener-Policy: same-origin, Cross-Origin-Embedder-Policy: require-corp. Hosts without them still work, single-threaded. threads() reports what resolved; no shutdown() needed — the pool never holds a process open.
Performance
large_pyramid, 4095 bodies, ms/step. AMD Ryzen 5900X, median of 4 interleaved reps, each best-of-4 × 199 timed steps (bun run bench; box3d built clang -O3 -march=native at the pin):
| threads | tumble · bun | tumble · node | box3d · C | | ----------: | -----------: | ------------: | --------: | | 1 | 17.9 | 15.6 | 8.3 | | 2 | 12.2 | 10.0 | — | | 4 (default) | 9.5 | 7.9 | 2.6 | | 6 | 9.1 | 7.9 | — | | 8 | 9.0 | 7.4 | 2.4 |
| bodies | 1 thread (bun / node) | 4 threads | | -----: | --------------------: | ----------: | | 210 | 0.66 / 0.55 | 0.47 / 0.43 | | 1035 | 3.4 / 2.7 | 1.6 / 1.4 |
Threaded tumble beats box3d on one C core (7.4 vs 8.3 ms) — that ratio is the badge, reproduced on every release by bun run bench under the identical protocol on one runner. For scale, box3d's own emscripten -pthread build is still ~2–3× faster (earlier same-protocol session; emsdk unavailable in this window to re-measure). tumble buys a readable, tree-shakeable, TS-native surface, not peak wasm throughput.
Verification
| gate | scope | status |
| --------------- | ---------------------------------------------------------------- | -------------- |
| bun test | 471 unit tests ported from box3d's suite | ci badge |
| fixture gate | 53 scenes, per-step world-state hash equality vs the C reference | maintainer-run |
| MT fixture gate | same 53 scenes (joints route through the pool too), bit-exact at 2 and 8 threads | maintainer-run |
| upstream parity | 141/162 in-scope upstream tests covered (87%) | parity badge |
Equality is exact, not tolerance-banded: every f32 op mirrors the C expression tree (Math.fround per op, no FMA, portable trig, scalar min/max semantics). Every upstream test carries a disposition in upstream/parity.json, cross-checked in CI.
License
MIT. Port © Dylan Ebert; Box3D © Erin Catto. See LICENSE · CONTRIBUTING.md.
