From 52ececa9173b9e5422e6248936ef6d4a95072dd7 Mon Sep 17 00:00:00 2001 From: Richard Zink Date: Mon, 12 Oct 2026 17:50:42 +0200 Subject: [PATCH] clouds: add new spherical cloud pass using terrain distance occlusion --- config.js | 1 + vendor/clouds3d.js | 114 +++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 112 insertions(+), 3 deletions(-) diff --git a/config.js b/config.js index b0b4e7f..3641be6 100644 --- a/config.js +++ b/config.js @@ -143,6 +143,7 @@ window.CONFIG = { // World units (1 unit ~ 12.9 km). Keep 'top' below the lowest flight // altitude so aircraft fly above the deck. clouds: { + enabled: true, top: 1.0, // ~12.9 km bottom: -0.2, // ~-2.6 km (deck base slightly below terrain) // Coverage per weather level, 0 = clear, 1 = overcast. Only the diff --git a/vendor/clouds3d.js b/vendor/clouds3d.js index 4bbc06e..e9f89b2 100644 --- a/vendor/clouds3d.js +++ b/vendor/clouds3d.js @@ -117,6 +117,7 @@ var FRAG_SRC = [ 'uniform float uAirHazeMaxAlpha;', 'uniform vec3 uAirHazeColor;', 'uniform sampler2D uTerrainDistTex;', + 'uniform float uCloudsOn;', 'uniform float uFrontHazeOn;', 'uniform float uFrontHazeDensity;', 'uniform float uFrontHazeScaleHeightUnits;', @@ -362,6 +363,28 @@ var FRAG_SRC = [ '}', 'return column;', '}', + 'vec2 cloudShellRoots(float alt, vec3 oc, vec3 rd) {', + 'float R = max(1.0, uEarthRadius);', + 'float r = R + max(0.0, alt);', + 'float b = 2.0 * dot(oc, rd);', + 'float c = dot(oc, oc) - r * r;', + 'float disc = b * b - 4.0 * c;', + 'if (disc < 0.0) return vec2(-1.0, -1.0);', + 'float sq = sqrt(disc);', + 'return vec2((-b - sq) * 0.5, (-b + sq) * 0.5);', + '}', + '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));', + 'if (soft < 0.75) {', + 'float d = fbm3(q * 1.80 + vec3(uTime * 0.045, 7.3, uTime * 0.035));', + '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 profile = 4.0 * h * (1.0 - h);', + 'profile = max(profile, 0.0);', + 'return smoothstep(baseThresh, baseThresh + 0.34 + soft * 0.28, n) * profile;', + '}', 'float hazeColumnToDistance(vec3 rd, float dist) {', 'if (dist <= 0.0) return 0.0;', 'if (uCurvatureOn < 0.5) return dist;', @@ -603,7 +626,72 @@ var FRAG_SRC = [ 'return;', '}', - 'if (uMode == 1) { gl_FragColor = vec4(0.0); return; }', + 'if (uMode == 1) {', + 'if (uCloudsOn < 0.5) { gl_FragColor = vec4(0.0); return; }', + 'float R = max(1.0, uEarthRadius);', + 'vec3 C = vec3(uCamPos.x, -R, uCamPos.z);', + 'vec3 oc = uCamPos - C;', + 'float camAlt = max(0.0, length(oc) - R);', + 'vec2 top = cloudShellRoots(uCloudTop, oc, rd);', + 'vec2 bot = cloudShellRoots(uCloudBot, oc, rd);', + 'float tEnter = 1e9;', + 'float tExit = -1e9;', + 'if (camAlt >= uCloudBot && camAlt <= uCloudTop) {', + 'tEnter = 0.0;', + 'tExit = max(maxPositiveRoot(top), maxPositiveRoot(bot));', + '} else if (camAlt > uCloudTop) {', + 'tEnter = minPositiveRoot(top);', + 'tExit = maxPositiveRoot(bot);', + 'if (tExit < 0.0) tExit = maxPositiveRoot(top);', + '} else {', + 'tEnter = minPositiveRoot(bot);', + 'tExit = maxPositiveRoot(top);', + 'if (tExit < 0.0) tExit = maxPositiveRoot(bot);', + '}', + 'if (tEnter > 1e8 || tExit <= tEnter) { gl_FragColor = vec4(0.0); return; }', + 'vec2 uv = gl_FragCoord.xy / uResolution;', + 'vec4 distTex = texture2D(uTerrainDistTex, uv);', + 'if (distTex.a > 0.5) {', + 'float distNorm = (distTex.r * 65536.0 + distTex.g * 256.0 + distTex.b) / 16777215.0;', + 'tExit = min(tExit, distNorm * max(1.0, uFarPlane));', + '}', + 'tExit = min(tExit, tEnter + 30.0);', + 'if (tExit <= tEnter) { gl_FragColor = vec4(0.0); return; }', + 'float stepSize = (tExit - tEnter) / 24.0;', + 'float t = tEnter + stepSize * 0.5;', + 'float baseThresh = mix(0.72, 0.40, uDensity);', + 'float sunUp = smoothstep(-0.20, 0.05, uSunAlt);', + 'float cosT = dot(rd, uSunDir);', + 'float phase = 0.30 + 0.70 * pow(max(0.0, cosT), 2.5);', + 'float trans = 1.0;', + 'vec3 cloudSum = vec3(0.0);', + 'for (int i = 0; i < 24; i++) {', + 'vec3 p = uCamPos + rd * t;', + 'p += rd * (ign(gl_FragCoord.xy + vec2(float(i) * 7.13, float(i) * 3.71)) - 0.5) * stepSize * 0.25;', + 'float alt = max(0.0, length(p - C) - R);', + 'if (alt > uCloudTop + 0.1 || alt < uCloudBot - 0.1) break;', + 'float soft = smoothstep(4.0, 20.0, t);', + 'float d = cloudDensitySpherical(p, alt, baseThresh, soft);', + 'if (d > 0.02) {', + 'float lit = clamp((alt - uCloudBot) / max(0.01, uCloudTop - uCloudBot), 0.0, 1.0);', + 'vec3 albC = uCloudAlbedo * mix(0.78, 1.0, lit);', + 'float selfLit = clamp(1.0 - d * 0.8, 0.18, 1.0);', + 'vec3 direct = albC * uSunColor * uSunInt * phase * sunUp * selfLit;', + 'vec3 ambC = albC * max(vec3(uSkyAmb), uStarAmb * vec3(0.7, 0.85, 1.0)) * 1.7;', + 'vec3 moonC = albC * uMoonColor * (0.35 + 0.65 * max(0.0, dot(rd, uMoonDir))) * uMoonInt * 3.0;', + 'vec3 cloudCol = direct + ambC + moonC;', + 'float alpha = clamp(d * stepSize * 1.30, 0.0, 1.0);', + 'cloudSum += cloudCol * alpha * trans;', + 'trans *= (1.0 - alpha);', + 'if (trans < 0.04) break;', + '}', + 't += stepSize;', + '}', + 'float cloudAlpha = 1.0 - trans;', + 'vec3 cloudColor = (cloudAlpha > 0.001) ? cloudSum / cloudAlpha : vec3(0.0);', + 'gl_FragColor = vec4(cloudColor, clamp(cloudAlpha, 0.0, 1.0));', + 'return;', + '}', '}' ].join('\n'); @@ -1040,6 +1128,7 @@ function init(canvas) { var uAirHazeMaxAlpha = gl.getUniformLocation(prog, 'uAirHazeMaxAlpha'); var uAirHazeColor = gl.getUniformLocation(prog, 'uAirHazeColor'); var uTerrainDistTex = gl.getUniformLocation(prog, 'uTerrainDistTex'); + var uCloudsOn = gl.getUniformLocation(prog, 'uCloudsOn'); var uFrontHazeOn = gl.getUniformLocation(prog, 'uFrontHazeOn'); var uFrontHazeDensity = gl.getUniformLocation(prog, 'uFrontHazeDensity'); var uFrontHazeScaleHeightUnits = gl.getUniformLocation(prog, 'uFrontHazeScaleHeightUnits'); @@ -1160,6 +1249,7 @@ function init(canvas) { uAirHazeGain: uAirHazeGain, uAirHazeMaxAlpha: uAirHazeMaxAlpha, uAirHazeColor: uAirHazeColor, uTerrainDistTex: uTerrainDistTex, + uCloudsOn: uCloudsOn, uFrontHazeOn: uFrontHazeOn, uFrontHazeDensity: uFrontHazeDensity, uFrontHazeScaleHeightUnits: uFrontHazeScaleHeightUnits, uFrontHazeGain: uFrontHazeGain, uFrontHazeMaxAlpha: uFrontHazeMaxAlpha, @@ -1315,6 +1405,7 @@ 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); // Terrain elevation grid (CONFIG.terrain). Heights come from Mapzen/ // TILEZEN "terrarium" tiles (Copernicus DEM) at ELEV_Z, fetched once @@ -2269,7 +2360,7 @@ var terrainDistW = 0; var terrainDistH = 0; var terrainDistOk = false; function ensureTerrainDistFbo(gl, w, h) { - if (!FRONT_HAZE_ON || w <= 0 || h <= 0) { terrainDistOk = false; return false; } + if ((!FRONT_HAZE_ON && !CLOUDS_ON) || w <= 0 || h <= 0) { terrainDistOk = false; return false; } if (terrainDistFbo && terrainDistW === w && terrainDistH === h) return terrainDistOk; if (terrainDistFbo) { gl.deleteFramebuffer(terrainDistFbo); @@ -5840,6 +5931,7 @@ window.clouds3dRender = function(target) { gl.uniform1f(s.uAirHazeMaxAlpha, AIR_HAZE_MAX_ALPHA); gl.uniform3f(s.uAirHazeColor, _airHazeColor[0], _airHazeColor[1], _airHazeColor[2]); gl.uniform1i(s.uTerrainDistTex, 7); + gl.uniform1f(s.uCloudsOn, CLOUDS_ON ? 1.0 : 0.0); gl.uniform1f(s.uFrontHazeOn, FRONT_HAZE_ON ? 1.0 : 0.0); gl.uniform1f(s.uFrontHazeDensity, FRONT_HAZE_DENSITY); gl.uniform1f(s.uFrontHazeScaleHeightUnits, FRONT_HAZE_SCALE_HEIGHT_UNITS); @@ -6162,7 +6254,7 @@ window.clouds3dRender = function(target) { gl.enableVertexAttribArray(terr.aColor); gl.vertexAttribPointer(terr.aColor, 3, gl.FLOAT, false, STRIDE, 32); } - var distPassOk = FRONT_HAZE_ON && ensureTerrainDistFbo(gl, canvas.width, canvas.height); + var distPassOk = (FRONT_HAZE_ON || CLOUDS_ON) && ensureTerrainDistFbo(gl, canvas.width, canvas.height); if (distPassOk) { gl.bindFramebuffer(gl.FRAMEBUFFER, terrainDistFbo); gl.viewport(0, 0, terrainDistW, terrainDistH); @@ -6715,6 +6807,22 @@ window.clouds3dRender = function(target) { gl.drawArrays(gl.TRIANGLES, 0, 3); } + if (CLOUDS_ON && terrainDistOk && terrainDistTex) { + gl.useProgram(s.prog); + gl.bindBuffer(gl.ARRAY_BUFFER, s.buf); + gl.enableVertexAttribArray(s.aPos); + gl.vertexAttribPointer(s.aPos, 2, gl.FLOAT, false, 0, 0); + gl.activeTexture(gl.TEXTURE7); + gl.bindTexture(gl.TEXTURE_2D, terrainDistTex); + gl.uniform1i(s.uTerrainDistTex, 7); + gl.disable(gl.DEPTH_TEST); + gl.depthMask(false); + gl.enable(gl.BLEND); + gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); + gl.uniform1i(s.uMode, 1); + gl.drawArrays(gl.TRIANGLES, 0, 3); + } + if (FRONT_HAZE_ON && terrainDistOk && terrainDistTex) { gl.useProgram(s.prog); gl.bindBuffer(gl.ARRAY_BUFFER, s.buf);