Guides
Terrain and materials
The water only looks right when the ground under it has the right shape. carveTerrain() cuts every bed, basin and pool into your own height function, and patchMaterial() makes your rocks and ground wet, tinted and lit by caustics where the water touches them.
#Carve the terrain
carveTerrain(heightFn) takes any (x, z) => height function and returns a new one with every body's profile blended in:
const base = (x, z) => 0.6 + Math.max(0, Math.abs(x) - 5) * 0.2; // your hills, noise, heightmap…
const height = water.carveTerrain(base);
const geo = new THREE.PlaneGeometry(90, 90, 220, 220).rotateX(-Math.PI / 2);
const p = geo.attributes.position;
for (let i = 0; i < p.count; i++) p.setY(i, height(p.getX(i), p.getZ(i)));
geo.computeVertexNormals();Use the returned function for everything that sits on the ground, too: rocks, trees, grass and the camera's floor.
#What each body cuts
| Body | Profile | Tuned by |
|---|---|---|
| River | A parabolic bed down to depth at the centre line, with a small step at the bank (0.25 m, or 20 % of the depth if shallower). Outside the bank the ground rises at bankSlope per metre and steepens further out | depth / depths, bankSlope (0.32), bankWidth (1.2 half widths), endBlend |
| Lake | A basin that reaches depth at shelf metres from the shore, and a bank rising at bankSlope per metre | depth (2.5), shelf, bankSlope (0.3), bankWidth (6 m) |
| Waterfall | A bowl at the landing point | plungeDepth, plungeRadius |
Each profile is blended into your terrain with a weight that falls to zero bankWidth beyond the bank. Where two bodies overlap, the lowest result wins, so a river's end cuts into a plunge pool instead of filling it. Where your base terrain is lower than the water, the bank profile raises it, so water never floats over a hole.
#When to skip it
If your terrain is sculpted by hand or comes from a heightmap that already has the channels, don't carve. Author the rivers so their y sits a little above the bed you already have, and set depth to match. depth only matters for the carve.
#Patch your materials
const rock = new THREE.MeshStandardMaterial({ color: 0x9a9a94, roughness: 0.8 });
water.patchMaterial(rock); // returns the same material
scene.add(new THREE.Mesh(rockGeometry, rock));patchMaterial() injects shader code into a built-in lit material. It reads the system's top-down water map, a render of every surface's level, flow and coverage, so each fragment knows whether it is under water and how deep.
| Effect | Where |
|---|---|
| Wet band | A noisy capillary band above the waterline that laps with the ripples. It is darker, more saturated and glossier, with a tide line |
| Splash zone | Wet ground around each waterfall plunge |
| Underwater tint | A lighter wet tint and a faint algae film on submerged surfaces |
| Sun through water | Direct sunlight is attenuated along its path through the water column, so muddy water darkens the bed |
| Caustics | Depth-focused, with dispersion that grows with depth. They animate faster in fast water and break up in rapids |
| Caustic reflections | Faint moving light on faces just above the surface |

clear preset with water.caustics at 0 and at the preset's 1. The ground and the rocks are ordinary MeshStandardMaterials passed through patchMaterial().#Which materials work
MeshStandardMaterialandMeshPhysicalMaterialget everything, including the glossier wet band.MeshLambertMaterialandMeshPhongMaterialget the colour and caustic parts.InstancedMeshworks: the patch reads the instance matrix. The demo's boulders and pebbles are instanced.- An existing
onBeforeCompileis chained, and the patch runs after your<color_fragment>edits. Patching the same material twice does nothing the second time.
The strength of the wet band follows water.wetness in the preset (the muddy preset raises it to 1.3). The caustics follow water.caustics, and they fade with depth through clarity and absorption.
#Water map precision
The water map covers the bounding box of all bodies plus 2 m, at the tier's waterMap resolution (512² on low, 1024² on medium and high, 2048² on ultra). A river system 200 m long gets texels of about 0.2 m on high. Water spread over several kilometres gets proportionally coarser wet bands and caustic masks. The map is re-rendered only when bodies change.