Millions of boids in the browser. A WebGPU flocking simulation tuned for Chrome/Blink — up to 4 million agents, artifact-free, in ~700 lines of dependency-free JavaScript and WGSL.
Live demo: chrisbenjaminsen.com/labs/blids
Written 100% by AI (Claude, Anthropic), guided by Chris Benjaminsen.
Public domain under The Unlicense — use it for anything, no attribution required.
No build step, no dependencies. Any static file server works:
python3 -m http.server 8471
# open http://localhost:8471 in a WebGPU-capable browser (Chrome/Edge)- boids — population, 65k to 4M
- radius — interaction range (auto-shrinks at high populations to keep the neighbor math exact)
- separation / alignment / cohesion — the classic Reynolds weights
- speed / size / noise — cruise speed, render size, per-boid wander
- constrain to circle — circular arena instead of screen-edge wrap
- 2x (half-rate sim) — simulate at half the display rate with interpolated rendering (roughly doubles the frame budget)
- restart — re-seed the flock; hold the pointer down to scatter boids
While dragging any slider its label shows the live value. The HUD also shows fps, GPU sim time, and a build tag.
Two cleanly separated layers.
Physics — all forces are exact pairwise functions of true Euclidean distance; nothing cell-, grid-, or lattice-shaped ever touches a force:
- Classic Reynolds rules: alignment and cohesion with uniform weight over the
interaction radius, and
(my_pos − their_pos)/d²separation over a 0.35× inner radius — with one amendment: every hard radius cut is replaced by a smooth "skirt" to zero. A step edge at distance r makes the flock condense into r-spaced clusters that read as a grid (verified: the artifact survived every acceleration architecture until the edges were smoothed). - Front-weighted vision — neighbors ahead count ~5× more than behind, like a real bird's field of view. This damps the band/stripe instabilities that symmetric-kernel flocks are prone to.
- World topology: torus (screen-edge wrap, default) or circular arena with a purely radial boundary force. Both verified artifact-free.
- A small per-boid random kick ("noise") keeps aligned flocks from crystallizing into a lattice; slide it to 0 for fully deterministic boids.
Performance:
- A counting-sort spatial grid (count → 3-level prefix scan → scatter) finds neighbors and makes their memory reads cache-coherent. It is used ONLY for lookup — candidates are accepted by true distance — so the grid cannot influence force direction or magnitude. Scales to 1024×1024 cells.
- The interaction radius auto-shrinks with population (the slider is an upper bound), so the neighbor loop stays exact — no sampling — at any count.
- Optional half-rate simulation with per-vertex interpolation: the sort's scatter buffer already holds previous positions at the same indices as the update output, so full-rate smooth motion costs one extra buffer read.
- Rendering is one instanced draw, 3 vertices per boid, vertex-pulled straight from the sim buffer. 4× MSAA (sub-pixel boids otherwise moiré against the pixel raster) with additive blending, so overlap reads as brightness.
- GPU timing via
timestamp-querywhen available.
Persistent "grid" patterns in a flocking sim can come from at least four distinct sources — this project hit all four:
- Grid-sampled force approximations — biased per-cell sampling and interpolated force fields, including a provable lattice force that appears when radial weights at cell centers don't form a partition of unity.
- Rendering — sub-pixel triangles moiré against the pixel raster (fixed by MSAA; additive blending also removes draw-order bias).
- Sharp kernel edges — flocks self-space into clusters at any hard cutoff distance, producing a "grid" scaled by the interaction radius.
- The world itself — periodic rectangular domains stabilize axis-aligned traveling bands (Vicsek banding), and straight walls seed axis-aligned density fronts.
The combination that works: exact pairwise forces, smooth kernels, MSAA, and axis-free boundaries. If you're building one of these and see a grid, check those four in that order — and put a build tag on screen so you never debug against a stale cached script.
#1000000 in the URL presets the count. #…-debug runs a fast-forward on the
first frame and logs a BLIDS_DEBUG line to the console: global alignment,
axis-affinity of headings (0.75 = isotropic, 1.0 = axis-locked), and
integrity counters.