Atmosphere
@voxolith/engine/atmosphere: timeOfDay, Atmosphere presets, blending, transitions, approach, and atmosphereFrame, which yields renderer settings.
Time of day and weather are layered in three:
- the renderer draws raw atmospheric effects (
fog,clouds,precipitation,surface,waterWindinFrameParams, all off at zero) and knows nothing about weather (Atmosphere under Renderer); - the engine,
@voxolith/engine/atmosphere, is the vocabulary and the translator: a way to describe the sky, presets, blending and transitions, and one function that turns time of day plus an atmosphere into the renderer's settings; - the game decides whether there is weather, when it changes, and how wetness and snow cover build up.
Keep policy out of the engine and meaning out of the renderer. The module has no simulation and no clock of its own, and it is optional: a game without weather never imports it.
import { atmosphereFrame, ATMOSPHERES, timeOfDay } from "@voxolith/engine/atmosphere";
renderer.render({ ...camera, ...atmosphereFrame(timeOfDay(0.5), { ...ATMOSPHERES.rain, wetness: 0.7 }), time });Time of day
timeOfDay(phase, options?) returns the
lighting half of FrameParams (everything but the camera) as a
Lighting: key light direction and colour, sky
and ground ambient, sun and moon directions, colours and intensities, sky top and horizon, and the
night factor.
phaseruns 0 to 1 over a day: 0 midnight, 0.25 sunrise, 0.5 noon, 0.75 sunset.- The sun rises in +X and sets in -X on a tilted arc towards +Z, as at mid latitudes: the noon sun stands about 55° up and off to one side, so walls are lit rather than only roofs. The moon is opposite.
- By day the key light follows the sun (never lower than a flattering elevation). At night it becomes a dim blue moonlight, and the ambient drops low enough for point lights to carry the scene. Around dusk both keys fade out together, so the hand-over is invisible.
TimeOfDayOptions:nightAmbient(0.12; 0 pitch black, 1 as bright as day) andmoonlight(0.35, relative to sunlight).
Animate a transition by animating phase; the sky passes through sunset on the way. The moon disc
lights nothing itself: lightDir, lightColor and the ambients do.
Describing weather
An Atmosphere:
| field | range | meaning |
|---|---|---|
cloud | 0 to 1 | clear to overcast |
precipitation | { kind, intensity } | kind is "none", "rain" or "snow"; intensity 0 to 1 |
wind | { direction, strength } | degrees the wind blows towards (0 = +Z, 90 = +X), strength 0 to 1 |
fog | 0 to 1 | mist on top of whatever the precipitation brings |
wetness | 0 to 1 | how wet the ground is |
cover | 0 to 1 | how much snow has settled |
wetness and cover are the ground's state rather than the sky's. They build up and fade over
time by the game's rules; presets leave them at 0.
ATMOSPHERES has presets: clear, cloudy,
overcast, rain, storm, snow, blizzard, fog.
Changing it
blendAtmosphere(a, b, t)mixes two. A change of precipitation kind fades one out before the other fades in (no sleet halfway between rain and snow), and the wind turns the short way round.makeAtmosphereTransition(start?)returns anAtmosphereTransition:set(target, seconds = 4)eases towards a new atmosphere from wherever it is now (so a change of mind mid-transition does not jump),update(dt)returns the current one, andchanging()says whether to keep rendering.approach(value, target, rate, dt)moves a value towards a target by at mostrate * dt, never past it. It is for a game's own accumulators: rain soaking the ground, snow settling, things drying.
const weather = makeAtmosphereTransition(ATMOSPHERES.clear);
let wetness = 0;
function frame(dt: number, phase: number) {
if (forecastSaysRain()) weather.set(ATMOSPHERES.rain, 8);
const atm = weather.update(dt);
const raining = atm.precipitation.kind === "rain" ? atm.precipitation.intensity : 0;
wetness = approach(wetness, raining > 0 ? 1 : 0, raining > 0 ? 0.05 : 0.01, dt);
renderer.render({ ...camera, ...atmosphereFrame(timeOfDay(phase), { ...atm, wetness }), time });
}Into the renderer
atmosphereFrame(lighting, atmosphere, options?)
returns the lighting plus the renderer's AtmosphereParams
for one frame:
- Cloud blocks the direct key light and turns it into sky light: overcast dims and flattens the key light and greys the sky, keeping the night dark. Rain clouds are darker than white ones.
- Fog takes the horizon's colour; precipitation adds some fog of its own (snow more than rain).
- Wind tilts the rain and snow, drifts the clouds and drives the water (
waterWind). - Precipitation particles are lit like the scene, mostly ambient with some key light.
wetnessandcoverbecome the renderer'ssurfacesettings.- With a clear atmosphere the lighting passes through unchanged.
AtmosphereFrameOptions:
coverColor for settled snow, rainSpeed (70 voxels per second) and snowSpeed (8), and
voxelsPerMetre.
Scale
Everything is tuned for 10 voxels per metre. atmosphereFrame(lighting, atmosphere, { voxelsPerMetre: 100 })
gives the same weather in a finer world, in metres: fog per voxel thins, rain and snow fall more
voxels per second, and the renderer's effectScale keeps waves and drops their size.
Continuous rendering
Rain, snow and water animate from FrameParams.time, so an app with weather or water on screen
renders continuously. With a still camera under vsync, pass retryAfterMs: Infinity to
makePerf and call perf.reprobe() when the view moves; see
Quality and performance.
Animation
@voxolith/engine/animation: rigs and quaternion clips, the animator, forward kinematics, baking posed models by inverse mapping, cover, wounds, severing.
MagicaVoxel
@voxolith/engine/vox: .vox files into entities with one role per colour and entities back into .vox, with the Z-up to Y-up swizzle and size limits.