# Water: seas, shores, lakes and rivers

Everything a game on this engine can do with water, and how: scene data, script calls and the editor. A person or an
agent making a game needs sections 1 to 12 and never touches the engine's source. Sections 13 and 14 are for work on
the engine itself. The full field tables are in [API.md](API.md) ("The sea and floating bodies", "Lakes", "Rivers,
waterfalls and hydrology lakes", `ctx.water`) and [VFX.md](VFX.md) ("Water ripples", "Hull masks", "Wake settings");
this guide is the map, with a working example of each piece.

## 1. The pieces

| You want | You write | Where | Physics or picture |
|---|---|---|---|
| a sea | `meta.environment.ocean` | the scene | both: bodies float on the waves you see |
| a shoreline | nothing (it is automatic); tune with `ocean.shore` | the scene | picture; physics with a `bathymetry` |
| a lake or pond | a `water` component on an entity | an entity | both |
| rivers, waterfalls, terrain lakes | a `waterways` component beside a terrain | an entity | both |
| something that floats | a `buoyancy` component on a dynamic body | an entity | physics |
| a boat's wake | a `waterWake` component, `ctx.entity.setWaterWake` | an entity, a script | picture |
| a dry deck in a rough sea | a `hullMask` component | an entity | picture |
| rings where things touch the water | `meta.environment.ripples`, `ctx.water.poke` | the scene, a script | picture |
| to read or change the water in play | `ctx.water.sample`, `ctx.water.setSea` | a script | both |
| to trade quality for speed | `render.quality.overrides.water` | the project | picture |

Three rules hold everywhere:

- **Physics is the same in every renderer.** The waves bodies float on are computed on the CPU, so they work in a
  standard-renderer project and on a headless server. The detailed picture (waves you can see, foam, the shoreline,
  spray) is the HD renderer's (`render.profile: "hd"`); the standard renderer draws flat tinted water.
- **Same seed, same time, same water.** A replay reproduces it and every player in a session sees the same crest.
- **"Render only" fields never change a replay.** Every `look` block, wakes, hull masks and ripples are picture only.

An agent changes any of this with `scene.patch` (the sea at `/meta/environment/ocean`, components on their
entities); a person uses the editor (section 9). To check a scene, run the validator (`argus validate <scene file>`;
an agent's scene edits are validated as they are applied): every range below is enforced and a refusal names the
field.

## 2. A sea in five lines

A scene has at most one sea. It is unbounded, lies at `seaLevel`, and exists when the block does.

```json
"meta": { "environment": { "ambient": 0.4,
  "ocean": {
    "seaLevel": 0,
    "seed": 7,
    "layers": [
      { "kind": "wind",  "windSpeedMps": 13, "fetchKm": 200 },
      { "kind": "swell", "heightM": 2.0, "periodS": 11 }
    ]
  }
} }
```

- **You describe the weather, the engine makes the waves.** A `wind` layer is a wind speed over a fetch of open
  water; a `swell` layer is a height and a period arriving from somewhere else. Up to 6 layers, summed, each with its
  own `directionRad`. With no layers, `beaufort` (0 to 12, default 4) says it in one number.
- **A standard-renderer project** with an ocean gets the warning `render-ocean-without-hd`: bodies still float, the
  sea is drawn as a flat plane.

### What you can set

| Field | What it does |
|---|---|
| `seaLevel`, `seed` | height of the calm surface (default 0); the wave pattern (default 1) |
| `beaufort` | the sea state when there are no `layers` and no `weather`, 0..12 (default 4) |
| `layers[]` | `wind`: `windSpeedMps` 0..60, `fetchKm` 1..2000. `swell`: `heightM` 0..40 and a required `periodS` 2..30. Both: `directionRad`, `spread` |
| `weather[]` | up to 32 sea states on simulation time (`timeS` ascending), each a `beaufort` or `layers`, blended smoothly |
| `choppiness` | crest sharpness, 0..1.5 (default 1) |
| `depthM` | one depth for the whole sea, 0.5..11000; left out, the sea is deep |
| `density` | the water's density, 900..1300 kg/m3 (default 1025; fresh water is 1000): it floats every body |
| `look.waterType` | the water's own colour: `oceanic-i` (clearest blue), `oceanic-ib` (tropical teal, the default), `oceanic-ii`, `oceanic-iii`, `coastal` (green to brown) |
| `look.foam`, `look.spray` | scale on whitecaps and on the spray off breaking crests, 0..2 (default 1) |
| `look.wakeFoamGain`, `look.wakeFoamBreakup`, `look.wakeFoamBreakupSize` | how white wakes are on this sea (0..4, default 1.2), and how far their foam breaks into clumps (0..1, default 0) of about this size in metres (default 4) |
| `look.flatColor` | standard renderer only: the flat sea's hex colour exactly as it shows, unlit (a stylised sea) |
| `shore` | the shoreline's controls (section 3) |
| `bathymetry` | a 16-bit height image of the ground under the sea, for the physics as well as the picture (section 3) |
| `waveMask` | wave paint: a greyscale image that scales the waves over a rectangle (a sheltered harbour is a dark patch); the physics reads it too |

## 3. The shoreline

Put an ocean beside ground that reaches the sea (a terrain, or static meshes with a full collider) and the HD
picture has a shore: waves shoal and bend toward the beach, break, run up the sand as a sheet of water and foam,
leave the sand wet, and throw white water and spray at rocks. The engine reads the sea floor from the scene's own
ground when the sea is first drawn. Nothing is written to the scene and nothing needs setting up.

| `ocean.shore` field | What it does | Default |
|---|---|---|
| `runup` | how far waves run up the beach, 0..2; 0 turns the run-up off | 1 |
| `foam` | scale on the surf's and the swash's foam, 0..2 | 1 |
| `wetSandSeconds` | how long sand stays dark after the water leaves it, 1..600 | 30 |
| `wetSandStrength` | how dark wet sand is, 0..1 | 0.4 |
| `caustics` | light patterns on the bed of the shallows, 0..2 | 1 |
| `shoaling` | physics too: how much waves grow in shallow water, 0..2. Needs a `bathymetry` | 1 |
| `breakerIndex` | physics too: a wave breaks at this share of the depth, 0.4..1.2. Needs a `bathymetry` | 0.78 |

- **Picture only, unless you give a depth map.** The physics sea stays deep: a boat near that beach floats on
  open-water waves. Give the scene a `bathymetry` (`{ asset, originXz, sizeM, baseM, stepM }`, a 16-bit greyscale
  PNG) when bodies must feel the shallows; an explicit `bathymetry` also wins over the automatic one.
- **What counts as ground:** the terrain and ordinary static meshes with `colliderMode: "full"`. Instanced meshes
  are not read (a known gap).

## 4. Lakes and ponds

A `water` component makes its entity a bounded body of water. The entity's transform is the lake: its position is
the centre and its **height is the water level**, its yaw turns the outline, and its x and z scale stretch `size`.

```json
{ "id": "pond", "name": "Pond", "tags": [],
  "transform": { "position": [40, 6, -30], "rotation": [0, 0, 0, 1], "scale": [1, 1, 1] },
  "components": [ { "type": "water", "size": [60, 40], "shape": "ellipse", "windMps": 4,
                    "look": { "waterType": "lowland" } } ] }
```

| `water` field | What it does |
|---|---|
| `size` | full extent `[x, z]` in metres, before the entity's scale (required) |
| `shape` | `"box"` (default) or `"ellipse"` inside that box |
| `windMps`, `windDirectionRad`, `seed` | wind over the lake, 0..30 m/s: 0 (default) is flat water; above it the lake has small waves, bigger on a bigger lake |
| `priority` | breaks a tie between overlapping bodies at the same level, -100..100 |
| `look` | `waterType`: the sea's five or a fresh-water class (`alpine-clear`, the default; `alpine-glacial`, `lowland`, `tannin`); `foam` 0..2; `flatColor` for the standard renderer |

Where bodies of water overlap, the highest level wins, so a lake on land always shows and a lake below the sea is
swallowed by it. Bodies with `buoyancy` float on a lake whether or not the scene has a sea. At most 4 lakes are
drawn at once (the highest).

## 5. Rivers, waterfalls and terrain lakes

A terrain whose file carves drainage (a `rivers.extract` or `lake.fill` step in its `.terrain.json`,
[TERRAIN.md](TERRAIN.md)) knows where its rivers, waterfalls and lakes are. A `waterways` component switches them
on: drawn, and floated on, with bodies drifting downstream.

```json
{ "id": "valley", "name": "Valley", "tags": [],
  "transform": { "position": [0, 0, 0], "rotation": [0, 0, 0, 1], "scale": [1, 1, 1] },
  "components": [
    { "type": "terrain", "source": "valley.terrain.json" },
    { "type": "waterways", "look": { "waterType": "alpine-clear" }, "flowScale": 1,
      "overrides": [ { "river": 2, "flowScale": 0.5 }, { "waterfall": 0, "hidden": true } ] } ] }
```

| `waterways` field | What it does |
|---|---|
| `terrain` | the terrain entity; left out, this entity or the scene's only terrain |
| `rivers`, `waterfalls`, `lakes` | each on by default |
| `look` | `waterType` and `foam`, for all of them |
| `roughness`, `flowScale` | physics too: the beds' roughness (0.02..0.15, default 0.045) and a speed multiplier (0..4; 0 is still water) |
| `foam`, `mist` | scale on foam and on waterfall mist, 0..2 |
| `overrides[]` | up to 64, each naming one `river`, `waterfall` or `lake` by the terrain's id, with its own `look`, `flowScale` or `hidden` |

## 6. Things that float, boats, wakes and dry decks

Add `buoyancy` to a dynamic body and it floats, rolls with the waves and is carried by them, on the sea, a lake or
a river:

```json
{ "id": "raft", "name": "Raft", "tags": [],
  "transform": { "position": [0, 1, 0], "rotation": [0, 0, 0, 1], "scale": [1, 1, 1] },
  "components": [
    { "type": "collider", "shape": "box", "size": [3, 0.5, 4] },
    { "type": "rigidbody", "kind": "dynamic", "mass": 900 },
    { "type": "buoyancy", "resolution": 4 }
] }
```

| `buoyancy` field | What it does | Default |
|---|---|---|
| `resolution` | hull cells per axis cut from the collider, 1..8: more cells, a truer roll | 3 |
| `volumeScale` | the share of the collider's volume that displaces water; under 1 is a hollow hull | 1 |
| `linearDrag`, `quadraticDrag` | drag against the water, by speed and by speed squared | 500, 500 |
| `angularDrag` | damping of the body's spin | 0 |
| `waterVelocity` | carried by the waves and by a river's flow | true |
| `probes[]` | hand-placed hull cells instead of `resolution` (API.md) | none |

Whether it floats is its mass against the water it displaces. It needs a dynamic `rigidbody`
(`buoyancy-without-body`) and something to float in (`buoyancy-without-ocean`).

- **Boats you drive** are vehicles with a hull and thrusters: [VEHICLES.md](VEHICLES.md). They float by the same
  `buoyancy`.
- **A wake.** A floating hull leaves one in the HD picture with nothing set. A `waterWake` component shapes it:
  `strength` (0..1), `armFoam`, `trailFoam`, `fillFoam` (0..4), `trailWidth`, `trailGrowth`, `trailMaxWidth`,
  `armLife`, `trailLife`, `fillLife` (seconds), and `solve` (a simulated wake for one hull, HD). It also gives a
  wake to a boat with no `buoyancy` that a script moves. `ctx.entity.setWaterWake({ thrust, planing, immersion,
  thrustAt, bow })` tells the wake what the script knows.
- **A dry deck.** A rigid hull does not bend with the waves, so in a rough sea a crest can show through the deck.
  A `hullMask` component holds the drawn water down inside the hull's footprint: `halfLength` and `halfWidth`
  (required, metres), `center`, `bowLength` and `bowWidth` (a pointed bow), `capHeight`, `edgeSoftness`,
  `skirtWidth` and `skirtCap`.

```json
{ "type": "waterWake", "trailFoam": 1.5, "trailLife": 8 },
{ "type": "hullMask", "halfLength": 3.4, "halfWidth": 1.4, "bowLength": 1.8, "capHeight": 0.15 }
```

## 7. From a script, while the game runs

```ts
update(ctx) {
  const p = ctx.entity.worldPosition
  const w = ctx.water.sample(p[0], p[2])          // { height, normal, velocity, kind } or null
  if (w && p[1] < w.height) ctx.body?.addForce([0, 50, 0])
}
```

| Call | What it does |
|---|---|
| `ctx.water.sample(x, z)` | the water at a point: `height`, `normal`, `velocity` and `kind` (`"sea"`, `"lake"` or `"river"`); null where there is none |
| `ctx.water.setSea({ beaufort: 8, blendS: 30 })` | change the running sea, blended over `blendS` seconds (default 5); also takes `{ layers: [...] }`. Bodies and the picture change together and a replay reproduces it. False when the scene has no sea |
| `ctx.water.poke(x, z, height?, radius?)` | drop a ripple (picture only; the scene needs `meta.environment.ripples`) |
| `ctx.entity.setWaterWake(feed)` | what a script knows about its hull's wake (picture only); null forgets it |
| `ctx.vehicle?.water` | a boat's water handling on the last tick: speed through the water, whether a thruster is in it, the slam of a landing |

Ripples are a scene block, `"ripples": {}` under `meta.environment` (HD): rings run out where a floating body, a
splash, an effect or `poke` touches the water. `cells` (64..512), `cellM` (0.1..1), `speedMps` and `damping` tune
the patch around the camera.

## 8. Recipes for a game

Each is the whole change: a block of scene data or a few script lines.

**A calm tropical lagoon.** Low wind, the clear teal water, little foam:

```json
"ocean": { "beaufort": 2, "look": { "waterType": "oceanic-ib", "foam": 0.6 } }
```

**A cold, murky coast with heavy surf.**

```json
"ocean": { "layers": [ { "kind": "wind", "windSpeedMps": 11 }, { "kind": "swell", "heightM": 2.5, "periodS": 12 } ],
           "look": { "waterType": "coastal", "foam": 1.3 },
           "shore": { "foam": 1.5, "runup": 1.3, "wetSandSeconds": 60 } }
```

**A storm that builds during the level.** Either as data, on the level's own clock:

```json
"ocean": { "seed": 3, "weather": [ { "timeS": 0, "beaufort": 3 }, { "timeS": 240, "beaufort": 9 } ] }
```

or from a script, when something happens in the game:

```ts
ctx.water.setSea({ beaufort: 9, blendS: 120 })
```

**A sheltered harbour in a rough sea.** Paint a greyscale PNG (128 is "as it is", darker is calmer), declare it as
a texture asset, and lay it over the harbour:

```json
"assets": { "harbour-mask": { "kind": "texture", "path": "assets/harbour-mask.png" } },
"meta": { "environment": { "ocean": { "beaufort": 7,
  "waveMask": { "asset": "harbour-mask", "originXz": [-100, -100], "sizeM": [200, 200], "minScale": 0.1 } } } }
```

**A pond in a garden.** The entity in section 4 with `"size": [8, 6]`, `"shape": "ellipse"` and no wind.

**A river to drift down.** A terrain with rivers and the `waterways` component (section 5); give the raft
`buoyancy` and it goes with the flow. `flowScale` speeds the river up or slows it.

**A boat that floats, leaves a wake and stays dry inside.** The raft of section 6 with the `waterWake` and
`hullMask` components beside its `buoyancy`. For one you drive, start from a vehicle ([VEHICLES.md](VEHICLES.md)).

**Rings where something lands.** Add `"ripples": {}` to `meta.environment`; floating bodies and splashes make rings
by themselves, and a script adds its own:

```ts
ctx.water.poke(x, z, 0.4, 1.0)
```

**Is this thing in the water, and which water?**

```ts
const w = ctx.water.sample(p[0], p[2])
const swimming = w !== null && p[1] < w.height      // w.kind says "sea", "lake" or "river"
```

**A stylised flat sea for the standard renderer.** One colour, exactly as written, with no sky mirrored in it:

```json
"ocean": { "beaufort": 3, "look": { "flatColor": "#2A7FB8" } }
```

## 9. In the editor

Everything above can be made and changed without writing JSON.

**Place water in the level (the Create menu, or "New in the scene" in Details):**

- **Create ▸ Ocean** gives the scene its sea (sea level 0, Beaufort 4) and brings World Settings to the front, where
  its rows are. A scene holds one.
- **Create ▸ Lake** puts a round 24 m lake where you are looking, selected, its surface just above the ground there.

**Edit a lake in the viewport.** A selected lake shows its waterline as an orange outline, and a click on its water
selects it. It is an ordinary entity for the gizmo: move it (its height is the water level), turn it, scale it to
resize it. Its Shape, Size, Wind, Seed and its look (Water, Foam) are rows in Details. Focus frames it.

**Edit the sea in World Settings ▸ Environment.** The viewport follows each change:

- **Ocean**: the tick box (off removes the sea), Sea level, Beaufort, Wave layers and Weather (as JSON lists, in
  the shapes above), Seed, Choppiness, Depth, Density, Wave paint.
- **Ocean look**: Water (the five water types), Foam, Spray, the three wake foam rows, Flat colour.
- **Shoreline**: Run-up, Surf foam, Wet sand seconds and strength, Caustics, and the two physics controls with the
  Depth map they need.
- **Water ripples**: the tick box and its four numbers.

Every row shows the engine's default until the scene writes its own, has a reset arrow, and has a hover line saying
what it does.

**Add component**, on a selected entity:

| Row | Component | Offered when |
|---|---|---|
| Floats on water | `buoyancy` | the entity has a dynamic Rigid body and the scene has water (or air) |
| Lake or pond | `water` | always |
| Rivers and waterfalls | `waterways` | the scene has a terrain |
| Wake on water | `waterWake` | the scene has water |
| Keeps water out | `hullMask` | the scene has water |

A row that cannot be added is greyed out with the reason. Once added, each component's fields are rows in Details.
A wake's `solve` block, a river's per-id `overrides` and hand-placed `probes` are edited in the component's raw
view.

An agent names the same fields by pointer (`/meta/environment/ocean/look/foam`).

## 10. Quality and cost

The quality preset (`render.quality.preset`) sets how finely the sea is drawn; these knobs under
`render.quality.overrides.water` change single things:

| Knob | Meaning |
|---|---|
| `water.spray` | spray off breaking crests on or off |
| `water.mistPercent` | mist particles (the surf's and a waterfall's), 0..100 |
| `water.lakeWaves` | lakes draw their own waves |
| `water.riverLodBias` | coarser river ribbons |
| `water.rtReflections` | ray-traced reflections in the water (needs ray tracing) |

Measured GPU time of the water's own passes (RTX 3080, the engine's water bench, 10 Nov 2026; the shoreline row 11 Nov), ms:

| View | 1080p Low | 1080p High | 1440p Low | 1440p High |
|---|---|---|---|---|
| Open ocean, Beaufort 2 | 0.16 | 0.43 | 0.20 | 0.59 |
| Open ocean, Beaufort 6 | 0.17 | 0.50 | 0.23 | 0.70 |
| Open ocean, Beaufort 10 | 0.19 | 0.57 | 0.26 | 0.81 |
| A coast in view, automatic shoreline | 0.23 | 0.61 | 0.31 | 0.77 |
| Surf filling the view, over a `bathymetry` | 0.37 | 0.82 | 0.52 | 1.10 |
| No ocean in the scene | 0 | 0 | 0 | 0 |

A scene without an ocean runs no water pass and allocates nothing for one.

**Resolution matters to how the sea looks.** The High and Ultra presets render at about two thirds of the output size
and upscale (`upscale.mode: "quality"`). Foam and wave detail are visibly softer that way; for stills and film set
`upscale.mode: "native-aa"`.

## 11. What the validator says

| Code | Meaning |
|---|---|
| `ocean-weather-not-ascending`, `ocean-swell-without-period`, `ocean-layer-field-mismatch`, `ocean-wind-out-of-range` | a sea's layers or weather keys are malformed |
| `ocean-bathymetry-asset-missing`, `ocean-bathymetry-not-16bit`, `ocean-bathymetry-range-invalid` | the depth map |
| `ocean-wave-mask-asset-missing`, `ocean-wave-mask-not-greyscale`, `ocean-wave-mask-range-invalid` | the wave paint |
| `ocean-shore-without-bathymetry` | a warning: `shore.shoaling` or `shore.breakerIndex` set with no `bathymetry` (the picture's controls never raise it) |
| `buoyancy-without-ocean`, `buoyancy-without-body` | a floating body with nothing to float in, or no dynamic body |
| `water-shape-invalid`, `water-waves-invalid` | a lake's `size` or wind |
| `waterways-without-terrain`, `waterways-look-invalid`, `waterways-override-invalid` | rivers with no terrain, a number out of range, a bad override |
| `water-wake-out-of-range`, `hull-mask-out-of-range`, `ripples-out-of-range` | a value outside its range (clamped, with a warning) |
| `render-ocean-without-hd`, `render-waterways-without-hd` | warnings: the standard renderer draws this water flat |

The agent's `explain.rule` says what each means and how to fix it.

## 12. Limits

- The standard renderer draws the sea and lakes as flat tinted water (a lake to its outline, box or ellipse) and
  rivers as flat ribbons; waves, foam, the shoreline, wakes' shapes and ripples are the HD renderer's.
- The automatic shoreline is picture only; the physics needs a `bathymetry`.
- The sea floor is read once, when the sea is first drawn; ground that changes later is not re-read.
- The surf's wave train follows the scene's first sea state; `ctx.water.setSea` changes the open sea at once but the
  surf only at the next load.
- Breaking waves lean and are thrown forward but do not overturn.
- One sea per scene, at one level. At most 4 lakes are drawn at once.
- In the editor the sea is set by its rows, not by a handle in the viewport; a lake is the only water with a gizmo.

## 13. For engine work: how it is built

Nothing in sections 1 to 12 needs this. It is for changing the engine itself.

Two halves that share one description of the sea (`argus_ocean::OceanParams`):

```
scene JSON  meta.environment.ocean
   |  crates/app/src/water.rs   ocean_params_from_scene, hd_ocean_settings, World::sync_hd_ocean
   v
argus_ocean (crates/ocean)             the sea itself, on the CPU, deterministic
   spectrum.rs   wind sea (JONSWAP) + swell -> a directional spectrum and its random amplitudes
   field.rs      the same waves band-limited for gameplay: height and velocity at a point
   buoyancy.rs   forces on a body from that field
   shore.rs      ground under the sea: shoaling, the breaker cap, beach profiles, run-up
   |
   v
hd::water (crates/render/src/hd/water)  the picture, on the GPU, every frame
   cascades.rs + cascades.wgsl   the spectrum evolved to now and transformed (FFT) at 3 or 4 scales:
                                 displacement, slopes and a foam energy that is born on breaking crests and fades
   surface.rs  + surface.wgsl    a camera-centred mesh displaced by the cascades and shaded in one pass:
                                 reflection, refraction, the water's colour, foam, the shore's surf and lather
   bathymetry.rs, surf.rs        the sea floor as a raster, and the swell's phase over it (crest lines that bend)
   wet.rs      + wet.wgsl        the swash on the ground the scene draws: a sheet of water, foam and wet sand
   spray.rs    + spray.wgsl      spray off whitecaps and mist off the surf (a particle pool on the GPU)
   wake.rs, wake_solve.rs, ripples.rs   wakes and ripples (picture only)
   river.rs, falls.rs, edge.rs          rivers, waterfalls, wet banks
```

- **The automatic shoreline's ground** is baked in `crates/app/src/shore_ground.rs` (terrain + static mesh colliders
  -> one height raster over the band near sea level) and handed to the renderer with
  `ShoreSettings::ground_drawn = true`.
- **Shader variants.** `surface.wgsl` is compiled several ways. Lines between `// SHORE-ONLY-BEGIN` / `-END`,
  `LAKE-ONLY`, `WAKE-ONLY` and `UNDER-ONLY` markers are removed from the variants that do not need them, so the
  plain open sea pays for none of the shore. A variable used outside a marked block must be declared outside it.
- **The open sea is protected.** A change to the shore must leave the open-sea pictures pixel-identical (see the
  check in section 14).

More depth: [status/hd-water.md](status/hd-water.md) (the design and its history),
[status/hd-shore-default.md](status/hd-shore-default.md) (the automatic shoreline),
[status/hd-water-perf.md](status/hd-water-perf.md) (where the milliseconds go).

## 14. For engine work: changing the ocean system

Tools, all headless:

```sh
# The look tool: stills of fixed scenes on any GPU, or on the CPU renderer (about a minute a shot).
cargo build -p argus-render --features hd --bin argus-hd-water-look
argus-hd-water-look --set base  --out out/            # eight open-sea shots: calm, moderate, whitecaps, storm...
argus-hd-water-look --set beach --ground drawn --only beach-down,beach-swash --out out/   # the automatic shoreline

# The bench: GPU time of every water pass, at Low and High, in a dozen fixed views.
cargo build --release -p argus-render --features hd --bin argus-hd-water-bench
argus-hd-water-bench --display 1920x1080 --out bench/           # ARGUS_WATER_BENCH_DRAWN=1: the shore view with
                                                                # the automatic shoreline

# The tests.
cargo test -p argus-render --features hd --test suite -- hd_water
cargo test -p argus --features hd --lib -- shore_ground water::tests
cargo test -p argus-ocean
```

Recipes:

- **A new scene control, end to end.** Add the field to `OceanLook` or `OceanShore` in
  `packages/contract/src/schema.ts` and regenerate (`pnpm --filter @argus/contract gen`); read it in
  `crates/app/src/water.rs` (`hd_ocean_settings` or `hd_shore_settings`); carry it in `OceanSettings` /
  `ShoreSettings`; put it in the surface's uniform block (`surface.rs`, the struct mirrored at the top of
  `surface.wgsl`); use it in the shader; add its row to the editor (`crates/editor/src/world_settings.rs`, `FIELDS`
  and `engine_default`), its line to API.md, and a changelog entry.
- **The foam's look.** Open-sea whitecaps: the foam energy in `cascades.wgsl` and the lace cut in `surface.wgsl`
  (search `lace_dens`). The surf's foam: `foam_lace` in `surface.wgsl`; the sheet on the sand: `film_train` and
  `film_foam_lace` in `wet.wgsl`. Keep the two laces the same function.
- **A breaking wave's shape.** `surf_wave` in `surface.wgsl` (height, lean, the forward throw, the roller's foam).
  The vertex and fragment stages both call it; the slope it returns must be the slope of the shape it returns.
- **The run up the beach.** `film_train` in `wet.wgsl`: one wave's swash as a body of water thrown up a slope.
- **Spray.** `spray.wgsl`: `main` (whitecaps) and `surf_main` (the surf, bursts on rocks).
- **A new kind of wave.** `crates/ocean/src/spectrum.rs`: a sea is a list of `WaveLayer`s, each a `LayerSpectrum`
  (JONSWAP, Pierson-Moskowitz or swell today), a size, a direction and a spread. Add a variant to `LayerSpectrum`
  and its shape to `LayerModel` (`omega_spectrum`, `peak`). The physics sea and the picture both read the modes the
  spectrum produces, so a new kind floats bodies and is drawn with no change to the renderer. The scene's spelling
  is `OceanWaveLayer` and `OceanLayerKind` in the contract's schema (`packages/contract/src/schema.ts`), read by `crates/app/src/water.rs`. The golden hashes in
  `crates/ocean/src/lib.rs` pin the existing seas: they must not move.
- **A new water colour.** `WaterType` in `crates/render/src/hd/water/surface.rs` (absorption and scattering per
  colour channel), its name in `ocean.look.waterType` (schema), and the editor's list (`WATER_TYPE` in
  `world_settings.rs`; a test there fails until the two lists agree).
- **A different surface altogether** (a stylised sea, a custom shader): keep `crates/ocean` as it is, so bodies
  still float on real waves, and change the look in `surface.wgsl`. `WaterTerms` and `water_look`, the function that composes them,
  are the one place the final colour is made; everything above that point computes terms (the normal, Fresnel,
  the reflection, foam cover, depth) that a new look can reuse.

Rules that save a day:

1. **Look at one term at a time.** When a picture is wrong, return one factor as the colour in a throwaway build
   before changing anything. Two faults in this system were each misdiagnosed twice by reasoning and found in one
   debug render.
2. **Compile both variants** before a long build: a `--set beach --ground drawn` shot and a `--set base` shot.
   WGSL reserves ordinary words (`set`, `throw`, `from`), and a local named like a uniform (`wet`) shadows it.
3. **Check the open sea did not move**: render `--set base` on `main` and on your branch and compare the files.
4. **Judge at native resolution and in motion.** Upscaling hides hard edges; a still hides timing.
5. **Measure cost with the bench**, not with frame timings from a recording run (they read many times too high).

