Skip to content

Water Mask Demo

App: apps/water_mask_demo/

Proves the owner ask — “keep water from rendering inside boats and things” — on a floating boat. A stylized (Roystan Toon) water surface covers the scene; its fragment shader discards fragments inside the boat’s world-XZ footprint, so an open-hull boat sits DRY (its interior — a deck and a crate — shows instead of being flooded). The boat floats: one Jolt body carries the hull, Physics.applyBuoyancy samples the same Water the surface implies (four hull-corner probes → it rocks), and each frame the game rewrites the mask footprint from the boat body’s world XZ so the discard tracks the drifting, rocking hull. The water also paints a depth-based shoreline (foam) around the rocks and the hull using the scene depth in the mesh ABI (engine PLM-263, set 0 binding 5).

Built on existing public surface only — MeshGen, Graphics.drawMesh, explicit camera matrices, Graphics.opaqueCaptureEnabled, Mesh.setCustomColor, Water + Physics.applyBuoyancy, and SceneNode.getWorldMatrix. No new engine API, no new Wren class.

Terminal window
% ./plume3d water_mask_demo

An open-topped boat hull (four walls + a floor, no lid) with a crate riding inside floats and rocks on the swell, three rocks poke through, and a sandy seabed lies below. The water outside the hull is depth-graded blue with foam where it meets the rocks and the hull; inside the hull the water is masked out, so the deck and crate stay dry as the boat bobs. The demo screenshots water_mask.png after the boat settles onto the swell (frame 200), then exits.

Floating the boat — one body, walls at its transform

Section titled “Floating the boat — one body, walls at its transform”

The hull is one dynamic box body (low density → it floats high so the rim clears the water and the interior stays dry). Its four walls, floor, and crate are drawn at the body’s world transform, so the whole boat rocks rigidly, and Physics.applyBuoyancy samples the four hull corners each frame so differential submersion rocks it:

_water = Water.new()
_water.setWaves(0.5, 13.0, 0.34, 1.2) // one source of truth: drives the CPU height query for buoyancy
// ... one Physics.addDynamicBox hull body + wall/floor/crate meshes ...
Physics.applyBuoyancy(_hullNode, _water, _t, _probes, 220.0, 0.5) // 4 corner probes → it bobs and rocks
var boat = _hullNode.getWorldMatrix()
// each wall drawn at boat * wallLocal so the hull moves as one rigid boat

The mask — a moving, game-driven set-5 footprint

Section titled “The mask — a moving, game-driven set-5 footprint”

The water is the shared Toon surface, so its colour params own p0/p1; the footprint therefore rides in a free set-5 slot (p6) so it coexists with them. Each frame the game rewrites p6 from the boat body’s current world XZ, so the discard follows the floating hull:

// setCustomColor's first arg is a BYTE OFFSET; 96 -> customParam(6). m = the boat body's world matrix.
_water_mesh.setCustomColor(96, m[12], m[14], 2.2, 3.2) // p6: footprint centre (world XZ) + half-extent

The Toon water shader (on water_common.slangh) reads p6 and discards inside the axis-aligned box before shading:

float2 center = plume3d_customParam(6).xy;
float2 halfExt = plume3d_customParam(6).zw;
if (halfExt.x > 0.001 && halfExt.y > 0.001) {
float2 rel = i.WorldPos.xz - center;
if (abs(rel.x) < halfExt.x && abs(rel.y) < halfExt.y) discard; // no water inside the open hull
}

Arbitrary exclusion shapes (many boats, docks, terrain cut-outs) drive the same discard from a sampled mask texture instead — a top-down footprint rendered into an A#7 capture target and bound at a set-5 custom-texture slot. The mechanism is identical; only the mask source changes.

For the foam band, the shader reads the opaque scene depth behind the water at set 0 binding 5 and compares it against the water surface’s own depth. Both are linearized with the same camera near/far (carried in customParam(1).yz) so the eye-space gap is consistent:

float raw = plume3d_sceneDepthLoad(int2(i.PositionCS.xy));
float sceneEye = plume3d_linearizeDepth(raw, nearZ, farZ); // seabed/object behind the water
float waterEye = plume3d_linearizeDepth(i.PositionCS.z, nearZ, farZ); // the water surface itself
float depthBelow = max(0.0, sceneEye - waterEye); // surface -> seabed/object gap

Deep water is blue; it lightens toward the shore; a bright foam band appears where an object meets the surface (depthBelow → 0) — around the rocks and the boat hull.

Both the mask and the shoreline need the opaque scene captured before the transparent water draws. Each frame in draw(), enable capture, draw the opaque geometry (seabed, hull walls, rocks), then the transparent water, and add a passthrough post effect (opaque capture needs the offscreen path):

Graphics.opaqueCaptureEnabled(true) // split opaque -> copy -> transparent (colour AND depth)
Graphics.drawMesh(_seabed, _lit, ...) // opaque phase (writes depth for the water to read)
// ... hull walls + rocks ...
Graphics.drawMesh(_water, _waterShader, ...) // transparent phase -> masked + reads binding 5
Graphics.addPostEffect("shaders/present", []) // passthrough present drives the offscreen path
  • Immediate-mode demos must drive view/proj explicitly. This demo draws immediate-mode (no Scene.draw()), so each frame it sets Graphics.setViewProjectionEnabled(true) and feeds the camera’s matrices via setViewMatrix/setProjectionMatrix. Cam.orbit is the rig system and needs Scene.draw() to apply its view/proj — using it here would leave a stale/partial view-proj (a near-eye-level water plane then renders only its far half).
  • setCustomColor’s first argument is a BYTE OFFSET, not a slot index — 0 fills plume3d_customParam(0), 16 fills plume3d_customParam(1) (see Custom material (set 5)). Writing the second block at offset 1 clobbers block 0.
  • No new Wren surface. The buoyant masked boat is composed from already-shipped capabilities — the set-5 custom material, the PLM-263 mesh scene depth, Water + Physics.applyBuoyancy, and SceneNode.getWorldMatrix — this is a demo/composition, not a scripting change.
  • The footprint tracks the body, not the reverse. Because the mask is rewritten from the body’s XZ each frame, the discard stays aligned as the hull drifts and rocks (the boat barely yaws, so an axis-aligned box footprint suffices).

Related: Buoyancy Demo (Water rocking floaters), Scene Depth (binding 5), Refraction (opaque colour, binding 6), Planar Reflection (binding 7). Render-verified on MoltenVK: the boat floats and rocks, the water is masked out of the moving footprint (deck + crate stay dry), and foam collars ring the rocks.