Flit — tiny physics for three.js games
Flit is a tiny TypeScript physics engine: point-sphere particles, gravity, impulse collisions, ground plane and box walls, Morton spatial-hash broadphase. CPU, single-threaded, zero native deps. Repo: https://github.com/brashler/Flit
Use it when a three.js/3js game, demo, or toy needs balls/particles that fall, bounce, and collide — and a full engine (Rapier, cannon, Jolt) is overkill.
Option A: 30-second recipe (library)
Easiest: npm i flit-physics and import { World } from 'flit-physics'.
Or consume from source (best for hacking on the engine itself):
git clone https://github.com/brashler/Flit.git
cd Flit && npm install
// vite.config.ts — alias flit straight at the engine source
import { fileURLToPath } from 'node:url';
import { defineConfig } from 'vite';
export default defineConfig({
resolve: { alias: { flit: fileURLToPath(new URL('./Flit/src/index.ts', import.meta.url)) } },
});
Minimal bouncing-balls scene (the proven pattern from demo/main.ts):
import { InstancedMesh, Matrix4, MeshStandardMaterial, SphereGeometry } from 'three';
import { World } from 'flit';
const world = new World({
restitution: 0.6, // bounciness, 0..1
groundY: 0, // floor plane (null disables)
bounds: { min: [-9, 0, -9], max: [9, 12, 9] }, // optional walls
});
for (let i = 0; i < 200; i++) {
world.addParticle({
position: [(Math.random() - 0.5) * 14, 4 + Math.random() * 8, (Math.random() - 0.5) * 14],
velocity: [(Math.random() - 0.5) * 6, 0, (Math.random() - 0.5) * 6],
radius: 0.35,
mass: 1, // 0 = static
});
}
const balls = new InstancedMesh(new SphereGeometry(1, 20, 14), new MeshStandardMaterial(), 200);
const m = new Matrix4();
// per frame, fixed timestep:
world.step(1 / 60);
for (let i = 0; i < world.count; i++) {
const r = world.radiusOf(i);
m.makeScale(r, r, r);
m.setPosition(world.positions[i * 3], world.positions[i * 3 + 1], world.positions[i * 3 + 2]);
balls.setMatrixAt(i, m);
}
balls.instanceMatrix.needsUpdate = true;
Key facts: world.positions/velocities are live flat Float32Arrays (3 per
body); indices from addParticle are stable; use a fixed timestep;
world.count is the live body count. Full working demo: npm run demo in
the repo (220 balls, ?n=8000 for load testing).
Option B: no-code recipe (MCP server)
For agents that should drive a sim without embedding the library, the repo ships an MCP server (stdio):
{
"mcpServers": {
"flit": {
"command": "npx",
"args": ["vite-node", "mcp/server.ts"],
"cwd": "<path-to-Flit-clone>"
}
}
}
Tools: flit_info (meta) · flit_reset (gravity/restitution/ground/bounds) ·
flit_spawn (presets: rain, explosion, grid, fountain — auto-configures
matching world) · flit_add_particles · flit_step (returns flat xyz
positions) · flit_state (positions + radii for InstancedMesh sync).
Typical flow: flit_spawn({preset: "rain", count: 200}) → loop
flit_step({steps: 1}) and copy positions into instance matrices.
Performance envelope (measured, don't guess)
- ~0.93 ms/step at 1,000 bodies; flat O(N) ≈ 1.2 ms per 1,000 at constant density through 8,000 while bodies are scattered.
- Caveat: with gravity, bodies rain into dense floor piles within
2-4s; settled piles are contact-solver dominated (4.3 ms per 1,000 at 8k). Keep games in the sparse-to-medium regime, or pursuedocs/issues/001-settled-pile-performance.md(open, self-contained brief: island sleeping, adaptive iterations, warm starting).
Rules for working ON the engine (contributing agents)
- Measure before and after any perf change (
npm run bench,npm run bench:scaling). Numbers go in the commit message. Never guess where the time goes — instrument first. npm testandnpx tsc --noEmitmust pass before every commit.- Settled by measurement — do not re-litigate without new data: 63-bit BigInt cell keys (13× alloc regression), sorted-array + binary-search broadphase (1.4× slower than Map probing), npm morton codecs (7–142× slower than in-house). Details in commit history.
- Third-party code keeps its notices.
src/distances-flann.tsis FLANN (BSD-2) — the header stays. SeeTHIRD_PARTY_NOTICES.md. Never strip attribution, never add code whose license you can't name. - Small diffs, one concept per commit. The library has no three.js dependency — keep it that way (three is a demo/dev concern only).
File map
src/world.ts— engine core (particles, integration, contact solver)src/spatial-hash.ts— Morton broadphase (ordered-probe visiting)src/morton.ts,src/bit-utils.ts,src/approx-distance.ts,src/distances-flann.ts— utilities (see file headers for provenance)bench/— deterministic benchmarks (broadphase, world step, scaling, lib bake-off)demo/— three.js demo (the integration reference)mcp/server.ts— MCP servertest/— vitest suite