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, speedwater.setWind(1.0, 0.3) // wave direction — match Graphics.setWind's x,zwater.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 dtOne source of truth
Section titled “One source of truth”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:
applyToMeshis what links them. It writes the four wave params (setWaves) into the mesh’s set-5 custom-materialp3slot (byte 48) — the same slot the water shader reads its waves from. Call it once aftersetWaves/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
WaterwithsetWindand on the renderer withGraphics.setWindusing the samex, z, or the CPU query steers the waves one way while the GPU steers them another.
Constructor
Section titled “Constructor”Water.new()
Section titled “Water.new()”Returns: Water — a new water model with default waves. Configure it with the setters
below, then applyToMesh and/or heightAt.
Configuring the waves
Section titled “Configuring the waves”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 steepness0..1(peakiness of the crests; keep ≲0.4for an accurate height query — see the note above).speed(Num) — the base wave speed (how fast crests travel).
setWind(x, z)
Section titled “setWind(x, z)”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.
setBaseY(y)
Section titled “setBaseY(y)”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.
setRealistic(v)
Section titled “setRealistic(v)”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) —trueselects 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 octavessetSeascape(v)
Section titled “setSeascape(v)”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) —trueselects 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 octavesDriving render and query
Section titled “Driving render and query”applyToMesh(mesh)
Section titled “applyToMesh(mesh)”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 subdividedMeshGen.planeor a sweptMeshGen.ribbon). Its shader must read waves fromp3(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).
heightAt(x, z, time)
Section titled “heightAt(x, z, time)”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 owndtand passes it here (it tracks the shader’s host clock — the frame time is renderer-owned and not otherwise Wren-visible). Pass the same accumulatedtimeyou handPhysics.applyBuoyancyso the two agree frame to frame.
// Bob a marker on the surface each frame._t = _t + dtvar y = _water.heightAt(markerX, markerZ, _t)_markerNode.setPosition(markerX, y, markerZ)Floating physics bodies
Section titled “Floating physics bodies”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.
See also
Section titled “See also”- 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 aSplineribbon. Physics.applyBuoyancy— the multi-point Jolt buoyancy that consumes aWater.BlendResult.instantiateWater+ the Blend Water Demo — author the water surface in Blender by tagging a meshplume3d_water, soScene.loadBlendScenedraws it with no Wren wiring (engine PLM-274). Pair it with thisWaterwhen you also need the CPU height query for buoyancy on that surface.