sunset / reef · updated 4 Oct 2026, evening

How every part of the reef scene is drawn

One frame, in the order the GPU draws it, then a lookup from anything you can see on screen to the pass and file that makes it, then the data computed offline that those passes read.

Per-frame GPU pass Stage inside a pass (shader code) Computed offline, loaded as data

One frame, in draw order

Everything is drawn every frame except the clouds and the sky reflection cube, which are re-baked every 4 s because they move slowly. All of it runs in renderFrame() in js/main.js.

pass

Caustic map

A grid of 800 × 800 sun rays is refracted through the exact water surface and dropped onto the seabed. Where rays bunch up the map gets brighter. It covers a 30 m square ahead of the camera and is refreshed every other frame while you walk around.

js/caustics.js → 1536² texture
pass

Clouds and sky cube

Volumetric cumulus are ray-marched into a sky-dome texture, and the sky cube used for water reflections is re-rendered. Both happen only every 4 s.

js/clouds.js · js/sky.js
pass

Sun shadow maps

Two depth maps from the sun: a wide one covering 52 m at 4096² (1.3 cm per pixel) and a near patch covering 12 m at 2048² (0.6 cm per pixel) centred where you're looking.

three.js shadow maps · placeShadow()
pass

Opaque scene

  • Sky dome
  • Terrain: sand, lagoon floor and reef, with caustics, wet sand and shadows
  • Hero palms with wind
  • Background palms and bushes (instanced)
  • Beach wrack
js/terrain.js · js/palm.js · js/vegetation.js · js/wrack.js
pass

Wave march, half resolution

The costliest part of the frame. The dense band of the water mesh is drawn at half resolution with a shader that only runs the march through the curl. The full-resolution water then reads it back, matching by depth so the lip's silhouette stays sharp.

js/water.js (MARCH_PASS) · main.js
pass

Water: one mesh plus a far-ocean ring

The water is drawn over a copy of the opaque image, with its own depth test, so it can refract what's behind it.

Vertex: where the surface issurf() adds up the reef breaker (from the wave atlas), lumps on its steep face, the shore break, swash, wind chop and the bent swell. A dense band of rows rides each curling crest (bandRowN).
Fragment: surface lookReflection (sky cube plus screen-space), refraction, water colour from the path-traced lookup table, per-pixel ripples, chop slope on sparse rows, surface foam.
Fragment: inside the curlUpsamples the half-resolution march: marchWave() follows the refracted ray through the baked breaker volume, giving the glassy tube, the lit seabed seen through the wave, the palm shadow through the water and bubbles. It marches at full resolution only at edges where the half-resolution result doesn't fit.
js/water.js · js/breaker.js · js/bathy.js
pass

Whitewater volume

At half resolution, rays are marched through a foam density field: the baked splash and bore volume, swirling churn noise, and the foam trail left behind. Each ray stops at the water or the scene.

js/whitewater.js · js/phase.js
pass

Post

Eye adaptation, then local tone mapping in log space, which compresses the large-scale lighting and keeps detail. The result goes to the screen.

js/post.js

What you see, and where it comes from

The same thing on screen is often made by two passes. The curl's shape comes from the water mesh, while its glassy inside comes from the march in the water fragment shader.

On screenMade byData it reads
Shape of the breaking wave and lipwater vertex · surf()dense band of rows on the curlbreaker atlasreef timing
Glassy inside of the tube, seabed seen through itwater fragment · marchWave()breaker SDF volumecaustic mapshadow maps
Splash where the lip lands, whitewater pile, borewhitewater passwhitewater volumephase functions
Foam trail left behind the wavewhitewater passtrail term (fades over ~7 s)break timing only
White streaks on the face and surface foamwater fragmentatlas foam channelbreaker atlas
Bubbles inside the wavewater fragmentbubble density in the marchchop-seeded streaks
Swell lines, bending round the islandwater vertexswellField(); sharpens in the shallowsbent swell field
Surf on the island's other beachesfar-ocean fragmentswellBreak(): where the swell passes breaking heightbent swell field
Lumps and boils on the wave facewater vertexface displacement in surf()Options → Face lumps
Wind chop and fine rippleswater vertexchop; water fragmentripplesocean slopesnoise textures
Shimmering light on the sand underwatercaustic mapread by terrain and the marchlive water surface
Palm shadow on sand and through the watershadow mapsnear patch first, wide map outside it—
Water colour with depthwater fragmentwater colour table
Clouds and skyclouds and sky cube—
Small waves at the beach and swashwater vertexshore break: a scaled copy of the atlasbreaker atlas

Where the wave's shape and timing come from

Nothing about the break is hand-timed any more. Each column of the reef asks the same chain of questions.

→
→
→

Computed offline

These are baked by scripts in tools/ and loaded from data/. Changing the seabed means re-running the swell and timing bakes. Changing the wave shape means re-running the atlas, then the SDF and whitewater bakes.

Breaker atlas

The 2D wave profile against surface label and time, with vertices crowded into the lip tip. Also stores the surface tangent and 2D lighting.

bake_breaker.py → breaker.bin, _tan, _light

Breaker SDF volume

Distance to the water surface around the crest, through the break. This is what marchWave() steps through.

bake_sdf.py → breaker_sdf.bin

Whitewater volume

Dense foam, mist and bubble density through the collapse, with the splash thrown forward from the landing lip.

bake_ww.py → breaker_ww.bin

Bent swell field

Steady complex wave amplitude over 3.6 × 3.2 km at 8 m cells, from a GPU wave solver run over the real depths.

bake_swell.py → swell.bin

Reef timing

Crest arrival time every 2 m along the reef, smoothed, with the peel capped at 14 m/s.

bake_reef_timing.py → reef_timing.json

Water colour table

Monte Carlo path tracing of clear tropical water over carbonate sand. It replaces every hand-tuned water colour.

pt_water.py → water_lut.bin

Scattering phase functions

How light scatters off a single bubble and a single droplet, from exact geometric optics.

scatter_profile.py → phase_*.json

Ocean slopes, sand textures

A looping FFT ocean for fine ripples, and sand albedo and detail maps.

bake_ocean.py · bake_sand*.py

Comparison renders

Press R for the traced reference, or Scene → Ray-traced clip to bake a few seconds of your current view both ways and compare them with a split slider. The tracer follows the same surface, seabed and caustic map as the live view. Palms, dry land and sky come from the live image, and the whitewater is drawn over the top.

Removed in the clean-up

Still in the git history (tag reef-baseline), no longer in the live code.