b3d-weather-cell
A place whose weather differs — a lee behind a ridge, a squall, a storm —
declared as a circle on the map with what it adds at its centre. Weather
consumers (clouds, water, ambient) ask the scene for the weather WHERE THEY ARE
(b3d.weatherAt(x, z)), so a cell is local: calm in the lee, wind past it.
drift="wind" makes it a weather system: it travels with the scene's wind,
and with a lifetime it grows, holds and dies rather than popping. See
weather for the composition rules and WEATHER-DESIGN.md for where this is
going (storm visuals, lightning, shafts).
Demo
The camera flies along a row of flagpoles and straight through a LEE: a cell (the ring on the water) whose wind cancels the scene's. Each flag shows the wind WHERE IT STANDS, streaming downwind in the open and hanging limp inside the ring; the leaves and waves around you go calm there too. The readout is the weather where the camera is.
import { b3d, b3dWeatherCell, b3dAmbient, b3dWater, b3dLight, b3dSkybox, sceneDelta } from 'tosijs-3d'
import { elements } from 'tosijs'
const { div } = elements
const readout = div({ style: { position: 'absolute', left: '12px', bottom: '12px', color: '#fff', font: '14px monospace', textShadow: '0 1px 2px #000' } })
const LEE = { x: 0, z: 0, radius: 70 }
const scene = b3d(
{
windSpeed: 12,
windBearingDeg: 90, // blowing toward +X
sceneCreated(el, babylon) {
const cam = new babylon.FreeCamera('fly', new babylon.Vector3(-150, 6, -25), el.scene)
cam.setTarget(new babylon.Vector3(-110, 2, 40))
el.setActiveCamera(cam)
// THE FRAME OF REFERENCE: flags that each show the wind where they stand.
const poleMat = new babylon.StandardMaterial('pole', el.scene)
poleMat.diffuseColor = new babylon.Color3(0.85, 0.85, 0.8)
const flagMat = new babylon.StandardMaterial('flag', el.scene)
flagMat.diffuseColor = new babylon.Color3(1, 0.45, 0.1)
flagMat.emissiveColor = new babylon.Color3(0.6, 0.22, 0.04) // readable backlit
flagMat.backFaceCulling = false
const flags = []
for (let x = -160; x <= 160; x += 20) {
const z = 18
const pole = babylon.MeshBuilder.CreateCylinder('pole', { height: 7, diameter: 0.25 }, el.scene)
pole.position.set(x, 3.5, z)
pole.material = poleMat
const hinge = new babylon.TransformNode('hinge', el.scene)
hinge.position.set(x, 6.6, z)
const flag = babylon.MeshBuilder.CreatePlane('flag', { width: 3, height: 1.4 }, el.scene)
flag.parent = hinge
flag.position.x = 1.5 // hangs from the pole, downwind of it
flag.material = flagMat
flags.push({ x, z, hinge })
}
// The lee's edge, drawn on the water.
const ring = babylon.MeshBuilder.CreateTorus('lee', { diameter: LEE.radius * 2, thickness: 0.6, tessellation: 96 }, el.scene)
ring.position.set(LEE.x, 0.3, LEE.z)
const ringMat = new babylon.StandardMaterial('lee', el.scene)
ringMat.emissiveColor = new babylon.Color3(0.3, 0.9, 1)
ringMat.disableLighting = true
ring.material = ringMat
let dir = 1
el.scene.onBeforeRenderObservable.add(() => {
const dt = sceneDelta(el.scene)
cam.position.x += dir * 14 * dt
if (Math.abs(cam.position.x) > 150) dir = -dir
cam.setTarget(cam.position.add(new babylon.Vector3(dir * 40, -4, 65)))
for (const f of flags) {
const w = el.weatherAt(f.x, f.z).wind
const speed = Math.hypot(w.x, w.z)
const out = Math.min(1, speed / 10)
// Point downwind (local +X along the wind) and stand out with it:
// limp in calm air, streaming in a breeze.
f.hinge.rotation.y = speed > 0.01 ? Math.atan2(-w.z, w.x) : f.hinge.rotation.y
f.hinge.rotation.z = -(1 - out) * 1.35
}
const w = el.weatherHere().wind
readout.textContent = `wind here: ${Math.hypot(w.x, w.z).toFixed(1)} m/s`
})
},
},
b3dLight({ y: 1, intensity: 0.9 }),
b3dSkybox({ timeOfDay: 15, realtimeScale: 0 }),
b3dWater({ y: 0, waterSize: 600, follow: true }),
b3dAmbient({ preset: 'leaves', radius: 14 }),
// The lee: at its centre it adds the opposite of the scene's wind.
b3dWeatherCell({ x: LEE.x, z: LEE.z, radius: LEE.radius, windSpeed: 12, windBearingDeg: 270 }),
)
preview.append(scene, readout)
.preview { position: relative; }
tosi-b3d { width: 100%; height: 100%; }
Attributes
| Attribute | Default | Description |
|---|---|---|
x z |
0 |
Centre, world XZ |
radius |
200 |
Reach (m); influence eases to nothing at the rim |
windSpeed / windBearingDeg |
0 / 0 |
Wind this cell ADDS at its centre (bearing: where it goes, north-up, clockwise). A lee adds the opposite of the scene's |
coverage |
0 |
Cloud cover it adds (summed, clamped 0–2). A storm counts IN FULL, so a cell past 1 closes the sky over its inner part; past 1 it is also darker underneath. (A tower rising out of the deck is opt-in, the deck's stormRise.) 0 = no opinion |
precipitation |
0 |
Rain/snow 0–1 (the max of overlapping cells wins) |
storminess |
0 |
Lightning likelihood 0–1 (max wins) |
temperature |
0 |
Temperature offset in degrees (summed) |
drift |
'off' |
'wind' = a weather system: it travels with the scene's wind |
lifetime |
0 |
Seconds; grows, holds and dies over it (0 = permanent). An ended cell does nothing and fires ended |
grow |
0 |
Seconds to GATHER from nothing to full (0 = at once). Independent of lifetime, so a permanent storm can still build; with both, it gathers and then ends on its lifetime |