clouds: advect cloud field with Open-Meteo wind

This commit is contained in:
Richard Zink committed 2026-10-12 21:00:23 +02:00
1 parent 0d8a5f93d3
commit 45e35982da
3 files changed
+73 -16

No files matched your search

+29 -4
View File
@@ -2133,8 +2133,8 @@ window.weatherThemes = {
function fetchWeather() { function fetchWeather() {
var url = 'https://api.open-meteo.com/v1/forecast?latitude=' + WEATHER_LAT + var url = 'https://api.open-meteo.com/v1/forecast?latitude=' + WEATHER_LAT +
'&longitude=' + WEATHER_LON + '&longitude=' + WEATHER_LON +
'&current=temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m' + '&current=temperature_2m,relative_humidity_2m,weather_code,wind_speed_10m,wind_direction_10m' +
'&hourly=temperature_2m,weather_code' + '&hourly=temperature_2m,weather_code,wind_speed_10m,wind_direction_10m' +
'&daily=weather_code,temperature_2m_max,temperature_2m_min' + '&daily=weather_code,temperature_2m_max,temperature_2m_min' +
'&timezone=Europe/Berlin&forecast_days=5'; '&timezone=Europe/Berlin&forecast_days=5';
@@ -2147,6 +2147,10 @@ window.weatherThemes = {
window.BackgroundCache.put('meta/weather', { json: data }); window.BackgroundCache.put('meta/weather', { json: data });
} }
currentWeatherCode = data.current.weather_code; currentWeatherCode = data.current.weather_code;
window.__currentWind = {
speedKmh: Number(data.current.wind_speed_10m || 0),
dirDeg: Number(data.current.wind_direction_10m || 0)
};
// while the weather sim runs it owns the theme - a live // while the weather sim runs it owns the theme - a live
// poll must not override the configured test weather // poll must not override the configured test weather
if (!__weatherSimActive) if (!__weatherSimActive)
@@ -2163,6 +2167,10 @@ window.weatherThemes = {
weatherData = rec.json; weatherData = rec.json;
window.__weatherStale = true; window.__weatherStale = true;
currentWeatherCode = rec.json.current.weather_code; currentWeatherCode = rec.json.current.weather_code;
window.__currentWind = {
speedKmh: Number(rec.json.current.wind_speed_10m || 0),
dirDeg: Number(rec.json.current.wind_direction_10m || 0)
};
if (!__weatherSimActive) if (!__weatherSimActive)
setWeatherTheme(getWeatherThemeName(currentWeatherCode)); setWeatherTheme(getWeatherThemeName(currentWeatherCode));
updateWeatherDisplay(); updateWeatherDisplay();
@@ -2220,6 +2228,12 @@ window.weatherThemes = {
var curIdx = findHourlyIndex(eff, hourly.time); var curIdx = findHourlyIndex(eff, hourly.time);
curTemp = hourly.temperature_2m[curIdx]; curTemp = hourly.temperature_2m[curIdx];
curCode = hourly.weather_code[curIdx]; curCode = hourly.weather_code[curIdx];
if (hourly.wind_speed_10m && hourly.wind_direction_10m) {
window.__currentWind = {
speedKmh: Number(hourly.wind_speed_10m[curIdx] || 0),
dirDeg: Number(hourly.wind_direction_10m[curIdx] || 0)
};
}
if (!__weatherSimActive && curCode !== currentWeatherCode) { if (!__weatherSimActive && curCode !== currentWeatherCode) {
currentWeatherCode = curCode; currentWeatherCode = curCode;
setWeatherTheme(getWeatherThemeName(curCode)); setWeatherTheme(getWeatherThemeName(curCode));
@@ -2231,9 +2245,20 @@ window.weatherThemes = {
var tempDisplay = currentUnits === 'imperial' var tempDisplay = currentUnits === 'imperial'
? Math.round(curTemp * 9 / 5 + 32) + '\u00b0F' ? Math.round(curTemp * 9 / 5 + 32) + '\u00b0F'
: Math.round(curTemp) + '\u00b0C'; : Math.round(curTemp) + '\u00b0C';
var curWindSpeed = current.wind_speed_10m;
var curWindDir = current.wind_direction_10m;
if (SIM_ACTIVE && hourly.wind_speed_10m && hourly.wind_direction_10m) {
var curIdx = findHourlyIndex(eff, hourly.time);
curWindSpeed = hourly.wind_speed_10m[curIdx];
curWindDir = hourly.wind_direction_10m[curIdx];
}
window.__currentWind = {
speedKmh: Number(curWindSpeed || 0),
dirDeg: Number(curWindDir || 0)
};
var windDisplay = currentUnits === 'imperial' var windDisplay = currentUnits === 'imperial'
? Math.round(current.wind_speed_10m * 0.621371) + ' mph' ? Math.round(curWindSpeed * 0.621371) + ' mph'
: Math.round(current.wind_speed_10m) + ' km/h'; : Math.round(curWindSpeed) + ' km/h';
var currentHtml = '<div class="weather-current-icon">' + weatherSVG(curCode) + '</div>' + var currentHtml = '<div class="weather-current-icon">' + weatherSVG(curCode) + '</div>' +
'<div><div class="weather-current-temp"><span>' + tempDisplay + '</span><span class="weather-location" id="weather-location">' + '<div><div class="weather-current-temp"><span>' + tempDisplay + '</span><span class="weather-location" id="weather-location">' +
((window.CONFIG && window.CONFIG.location && (window.CONFIG.location.label || (window.CONFIG.location.lat.toFixed(2) + ',' + window.CONFIG.location.lon.toFixed(2)))) || '') + '</span></div>' + ((window.CONFIG && window.CONFIG.location && (window.CONFIG.location.label || (window.CONFIG.location.lat.toFixed(2) + ',' + window.CONFIG.location.lon.toFixed(2)))) || '') + '</span></div>' +
+10 -1
View File
@@ -152,7 +152,16 @@ window.CONFIG = {
// only ever reports sunny/cloudy/stormy, so the deck keeps that // only ever reports sunny/cloudy/stormy, so the deck keeps that
// coverage through the night as well - 'night' is used by the weather // coverage through the night as well - 'night' is used by the weather
// simulation (weatherSim: true) alone. // simulation (weatherSim: true) alone.
density: { sunny: 0.30, cloudy: 0.70, stormy: 1.00, night: 0.45 } density: { sunny: 0.30, cloudy: 0.70, stormy: 1.00, night: 0.45 },
// Cloud motion from Open-Meteo wind. Open-Meteo reports wind FROM
// the reported compass direction; clouds advect toward the opposite
// bearing. speedScale exaggerates real advection so the motion is
// visible at the world scale (1 unit ~ 12.9 km).
wind: {
speedScale: 80,
minSpeed: 0.004,
maxSpeed: 0.08
}
// density: { sunny: 0.05, cloudy: 0.9, stormy: 1.0, night: 0.2 } // density: { sunny: 0.05, cloudy: 0.9, stormy: 1.0, night: 0.2 }
}, },
+34 -11
View File
@@ -60,6 +60,7 @@ var FRAG_SRC = [
'precision highp float;', 'precision highp float;',
'uniform vec2 uResolution;', 'uniform vec2 uResolution;',
'uniform float uTime;', 'uniform float uTime;',
'uniform vec3 uWind;',
'uniform sampler2D uGround;', 'uniform sampler2D uGround;',
'uniform vec2 uGroundCenter;', // current mosaic centre (world units) 'uniform vec2 uGroundCenter;', // current mosaic centre (world units)
'uniform vec2 uGroundSize;', // mosaic side extents (canvas tiles: W x H) 'uniform vec2 uGroundSize;', // mosaic side extents (canvas tiles: W x H)
@@ -203,13 +204,13 @@ var FRAG_SRC = [
'}', '}',
'float cloudDensity(vec3 p, float baseThresh, float soft) {', 'float cloudDensity(vec3 p, float baseThresh, float soft) {',
'float py = p.y + earthDrop(p.xz);', 'float py = p.y + earthDrop(p.xz);',
'vec3 cp = vec3(p.x, py, p.z);', 'vec3 cp = vec3(p.x, py, p.z) + uWind * uTime;',
'float n = fbm3(cp * 0.22 + vec3(0.0, 0.0, uTime * 0.05));', 'float n = fbm3(cp * 0.22 + vec3(0.0, 0.0, uTime * 0.02));',
// The detail octave weighs at most 0.20 and fades to zero with // The detail octave weighs at most 0.20 and fades to zero with
// distance; past soft = 0.95 it contributes under 1 %, so the whole // distance; past soft = 0.95 it contributes under 1 %, so the whole
// 4-octave lookup is skipped for the (numerous) far samples. // 4-octave lookup is skipped for the (numerous) far samples.
'if (soft < 0.75) {', 'if (soft < 0.75) {',
'float d = fbm3(cp * 1.80 + vec3(uTime * 0.045, 7.3, uTime * 0.035));', 'float d = fbm3(cp * 1.80 + vec3(uTime * 0.015, 7.3, uTime * 0.012));',
'n = n + d * mix(0.22, 0.0, soft / 0.75);', 'n = n + d * mix(0.22, 0.0, soft / 0.75);',
'}', '}',
// Cloud layer between uCloudBot and uCloudTop, densest in the // Cloud layer between uCloudBot and uCloudTop, densest in the
@@ -397,10 +398,10 @@ var FRAG_SRC = [
'return vec2((-b - sq) / (2.0 * a), (-b + sq) / (2.0 * a));', 'return vec2((-b - sq) / (2.0 * a), (-b + sq) / (2.0 * a));',
'}', '}',
'float cloudDensitySpherical(vec3 p, float alt, float baseThresh, float soft) {', 'float cloudDensitySpherical(vec3 p, float alt, float baseThresh, float soft) {',
'vec3 q = vec3(p.x, alt, p.z);', 'vec3 q = vec3(p.x, alt, p.z) + uWind * uTime;',
'float n = fbm3(q * 0.22 + vec3(0.0, 0.0, uTime * 0.05));', 'float n = fbm3(q * 0.22 + vec3(0.0, 0.0, uTime * 0.02));',
'if (soft < 0.75) {', 'if (soft < 0.75) {',
'float d = fbm3(q * 1.80 + vec3(uTime * 0.045, 7.3, uTime * 0.035));', 'float d = fbm3(q * 1.80 + vec3(uTime * 0.015, 7.3, uTime * 0.012));',
'n = n + d * mix(0.22, 0.0, soft / 0.75);', 'n = n + d * mix(0.22, 0.0, soft / 0.75);',
'}', '}',
'float h = clamp((alt - uCloudBot) / max(0.01, uCloudTop - uCloudBot), 0.0, 1.0);', 'float h = clamp((alt - uCloudBot) / max(0.01, uCloudTop - uCloudBot), 0.0, 1.0);',
@@ -852,13 +853,13 @@ var CLOUD_SAMPLER_GLSL = [
'}', '}',
'float cloudDensity(vec3 p, float baseThresh, float soft) {', 'float cloudDensity(vec3 p, float baseThresh, float soft) {',
'float py = p.y + earthDrop(p.xz);', 'float py = p.y + earthDrop(p.xz);',
'vec3 cp = vec3(p.x, py, p.z);', 'vec3 cp = vec3(p.x, py, p.z) + uWind * uTime;',
'float n = fbm3(cp * 0.22 + vec3(0.0, 0.0, uTime * 0.05));', 'float n = fbm3(cp * 0.22 + vec3(0.0, 0.0, uTime * 0.02));',
// The detail octave weighs at most 0.20 and fades to zero with // The detail octave weighs at most 0.20 and fades to zero with
// distance; past soft = 0.95 it contributes under 1 %, so the whole // distance; past soft = 0.95 it contributes under 1 %, so the whole
// 4-octave lookup is skipped for the (numerous) far samples. // 4-octave lookup is skipped for the (numerous) far samples.
'if (soft < 0.75) {', 'if (soft < 0.75) {',
'float d = fbm3(cp * 1.80 + vec3(uTime * 0.045, 7.3, uTime * 0.035));', 'float d = fbm3(cp * 1.80 + vec3(uTime * 0.015, 7.3, uTime * 0.012));',
'n = n + d * mix(0.22, 0.0, soft / 0.75);', 'n = n + d * mix(0.22, 0.0, soft / 0.75);',
'}', '}',
'float h = clamp((py - uCloudBot) / (uCloudTop - uCloudBot), 0.0, 1.0);', 'float h = clamp((py - uCloudBot) / (uCloudTop - uCloudBot), 0.0, 1.0);',
@@ -885,6 +886,7 @@ var TERRAIN_FRAG_SRC = [
'uniform vec3 uHorizonColor;', // fog colour taken from the same light model 'uniform vec3 uHorizonColor;', // fog colour taken from the same light model
'uniform float uStarAmb;', // diffuse starlight + airglow (night only) 'uniform float uStarAmb;', // diffuse starlight + airglow (night only)
'uniform float uTime;', // must match the sky pass: same cloud field 'uniform float uTime;', // must match the sky pass: same cloud field
'uniform vec3 uWind;', // cloud advection vector, world units / s
'uniform float uCloudBot;', 'uniform float uCloudBot;',
'uniform float uCloudTop;', 'uniform float uCloudTop;',
'uniform float uDensity;', // cloud coverage, same as the sky pass 'uniform float uDensity;', // cloud coverage, same as the sky pass
@@ -1087,6 +1089,7 @@ function init(canvas) {
var aPos = gl.getAttribLocation(prog, 'aPos'); var aPos = gl.getAttribLocation(prog, 'aPos');
var uRes = gl.getUniformLocation(prog, 'uResolution'); var uRes = gl.getUniformLocation(prog, 'uResolution');
var uTime = gl.getUniformLocation(prog, 'uTime'); var uTime = gl.getUniformLocation(prog, 'uTime');
var uWind = gl.getUniformLocation(prog, 'uWind');
var uGround = gl.getUniformLocation(prog, 'uGround'); var uGround = gl.getUniformLocation(prog, 'uGround');
var uGroundCenter = gl.getUniformLocation(prog, 'uGroundCenter'); var uGroundCenter = gl.getUniformLocation(prog, 'uGroundCenter');
var uGroundSize = gl.getUniformLocation(prog, 'uGroundSize'); var uGroundSize = gl.getUniformLocation(prog, 'uGroundSize');
@@ -1223,6 +1226,7 @@ function init(canvas) {
var terrainUFarPlane = terrainProg ? gl.getUniformLocation(terrainProg, 'uFarPlane') : null; var terrainUFarPlane = terrainProg ? gl.getUniformLocation(terrainProg, 'uFarPlane') : null;
var terrainUstarAmb = terrainProg ? gl.getUniformLocation(terrainProg, 'uStarAmb') : null; var terrainUstarAmb = terrainProg ? gl.getUniformLocation(terrainProg, 'uStarAmb') : null;
var terrainUtime = terrainProg ? gl.getUniformLocation(terrainProg, 'uTime') : null; var terrainUtime = terrainProg ? gl.getUniformLocation(terrainProg, 'uTime') : null;
var terrainUwind = terrainProg ? gl.getUniformLocation(terrainProg, 'uWind') : null;
var terrainUcloudbot = terrainProg ? gl.getUniformLocation(terrainProg, 'uCloudBot') : null; var terrainUcloudbot = terrainProg ? gl.getUniformLocation(terrainProg, 'uCloudBot') : null;
var terrainUcloudtop = terrainProg ? gl.getUniformLocation(terrainProg, 'uCloudTop') : null; var terrainUcloudtop = terrainProg ? gl.getUniformLocation(terrainProg, 'uCloudTop') : null;
var terrainUdensity = terrainProg ? gl.getUniformLocation(terrainProg, 'uDensity') : null; var terrainUdensity = terrainProg ? gl.getUniformLocation(terrainProg, 'uDensity') : null;
@@ -1250,7 +1254,7 @@ function init(canvas) {
return { return {
gl: gl, prog: prog, buf: buf, aPos: aPos, gl: gl, prog: prog, buf: buf, aPos: aPos,
blackTex: blackTex, blackTex: blackTex,
uRes: uRes, uTime: uTime, uGround: uGround, uRes: uRes, uTime: uTime, uWind: uWind, uGround: uGround,
uGroundCenter: uGroundCenter, uGroundSize: uGroundSize, uGroundCenter: uGroundCenter, uGroundSize: uGroundSize,
uCorridorRot: uCorridorRot, uCorridorRot: uCorridorRot,
uViewProjInv: uViewProjInv, uViewProjInv: uViewProjInv,
@@ -1314,7 +1318,7 @@ function init(canvas) {
uHazeMaxAlpha: terrainUHazeMaxAlpha, uHazeColor: terrainUHazeColor, uHazeMaxAlpha: terrainUHazeMaxAlpha, uHazeColor: terrainUHazeColor,
uDistancePassOn: terrainUDistancePass, uFarPlane: terrainUFarPlane, uDistancePassOn: terrainUDistancePass, uFarPlane: terrainUFarPlane,
uStarAmb: terrainUstarAmb, uStarAmb: terrainUstarAmb,
uTime: terrainUtime, uCloudBot: terrainUcloudbot, uTime: terrainUtime, uWind: terrainUwind, uCloudBot: terrainUcloudbot,
uCloudTop: terrainUcloudtop, uDensity: terrainUdensity, uCloudTop: terrainUcloudtop, uDensity: terrainUdensity,
uCloudShadowOn: terrainUcloudshadow, uCloudShadowOn: terrainUcloudshadow,
uCloudShadowDensity: terrainUcsDens, uCloudShadowDensity: terrainUcsDens,
@@ -1435,6 +1439,22 @@ if (window.CONFIG && window.CONFIG.clouds) {
CLOUD_TOP = Math.max(0.2, Math.min(CLOUD_TOP, 2.0)); CLOUD_TOP = Math.max(0.2, Math.min(CLOUD_TOP, 2.0));
CLOUD_BOT = Math.min(CLOUD_BOT, CLOUD_TOP - 0.4); CLOUD_BOT = Math.min(CLOUD_BOT, CLOUD_TOP - 0.4);
var CLOUDS_ON = !(window.CONFIG && window.CONFIG.clouds && window.CONFIG.clouds.enabled === false); var CLOUDS_ON = !(window.CONFIG && window.CONFIG.clouds && window.CONFIG.clouds.enabled === false);
var CLOUD_WIND_CFG = (window.CONFIG && window.CONFIG.clouds && window.CONFIG.clouds.wind) || {};
var CLOUD_WIND_SCALE = (typeof CLOUD_WIND_CFG.speedScale === 'number') ? Math.max(0, CLOUD_WIND_CFG.speedScale) : 80;
var CLOUD_WIND_MIN = (typeof CLOUD_WIND_CFG.minSpeed === 'number') ? Math.max(0, CLOUD_WIND_CFG.minSpeed) : 0.004;
var CLOUD_WIND_MAX = (typeof CLOUD_WIND_CFG.maxSpeed === 'number') ? Math.max(0, CLOUD_WIND_CFG.maxSpeed) : 0.08;
function cloudWindVector() {
var w = window.__currentWind || {};
var speedKmh = Number(w.speedKmh || 0);
var dirDeg = Number(w.dirDeg || 0);
var speed = speedKmh * CLOUD_WIND_SCALE / (KM_PER_TILE * 3600);
speed = Math.max(CLOUD_WIND_MIN, Math.min(CLOUD_WIND_MAX, speed));
// Open-Meteo wind_direction_10m is the compass bearing the wind blows
// FROM. Clouds advect toward the opposite bearing. World +X = east,
// +Z = north, bearing 0 = north and clockwise.
var rad = (dirDeg + 180) * Math.PI / 180;
return [Math.sin(rad) * speed, 0.0, Math.cos(rad) * speed];
}
// Terrain elevation grid (CONFIG.terrain). Heights come from Mapzen/ // Terrain elevation grid (CONFIG.terrain). Heights come from Mapzen/
// TILEZEN "terrarium" tiles (Copernicus DEM) at ELEV_Z, fetched once // TILEZEN "terrarium" tiles (Copernicus DEM) at ELEV_Z, fetched once
@@ -6130,6 +6150,8 @@ window.clouds3dRender = function(target) {
gl.uniform2f(s.uRes, canvas.width, canvas.height); gl.uniform2f(s.uRes, canvas.width, canvas.height);
var now = performance.now() / 1000; var now = performance.now() / 1000;
gl.uniform1f(s.uTime, now); gl.uniform1f(s.uTime, now);
var _cloudWind = cloudWindVector();
gl.uniform3f(s.uWind, _cloudWind[0], _cloudWind[1], _cloudWind[2]);
var pal = window.__currentPalette; var pal = window.__currentPalette;
if (!pal) { if (!pal) {
@@ -6485,6 +6507,7 @@ window.clouds3dRender = function(target) {
// sky pass (same noise, same uTime, same coverage) and marches from // sky pass (same noise, same uTime, same coverage) and marches from
// the surface towards the sun through the deck. // the surface towards the sun through the deck.
gl.uniform1f(terr.uTime, now); gl.uniform1f(terr.uTime, now);
gl.uniform3f(terr.uWind, _cloudWind[0], _cloudWind[1], _cloudWind[2]);
gl.uniform1f(terr.uCloudBot, CLOUD_BOT); gl.uniform1f(terr.uCloudBot, CLOUD_BOT);
gl.uniform1f(terr.uCloudTop, CLOUD_TOP); gl.uniform1f(terr.uCloudTop, CLOUD_TOP);
gl.uniform1f(terr.uDensity, density); gl.uniform1f(terr.uDensity, density);