Fish schooling simulation running entirely in the browser, in two flavours:
- 2D (
/2d/) — top-down view, vanilla JS + Canvas, sprite fish. - 3D (
/3d/) — glass aquarium with a free orbit/fly camera, Three.js (vendored locally in3d/vendor/, no CDN).
Both are 100% client-side — the server only serves static files; all simulation and tuning happens in the browser in real time.
python3 server.py # http://localhost:8000
python3 server.py 9090 # custom portThen open:
http://localhost:8000/— landing page linking to bothhttp://localhost:8000/2d/— 2D simulationhttp://localhost:8000/3d/— 3D simulation
Any static file server works too (e.g. python3 -m http.server), as long as
it serves the whole directory so /2d/ and /3d/ resolve.
- Drag — orbit around the tank.
- Scroll — zoom in / out.
- W A S D — fly the view horizontally; Q / E — down / up; hold Shift to move faster.
The 3D version runs the same Couzin model extended to three dimensions (wider orientation zone by default, since 3D neighbourhoods are sparser). Fish are simple oriented 3D meshes for now; the shark is a low-poly glTF model. Rocks are procedural: each is built from several hexagonal prisms merged into one vertex-coloured geometry, with 6 deterministic templates to choose from (a new rock picks one at random). Rocks have flat bottoms and sit on the tank floor. Seaweed is procedural too — 4 templates of bent tapered blades, anchored to the floor with a gentle current sway. Rocks, sharks and seaweed are added via buttons since the mouse drives the camera:
- Camera: left-drag orbits, right-drag pans, scroll zooms, WASD/QE fly.
- Click a rock or seaweed to open its transform gizmo (all edits happen through the gizmo — objects can't be dragged directly): drag the arrows to move along an axis (rock Y lifts it off the floor), the rings to rotate, the coloured + handles to resize along one axis, or the gold + to resize uniformly. A floating ✕ at the object's top-right (or the Delete/X key) removes it. Seaweed shows a reduced gizmo (slide on the floor, spin, uniform resize). Sharks are selectable too — a pulsing ring marks the selected shark and the ✕ (which follows it as it swims) or Delete/X removes just that one. The gizmo's layout rescales with the object so handles stay outside the mesh, while lines render at a constant 1px width; the selected object is highlighted.
Seaweed is decorative; rocks are solid obstacles. Collision uses two levels: a fitted bounding sphere for the cheap broad-phase test, then a set of ~8 sub-spheres (k-means-fitted to each template's vertices once at load) for the actual avoidance/contact. The narrow-phase query runs in the rock's template space, so under a non-uniform stretch each sub-sphere acts as a world-space ellipsoid — a 5x-stretched slab collides like a slab, not a row of fat balls — and moving/rotating/resizing adapt instantly without re-fitting. The 🔴 Collision zones button toggles a debug view that renders every no-swim volume (rock sub-spheres and shark bodies) as translucent red.
Implements the zone-based schooling model (Aoki/Couzin) described in Wikipedia: Shoaling and schooling — Modelling school behaviour:
- Zone of repulsion — fish always steer directly away from neighbors that get this close (highest priority).
- Zone of orientation — fish align their heading with neighbors in this band.
- Zone of attraction — fish steer toward neighbors in this band, keeping the school together.
- Field of view — fish are blind in a cone behind them; the blind cone applies to orientation/attraction but not repulsion.
- Heading changes are limited by a max turn rate, plus random noise.
Obstacles, predators, and the tank walls inject high-priority escape vectors on top of the schooling behavior, and hard constraints guarantee fish never leave the box or penetrate an obstacle.
Fish bodies are solid: the repulsion zone applies across all fish types (so different types avoid each other without schooling together), and a position-based separation pass each frame keeps fish bodies from overlapping — same type or not. Sharks are solid too: they keep separation from each other (so multiple sharks don't merge onto one chase route), and neither fish nor other sharks can overlap a shark's body.
- Fish types — tabs at the top of the panel; the "+" tab adds a new type with its own color and its own full set of schooling parameters. Fish only school with their own type (the zone model ignores other types entirely). A type can be removed with the trash button under its sliders.
- All constants are sliders on the left panel and take effect immediately, including per-type fish count.
- Rock mode — click the water to place a rock obstacle (radius set by its slider; the collision shape is a circle). Drag on any rock — in any mode — to resize it: pull outward to grow, inward to shrink. A dashed ring shows the collision radius while resizing.
- Predator mode — click to place a shark that chases the nearest fish; fish flee it. Predators are draggable.
- Seaweed mode — click to plant decorative seaweed (random variant and size); fish swim through it.
- Erase mode — click a rock, shark, or seaweed to remove it one by one.
- Pause/resume, reset fish positions, clear all placed items.
- Persistence — all sliders, fish types, and placed rocks / sharks /
seaweed are saved to
localStorageautomatically and restored on reload. "Reset settings" forgets everything and returns to defaults.
Uses a spatial hash grid for neighbor queries, so ~1000+ fish stay smooth.
- Fish sprites (2D): Kenney — Fish Pack 2.0
(CC0,
2d/assets/LICENSE-kenney-fish-pack.txt) - Shark sprite (2D): "Cartoon Shark 2" on Openclipart
(CC0,
2d/assets/LICENSE-shark.txt), rasterized to PNG - 3D rendering: Three.js r160 (MIT), vendored at
3d/vendor/ - 3D shark model: "Shark" by Poly by Google
(CC-BY 3.0 — attribution required; see
3d/assets/LICENSE-shark-model.txt)
index.html landing page (links to /2d/ and /3d/)
server.py one-port static server with /2d + /3d routing
2d/index.html 2D simulation
2d/assets/ fish/shark/rock/seaweed sprites + licenses
3d/index.html 3D simulation
3d/vendor/ Three.js (local, no CDN)