Talos docs

Water: seas, shores, lakes and rivers

Since next

Everything a game on this engine can do with water, and how: scene data, script calls and the editor.

Plain text

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 ("The sea and floating bodies", "Lakes", "Rivers, waterfalls and hydrology lakes", ctx.water) and VFX.md ("Water ripples", "Hull masks", "Wake settings"); this guide is the map, with a working example of each piece.

1. The pieces

You wantYou writeWherePhysics or picture
a seameta.environment.oceanthe sceneboth: bodies float on the waves you see
a shorelinenothing (it is automatic); tune with ocean.shorethe scenepicture; physics with a bathymetry
a lake or ponda water component on an entityan entityboth
rivers, waterfalls, terrain lakesa waterways component beside a terrainan entityboth
something that floatsa buoyancy component on a dynamic bodyan entityphysics
a boat's wakea waterWake component, ctx.entity.setWaterWakean entity, a scriptpicture
a dry deck in a rough seaa hullMask componentan entitypicture
rings where things touch the watermeta.environment.ripples, ctx.water.pokethe scene, a scriptpicture
to read or change the water in playctx.water.sample, ctx.water.setSeaa scriptboth
to trade quality for speedrender.quality.overrides.waterthe projectpicture

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.

"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

FieldWhat it does
seaLevel, seedheight of the calm surface (default 0); the wave pattern (default 1)
beaufortthe 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
choppinesscrest sharpness, 0..1.5 (default 1)
depthMone depth for the whole sea, 0.5..11000; left out, the sea is deep
densitythe water's density, 900..1300 kg/m3 (default 1025; fresh water is 1000): it floats every body
look.waterTypethe 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.sprayscale on whitecaps and on the spray off breaking crests, 0..2 (default 1)
look.wakeFoamGain, look.wakeFoamBreakup, look.wakeFoamBreakupSizehow 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.flatColorstandard renderer only: the flat sea's hex colour exactly as it shows, unlit (a stylised sea)
shorethe shoreline's controls (section 3)
bathymetrya 16-bit height image of the ground under the sea, for the physics as well as the picture (section 3)
waveMaskwave 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 fieldWhat it doesDefault
runuphow far waves run up the beach, 0..2; 0 turns the run-up off1
foamscale on the surf's and the swash's foam, 0..21
wetSandSecondshow long sand stays dark after the water leaves it, 1..60030
wetSandStrengthhow dark wet sand is, 0..10.4
causticslight patterns on the bed of the shallows, 0..21
shoalingphysics too: how much waves grow in shallow water, 0..2. Needs a bathymetry1
breakerIndexphysics too: a wave breaks at this share of the depth, 0.4..1.2. Needs a bathymetry0.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.

{ "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 fieldWhat it does
sizefull extent [x, z] in metres, before the entity's scale (required)
shape"box" (default) or "ellipse" inside that box
windMps, windDirectionRad, seedwind over the lake, 0..30 m/s: 0 (default) is flat water; above it the lake has small waves, bigger on a bigger lake
prioritybreaks a tie between overlapping bodies at the same level, -100..100
lookwaterType: 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) knows where its rivers, waterfalls and lakes are. A waterways component switches them on: drawn, and floated on, with bodies drifting downstream.

{ "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 fieldWhat it does
terrainthe terrain entity; left out, this entity or the scene's only terrain
rivers, waterfalls, lakeseach on by default
lookwaterType and foam, for all of them
roughness, flowScalephysics too: the beds' roughness (0.02..0.15, default 0.045) and a speed multiplier (0..4; 0 is still water)
foam, mistscale 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:

{ "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 fieldWhat it doesDefault
resolutionhull cells per axis cut from the collider, 1..8: more cells, a truer roll3
volumeScalethe share of the collider's volume that displaces water; under 1 is a hollow hull1
linearDrag, quadraticDragdrag against the water, by speed and by speed squared500, 500
angularDragdamping of the body's spin0
waterVelocitycarried by the waves and by a river's flowtrue
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. 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.
{ "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

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])
}
CallWhat 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?.watera 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:

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

A cold, murky coast with heavy surf.

"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:

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

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

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:

"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).

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

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

Is this thing in the water, and which water?

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:

"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:

RowComponentOffered when
Floats on waterbuoyancythe entity has a dynamic Rigid body and the scene has water (or air)
Lake or pondwateralways
Rivers and waterfallswaterwaysthe scene has a terrain
Wake on waterwaterWakethe scene has water
Keeps water outhullMaskthe 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:

KnobMeaning
water.sprayspray off breaking crests on or off
water.mistPercentmist particles (the surf's and a waterfall's), 0..100
water.lakeWaveslakes draw their own waves
water.riverLodBiascoarser river ribbons
water.rtReflectionsray-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:

View1080p Low1080p High1440p Low1440p High
Open ocean, Beaufort 20.160.430.200.59
Open ocean, Beaufort 60.170.500.230.70
Open ocean, Beaufort 100.190.570.260.81
A coast in view, automatic shoreline0.230.610.310.77
Surf filling the view, over a bathymetry0.370.820.521.10
No ocean in the scene0000

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

CodeMeaning
ocean-weather-not-ascending, ocean-swell-without-period, ocean-layer-field-mismatch, ocean-wind-out-of-rangea sea's layers or weather keys are malformed
ocean-bathymetry-asset-missing, ocean-bathymetry-not-16bit, ocean-bathymetry-range-invalidthe depth map
ocean-wave-mask-asset-missing, ocean-wave-mask-not-greyscale, ocean-wave-mask-range-invalidthe wave paint
ocean-shore-without-bathymetrya warning: shore.shoaling or shore.breakerIndex set with no bathymetry (the picture's controls never raise it)
buoyancy-without-ocean, buoyancy-without-bodya floating body with nothing to float in, or no dynamic body
water-shape-invalid, water-waves-invalida lake's size or wind
waterways-without-terrain, waterways-look-invalid, waterways-override-invalidrivers with no terrain, a number out of range, a bad override
water-wake-out-of-range, hull-mask-out-of-range, ripples-out-of-rangea value outside its range (clamped, with a warning)
render-ocean-without-hd, render-waterways-without-hdwarnings: 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 (the design and its history), status/hd-shore-default.md (the automatic shoreline), status/hd-water-perf.md (where the milliseconds go).

14. For engine work: changing the ocean system

Tools, all headless:

# 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 WaveLayers, 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).