What ships in the kit
Every still below is a frame from the regression rig — the same frames the test suite checks on WebGPU and WebGL2 — so what you see is what the code renders. The demo scenes are one zip-open away (demo/index.html); the example pages live in examples/ and import ../src directly, no build. Each card names the handbook section that explains it.
Scenes in the live demo
Beach — shallow water & breaking surf
A depth field fades the swell as the bed rises; an analytical shore train shoals, breaks at H/h = 0.78 and washes up a wet-sand band. The boat grounds.
shallow.setDepthFunction((x, z) => 0.018 * (z + 150) + 0.4).bake();
new OceanFFT({ ..., shallowWater: shallow });
The break zone
Green's-law shoaling, a steepening face, the foam lip, the bore and the swash — all from one setWave call, in the shader and in the CPU mirror the hulls read.
shallow.setWave({ period: 7, amplitude: 1.15, steepness: 0.85, trainReach: 3.2 });
Harbour — drop-in boats
The trawler is a CC0 glTF through attachHull: measured, seated, buoyant, masked, with bow + stern wake and spray, on a waypoint patrol. The yacht is the anchored kind.
const hull = attachHull(gltf.scene, { ocean, buoyancy, waterMask, wakeField, scene, length: 26, kind: "dynamic" });
hull.setInput({ throttle: 0.55, steer });
Night harbour — lights on the water
Buoy lamps and dock posts glint in the surface shader (six slots, point or spot, lights or descriptors) plus a moon path; Hurricane flashes lightning through the same slots.
ocean.setLights(harbour.getLights(night)); ocean.setMoon(moonDir, 0xa9bfe8, 0.55 * night);
River — flow maps
A current field advects ripple normals and foam streaks and drifts every floater downstream; the banks come from the same depth field as the beach.
flow.setFlowFunction((x, z) => riverFlowAt(x, z)).bake(); buoyancy.setFlow(flow, 1);
Crates — anything touches water
Press F: a crate drops, splashes (ripple ring + spray burst), bobs, rides the current and stamps a wake when it moves.
interaction.add(crate, { radius: 0.8, depth: 0.4 });
interaction.update(dt, ocean.simTime);
Occluded caustics
A light-space depth map of hulls, dock and sea stacks; seabed caustics, prop caustics and the light shafts go dark beneath them.
const occ = createSunOcclusion(renderer, scene, { size: 512, extent: 200 });
setCausticOcclusion(occ); occ.add(boat);
Underwater — schools, shafts, scatter
Boid fish schools that part around the boat, sun in-scatter in the fog, light shafts faded by the water, prop-wash bubbles behind a driven hull.
underwater.update(t, dt, camera, { sun, threat: boat.position, motion: boat.getMotionState() });
Prop wash from below
The hull's motion state drives a bubble plume at the stern; the same state feeds the spray, the wake generators and the water mask.
hull.getMotionState(); // { x, y, z, heading, speed, slam, halfLen, halfWid }
Hurricane — lightning
The storm mode schedules strikes; each one is a light descriptor in slot 5 plus a sky flash, so the sea answers with a glint path like any lamp.
ocean.setLights([...lamps.slice(0, 5), { position: strike, color: 0xd6e4ff, intensity: 9e4 * flash }]);
Example pages in examples/
multi-body.html — several water bodies
Open sea plus a hill lake at +6 m and a rock pool at +9 m, each its own OceanFFT with its own spectrum, colours and bed, sharing one sky.
const lake = new OceanFFT({ geometry: "plane", size: 216, reflector: false, ... });
lake.position.y = 6; sea.setReflectionLayers(camera, [3, 7]);
r3f.html — React Three Fiber
<Tidewater> and <Ocean> from r3f/, run straight off esm.sh with no build; mode buttons, a time slider, crates that ride the exact CPU height mirror.
<Canvas gl={createTidewaterRenderer}><Tidewater mode="sunset"><Ocean /></Tidewater></Canvas>
water-canvas — one HTML tag
The custom element for non-Three.js pages: mode, time, controls, quality, dynamic resolution and the deterministic clock as attributes.
<water-canvas mode="reef" time="14:00" controls="orbit" dynamic-resolution="60"></water-canvas>
multiplayer.html — two clients, one sea
Same seed, same tick, same wave on every client; add latency and jitter, knock one out of step and watch NetClock slew it back.
el.netClock = new NetClock({ ... }); el.syncToTick(host.tick);