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.
Run from root
Section titled “Run from root”% ./plume3d weather_demoIt 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)”| File | Kind | What it is |
|---|---|---|
shaders/src/precip.slang | instanced shader | Rain + 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.slang | sky shader | Dynamic 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.slang | ground shader | Rain 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.slang | ground shader | The plain lit ground when it isn’t raining — ambient fill + atmospheric distance/height fog (the reused A#6 scene shader). |
Scripts/Weather.wren | policy driver | The 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.wren | example | Builds the rain + snow instanced fields, cycles the states, and swaps in the wet-ground shader while raining. |
The Weather state machine
Section titled “The Weather state machine”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 nowEach 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:
| State | Cloud coverage | Wind strength | Height fog (baseY, falloff) | Precip | Lightning |
|---|---|---|---|---|---|
clear | 0.35 (light) | 0.4 | off (0, 0) | — | — |
rain | 0.72 | 1.7 | (1.2, 0.16) low mist | rain | — |
snow | 0.60 | 0.8 | (1.6, 0.20) hazy air | snow | — |
storm | 0.94 (overcast) | 2.7 | (2.0, 0.24) heavy murk | rain | auto-strikes |
How it’s driven
Section titled “How it’s driven”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 = truevar 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 scrollGraphics.setAmbient(r, g, b) // ambient fill (lightning spikes this)Graphics.setFog(r, g, b, density, start, end) // atmospheric distance fogGraphics.setFogHeight(baseY, falloff) // low ground mist / murkGraphics.setWind(x, y, z, strength) // rain slants + snow flutters with this_rain.isVisible = true; _snow.isVisible = false // toggle the precip fields4 — 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:
- Global frame + wind uniform (Block RFX A#1) — the
timethe GPU fall reads and thewindthe rain slants with, viaGraphics.setWind. - Enhanced GPU instancing (Block SPL) — thousands of per-instance particles in one draw,
with per-instance params (speed / seed / mode) through
InstancedMesh.addInstanceColored+setInstanceParams. - A#6 atmosphere knobs —
setSky/setAmbient/setFog/setFogHeightthe states drive — the same knobs the Sky pack’s time-of-day driver uses.
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
precipshader 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-meshnewMesh(.., 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 thescatter-multiregression). Rain and snow could now be separate per-mesh-shader meshes; the shared-shader + mode param is kept here purely as the cheaper path.
Extending it (still pack content)
Section titled “Extending it (still pack content)”- Thunder audio — a natural add:
Source.queueSamplesa procedural rumble (or play a loaded clip) a beat afterbolt(). 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.
- Graphics — Sky, fog & ambient (A#6) —
setSky/setAmbient/setFog/setFogHeight, the neutral atmosphere knobs the driver writes into. - Graphics — Global frame uniforms —
setWindand the host frame clock the precip shader reads. - InstancedMesh — per-instance colour, params & culling
—
addInstanceColored/setInstanceParamsfill the rain and snow fields. - The sky ABI
plume3d_sky.slang(the cloud shader) and the mesh vertex/frame bindings the instancedprecipshader hand-declares.
Notes and limitations
Section titled “Notes and limitations”- Content pack, not an engine feature. The whole weather system is
Weather.wrenplus four.slangfiles — 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_groundripple 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.queueSamplesextension (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.