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.
Run from root
Section titled “Run from root”% ./plume3d water_mask_demoAn 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 rocksvar boat = _hullNode.getWorldMatrix()// each wall drawn at boat * wallLocal so the hull moves as one rigid boatThe 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-extentThe 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.
The shoreline — PLM-263 scene depth
Section titled “The shoreline — PLM-263 scene depth”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 waterfloat waterEye = plume3d_linearizeDepth(i.PositionCS.z, nearZ, farZ); // the water surface itselffloat depthBelow = max(0.0, sceneEye - waterEye); // surface -> seabed/object gapDeep 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.
The gate — opaqueCaptureEnabled
Section titled “The gate — opaqueCaptureEnabled”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 5Graphics.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 setsGraphics.setViewProjectionEnabled(true)and feeds the camera’s matrices viasetViewMatrix/setProjectionMatrix.Cam.orbitis the rig system and needsScene.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 —0fillsplume3d_customParam(0),16fillsplume3d_customParam(1)(see Custom material (set 5)). Writing the second block at offset1clobbers 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, andSceneNode.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.