The execution model is in blender-core. Sky, sun/moon, fog and mood come from blender-time-of-day, so run scene_mood.py first, then add weather. Rendering settings are in blender-rendering.
One call each
scripts/precipitation.py -- --type rain --intensity heavy --ground Street Roofs --wind 2 0
scripts/surface_weather.py -- --objects Street Car Walls --wetness 0.7 --puddles 0.4 --ripples 1 --streaks 0.5
scripts/lightning.py -- --flashes 2 --strokes 3
scripts/precipitation.py -- --type snow --intensity moderate --ground Terrain
scripts/surface_weather.py -- --objects Terrain Roofs --snow 0.8 --snow-depth 0.06
scripts/surface_weather.py -- --objects Street Car Walls --remove # exact restore
A rainy night: scene_mood.py --time night --fog-visibility 250 → rain → wetness + puddles → lightning. Rain and puddles at night are what sell a wet scene: wet surfaces reflect every practical light.
precipitation.py (rain, snow, dust, ash, embers)
- Builds a GN modifier on
ENV_Weather_<Type>. Its box covers what the camera sees across the frame range: world-fixed, so moving cameras don't drag the rain. With no camera, it covers a 18 m box. The floor is the lowest top of--ground. - Seamless loop with no simulation: each particle lives N whole times per loop (
Lives), so the last frame + 1 equals the first frame exactly. It renders any frame on its own and works headless. - Real speeds: rain 7 m/s (9 at heavy/storm), snow 1, ash 0.6, dust 0.15, embers rise at 1.5, combined with
--wind X Y(m/s). - Ground: a raycast against the
--groundobjects stops particles at the first hit (roofs, awnings, streets). Rain spawns splash crowns lasting 80 ms at each hit, aligned to the surface normal. - Streaks, not motion blur: rain is a 3-sided streak of length = speed × ½ frame (a 180° shutter), aligned to the travel direction. Motion blur off is cheaper and looks the same.
- Particles closer than 1.5 m to the camera are hidden (
Near Clear), because huge blobs ruin shots. The viewport shows 10% (Viewport Fraction), renders show 100%. - Density per intensity (particles/m³): rain 5/15/40/80, snow 10/30/80/160. Override with
--density. The count is capped by--max-count(400k), and the result warns when it caps. - All values remain modifier inputs (Count, Speed, Wind, Lives, Particle Size, Streak Length, Sway, Ground, Splash...), so tweak them there instead of re-running.
surface_weather.py (wet, puddles, ripples, streaks, snow)
- Puts one
ENV_Weathershader group in front of each Principled BSDF and records the originals on the material.--removerestores links and values exactly. - Wet (Lagarde model): porous albedo darkens (up to 55% ×
--porosity), roughness drops, and a water film is added as Coat. Use porosity 0.6–0.9 for asphalt, concrete, wood and fabric, and 0.1–0.2 for metal, glass and plastic. - Puddles: domain-warped world-space noise on up-facing faces. Coverage ≈
--puddles, feature size--scale(m, default 3). Inside a puddle: full coat, mirror coat roughness 0.02, and a flattened base normal, because water fills the cracks. - Ripples: expanding rings, one per ~17 cm cell, driven by a
framedriver (a simple expression, so it works without auto-run scripts). They only show inside puddles. - Streaks: vertical noise on walls adds coat runs.
- Snow: up-facing coverage with a noisy edge, albedo 0.9, roughness 0.6 and a fine bump.
--snow-depthadds real thickness with theENV_SnowCoverGN modifier: up-facing faces are subdivided and extruded upward by a noise height that is never zero, so there's no z-fighting. It uses theENV_M_Snowmaterial with subsurface. --vertex-maskwrites theENV_Weathercolor attribute (R puddles, G snow, B walls) for engine shaders.
lightning.py
- A strike is 3 return strokes ~50 ms apart (at least 2 frames apart, so the flicker reads at 24 fps), each a peak followed by a 2-frame decay.
--strokes,--flashesor--framesset the timing. The flash is a coolENV_Storm_Flashsun (12° soft) keyed against the scene's key light (×2.5). ENV_Storm_Boltis a branching emissive curve (midpoint displacement plus 4 branches), keyed with the flash. By default it is 300 m away, in frame and off-center. The world background strength flashes too (--no-skyto skip).- Keys use LINEAR interpolation (flashes must snap, not ease).
Rules
- Rain is visible only when backlit or side-lit against a darker background: put a practical or bright sky behind it. Front-lit rain disappears.
- Combine layers: fog (atmosphere) + rain streaks (mid) + splashes/ripples (ground) + wet reflections. Any one alone looks fake.
- Real scale is mandatory: speeds, densities and puddle sizes are in meters (
blender-scene-setup). - Check with a render at the final resolution. Rain density and streak visibility don't show in the viewport (10% count, no fog).
- Run
blender-asset-qaon the snow geometry if the asset will be exported.
Games and real-time
Weather is an engine system (Unreal Niagara, Unity VFX Graph, Roblox ParticleEmitter). From Blender, export the masks: the vertex color attribute (--vertex-mask) or baked textures (blender-texture-baking self-bake of the wet material), plus snow cover meshes. Details are in references/realtime-engines.md. Physical values and the look library are in references/weather-values.md.
Gotchas
- GN particles are instances, so
object.datastays empty. Read results through the evaluated depsgraph (depsgraph.object_instances). - Background (
-b) evaluation counts as the viewport for theIs Viewportnode, so a headless test sees 10% of the count. Renders get 100%. - Ground objects are linked into the
ENV_Weather_<Type>_Groundcollection (linked, not moved). surface_weatherchanges shared materials: every object using that material gets wet. Duplicate materials first to limit it.- Coat appears in both engines. EEVEE needs raytracing on (
eevee.use_raytracing) for good puddle reflections.