World

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

PropertyTypeDefaultDescription
levelVector10World-Y of the water plane.
colorColor(0, 0.3, 0.5)Deep-water diffuse color.
shoreColorColor(0.58, 0.79, 0.85)Shallow / shore diffuse color.
shoreDepthTransitionNumericInterval[0.7, 2]Depth range (world units) over which shore color fades to deep color.
waveSpeedVector11.8Wave animation speed.
waveAmplitudeVector10.3Per-fragment wave height modifier (visual only, not geometry).
waveFrequencyVector11Spatial frequency of the sinusoidal wave pattern.
scatteringVector11.2Controls 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:

  1. The scene depth texture (from the ColorAndDepth framebuffer) gives the distance from the camera to whatever is behind the water.
  2. 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.
  3. depth = waterLevel − terrainHeight. Shallow areas near the shore transition from shoreColor to color over shoreDepthTransition.
  4. 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.
  5. A sine wave (sin(worldX + worldZ + time × waveSpeed) / π) modulates depth by ± waveAmplitude each 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

MethodDescription
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

PropertyTypeDefaultDescription
enabledbooleantrueEnable / disable the cloud-shadow define. Triggers a material recompile.
variabilitynumber0.37How 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.