Skip to content

Weather Demo

App: apps/weather_demo/

Demonstrates the Weather content pack (engine PLM-265): rain, snow, thunder, wind, dynamic clouds, height fog, and rain-splash ripples, driven by a pure-Wren Weather state machine over the engine’s neutral knobs — with zero engine source changes. It is the third content pack of the Environment & Stylization program. Where the Toon pack proved the shader half of Plume3D’s engine-vs-pack boundary and the Sky / Weather pack proved the policy half, Weather combines both: instanced particles (rain/snow via GPU-animated instancing), a cloud sky shader (on setSky), a rain-ripple ground shader, and a policy driver that ties them together.

Terminal window
% ./plume3d weather_demo

It cycles clear → rain → snow → storm (storm adds a lightning flash) and screenshots each (weather_clear.png / weather_rain.png / weather_snow.png / weather_storm.png).

What’s in the pack (all content — no engine change)

Section titled “What’s in the pack (all content — no engine change)”
FileKindWhat it is
shaders/src/precip.slanginstanced shaderRain + snow in one shader. Each instance’s mode (params.z: 0 = rain, 1 = snow) selects streak-vs-flake behaviour; the fall is computed on the GPU from the frame time + a per-instance seed, so thousands of particles cost one instanced draw each. Rain slants with the wind; snow drifts and flutters.
shaders/src/clouds.slangsky shaderDynamic clouds. A full-screen sky on the Graphics.setSky hook (#include "plume3d_sky.slang"): a horizon→zenith gradient plus an animated fbm cloud layer projected onto a sky plane — a “performant volumetric-look” without a true 3D raymarch. Coverage is a param (clear → light, storm → overcast); the driver advances a scroll param each frame to drift them.
shaders/src/wet_ground.slangground shaderRain splashes. The lit ground (ambient + distance/height fog, like scene) plus animated impact ripples — expanding rings popping across the surface from the frame time. The demo swaps the ground to this while it’s raining. The same ripple pattern is what a water surface wants for rain bloops — that lands with the Water pack.
shaders/src/scene.slangground shaderThe plain lit ground when it isn’t raining — ambient fill + atmospheric distance/height fog (the reused A#6 scene shader).
Scripts/Weather.wrenpolicy driverThe reusable state machine — clear / rain / snow / storm. Each state drives the cloud sky + coverage, ambient, distance and height fog, and global wind, and toggles the rain/snow InstancedMeshes. Storm auto-fires lightning (a bright ambient flash that decays over a few frames).
main.wrenexampleBuilds the rain + snow instanced fields, cycles the states, and swaps in the wet-ground shader while raining.

The whole weather policy lives in Scripts/Weather.wren. Construct it once with the two precipitation InstancedMeshes, then set a state and tick it each frame:

import "Scripts/Weather" for Weather
_weather = Weather.new(_rain, _snow) // the two precip InstancedMeshes
// ...each frame:
_weather.setState("storm") // clear / rain / snow / storm — idempotent per state
_weather.update(dt) // drifts the clouds + auto-fires storm lightning
_weather.bolt() // trigger a lightning flash now

Each state carries its own atmosphere. setState(s) writes the base look; update(dt) advances the cloud scroll (re-issuing the sky every frame so the clouds drift) and, in storm, strikes at random intervals — a bright ambient spike that decays over a few frames:

StateCloud coverageWind strengthHeight fog (baseY, falloff)PrecipLightning
clear0.35 (light)0.4off (0, 0)——
rain0.721.7(1.2, 0.16) low mistrain—
snow0.600.8(1.6, 0.20) hazy airsnow—
storm0.94 (overcast)2.7(2.0, 0.24) heavy murkrainauto-strikes

1 — Load the precip shader FIRST so it is the active shader for the instanced precipitation, then the ground and cloud shaders:

Graphics.loadShader("shaders/precip") // FIRST → the active instanced-precip shader
_sceneShader = Graphics.loadShader("shaders/scene")
_wetGround = Graphics.loadShader("shaders/wet_ground")
Graphics.loadShader("shaders/clouds") // the dynamic cloud sky (driven via setSky)

2 — Build the rain and snow fields as GPU-instanced meshes — each a couple thousand streak instances carrying a per-instance mode (0 rain / 1 snow), speed, and seed via enhanced instancing:

var im = _scene.createInstancedMesh(name)
im.setMesh(_streak) // one active precip shader (no per-mesh shader — see below)
im.useGpuInstancing = true
var idx = im.addInstanceColored(x, 0, z, 1.0, cr, cg, cb)
im.setInstanceParams(idx, speed, seed, mode) // params: x = speed, y = seed, z = mode (0 rain / 1 snow)

3 — Drive the atmosphere from the Weather state. The driver writes only into the engine’s neutral knobs. The cloud sky (mode 0) packs horizon + coverage into param(0) and zenith + the animated scroll into param(1):

Graphics.setSky("shaders/clouds", 0,
[horizon.r, horizon.g, horizon.b, coverage, // param(0): rgb + cloud coverage
zenith.r, zenith.g, zenith.b, scroll]) // param(1): rgb + drift scroll
Graphics.setAmbient(r, g, b) // ambient fill (lightning spikes this)
Graphics.setFog(r, g, b, density, start, end) // atmospheric distance fog
Graphics.setFogHeight(baseY, falloff) // low ground mist / murk
Graphics.setWind(x, y, z, strength) // rain slants + snow flutters with this
_rain.isVisible = true; _snow.isVisible = false // toggle the precip fields

4 — Swap the ground shader while raining. The demo draws the ground with the wet-ground (rain-splash) shader whenever the state is rain or storm, and the plain lit shader otherwise:

var raining = (_weather.state == "rain" || _weather.state == "storm")
Graphics.drawMesh(_ground, raining ? _wetGround : _sceneShader, model)

The engine capabilities it stands on (all already shipped)

Section titled “The engine capabilities it stands on (all already shipped)”

Nothing the pack touches is new engine surface:

The engine stays policy-free: it exposes wind, time, instancing, and the atmosphere knobs. The weather — which state looks like what, when it thunders, how hard the wind blows — lives entirely in Weather.wren. Add states (overcast, fog, hail) or re-theme by editing it; no engine change.

Two engine notes worth knowing (baked into the pack’s shape)

Section titled “Two engine notes worth knowing (baked into the pack’s shape)”
  • An instanced shader hand-declares its minimal descriptor set (set-0 view/proj + the frame block + the set-2 instance SSBO) and must not #include "plume3d.slang" — the full mesh ABI’s extra set-0 samplers corrupt the set-2 SSBO binding under MoltenVK, and the particles collapse to the origin.
  • Rain and snow share one active precip shader with a per-instance mode param (params.z) — one instanced draw covers both. This stays the leaner choice, but it is no longer forced: PLM-264 is fixed, so a per-mesh newMesh(.., shader) on an instanced prototype now renders correctly (the bug was that the instanced draw dropped the prototype’s per-mesh shader and fell back to the active shader — not an SSBO issue; see the scatter-multi regression). Rain and snow could now be separate per-mesh-shader meshes; the shared-shader + mode param is kept here purely as the cheaper path.
  • Thunder audio — a natural add: Source.queueSamples a procedural rumble (or play a loaded clip) a beat after bolt(). This v1 ships the visual lightning flash only.
  • Precip that follows the player — move each InstancedMesh’s volume with the camera each frame (or feed a camera-relative origin) so the field is always around the viewer.
  • Accumulation / wetness — a game can darken ground materials or raise a water level as policy.
  • Content pack, not an engine feature. The whole weather system is Weather.wren plus four .slang files — the engine gains nothing. Packaging, licensing, and any bespoke particle art/audio for a shippable pack are owner/business decisions; this in-repo demo is the exercised, gate-covered reference.
  • Rain bloops on water are deferred to the Water pack. The wet_ground ripple field is the reusable pattern; on a water surface it becomes the bloop displacement/normal — but that needs a water surface, which rides with the Water pack (phase 3).
  • Thunder is visual in v1. The storm fires a lightning ambient-flash; the audio rumble is a documented Source.queueSamples extension (above), not built here.
  • The cloud sky is a projected fbm layer, not a volumetric raymarch — a performant “volumetric-look” chosen deliberately; true volumetric clouds are out of scope.