b3d-lightning
Lightning and thunder from the weather. Put one in a scene; it watches every
<tosi-b3d-weather-cell> with storminess and strikes
under them, seeded, so the same seed gives the same storm.
Demo
import { b3d, b3dSun, b3dSkybox, b3dCloudDeck, b3dWeatherCell, b3dLightning, b3dTerrain, b3dDecorator, label3d, slider3d } from 'tosijs-3d'
import { tosi } from 'tosijs'
const { lightningDemo: s } = tosi({
lightningDemo: { radius: 600, rate: 1, coverage: 0.15, brightness: 1, timeOfDay: 20.2 },
})
// Hills, trees and a hamlet between you and the storm, so each strike has
// something to light and to throw shadows from.
const terrain = b3dTerrain({
seed: 7,
biome: 'on',
surfaceType: 'cylinder',
radius: 1000,
cylinderHeight: 1000,
tileSize: 128,
lodLevels: 3,
splitFactor: 2,
reach: 5000,
grossScale: 0.01,
detailScale: 0.09,
horizScale: 3.31,
grossAmplitude: 60,
detailAmplitude: 10,
biomeSeaLevel: 0,
biomeLapseRate: 0.15 / 60,
biomeTemperature: 0.3,
biomeMoisture: 0.75,
})
preview.append(
b3d(
{
sceneCreated(el, BABYLON) {
// You stand at the ORIGIN (far from it the terrain would rebase the
// world under the camera) and the storm is over the hamlet, a
// kilometre off: close strikes light from ABOVE and throw hard
// shadows; a distant one only rakes the slopes facing it.
const cam = new BABYLON.FreeCamera('c', new BABYLON.Vector3(0, 60, 0), el.scene)
cam.setTarget(new BABYLON.Vector3(800, 300, 650))
cam.maxZ = 20000
cam.attachControl(el.parts.canvas, true)
el.setActiveCamera(cam)
// Once the terrain can say how high the ground is: stand on it, and
// build the hamlet on it, between you and the storm.
const mat = new BABYLON.StandardMaterial('house', el.scene)
mat.diffuseColor = new BABYLON.Color3(0.75, 0.72, 0.66)
let built = false
el.scene.onBeforeRenderObservable.add(() => {
const h = terrain.heightSampler?.()
if (built || !h) return
built = true
cam.position.y = h(cam.position.x, cam.position.z) + 25
for (let i = 0; i < 18; i++) {
const x = 300 + (i % 6) * 70 + (i * 37) % 23
const z = 250 + Math.floor(i / 6) * 70 + (i * 53) % 19
const tall = 8 + (i * 7) % 14
const house = BABYLON.MeshBuilder.CreateBox('house', { width: 14, depth: 18, height: tall }, el.scene)
house.position.set(x, h(x, z) + tall / 2 - 1, z)
house.material = mat
house.receiveShadows = true
}
})
},
panelTitle: 'Lightning',
scenePanel: () => [
slider3d({ label: 'storm size (m)', value: s.radius, min: 200, max: 3000, step: 50 }),
slider3d({ label: 'how often', value: s.rate, min: 0, max: 5, step: 0.1 }),
slider3d({ label: 'cloud cover', value: s.coverage, min: 0, max: 1.2, step: 0.05 }),
slider3d({ label: 'brightness', value: s.brightness, min: 0, max: 3, step: 0.1 }),
slider3d({ label: 'time of day', value: s.timeOfDay, min: 0, max: 24, step: 0.25 }),
],
},
b3dSun({}),
b3dSkybox({ timeOfDay: s.timeOfDay, realtimeScale: 0 }),
terrain,
b3dDecorator({ budget: 3000, radius: 1200 }),
b3dCloudDeck({ altitude: 380, coverage: s.coverage }),
b3dWeatherCell({ x: 800, z: 650, radius: s.radius, coverage: 1.8, storminess: 1 }),
b3dLightning({ seed: 4, rate: s.rate, brightness: s.brightness }),
)
)
.preview { height: 100%; }
tosi-b3d { width: 100%; height: 100%; }
A storm at night. Watch the landscape jump out of the dark with each strike, the cloud light from inside, and count the seconds to the thunder (click the scene first; browsers only play sound after a gesture). Open the settings panel (the owl) for the storm's size, how often it strikes, the cloud cover around it and how dramatic the flash is.
What it does
- The flash lights the cloud around the strike from inside: its underside from below, the whiteout when you are IN the cloud, its top from above. And it lights the whole landscape from above, falling off with the storm's distance, with a harder light on the ground under the strike.
- The bolt is a jagged, branching channel from the cloud base to the ground, flickering through its re-strokes.
- Thunder arrives at the speed of sound: a flash a kilometre away rumbles about three seconds later, so you can count how far away the storm is.
- Sprites: over strong storms, rarely, a brief red-pink crown with tendrils high ABOVE the storm. Real ones stand 50–90 km up; here they are placed within the far plane.
Every strike dispatches strike (bubbling) with { kind, x, z, t, distance }, where kind is 'ground', 'cloud' or 'sprite', so gameplay can react.
When and where it strikes
The pure half (when, where, bolt shape, flicker, thunder delay) has no Babylon
in it: strikesBetween, boltPath, flashAt and thunderDelay.
Strikes are seeded. Time is cut into short slots, and each slot of each
storm cell either strikes or not by a hash of (seed, cell, slot), with a
chance set by the cell's storminess. So the same seed gives the same storm
every run, a strike never depends on frame rate, and asking "what struck
between t0 and t1" is a pure question the simulation can ask too.
Three kinds, because a storm is not one flash:
| kind | where | share |
|---|---|---|
ground |
cloud to ground: a bolt, the brightest flash, thunder | most |
cloud |
inside the cloud: the tower lit from within, no bolt | many |
sprite |
ABOVE the storm, 50–90 km up in reality: brief red-pink crowns and tendrils, only over strong storms | rare |
Attributes
| Attribute | Default | Description |
|---|---|---|
seed |
1 |
Same seed, same storm |
bolts |
'on' |
Draw the bolt for cloud-to-ground strikes |
sprites |
'on' |
Red-pink sprites above strong storms |
thunder |
'on' |
Procedural thunder, delayed by distance |
volume |
0.8 |
Thunder loudness |
groundLight |
2.5 |
How hard a ground strike's own light hits the scene: from the middle of the channel (halfway between the cloud base and the ground under the strike), the one that casts the shadows (the whole-sky fill is separate) |
darken |
0.5 |
How much a storm near you takes from the sun AND the ambient light (half, at full storm, easing with distance), so the flashes stand out. Applied through the cloud deck, which owns those lights |
shadows |
'on' |
Hard shadows from the strike. ONE shadow map, kept, re-rendered once per new brightest ground strike (never per frame) |
shadowSize |
1024 |
Its resolution |
flashLight |
'point' |
The strike's shadowed light. 'point': shadows radiate from the bolt in every direction (a cube map: six shadow renders, once per new strike). 'directional': one render, correct looking toward the strike; the cheap choice for low-end devices |
shadowRange |
600 |
How far from you (m) things cast them: the shadows you can see, and a tight set keeps the map sharp |
rate |
1 |
How OFTEN, as a multiple of the natural rate (about 0.6 strikes a second over a storm at storminess 1; at most 4) |
brightness |
1 |
How DRAMATIC a flash is: the whole landscape lit from above (falling off with the storm's distance), the cloud lit from inside, and the bolt's glow, together |
color |
'#dce4ff' |
The flash's colour |