Water & overlays
The Water component renders a depth-aware ocean plane with shore transitions and wave animation; TerrainOverlay paints per-tile RGBA on top of the terrain; and the Clouds subsystem adds procedural moving cloud shadows.
Three features in Meep sit on top of the Terrain system and
share its shader: a water plane (Water + WaterSystem), a per-tile RGBA overlay
(TerrainOverlay), and procedural cloud shadows (Clouds). None of them require
the terrain, but they are designed to work together with it.
Water
Water is an ECS component. Add it to the same entity as Terrain (or any entity
in the scene) and WaterSystem renders a flat 800 × 800 world-unit plane at a
configurable height. The material reads the scene depth buffer and the terrain
height sampler to compute a per-fragment water depth and modulate color and
opacity accordingly.
Registering
import Water from "@woosh/meep-engine/src/engine/graphics/ecs/water/Water.js";
import WaterSystem from "@woosh/meep-engine/src/engine/graphics/ecs/water/WaterSystem.js";
em.addSystem(new WaterSystem(engine.graphics));
Creating a water body
const water = new Water();
water.level.set(2.0); // world-Y of the water surface
water.color.set(0, 0.3, 0.5); // deep-water color (RGB 0–1)
water.shoreColor.set(0.58, 0.79, 0.85); // color at shallow shore edge
water.shoreDepthTransition.min = 0.7; // depth below which shore color starts (m)
water.shoreDepthTransition.max = 2.0; // depth at which deep color is fully reached (m)
water.waveSpeed.set(1.8); // animation speed
water.waveAmplitude.set(0.3); // wave height modifier
water.waveFrequency.set(1); // spatial frequency of waves
water.scattering.set(1.2); // higher = more opaque deep water
new Entity().add(new Transform()).add(water).build(ecd);
EngineHarness.buildTerrain adds Water automatically unless you pass
enableWater: false. In the raycast-vehicle demo
water is explicitly disabled (enableWater: false) because the terrain sits well
above sea level.
Properties
| Property | Type | Default | Description |
|---|---|---|---|
level | Vector1 | 0 | World-Y of the water plane. |
color | Color | (0, 0.3, 0.5) | Deep-water diffuse color. |
shoreColor | Color | (0.58, 0.79, 0.85) | Shallow / shore diffuse color. |
shoreDepthTransition | NumericInterval | [0.7, 2] | Depth range (world units) over which shore color fades to deep color. |
waveSpeed | Vector1 | 1.8 | Wave animation speed. |
waveAmplitude | Vector1 | 0.3 | Per-fragment wave height modifier (visual only, not geometry). |
waveFrequency | Vector1 | 1 | Spatial frequency of the sinusoidal wave pattern. |
scattering | Vector1 | 1.2 | Controls how fast opacity rises with depth (higher = opaquer). |
All properties fire onChanged signals so the shader uniforms update reactively
when you write to them after the component is linked.
How the shader works
The water plane is a single 800 × 800 quad rendered with BackSide so it is
visible from above. In the fragment shader:
- The scene depth texture (from the
ColorAndDepthframebuffer) gives the distance from the camera to whatever is behind the water. - The terrain height texture is sampled (with a 13-tap Gaussian blur to avoid noise) at the UV that corresponds to the fragment’s world XZ position, giving the terrain height at that point.
depth = waterLevel − terrainHeight. Shallow areas near the shore transition fromshoreColortocolorovershoreDepthTransition.- A scattering fog factor (
1 − exp(−viewDepth × scattering)) modulates alpha so the water fades to transparent at the very shore edge and becomes fully opaque in deep water. - A sine wave (
sin(worldX + worldZ + time × waveSpeed) / π) modulatesdepthby ±waveAmplitudeeach frame to give the water surface a gentle, animated look.
The water mesh has a fixed world-space size of 800 units and is automatically
repositioned when level changes. If your terrain is larger than 800 units you
will need to scale the entity’s Transform or substitute a larger geometry.
Terrain overlays
TerrainOverlay is a RGBA texture the size of the terrain grid that is blended
over the terrain surface by the splat material. It is accessed via
terrain.overlay — there is no separate ECS component.
import Vector4 from "@woosh/meep-engine/src/core/geom/Vector4.js";
const overlay = terrain.overlay;
// Paint a single grid cell red (x, y in grid coordinates).
overlay.paintPoint(12, 8, new Vector4(1, 0, 0, 0.6));
// Clear a cell.
overlay.clearPoint(12, 8);
// Alpha-blend a color onto an existing cell.
overlay.paintPointAlphaBlend(12, 8, new Vector4(0, 1, 0, 0.4));
// Replace the entire overlay with raw RGBA data (Uint8Array, length = width × height × 4).
overlay.writeData(myRgbaBuffer);
// Save and restore overlay state (e.g. while showing a build preview).
overlay.push(); // snapshot current data, then clear
// ... draw preview ...
overlay.pop(); // restore snapshot
// Set the per-tile sprite image (tiling texture drawn inside each cell).
overlay.baseTileImage = "./textures/grid-tile.png";
// Control the cell border width (fraction 0–1).
overlay.borderWidth.set(0.08);
The overlay texture uses NearestFilter on both axes, so each grid cell maps to
exactly one texel — suitable for turn-based or tile-selection UIs where you want
crisp cell boundaries. The borderWidth uniform on the splat shader controls how
much of each cell is the border.
Overlay API summary
| Method | Description |
|---|---|
paintPoint(x, y, vec4) | Write RGBA (float 0–1) to grid cell (x, y). |
clearPoint(x, y) | Zero the cell (fully transparent). |
paintPointAlphaBlend(x, y, vec4) | Alpha-blend vec4 over the existing cell value. |
readPoint(x, y, result) | Read the current RGBA into result (a Vector4). |
paintSampler(src, dx, dy, dw, dh) | Blit a Sampler2D into the overlay at offset (dx, dy), scaling to (dw, dh). |
paintImage(canvas, dx, dy, dw, dh) | Same as paintSampler but from an HTMLCanvasElement. |
writeData(uint8Array) | Replace the whole overlay with raw RGBA bytes. |
clear() | Zero the entire overlay. |
push() / pop() | Save / restore overlay state on a stack. |
update() | Mark the GPU texture dirty after external changes to sampler.data. |
Procedural cloud shadows
Each Terrain component owns a Clouds instance (terrain.clouds). It is
enabled by default. The cloud-shadow effect is a compile-time define in the splat
shader (SHADOWMAP_CLOUDS); it has no render-pass cost when disabled.
// Toggle shadows.
terrain.clouds.enabled = true;
// Change animation speed (world units per second, X and Z).
terrain.clouds.setSpeed(0.5, -0.5);
// Variability controls how much the three cloud layers diverge in speed.
// Higher values produce more complex, morphing patterns.
terrain.clouds.variability = 0.37;
Internally, three tiling noise textures (data/textures/noise/tile_256.png) drift
at three slightly different speeds derived from the base speed and variability.
They are sampled in the fragment shader and multiplied together to produce a
darkening factor that varies over space and time. The amount and intensity are
fixed at compile time (f_CloudsAmount = 0.8, f_CloudsIntensity = 0.2) and
are not currently exposed as runtime properties.
Clouds is driven by terrain.update(timeDelta), which TerrainSystem calls
every frame.
Cloud properties
| Property | Type | Default | Description |
|---|---|---|---|
enabled | boolean | true | Enable / disable the cloud-shadow define. Triggers a material recompile. |
variability | number | 0.37 | How much the three speed layers diverge. |
setSpeed(x, y) | method | (0.5, −0.5) | Base drift speed in world-units/s along X and Z. |