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

+34 -11
View File
@@ -60,6 +60,7 @@ var FRAG_SRC = [
'precision highp float;',
'uniform vec2 uResolution;',
'uniform float uTime;',
'uniform vec3 uWind;',
'uniform sampler2D uGround;',
'uniform vec2 uGroundCenter;', // current mosaic centre (world units)
'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 py = p.y + earthDrop(p.xz);',
'vec3 cp = vec3(p.x, py, p.z);',
'float n = fbm3(cp * 0.22 + vec3(0.0, 0.0, uTime * 0.05));',
'vec3 cp = vec3(p.x, py, p.z) + uWind * uTime;',
'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
// distance; past soft = 0.95 it contributes under 1 %, so the whole
// 4-octave lookup is skipped for the (numerous) far samples.
'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);',
'}',
// 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));',
'}',
'float cloudDensitySpherical(vec3 p, float alt, float baseThresh, float soft) {',
'vec3 q = vec3(p.x, alt, p.z);',
'float n = fbm3(q * 0.22 + vec3(0.0, 0.0, uTime * 0.05));',
'vec3 q = vec3(p.x, alt, p.z) + uWind * uTime;',
'float n = fbm3(q * 0.22 + vec3(0.0, 0.0, uTime * 0.02));',
'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);',
'}',
'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 py = p.y + earthDrop(p.xz);',
'vec3 cp = vec3(p.x, py, p.z);',
'float n = fbm3(cp * 0.22 + vec3(0.0, 0.0, uTime * 0.05));',
'vec3 cp = vec3(p.x, py, p.z) + uWind * uTime;',
'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
// distance; past soft = 0.95 it contributes under 1 %, so the whole
// 4-octave lookup is skipped for the (numerous) far samples.
'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);',
'}',
'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 float uStarAmb;', // diffuse starlight + airglow (night only)
'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 uCloudTop;',
'uniform float uDensity;', // cloud coverage, same as the sky pass
@@ -1087,6 +1089,7 @@ function init(canvas) {
var aPos = gl.getAttribLocation(prog, 'aPos');
var uRes = gl.getUniformLocation(prog, 'uResolution');
var uTime = gl.getUniformLocation(prog, 'uTime');
var uWind = gl.getUniformLocation(prog, 'uWind');
var uGround = gl.getUniformLocation(prog, 'uGround');
var uGroundCenter = gl.getUniformLocation(prog, 'uGroundCenter');
var uGroundSize = gl.getUniformLocation(prog, 'uGroundSize');
@@ -1223,6 +1226,7 @@ function init(canvas) {
var terrainUFarPlane = terrainProg ? gl.getUniformLocation(terrainProg, 'uFarPlane') : null;
var terrainUstarAmb = terrainProg ? gl.getUniformLocation(terrainProg, 'uStarAmb') : 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 terrainUcloudtop = terrainProg ? gl.getUniformLocation(terrainProg, 'uCloudTop') : null;
var terrainUdensity = terrainProg ? gl.getUniformLocation(terrainProg, 'uDensity') : null;
@@ -1250,7 +1254,7 @@ function init(canvas) {
return {
gl: gl, prog: prog, buf: buf, aPos: aPos,
blackTex: blackTex,
uRes: uRes, uTime: uTime, uGround: uGround,
uRes: uRes, uTime: uTime, uWind: uWind, uGround: uGround,
uGroundCenter: uGroundCenter, uGroundSize: uGroundSize,
uCorridorRot: uCorridorRot,
uViewProjInv: uViewProjInv,
@@ -1314,7 +1318,7 @@ function init(canvas) {
uHazeMaxAlpha: terrainUHazeMaxAlpha, uHazeColor: terrainUHazeColor,
uDistancePassOn: terrainUDistancePass, uFarPlane: terrainUFarPlane,
uStarAmb: terrainUstarAmb,
uTime: terrainUtime, uCloudBot: terrainUcloudbot,
uTime: terrainUtime, uWind: terrainUwind, uCloudBot: terrainUcloudbot,
uCloudTop: terrainUcloudtop, uDensity: terrainUdensity,
uCloudShadowOn: terrainUcloudshadow,
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_BOT = Math.min(CLOUD_BOT, CLOUD_TOP - 0.4);
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/
// 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);
var now = performance.now() / 1000;
gl.uniform1f(s.uTime, now);
var _cloudWind = cloudWindVector();
gl.uniform3f(s.uWind, _cloudWind[0], _cloudWind[1], _cloudWind[2]);
var pal = window.__currentPalette;
if (!pal) {
@@ -6485,6 +6507,7 @@ window.clouds3dRender = function(target) {
// sky pass (same noise, same uTime, same coverage) and marches from
// the surface towards the sun through the deck.
gl.uniform1f(terr.uTime, now);
gl.uniform3f(terr.uWind, _cloudWind[0], _cloudWind[1], _cloudWind[2]);
gl.uniform1f(terr.uCloudBot, CLOUD_BOT);
gl.uniform1f(terr.uCloudTop, CLOUD_TOP);
gl.uniform1f(terr.uDensity, density);