Skip to content

Water

The CPU mirror of the stylized water surface (engine PLM-270 / ADR 0091, Block WATER). The stylized water pack displaces its mesh with a Gerstner wave sum in the vertex shader, so the water you see is animated on the GPU and nothing on the CPU could answer “how high is the water at (x, z) right now?” — which is exactly what gameplay (buoyancy, placement, splash spawns) needs. Water closes that gap: it holds the wave parameters and reproduces the shader’s Gerstner sum exactly on the CPU, so one Water drives both the rendered surface and the height query and they cannot drift.

import "engine" for Water, Graphics
var water = Water.new()
water.setWaves(0.55, 13.0, 0.34, 1.2) // gain, wavelength, steepness, speed
water.setWind(1.0, 0.3) // wave direction — match Graphics.setWind's x,z
water.setBaseY(0.0) // still-water surface Y (the water plane's Y)
// Push the SAME waves into the water mesh so the render matches the query.
water.applyToMesh(_waterMesh)
Graphics.setWind(1.0, 0.0, 0.3, 1.0) // wind is global — keep it in sync
// Later, per frame: where is the surface under a point, right now?
var y = water.heightAt(px, pz, _time) // _time = the game's accumulated dt

Water and the water shader share one formula. The engine’s device-free water_wave.h reproduces water_common.slangh’s Gerstner vertex sum bit-for-bit — the same three waves, the same wind-steered directions, the same per-wave scaling and phase — so a float bobs on the exact waves rendered on screen. Two things follow from that, and both are how you keep them aligned:

  • applyToMesh is what links them. It writes the four wave params (setWaves) into the mesh’s set-5 custom-material p3 slot (byte 48) — the same slot the water shader reads its waves from. Call it once after setWaves/setBaseY (and again if you re-tune the waves) so the surface and the query use the same values.
  • Wind is global. The wave direction comes from the engine’s global wind, not from the mesh. Set it on Water with setWind and on the renderer with Graphics.setWind using the same x, z, or the CPU query steers the waves one way while the GPU steers them another.

Returns: Water — a new water model with default waves. Configure it with the setters below, then applyToMesh and/or heightAt.

Set these once (and re-applyToMesh if you change them). They are the master knobs the three internal Gerstner waves are scaled from — the shader derives its per-wave lengths, steepness and speeds from these same four numbers.

setWaves(gain, wavelength, steepness, speed)

Section titled “setWaves(gain, wavelength, steepness, speed)”

Set the master wave parameters.

Parameters:

  • gain (Num) — overall height gain; scales the whole wave sum (bigger swell).
  • wavelength (Num) — the base wavelength in world units (crest-to-crest of the dominant wave).
  • steepness (Num) — the base steepness 0..1 (peakiness of the crests; keep ≲ 0.4 for an accurate height query — see the note above).
  • speed (Num) — the base wave speed (how fast crests travel).

Set the horizontal wave direction (x, z). Match this to the x, z you pass Graphics.setWind so the CPU query and the rendered surface travel the same way.

Set the still-water surface Y — the height of the flat water plane before any wave displacement. heightAt returns this plus the wave height; it is the plane’s world Y.

Switch the height query to the 5-octave Realistic profile — the CPU mirror of the water_realistic surface — so heightAt and buoyancy track the rendered realistic swell exactly. Default is false: the legacy 3-wave profile, unchanged byte-for-byte for every existing consumer.

Parameters:

  • v (Bool) — true selects the 5-octave Realistic mirror; false (the default) keeps the 3-wave profile.

Call it alongside setWaves on water you render with the Realistic variant — the plume3d_water="realistic" tag or the immediate-mode water_realistic shader — so the CPU query uses the same five octaves the vertex shader draws (directions d1..d5 + per-octave length/steepness/speed factors). Water rendered flat or Toon needs nothing (gain 0 is flat either way). Additive — no existing signature changed, and the default preserves the original 3-wave contract (engine WTR2 #4, ADR 0098; the mirror is unit-tested against the shader’s 5-octave vertex so the two cannot drift).

_water = Water.new()
_water.setWaves(0.6, 13.0, 0.55, 1.0) // match the realistic surface's p3 waves
_water.setRealistic(true) // the surface is water_realistic (5-octave) → buoyancy uses the SAME octaves

Switch the height query to the seascape octave profile — the CPU mirror of the water_seascape surface (the Alekseev |sin|/|cos| octave heightfield from the Water Raymarch seascape playground) — so heightAt and buoyancy track the rendered seascape swell. Here setWaves is reinterpreted: gain is the seascape height (seaHeight) and wavelength is the base frequency (seaFrequency); steepness/speed are unused. Takes precedence over setRealistic. Default is false.

Parameters:

  • v (Bool) — true selects the seascape octave mirror; false (the default) keeps the current (3-wave or Realistic) profile.

Call it on water rendered with the Seascape shader (the water_seascape mesh surface) so floaters bob on the same octave field the vertex shader displaces. The octave sum is centred on the water plane (a −0.9·gain bias) so the height query, the buoyancy, and the registered waterline all line up with where the water visually sits. Additive — no existing signature changed, and the default preserves the original contract (engine ADR 0099; the mirror is unit-tested against the shader’s octaves).

_water = Water.new()
_water.setWaves(0.9, 0.08, 0.0, 0.0) // seascape: gain = seaHeight, wavelength = seaFrequency
_water.setSeascape(true) // the surface is water_seascape → buoyancy uses the SAME octaves

Write the current wave params (setWaves) into mesh’s set-5 custom-material p3 slot (byte 48), so the rendered surface uses the same waves as the height query.

Parameters:

  • mesh (Mesh) — the water surface mesh (a subdivided MeshGen.plane or a swept MeshGen.ribbon). Its shader must read waves from p3 (the stylized water core does).

This only touches p3. The rest of the water look — colours, foam, refraction, the Toon knobs — is set separately through the other set-5 params (see the Water Demo param table).

Returns: Num — the water surface Y at world (x, z) and time, matching the rendered Gerstner waves.

Parameters:

  • x, z (Num) — the world-XZ column to sample.
  • time (Num) — the current time, passed explicitly: the game accumulates its own dt and passes it here (it tracks the shader’s host clock — the frame time is renderer-owned and not otherwise Wren-visible). Pass the same accumulated time you hand Physics.applyBuoyancy so the two agree frame to frame.
// Bob a marker on the surface each frame.
_t = _t + dt
var y = _water.heightAt(markerX, markerZ, _t)
_markerNode.setPosition(markerX, y, markerZ)

For buoyancy — making a boat, raft, or crate float, bob, and rock on these waves — pass this Water to Physics.applyBuoyancy each frame. It samples heightAt at multiple probe points across the body’s footprint and applies an upward force at each, so differential submersion produces real rocking/tilting torque, not just a bob. See the Buoyancy Demo (apps/buoyancy_demo).

If the water you render is the Realistic variant, also call setRealistic(true) on this Water so the float tracks the drawn 5-octave swell rather than the default 3-wave surface (engine WTR2 #4, ADR 0098) — the Water Playground is the reference.

  • Water Demo (apps/water_demo) — the stylized water surface (PBR + Toon), the shader side this class mirrors.
  • Buoyancy Demo (apps/buoyancy_demo) — Water + Physics.applyBuoyancy: rafts rock on their 4 corners, cubes bob at three densities.
  • River Demo (apps/river_demo) — the same stylized water flowing down a Spline ribbon.
  • Physics.applyBuoyancy — the multi-point Jolt buoyancy that consumes a Water.
  • BlendResult.instantiateWater + the Blend Water Demo — author the water surface in Blender by tagging a mesh plume3d_water, so Scene.loadBlendScene draws it with no Wren wiring (engine PLM-274). Pair it with this Water when you also need the CPU height query for buoyancy on that surface.