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.
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 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
lookblock, 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
windlayer is a wind speed over a fetch of open water; aswelllayer is a height and a period arriving from somewhere else. Up to 6 layers, summed, each with its owndirectionRad. 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 explicitbathymetryalso 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 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) 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 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:
{ "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. They float by the same
buoyancy. - A wake. A floating hull leaves one in the HD picture with nothing set. A
waterWakecomponent shapes it:strength(0..1),armFoam,trailFoam,fillFoam(0..4),trailWidth,trailGrowth,trailMaxWidth,armLife,trailLife,fillLife(seconds), andsolve(a simulated wake for one hull, HD). It also gives a wake to a boat with nobuoyancythat 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
hullMaskcomponent holds the drawn water down inside the hull's footprint:halfLengthandhalfWidth(required, metres),center,bowLengthandbowWidth(a pointed bow),capHeight,edgeSoftness,skirtWidthandskirtCap.
{ "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])
}
| 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:
"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:
| 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.setSeachanges 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 withShoreSettings::ground_drawn = true. - Shader variants.
surface.wgslis compiled several ways. Lines between// SHORE-ONLY-BEGIN/-END,LAKE-ONLY,WAKE-ONLYandUNDER-ONLYmarkers 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
OceanLookorOceanShoreinpackages/contract/src/schema.tsand regenerate (pnpm --filter @argus/contract gen); read it incrates/app/src/water.rs(hd_ocean_settingsorhd_shore_settings); carry it inOceanSettings/ShoreSettings; put it in the surface's uniform block (surface.rs, the struct mirrored at the top ofsurface.wgsl); use it in the shader; add its row to the editor (crates/editor/src/world_settings.rs,FIELDSandengine_default), its line to API.md, and a changelog entry. - The foam's look. Open-sea whitecaps: the foam energy in
cascades.wgsland the lace cut insurface.wgsl(searchlace_dens). The surf's foam:foam_laceinsurface.wgsl; the sheet on the sand:film_trainandfilm_foam_laceinwet.wgsl. Keep the two laces the same function. - A breaking wave's shape.
surf_waveinsurface.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_traininwet.wgsl: one wave's swash as a body of water thrown up a slope. - Spray.
spray.wgsl:main(whitecaps) andsurf_main(the surf, bursts on rocks). - A new kind of wave.
crates/ocean/src/spectrum.rs: a sea is a list ofWaveLayers, each aLayerSpectrum(JONSWAP, Pierson-Moskowitz or swell today), a size, a direction and a spread. Add a variant toLayerSpectrumand its shape toLayerModel(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 isOceanWaveLayerandOceanLayerKindin the contract's schema (packages/contract/src/schema.ts), read bycrates/app/src/water.rs. The golden hashes incrates/ocean/src/lib.rspin the existing seas: they must not move. - A new water colour.
WaterTypeincrates/render/src/hd/water/surface.rs(absorption and scattering per colour channel), its name inocean.look.waterType(schema), and the editor's list (WATER_TYPEinworld_settings.rs; a test there fails until the two lists agree). - A different surface altogether (a stylised sea, a custom shader): keep
crates/oceanas it is, so bodies still float on real waves, and change the look insurface.wgsl.WaterTermsandwater_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:
- 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.
- Compile both variants before a long build: a
--set beach --ground drawnshot and a--set baseshot. WGSL reserves ordinary words (set,throw,from), and a local named like a uniform (wet) shadows it. - Check the open sea did not move: render
--set baseonmainand on your branch and compare the files. - Judge at native resolution and in motion. Upscaling hides hard edges; a still hides timing.
- Measure cost with the bench, not with frame timings from a recording run (they read many times too high).