Guides
Shorelines
Give the water your terrain's height function and it bakes a seabed. Waves then shoal over the shallows, breakers roll up the beach and break by depth, and swash foam runs up the sand. Without one, the sea is deep everywhere.

#Bake a seabed
const water = await WaterSystem.create({
renderer, scene, camera,
bathymetry: {
heightAt: (x, z) => terrainHeight(x, z), // > 0 is land, sea level is 0
bounds: { minX: -350, minZ: -350, maxX: 350, maxZ: 350 }, // outside it the sea is deep
},
});
// or later, and again whenever the terrain changes
water.setBathymetry({ heightAt, bounds, resolution: 512, shoreTrainMinRadius: 40 });
water.setBathymetry(null); // back to open ocean| Field | Default | |
|---|---|---|
heightAt(x, z) | — | Terrain height in metres at world (x, z). Positive is land |
bounds | — | { minX, minZ, maxX, maxZ }, the area to bake |
resolution | the tier's bathymetryResolution | Texels per side: 192 (low) to 512 (ultra) |
shoreTrainMinRadius | 40 | 1.1 Land masses with a smaller equivalent radius (m) get no shore waves. 0 puts them everywhere |
The bake samples heightAt once on a grid and stores, per texel, the seabed height, the exact signed distance to the shoreline and the direction toward it. The CPU keeps the same numbers, so sampleHeight and buoyancy see the shore waves too. It runs on the main thread and takes about 100–300 ms for 384² with a noise-based height function. If you don't pass resolution, a quality change re-bakes at the new tier's size.
#What the seabed does
- Shoaling. Long waves lose height over shallow water.
- Breakers. Shorter waves travel down the distance field toward the shore, grow as the water shallows (Green's law), sharpen toward
shoreSteepnessas the breaking index H / 0.78 d approaches 1, and whiten past it. - Swash. Foam runs up and down real beaches within about 6–18 m of the waterline.
- Contact line. Anything else that pierces the surface, such as a dock, a hull or a wreck, gets a thin foam line from the depth buffer instead of being treated as a beach.
#The shore dials
| Param | Default | Effect |
|---|---|---|
shoreWaves | 1 | Breaker amplitude. 0 turns shore waves off |
shoreFoam | 1 | Breaker, swash and contact-line foam |
shoreSteepness | 0.92 | 1.1 Crest steepness (Q·k·a) the shore trains reach where they break |
water.setParams({ shoreWaves: 1.4, shoreFoam: 1.2 }); // heavier surf
water.setParams({ shoreWaves: 0 }); // a sheltered harbourThe breakers scale with the open sea: their amplitude follows the significant wave height, and their wavelength is 0.4 × wavelength, clamped to 7–30 m. A storm preset gives you a heavy shore break without touching these dials.
#Wide shelves and small islets
Version 1.1 changed how shore waves behave away from the beach, after they drew foam rings over wide shallow shelves and bull's-eye rings round small islands:
- Offshore, the trains keep their own gentle steepness. Only crests past a breaking index of about 0.6 fold into whitecaps.
- Trains only exist where the water is shallower than about 2–4× the breaker depth, and they carry a slow phase wobble so they don't form perfect contour rings.
- On a flat shelf (a baked seabed slope under about 1–4 %) they become broken, depth-limited bores (H < 0.44 d) instead of breaking crests. Beach faces break as before.
- Land masses smaller than
shoreTrainMinRadiusget no trains at all.
examples/lagoon/ is the stress test: a 1–2.5 m deep lagoon out to about 330 m, and a 20 m islet. Add ?terrain=beach for a plain 1:25 beach, ?view=drone|islet|beach|surf for a camera, and ?shoreWaves=, ?shoreFoam= or ?shoreSteepness= to try values.
#Limits
- There's no true wave overturning. Steep crests are clamped, not curled.
- Sea level is fixed at
y = 0, so there's no tide. - Outside
boundsthe sea is deep, so make the bounds cover every shore the camera can see.