From 989310ab3b0750c24a996b93686bd47e0b6c73a1 Mon Sep 17 00:00:00 2001 From: Richard Zink Date: Mon, 12 Oct 2026 17:15:33 +0200 Subject: [PATCH] atmosphere: replace flickering full-screen haze with stable terrain haze --- config.js | 10 ++++++- vendor/clouds3d.js | 65 +++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 71 insertions(+), 4 deletions(-) diff --git a/config.js b/config.js index 8da7816..ffd4e21 100644 --- a/config.js +++ b/config.js @@ -317,7 +317,7 @@ window.CONFIG = { rayleighGain: 8.0, // exposure for the physical sky term distanceFog: false, // legacy aerial perspective is off haze: { - enabled: true, + enabled: false, // full-screen haze pass causes horizon/sky flicker heightKm: 100, scaleHeightKm: 8.5, density: 0.18, @@ -325,6 +325,14 @@ window.CONFIG = { gain: 1.0, maxAlpha: 0.55 }, + terrainHaze: { + enabled: true, + scaleHeightKm: 8.5, + density: 0.22, + gain: 1.0, + maxAlpha: 0.65, + color: [0.58, 0.64, 0.72] + }, model: { enabled: false // legacy full atmosphere model remains off }, diff --git a/vendor/clouds3d.js b/vendor/clouds3d.js index 0587cf9..088589d 100644 --- a/vendor/clouds3d.js +++ b/vendor/clouds3d.js @@ -688,6 +688,12 @@ var TERRAIN_FRAG_SRC = [ 'uniform vec3 uCamPos;', // world-space camera, for the view direction 'uniform float uEarthRadius;', 'uniform float uCurvatureOn;', + 'uniform float uHazeOn;', + 'uniform float uHazeDensity;', + 'uniform float uHazeScaleHeightUnits;', + 'uniform float uHazeGain;', + 'uniform float uHazeMaxAlpha;', + 'uniform vec3 uHazeColor;', 'uniform float uEdgeFadeTiles;', 'uniform vec3 uWireColor;', // non-zero -> debug wireframe overlay (flat colour) 'varying vec3 vNormal;', @@ -697,6 +703,21 @@ var TERRAIN_FRAG_SRC = [ 'varying vec3 vWorld;', 'varying vec3 vColor;', CLOUD_SAMPLER_GLSL, + 'float terrainHazeColumn(vec3 world) {', + 'float dist = length(world - uCamPos);', + 'if (dist <= 0.0) return 0.0;', + 'if (uCurvatureOn < 0.5) return dist;', + 'float R = max(1.0, uEarthRadius);', + 'float Hs = max(0.05, uHazeScaleHeightUnits);', + 'vec3 C = vec3(uCamPos.x, -R, uCamPos.z);', + 'float column = 0.0;', + 'for (int i = 0; i < 8; i++) {', + 'vec3 p = mix(uCamPos, world, (float(i) + 0.5) / 8.0);', + 'float alt = max(0.0, length(p - C) - R);', + 'column += exp(-alt / Hs) * dist / 8.0 / Hs;', + '}', + 'return column;', + '}', 'void main() {', 'vec2 gUv = vUV;', // Edge fade: beyond the canvas the texture is clamped to the @@ -784,12 +805,19 @@ var TERRAIN_FRAG_SRC = [ // veil but into the airlight itself - blue-biased (Rayleigh, // 1/lambda^4) and brighter towards the sun through the (1+cos^2)/2 // phase function. - 'float tau = uAirDensity * dist * (0.75 + 0.25 * smoothstep(0.4, 0.0, vY));', - 'float trans = exp(-tau);', + 'float hazeTau = 0.0;', + 'if (uHazeOn > 0.5) {', + 'hazeTau = uHazeDensity * terrainHazeColumn(vWorld);', + '} else {', + 'hazeTau = uAirDensity * dist * (0.75 + 0.25 * smoothstep(0.4, 0.0, vY));', + '}', + 'float trans = exp(-hazeTau);', 'vec3 vd = normalize(vWorld - uCamPos);', 'float cosT = dot(vd, uSunDir);', 'float rphase = 0.5 + 0.5 * cosT * cosT;', - 'col = col * trans + uAirColor * rphase * (1.0 - trans);', + 'float hazeAlpha = clamp((1.0 - trans) * uHazeGain, 0.0, uHazeMaxAlpha);', + 'vec3 hazeCol = mix(uHazeColor, uAirColor, 0.25) * (0.75 + 0.25 * rphase);', + 'col = mix(col, hazeCol, hazeAlpha);', // Edge fade to the same horizon colour 'col = mix(horizonCol, col, edgeFade);', // Wireframe overlay: when CONFIG.terrain.wireframe is set the @@ -944,6 +972,12 @@ function init(canvas) { var terrainUEarthRadius = terrainProg ? gl.getUniformLocation(terrainProg, 'uEarthRadius') : null; var terrainUCurvatureOn = terrainProg ? gl.getUniformLocation(terrainProg, 'uCurvatureOn') : null; var terrainUedgeFade = terrainProg ? gl.getUniformLocation(terrainProg, 'uEdgeFadeTiles') : null; + var terrainUHazeOn = terrainProg ? gl.getUniformLocation(terrainProg, 'uHazeOn') : null; + var terrainUHazeDensity = terrainProg ? gl.getUniformLocation(terrainProg, 'uHazeDensity') : null; + var terrainUHazeScale = terrainProg ? gl.getUniformLocation(terrainProg, 'uHazeScaleHeightUnits') : null; + var terrainUHazeGain = terrainProg ? gl.getUniformLocation(terrainProg, 'uHazeGain') : null; + var terrainUHazeMaxAlpha = terrainProg ? gl.getUniformLocation(terrainProg, 'uHazeMaxAlpha') : null; + var terrainUHazeColor = terrainProg ? gl.getUniformLocation(terrainProg, 'uHazeColor') : null; var terrainUstarAmb = terrainProg ? gl.getUniformLocation(terrainProg, 'uStarAmb') : null; var terrainUtime = terrainProg ? gl.getUniformLocation(terrainProg, 'uTime') : null; var terrainUcloudbot = terrainProg ? gl.getUniformLocation(terrainProg, 'uCloudBot') : null; @@ -1021,6 +1055,9 @@ function init(canvas) { uCamPos: terrainUcampos, uEarthRadius: terrainUEarthRadius, uCurvatureOn: terrainUCurvatureOn, uEdgeFadeTiles: terrainUedgeFade, + uHazeOn: terrainUHazeOn, uHazeDensity: terrainUHazeDensity, + uHazeScaleHeightUnits: terrainUHazeScale, uHazeGain: terrainUHazeGain, + uHazeMaxAlpha: terrainUHazeMaxAlpha, uHazeColor: terrainUHazeColor, uStarAmb: terrainUstarAmb, uTime: terrainUtime, uCloudBot: terrainUcloudbot, uCloudTop: terrainUcloudtop, uDensity: terrainUdensity, @@ -2463,6 +2500,22 @@ var ATMO_MODEL_MAX_ALPHA = (typeof HAZE_CFG.maxAlpha === 'number') ? Math.max(0.05, Math.min(1.0, HAZE_CFG.maxAlpha)) : ((typeof ATMO_MODEL.maxAlpha === 'number') ? Math.max(0.05, Math.min(1.0, ATMO_MODEL.maxAlpha)) : 0.55); +var TERRAIN_HAZE_CFG = ATMO.terrainHaze || {}; +var TERRAIN_HAZE_ON = TERRAIN_HAZE_CFG.enabled !== false; +var TERRAIN_HAZE_SCALE_HEIGHT_KM = (typeof TERRAIN_HAZE_CFG.scaleHeightKm === 'number') + ? Math.max(1.0, Math.min(30.0, TERRAIN_HAZE_CFG.scaleHeightKm)) : 8.5; +var TERRAIN_HAZE_SCALE_HEIGHT_UNITS = TERRAIN_HAZE_SCALE_HEIGHT_KM / KM_PER_TILE; +var TERRAIN_HAZE_DENSITY = (typeof TERRAIN_HAZE_CFG.density === 'number') + ? Math.max(0.01, Math.min(5.0, TERRAIN_HAZE_CFG.density)) : 0.22; +var TERRAIN_HAZE_GAIN = (typeof TERRAIN_HAZE_CFG.gain === 'number') + ? Math.max(0.1, Math.min(5.0, TERRAIN_HAZE_CFG.gain)) : 1.0; +var TERRAIN_HAZE_MAX_ALPHA = (typeof TERRAIN_HAZE_CFG.maxAlpha === 'number') + ? Math.max(0.05, Math.min(1.0, TERRAIN_HAZE_CFG.maxAlpha)) : 0.65; +var TERRAIN_HAZE_COLOR = new Float32Array( + Array.isArray(TERRAIN_HAZE_CFG.color) && TERRAIN_HAZE_CFG.color.length >= 3 + ? [TERRAIN_HAZE_CFG.color[0], TERRAIN_HAZE_CFG.color[1], TERRAIN_HAZE_CFG.color[2]] + : [0.58, 0.64, 0.72] +); var CURV_CFG = (window.CONFIG && window.CONFIG.curvature) || {}; var CURVATURE_ON = CURV_CFG.enabled !== false; var CURV_EXAG = (typeof CURV_CFG.exaggeration === 'number') @@ -5845,6 +5898,12 @@ window.clouds3dRender = function(target) { gl.uniform1f(terr.uEarthRadius, EARTH_RADIUS_UNITS); gl.uniform1f(terr.uCurvatureOn, CURVATURE_ON ? 1.0 : 0.0); gl.uniform1f(terr.uEdgeFadeTiles, EDGE_FADE_TILES); + gl.uniform1f(terr.uHazeOn, TERRAIN_HAZE_ON ? 1.0 : 0.0); + gl.uniform1f(terr.uHazeDensity, TERRAIN_HAZE_DENSITY); + gl.uniform1f(terr.uHazeScaleHeightUnits, TERRAIN_HAZE_SCALE_HEIGHT_UNITS); + gl.uniform1f(terr.uHazeGain, TERRAIN_HAZE_GAIN); + gl.uniform1f(terr.uHazeMaxAlpha, TERRAIN_HAZE_MAX_ALPHA); + gl.uniform3f(terr.uHazeColor, TERRAIN_HAZE_COLOR[0], TERRAIN_HAZE_COLOR[1], TERRAIN_HAZE_COLOR[2]); gl.bindBuffer(gl.ARRAY_BUFFER, terr.vbo); var STRIDE = 11 * 4; // 11 floats per vertex