What it is
A night-blue weather instrument around a 3D Earth showing the global forecast for the next 48 hours. Wind 10 metres above the ground is drawn as tens of thousands of glowing streaks carried by the forecast wind, so the westerlies, the trade winds and the spirals of storms show up by themselves. Under them the globe is coloured by one of five variables (air temperature, wind speed, precipitation, cloud cover, sea-level pressure) with isolines for temperature and pressure, and the night side is shaded from the real position of the Sun for the hour shown. Click anywhere or find one of 45 cities for a readout of all five values, the wind direction and a 48-hour curve.
Data
- Forecasts come from the Open-Meteo API (keyless, CORS, CC BY 4.0). The page asks for a 7.5° × 10° grid (864 points, 49 hours from the current hour, one POST request because the coordinate list is too long for a URL) and keeps every third hour. Open-Meteo counts each point as one call and allows 600 a minute on the free tier, so the live grid is sized for a single request, and the answer is kept in
localStorage for an hour so reloads do not ask again. - A bundled snapshot (
public/lab/weather-globe/snapshot.json, about 345 KB, 206 KB gzip) holds a finer 5° grid (2,592 points, 17 steps of 3 hours). scripts/weather-snapshot.mjs builds it in three batches a minute apart and quantises each value to one byte (temperature in 0.5 °C, wind in 0.2 m/s, direction in 1.4°, pressure in 0.65 hPa), base64-encoded per channel. - The first frame waits at most 2.5 s for the live answer; if it comes later (up to 6 s) it replaces the snapshot in place. Otherwise the snapshot stays and the badge says so with its date.
- Coastlines are Natural Earth 1:50m land and 1:110m lakes (public domain).
Engineering decisions
- Plain Three.js with hand-written GLSL; no globe or weather library. The data module (
data.ts) is pure TypeScript and unit-tested: snapshot decoding, Open-Meteo parsing, bilinear sampling with longitude wrap, time interpolation, wind speed and direction to (u, v), colour ramps, units and formatting, the sub-solar point and static streamlines. - The active variable and the wind live in small half-float data textures (two forecast steps each), mixed in the shader for the exact fractional hour. The globe shader samples the variable with four offset bilinear taps, which rounds off the corners that plain bilinear filtering leaves on a coarse grid, maps it through a 256-pixel ramp texture and draws isolines with screen-space derivatives.
- Wind particles are GPU state: positions, ages and lifetimes in a ping-pong pair of float render targets (65,536 on the high tier, 16,384 on the low), advected by the (u, v) field with the longitude step scaled by 1 / cos(latitude). Each particle is drawn as one line segment from its previous to its current position.
- Trails accumulate in an equirectangular render target (world space, not screen space), faded a little each frame, and the globe samples it like any other texture. Turning or zooming the globe therefore never smears the trails.
- Without float render targets, 4,000 particles are advected on the CPU with the same maths and drawn into the same trail target.
?flow=cpu forces this path. - The base map is painted once into a canvas whose channels carry the land mask, coastlines and graticule; the shader colours them, so the look stays in one place.
- The camera is a small turntable (drag with momentum, pinch or Ctrl/⌘ + wheel zoom, eased flights to cities); the scene stops rendering when the page is hidden or scrolled away.
Performance and accessibility
- All text is HTML: masthead, legend, readout, controls and a description of the picture under the globe. The canvas is hidden from assistive technology.
- Reduced motion: no particle animation (up to 3,200 still streamlines are traced once for each hour shown), no auto-rotation, no autoplay, camera jumps instead of flights.
- The loader stays until the shaders are compiled, the particle buffers are warmed and the first frame is drawn.