From b9094ebd8b02fb8e9dff626224d937115218fb2f Mon Sep 17 00:00:00 2001 From: Richard Zink Date: Mon, 12 Oct 2026 13:59:59 +0200 Subject: [PATCH] atmosphere: add initial volumetric atmosphere shell --- config.js | 9 ++++ vendor/clouds3d.js | 105 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 113 insertions(+), 1 deletion(-) diff --git a/config.js b/config.js index 346e88c..1d5a4d5 100644 --- a/config.js +++ b/config.js @@ -305,6 +305,15 @@ window.CONFIG = { albedoGain: 1.0, // theme share on surface colours skyGain: 0.45, // diffuse skylight share on surfaces airGain: 0.55, // Rayleigh airlight strength (blue-biased) + distanceFog: false, // legacy aerial perspective is off + model: { + enabled: true, + heightKm: 35.0, // full atmosphere shell around the planet + scaleHeightKm: 8.5, // density falls exponentially with altitude + density: 0.12, // base optical density + grayTau: 1.5, // high optical depth becomes grey fog + gain: 2.0 // scattering brightness + }, hazeResidual: 0.30, // surface colour surviving at the full fog distance cloudBrightness: 1.0, // cloud albedo (theme hue, brightness from the model) cloudHaze: 0.22, // share of the ground haze applied to the deck diff --git a/vendor/clouds3d.js b/vendor/clouds3d.js index 3e78efc..0f62a5b 100644 --- a/vendor/clouds3d.js +++ b/vendor/clouds3d.js @@ -85,7 +85,15 @@ var FRAG_SRC = [ 'uniform float uSkyOn;', // 0 until the star texture is built 'uniform float uStarLon;', // sidereal rotation offset (radians) 'uniform vec3 uPole;', // north celestial pole in world coords - 'uniform int uMode;', // 0 = sky+stars, 1 = clouds (transparent over mesh) + 'uniform int uMode;', // 0 = sky+stars, 1 = clouds, 2 = atmosphere shell + 'uniform float uAtmoModelOn;', + 'uniform float uAtmoHeightUnits;', + 'uniform float uAtmoScaleHeightUnits;', + 'uniform float uAtmoDensity;', + 'uniform float uAtmoGrayTau;', + 'uniform float uAtmoGain;', + 'uniform vec3 uAtmoBlueColor;', + 'uniform vec3 uAtmoGrayColor;', 'uniform vec3 uSunColor;', // direct beam after atmospheric extinction 'uniform float uSunInt;', 'uniform vec3 uMoonColor;', @@ -227,6 +235,64 @@ var FRAG_SRC = [ 'vec3 ro = nearH.xyz / nearH.w;', 'vec3 rd = normalize(farH.xyz / farH.w - ro);', + 'if (uMode == 2) {', + 'if (uAtmoModelOn < 0.5) { gl_FragColor = vec4(0.0); return; }', + 'float R = max(1.0, uEarthRadius);', + 'float H = max(0.1, uAtmoHeightUnits);', + 'float Hs = max(0.01, uAtmoScaleHeightUnits);', + 'vec3 C = vec3(ro.x, -R, ro.z);', + 'vec3 oc = ro - C;', + 'float b = dot(oc, rd);', + 'float rOut = R + H;', + 'float cOut = dot(oc, oc) - rOut * rOut;', + 'float discOut = b * b - cOut;', + 'float tStart = 0.0;', + 'float tEnd = 100.0;', + 'if (discOut > 0.0) {', + 'float sq = sqrt(discOut);', + 'float t1 = -b - sq;', + 'float t2 = -b + sq;', + 'if (dot(oc, oc) > rOut * rOut) tStart = max(0.0, t1);', + 'tEnd = max(0.0, t2);', + '}', + 'float cIn = dot(oc, oc) - R * R;', + 'float discIn = b * b - cIn;', + 'float tInner = 100000.0;', + 'if (discIn > 0.0) {', + 'float sq = sqrt(discIn);', + 'float t1 = -b - sq;', + 'float t2 = -b + sq;', + 'if (t1 > 0.0) tInner = t1;', + 'else if (t2 > 0.0) tInner = t2;', + '}', + 'if (tInner < 100000.0) tEnd = min(tEnd, tInner);', + 'if (tEnd <= tStart) { gl_FragColor = vec4(0.0); return; }', + 'float step = (tEnd - tStart) / 32.0;', + 'float t = tStart + step * 0.5;', + 'float tau = 0.0;', + 'vec3 scatter = vec3(0.0);', + 'float cosTA = dot(rd, uSunDir);', + 'float cosTM = dot(rd, uMoonDir);', + 'float sunPhase = 0.25 + 0.75 * pow(max(0.0, cosTA), 2.0);', + 'float moonPhase = 0.25 + 0.75 * pow(max(0.0, cosTM), 2.0);', + 'for (int i = 0; i < 32; i++) {', + 'vec3 p = ro + rd * t;', + 'float alt = max(0.0, length(p - C) - R);', + 'float dens = exp(-alt / Hs);', + 'float dtau = uAtmoDensity * dens * step;', + 'float transBefore = exp(-tau);', + 'float grayMix = clamp(tau / max(0.01, uAtmoGrayTau), 0.0, 1.0);', + 'vec3 atmoCol = mix(uAtmoBlueColor, uAtmoGrayColor, grayMix);', + 'vec3 light = uSunColor * uSunInt * sunPhase + uMoonColor * uMoonInt * moonPhase * 2.0 + max(vec3(uSkyAmb), uStarAmb * vec3(0.7, 0.85, 1.0)) * 0.65;', + 'scatter += atmoCol * light * dtau * transBefore;', + 'tau += dtau;', + 't += step;', + '}', + 'float alpha = clamp(1.0 - exp(-tau), 0.0, 1.0);', + 'vec3 col = scatter / max(alpha, 1e-4) * uAtmoGain;', + 'gl_FragColor = vec4(col, alpha);', + 'return;', + '}', 'if (uMode == 0) {', // skyT floor at 0.5: at and below the horizon the sky keeps // the exact horizon colour - the same value the terrain's @@ -853,6 +919,11 @@ function init(canvas) { uCloudTop: uCloudTop, uCloudBot: uCloudBot, uElev: uElev, uElevOn: uElevOn, uElevScale: uElevScale, uSunDir: uSunDir, uMoonDir: uMoonDir, uMode: uMode, + uAtmoModelOn: uAtmoModelOn, uAtmoHeightUnits: uAtmoHeightUnits, + uAtmoScaleHeightUnits: uAtmoScaleHeightUnits, uAtmoDensity: uAtmoDensity, + uAtmoGrayTau: uAtmoGrayTau, uAtmoGain: uAtmoGain, + uEarthRadius: uEarthRadius, + uAtmoBlueColor: uAtmoBlueColor, uAtmoGrayColor: uAtmoGrayColor, uSunColor: uSunColor, uSunInt: uSunInt, uMoonColor: uMoonColor, uMoonInt: uMoonInt, uSkyAmb: uSkyAmb, uStarAmb: uStarAmb, uPhysical: uPhysical, uAirColor: uAirColor, uCloudAlbedo: uCloudAlbedo, @@ -2041,6 +2112,23 @@ var RAYLEIGH = [0.192, 0.451, 1.0]; var ATMO_AIR_GAIN = (typeof ATMO.airGain === 'number') ? Math.max(0.05, ATMO.airGain) : 0.55; var ATMO_HAZE_RESIDUAL = (typeof ATMO.hazeResidual === 'number') ? Math.max(0.02, Math.min(0.95, ATMO.hazeResidual)) : 0.30; +var ATMO_DISTANCE_FOG = ATMO.distanceFog !== false; +var ATMO_MODEL = ATMO.model || {}; +var ATMO_MODEL_ON = ATMO_MODEL.enabled !== false; +var ATMO_MODEL_HEIGHT_KM = (typeof ATMO_MODEL.heightKm === 'number') + ? Math.max(1.0, Math.min(80.0, ATMO_MODEL.heightKm)) : 35.0; +var ATMO_MODEL_HEIGHT_UNITS = ATMO_MODEL_HEIGHT_KM / KM_PER_TILE; +var ATMO_MODEL_SCALE_HEIGHT_KM = (typeof ATMO_MODEL.scaleHeightKm === 'number') + ? Math.max(1.0, Math.min(30.0, ATMO_MODEL.scaleHeightKm)) : 8.5; +var ATMO_MODEL_SCALE_HEIGHT_UNITS = ATMO_MODEL_SCALE_HEIGHT_KM / KM_PER_TILE; +var ATMO_MODEL_DENSITY = (typeof ATMO_MODEL.density === 'number') + ? Math.max(0.01, Math.min(5.0, ATMO_MODEL.density)) : 0.12; +var ATMO_MODEL_GRAY_TAU = (typeof ATMO_MODEL.grayTau === 'number') + ? Math.max(0.1, Math.min(10.0, ATMO_MODEL.grayTau)) : 1.5; +var ATMO_MODEL_GAIN = (typeof ATMO_MODEL.gain === 'number') + ? Math.max(0.1, Math.min(5.0, ATMO_MODEL.gain)) : 2.0; +var ATMO_MODEL_BLUE_COLOR = new Float32Array([0.22, 0.42, 0.95]); +var ATMO_MODEL_GRAY_COLOR = new Float32Array([0.72, 0.76, 0.82]); // Integrated starlight + airglow. Physically this is about a millionth of // daylight, so it is a stylised floor (CONFIG.atmosphere.starGain) that // keeps clouds, contrails and the ground from going fully black on a @@ -5112,6 +5200,15 @@ window.clouds3dRender = function(target) { gl.uniform3f(s.uMoonColor, _mBeam[0], _mBeam[1], _mBeam[2]); gl.uniform1f(s.uMoonInt, _moonI); gl.uniform1f(s.uSkyAmb, _amb * ATMO_SKY_GAIN); + gl.uniform1f(s.uAtmoModelOn, ATMO_MODEL_ON ? 1.0 : 0.0); + gl.uniform1f(s.uAtmoHeightUnits, ATMO_MODEL_HEIGHT_UNITS); + gl.uniform1f(s.uAtmoScaleHeightUnits, ATMO_MODEL_SCALE_HEIGHT_UNITS); + gl.uniform1f(s.uAtmoDensity, ATMO_MODEL_DENSITY); + gl.uniform1f(s.uAtmoGrayTau, ATMO_MODEL_GRAY_TAU); + gl.uniform1f(s.uAtmoGain, ATMO_MODEL_GAIN); + gl.uniform1f(s.uEarthRadius, EARTH_RADIUS_UNITS); + gl.uniform3fv(s.uAtmoBlueColor, ATMO_MODEL_BLUE_COLOR); + gl.uniform3fv(s.uAtmoGrayColor, ATMO_MODEL_GRAY_COLOR); gl.uniform1f(s.uStarAmb, _star); // Theme cloud hue kept, brightness normalised: the light model, not the // palette, decides how bright a cloud is. @@ -5937,6 +6034,12 @@ window.clouds3dRender = function(target) { gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); gl.uniform1i(s.uMode, 1); gl.drawArrays(gl.TRIANGLES, 0, 3); + + if (ATMO_MODEL_ON) { + gl.uniform1i(s.uMode, 2); + gl.drawArrays(gl.TRIANGLES, 0, 3); + } + // Restore default state for the next frame. gl.disable(gl.BLEND); gl.enable(gl.DEPTH_TEST);