# storm_thunder_3
Thunder sound clip for the lightning storm in the `fx_storm` scene. The scene's `Thunder3` entity
references it from an Audio component with autoplay off, looping off, the `sfx` channel, source type
`SoundEffect`, volume 1 and spatial off.
`StormDirector` plays it. The storm loops every 13.6 s through four strikes
(`storm/schedule.module`), and the director gives strike i the Audio component of the entity
`Thunder` ((i - 1) mod 3) + 1. During play, when the storm clock passes a strike's time plus the
distance from the strike's target to the hero camera divided by 343 m/s, it stops and restarts the
clip with volume `thunderVolume` (1 in the scene) times 1.0 for a tower strike or 0.85 for a ground
strike, and pitch `0.92 + 0.16 * ((i * 37) mod 11) / 10`. This clip sounds for strike 3, a ground
strike at 8.6 s (pitch 0.936).
`storm/audio.module` generates the samples in Luau and creates the clip with `asset.create` (mono,
32000 Hz); its `M.bake()` makes each storm clip whose name is still free. This clip comes from
`M.thunder(151, 12)`: seed 151, 12 seconds, ending in a 0.6 s fade.
`data.zaud` is the engine-native payload the runtime decodes and plays. Its header states Opus,
48000 Hz, 1 channel and 576000 frames, 12.0 seconds. `settings.yaml` sets the encoding: codec
`opus`, 96 kbps, VBR on, `forceMono` false, load type `auto`.
# StormDirector
StormDirector handles everything about a lightning strike apart from the particles. The storm graph
(the Generator on the same entity) owns the bolts, rain, splashes, sparks and smoke, and runs on one
looping clock. StormDirector reads that same clock every frame and, through the shared
`storm.schedule`, pours each strike's light over the rest of the scene: the sun swings to the bolt
and throws the tower's shadow, point lights blaze at the strike, the cloud deck and the puddles light
up, and the thunder follows at the speed of sound. Between strikes the values hold still, so its resting cost is one clock read.
The fx_storm scene uses it on the `storm` entity, next to the Generator that plays the
`storm.storm` graph, with `moonIntensity = 0.3` and `ambientIntensity = 0.05`.
It requires `storm.schedule`. It runs at `executionOrder = 60`. It finds these by name, and finds
them again every 2 seconds, whenever one of their entities is gone, and while it is still looking
for the generator:
| Name | What it reads |
|------|---------------|
| `sun` | Light: the moonlight that swings toward each bolt |
| `ambient` | Light: the ambient level |
| `FlashKey` | Light: the point light that blazes at the strike |
| `FlashFill` | Light: the fill light high in the cloud |
| `storm` | Generator: the storm clock |
| `Thunder1`, `Thunder2`, `Thunder3` | Audio: the thunder sounds |
| `storm_sky`, `storm_ground` | materials, resolved once as assets |
## Fields
| Field | Type | Default | Description |
|-------|------|---------|-------------|
| `flashGain` | number | 1.0 | Scene-wide brightness of a strike's light, as a multiplier on the tuned defaults. |
| `moonIntensity` | number | 1.7 | Moonlight before any strike: the sun light's intensity between strikes. |
| `ambientIntensity` | number | 0.26 | The ambient light's intensity between strikes. |
| `thunderVolume` | number | 1.0 | Thunder loudness. |
All fields are `Sync`.
## What each frame does
1. Reads the generator's time and turns it into the storm phase with `schedule.phase`.
2. Sums every strike's weighted `envelope` into `flash` (held at most 3, then scaled by
`flashGain`) and its weighted `afterglow` into `glow` (held at most 1.2). The strike with the
largest `envelope + afterglow` leads the lighting.
3. Carries each strike's `target` and `top` from the graph's own space into the world through the
`storm` entity's position and rotation (`Transform.localToWorld`).
4. In play (`update`), cues the thunder. Each strike's thunder plays when the phase passes the strike
time plus the distance from the camera drawing the viewport to the point halfway up the strike,
divided by 343. The strikes take turns over `Thunder1` to `Thunder3`; the volume is
`thunderVolume` (times 0.85 for a ground strike) and each strike gets its own pitch near 1.
5. When `flash`, `glow` or the leading strike has moved since the last write, it updates:
- the sun's direction, lerped from the moonlight direction toward the bolt's direction as the
flash burns, its intensity (`moonIntensity + flash * 7 + glow * 0.6`) and its colour, from
`{ 0.36, 0.46, 0.78 }` toward `{ 0.72, 0.80, 1.0 }`;
- the ambient light's intensity (`ambientIntensity + flash * 0.55 + glow * 0.08`);
- `FlashKey`, moved near the leading strike, at intensity `flash * 9000 + glow * 700`, and
`FlashFill` at `flash * 7000 + glow * 500`;
- `storm_sky`'s `flash` and `bolt_dir` (the direction from the camera drawing the viewport toward
the bolt's top), and `storm_ground`'s `flash` and `bolt_pos` (the strike target).
## Lifecycle
- `update(dt)`: runs the frame above with thunder cues.
- `editorUpdate(dt)`: runs the same frame, skipping the thunder cues.
Both use `dt` when given and 0.016 otherwise.
# storm_rain
Looping rain sound for the lightning storm in the `fx_storm` scene. The scene's `StormRain` entity
plays it from an Audio component with autoplay on, looping on, the `sfx` channel, source type
`Ambience`, volume 0.5 and spatial off.
`storm/audio.module` generates the samples in Luau and creates the clip with `asset.create` (mono,
32000 Hz); its `M.bake()` makes each storm clip whose name is still free. This clip comes from
`M.rainBed(7, 8.0)`, which generates 0.8 s more than the clip's 8 s and crossfades that tail into
the start, so the end meets the beginning when it loops.
`data.zaud` is the engine-native payload the runtime decodes and plays. Its header states PCM, 32000
Hz, 1 channel and 256000 frames, 8.0 seconds. `settings.yaml` sets codec `pcm`, `forceMono` false,
load type `auto`.
# storm_ground
Ground material for the lightning storm scene `fx_storm`. The scene's `ground` entity (a built-in
cube scaled 90 x 0.2 x 90 at (-100, -0.1, 220)) wears it. The `StormDirector` component on the
`storm` entity resolves it by name and, on each frame where the strike light changes, writes `flash`
and `bolt_pos` for the strongest current strike.
Shader: `storm_ground` (`storm/shaders/storm_ground.shader`). Its README: sodden ground, with
churned earth, rock, puddles with animated rain rings, and the lightning bolt's reflection in the
pools. From its header: world-space and texture-free; puddles fill the hollows of a procedural
height field and mirror the sky, rain rings spread on every wet patch, the strike's reflection is
traced from the reflected view ray to the bolt's column, and a distance haze matched to the sky's
horizon blends the far ground away.
Render state: the defaults (opaque blend, back faces culled, depth write on).
## Properties
mat.yaml sets `center_x` and `center_z`, every other property takes the shader default, and the
director writes two of them at runtime:
- `wet` 0.95: darkens the albedo, lowers roughness and fills more of the hollows with water.
- `puddles` 1.0: multiplier on puddle coverage.
- `flash` 0.0 (range 0 to 20): strike brightness, written by `StormDirector`; it scales the bolt's
reflection in the pools and brightens the distance haze.
- `bolt_pos` (0, 0, 0, 0): position of the strike's target, written by `StormDirector`; the shader
uses its x and z as the bolt's column.
- `fog` 0.011: density of the distance haze, applied as `1 - exp(-distance * fog)`.
- `center_x` -100, `center_z` 220: where the keep stands; the ground within 9 units of it stays a
drier apron and the puddles fill in from 9 to 16 units out.
# storm_thunder_1
Thunder sound clip for the lightning storm in the `fx_storm` scene. The scene's `Thunder1` entity
references it from an Audio component with autoplay off, looping off, the `sfx` channel, source type
`SoundEffect`, volume 1 and spatial off.
`StormDirector` plays it. The storm loops every 13.6 s through four strikes
(`storm/schedule.module`), and the director gives strike i the Audio component of the entity
`Thunder` ((i - 1) mod 3) + 1. During play, when the storm clock passes a strike's time plus the
distance from the strike's target to the hero camera divided by 343 m/s, it stops and restarts the
clip with volume `thunderVolume` (1 in the scene) times 1.0 for a tower strike or 0.85 for a ground
strike, and pitch `0.92 + 0.16 * ((i * 37) mod 11) / 10`. This clip sounds for strike 1, a ground
strike at 1.6 s (pitch 0.984), and strike 4, a tower strike at 11.0 s (pitch 1.0).
`storm/audio.module` generates the samples in Luau and creates the clip with `asset.create` (mono,
32000 Hz); its `M.bake()` makes each storm clip whose name is still free. This clip comes from
`M.thunder(117, 10)`: seed 117, 10 seconds, ending in a 0.6 s fade.
`data.zaud` is the engine-native payload the runtime decodes and plays. Its header states Opus,
48000 Hz, 1 channel and 480000 frames, 10.0 seconds. `settings.yaml` sets the encoding: codec
`opus`, 96 kbps, VBR on, `forceMono` false, load type `auto`.
# storm
A thunderstorm around a lone tower: rain driven slantwise by the wind that splashes off the ground
and the tower, lightning bolts striking on the schedule `schedule.module` beside it keeps, sparks and smoke
where each strike lands, and sheets of rain sweeping through. It is simulated on the GPU in one
solver family.
```lua
-- `graph` is this graph's reference: the AssetRef `asset.resolve` or a search returns for it.
local e = entity.spawn("storm", { position = { 0, 0, 0 } })
e.component.add("Generator", { graph = graph, playOn = "load", ending = "loop" })
```
| Output | What it draws |
|---|---|
| `rain` | The falling rain, `storm_streak` shader |
| `splash` | Splashes where rain lands, `storm_streak` shader |
| `sheets` | Sheets of rain sweeping through, `storm_sheet` shader |
| `bolt1` to `bolt4` | The lightning bolts, `lightning_bolt` shader |
| `sparks` | Sparks where strikes land, `storm_streak` shader |
| `smoke` | Smoke rising where strikes land, `storm_puff` shader |
The timeline has no end and plays at 60 fps. In the Procgen Effects Showcase world's `fx_storm` scene, the
`StormDirector` component on the same entity reads this clock and, on the same schedule, swings
the sun to each bolt, blazes point lights at the strike, lights the cloud deck and the puddles,
shakes the camera and plays the thunder.
# storm_thunder_2
Thunder sound clip for the lightning storm in the `fx_storm` scene. The scene's `Thunder2` entity
references it from an Audio component with autoplay off, looping off, the `sfx` channel, source type
`SoundEffect`, volume 1 and spatial off.
`StormDirector` plays it. The storm loops every 13.6 s through four strikes
(`storm/schedule.module`), and the director gives strike i the Audio component of the entity
`Thunder` ((i - 1) mod 3) + 1. During play, when the storm clock passes a strike's time plus the
distance from the strike's target to the hero camera divided by 343 m/s, it stops and restarts the
clip with volume `thunderVolume` (1 in the scene) times 1.0 for a tower strike or 0.85 for a ground
strike, and pitch `0.92 + 0.16 * ((i * 37) mod 11) / 10`. This clip sounds for strike 2, a tower
strike at 5.0 s (pitch 1.048).
`storm/audio.module` generates the samples in Luau and creates the clip with `asset.create` (mono,
32000 Hz); its `M.bake()` makes each storm clip whose name is still free. This clip comes from
`M.thunder(134, 11)`: seed 134, 11 seconds, ending in a 0.6 s fade.
`data.zaud` is the engine-native payload the runtime decodes and plays. Its header states Opus,
48000 Hz, 1 channel and 528000 frames, 11.0 seconds. `settings.yaml` sets the encoding: codec
`opus`, 96 kbps, VBR on, `forceMono` false, load type `auto`.
# tower
A lone stone keep: a plinth, a square shaft, a corbelled gallery with a crenellated parapet, a
round turret under a conical roof and an iron finial, 48.6 m to the tip of the finial. It is built
from boxes, cylinders, a cone and a sphere joined into one entity output: the stone draws with the
`storm_stone` shader and the finial with dark metal.
```lua
-- `graph` is this graph's reference: the AssetRef `asset.resolve` or a search returns for it.
local e = entity.spawn("tower", { position = { 0, 0, 0 } })
e.component.add("Generator", { graph = graph })
```
The output `keep` is the tower. It stands at the centre of the Procgen Effects Showcase world's `fx_storm` scene.
# effectShowcase
The procedural effects of the Procgen Effects Showcase world. Each one is a `.procGraph` that plays
through a `Generator`, and every shader, material and module a graph draws with sits beside it in
this package, so the package carries everything its graphs need. Each graph's README says what it
makes, how to place it and what each of its inputs changes.
## Contents
| Folder | Graphs | What they make |
|---|---|---|
| `combat/` | `impact`, `muzzleFlash`, `tracer` | A round striking a hard surface, the bloom at a barrel as a weapon fires, and the glowing streak of a round in flight. They draw with `fxFlash`, `fxSmoke` and `fxSpark` in `combat/shaders/`. |
| `blast/` | `wall_blast` | A charge blowing a breach through a stone block wall, on a 9.6 s loop. |
| `storm/` | `storm`, `tower` | A thunderstorm around a lone tower: wind-driven rain, lightning striking on the schedule `schedule.module` keeps, and sparks and smoke where strikes land. `tower` is the stone keep it stands around. |
| `rift/` | `arcane_rift` | A rift tearing open in the ground, its motes and rubble moved by the compute shaders in `rift/compute/`. |
| `dragon/` | `dragon_breath` | A dragon's fire breath sweeping a courtyard: a flame jet, rolling smoke and drifting embers. |
| `launch/` | `launch` | A heavy-lift rocket liftoff on one looping timeline, with the heat-haze material `rk_haze_mat`. |
| `waterfall/` | `wf_env`, `wf_water`, `wf_spray`, `wf_foam`, `wf_mist` | A waterfall built in parts: the cliff, basin and boulders, the falling and still water, the droplets, the foam where the falls land, and the mist with its rainbow. Its water materials are in `waterfall/materials/`. |
| `shaders/` | | `fx_noise`, the noise functions the waterfall and wall blast shaders include. |
## Playing one
A graph plays on a `Generator` component bound to it:
```lua
-- `graph` is the graph's reference: the AssetRef `asset.resolve` or a search returns for it.
local e = entity.spawn("impact", { position = { 0, 1, 0 } })
e.component.add("Generator", { graph = graph, playOn = "load" })
```
Set a graph's inputs with the Generator's `params`, e.g. `params = { size = 3 }`.