atmosphere: stable front haze using terrain distance texture

This commit is contained in:
Richard Zink committed 2026-10-12 17:29:56 +02:00
1 parent a33e6ddcfc
commit 8af10a4f4a
2 files changed
+168 -2

No files matched your search

+9 -1
View File
@@ -334,13 +334,21 @@ window.CONFIG = {
color: [0.58, 0.64, 0.72] color: [0.58, 0.64, 0.72]
}, },
airHaze: { airHaze: {
enabled: true, enabled: false, // replaced by stable front haze using terrain distance
scaleHeightKm: 8.5, scaleHeightKm: 8.5,
density: 0.16, density: 0.16,
gain: 1.0, gain: 1.0,
maxAlpha: 0.45, maxAlpha: 0.45,
color: [0.58, 0.64, 0.72] color: [0.58, 0.64, 0.72]
}, },
frontHaze: {
enabled: true,
scaleHeightKm: 8.5,
density: 0.22,
gain: 1.0,
maxAlpha: 0.65,
color: [0.58, 0.64, 0.72]
},
model: { model: {
enabled: false // legacy full atmosphere model remains off enabled: false // legacy full atmosphere model remains off
}, },
+159 -1
View File
@@ -116,6 +116,14 @@ var FRAG_SRC = [
'uniform float uAirHazeGain;', 'uniform float uAirHazeGain;',
'uniform float uAirHazeMaxAlpha;', 'uniform float uAirHazeMaxAlpha;',
'uniform vec3 uAirHazeColor;', 'uniform vec3 uAirHazeColor;',
'uniform sampler2D uTerrainDistTex;',
'uniform float uFrontHazeOn;',
'uniform float uFrontHazeDensity;',
'uniform float uFrontHazeScaleHeightUnits;',
'uniform float uFrontHazeGain;',
'uniform float uFrontHazeMaxAlpha;',
'uniform vec3 uFrontHazeColor;',
'uniform float uFarPlane;',
'uniform vec3 uCloudAlbedo;', // theme-tinted but brightness-normalised 'uniform vec3 uCloudAlbedo;', // theme-tinted but brightness-normalised
'uniform float uAirDensity;', // slant optical depth per world unit 'uniform float uAirDensity;', // slant optical depth per world unit
'uniform float uCloudHaze;', // clouds sit above most of the haze: much weaker 'uniform float uCloudHaze;', // clouds sit above most of the haze: much weaker
@@ -354,6 +362,21 @@ var FRAG_SRC = [
'}', '}',
'return column;', 'return column;',
'}', '}',
'float hazeColumnToDistance(vec3 rd, float dist) {',
'if (dist <= 0.0) return 0.0;',
'if (uCurvatureOn < 0.5) return dist;',
'float R = max(1.0, uEarthRadius);',
'float Hs = max(0.05, uFrontHazeScaleHeightUnits);',
'vec3 C = vec3(uCamPos.x, -R, uCamPos.z);',
'float column = 0.0;',
'for (int i = 0; i < 8; i++) {',
'float t = dist * (float(i) + 0.5) / 8.0;',
'vec3 p = uCamPos + rd * t;',
'float alt = max(0.0, length(p - C) - R);',
'column += exp(-alt / Hs) * dist / 8.0 / Hs;',
'}',
'return column;',
'}',
// Two modes driven by uMode: // Two modes driven by uMode:
// 0 = SKY: full-screen pass that paints the sky gradient and // 0 = SKY: full-screen pass that paints the sky gradient and
// stars into the framebuffer (opaque, alpha = 1). // stars into the framebuffer (opaque, alpha = 1).
@@ -434,6 +457,21 @@ var FRAG_SRC = [
'gl_FragColor = vec4(col, alpha);', 'gl_FragColor = vec4(col, alpha);',
'return;', 'return;',
'}', '}',
'if (uMode == 3) {',
'if (uFrontHazeOn < 0.5) { gl_FragColor = vec4(0.0); return; }',
'vec2 uv = gl_FragCoord.xy / uResolution;',
'float distNorm = texture2D(uTerrainDistTex, uv).a;',
'float column = 0.0;',
'if (distNorm > 0.0005 && distNorm < 0.999) {',
'column = hazeColumnToDistance(rd, distNorm * max(1.0, uFarPlane));',
'} else {',
'column = airHazeColumn(rd);',
'}',
'float tau = uFrontHazeDensity * column;',
'float alpha = clamp((1.0 - exp(-tau)) * uFrontHazeGain, 0.0, uFrontHazeMaxAlpha);',
'gl_FragColor = vec4(uFrontHazeColor, alpha);',
'return;',
'}',
'if (uMode == 0) {', 'if (uMode == 0) {',
// skyT floor at 0.5: at and below the horizon the sky keeps // skyT floor at 0.5: at and below the horizon the sky keeps
// the exact horizon colour - the same value the terrain's // the exact horizon colour - the same value the terrain's
@@ -751,6 +789,8 @@ var TERRAIN_FRAG_SRC = [
'uniform float uHazeGain;', 'uniform float uHazeGain;',
'uniform float uHazeMaxAlpha;', 'uniform float uHazeMaxAlpha;',
'uniform vec3 uHazeColor;', 'uniform vec3 uHazeColor;',
'uniform float uDistancePassOn;',
'uniform float uFarPlane;',
'uniform float uEdgeFadeTiles;', 'uniform float uEdgeFadeTiles;',
'uniform vec3 uWireColor;', // non-zero -> debug wireframe overlay (flat colour) 'uniform vec3 uWireColor;', // non-zero -> debug wireframe overlay (flat colour)
'varying vec3 vNormal;', 'varying vec3 vNormal;',
@@ -776,6 +816,11 @@ var TERRAIN_FRAG_SRC = [
'return column;', 'return column;',
'}', '}',
'void main() {', 'void main() {',
'if (uDistancePassOn > 0.5) {',
'float d = clamp(length(vViewPos) / max(1.0, uFarPlane), 0.0, 1.0);',
'gl_FragColor = vec4(0.0, 0.0, 0.0, d);',
'return;',
'}',
'vec2 gUv = vUV;', 'vec2 gUv = vUV;',
// Edge fade: beyond the canvas the texture is clamped to the // Edge fade: beyond the canvas the texture is clamped to the
// edge pixel, so the rim dissolves into the airlight instead. // edge pixel, so the rim dissolves into the airlight instead.
@@ -987,6 +1032,14 @@ function init(canvas) {
var uAirHazeGain = gl.getUniformLocation(prog, 'uAirHazeGain'); var uAirHazeGain = gl.getUniformLocation(prog, 'uAirHazeGain');
var uAirHazeMaxAlpha = gl.getUniformLocation(prog, 'uAirHazeMaxAlpha'); var uAirHazeMaxAlpha = gl.getUniformLocation(prog, 'uAirHazeMaxAlpha');
var uAirHazeColor = gl.getUniformLocation(prog, 'uAirHazeColor'); var uAirHazeColor = gl.getUniformLocation(prog, 'uAirHazeColor');
var uTerrainDistTex = gl.getUniformLocation(prog, 'uTerrainDistTex');
var uFrontHazeOn = gl.getUniformLocation(prog, 'uFrontHazeOn');
var uFrontHazeDensity = gl.getUniformLocation(prog, 'uFrontHazeDensity');
var uFrontHazeScaleHeightUnits = gl.getUniformLocation(prog, 'uFrontHazeScaleHeightUnits');
var uFrontHazeGain = gl.getUniformLocation(prog, 'uFrontHazeGain');
var uFrontHazeMaxAlpha = gl.getUniformLocation(prog, 'uFrontHazeMaxAlpha');
var uFrontHazeColor = gl.getUniformLocation(prog, 'uFrontHazeColor');
var uFarPlane = gl.getUniformLocation(prog, 'uFarPlane');
var uCloudAlbedo = gl.getUniformLocation(prog, 'uCloudAlbedo'); var uCloudAlbedo = gl.getUniformLocation(prog, 'uCloudAlbedo');
var uAirDensity = gl.getUniformLocation(prog, 'uAirDensity'); var uAirDensity = gl.getUniformLocation(prog, 'uAirDensity');
var uCloudHaze = gl.getUniformLocation(prog, 'uCloudHaze'); var uCloudHaze = gl.getUniformLocation(prog, 'uCloudHaze');
@@ -1041,6 +1094,8 @@ function init(canvas) {
var terrainUHazeGain = terrainProg ? gl.getUniformLocation(terrainProg, 'uHazeGain') : null; var terrainUHazeGain = terrainProg ? gl.getUniformLocation(terrainProg, 'uHazeGain') : null;
var terrainUHazeMaxAlpha = terrainProg ? gl.getUniformLocation(terrainProg, 'uHazeMaxAlpha') : null; var terrainUHazeMaxAlpha = terrainProg ? gl.getUniformLocation(terrainProg, 'uHazeMaxAlpha') : null;
var terrainUHazeColor = terrainProg ? gl.getUniformLocation(terrainProg, 'uHazeColor') : null; var terrainUHazeColor = terrainProg ? gl.getUniformLocation(terrainProg, 'uHazeColor') : null;
var terrainUDistancePass = terrainProg ? gl.getUniformLocation(terrainProg, 'uDistancePassOn') : 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 terrainUcloudbot = terrainProg ? gl.getUniformLocation(terrainProg, 'uCloudBot') : null; var terrainUcloudbot = terrainProg ? gl.getUniformLocation(terrainProg, 'uCloudBot') : null;
@@ -1096,7 +1151,13 @@ function init(canvas) {
uAirHazeOn: uAirHazeOn, uAirHazeDensity: uAirHazeDensity, uAirHazeOn: uAirHazeOn, uAirHazeDensity: uAirHazeDensity,
uAirHazeScaleHeightUnits: uAirHazeScaleHeightUnits, uAirHazeScaleHeightUnits: uAirHazeScaleHeightUnits,
uAirHazeGain: uAirHazeGain, uAirHazeMaxAlpha: uAirHazeMaxAlpha, uAirHazeGain: uAirHazeGain, uAirHazeMaxAlpha: uAirHazeMaxAlpha,
uAirHazeColor: uAirHazeColor, uCloudAlbedo: uCloudAlbedo, uAirHazeColor: uAirHazeColor,
uTerrainDistTex: uTerrainDistTex,
uFrontHazeOn: uFrontHazeOn, uFrontHazeDensity: uFrontHazeDensity,
uFrontHazeScaleHeightUnits: uFrontHazeScaleHeightUnits,
uFrontHazeGain: uFrontHazeGain, uFrontHazeMaxAlpha: uFrontHazeMaxAlpha,
uFrontHazeColor: uFrontHazeColor, uFarPlane: uFarPlane,
uCloudAlbedo: uCloudAlbedo,
uAirDensity: uAirDensity, uCloudHaze: uCloudHaze, uAirDensity: uAirDensity, uCloudHaze: uCloudHaze,
uSkyTex: uSkyTex, uSkyOn: uSkyOn, uStarLon: uStarLon, uPole: uPole, uSkyTex: uSkyTex, uSkyOn: uSkyOn, uStarLon: uStarLon, uPole: uPole,
uMoonTex: uMoonTex, uMoonTexOn: uMoonTexOn, uMoonR: uMoonR, uMoonTex: uMoonTex, uMoonTexOn: uMoonTexOn, uMoonR: uMoonR,
@@ -1125,6 +1186,7 @@ function init(canvas) {
uHazeOn: terrainUHazeOn, uHazeDensity: terrainUHazeDensity, uHazeOn: terrainUHazeOn, uHazeDensity: terrainUHazeDensity,
uHazeScaleHeightUnits: terrainUHazeScale, uHazeGain: terrainUHazeGain, uHazeScaleHeightUnits: terrainUHazeScale, uHazeGain: terrainUHazeGain,
uHazeMaxAlpha: terrainUHazeMaxAlpha, uHazeColor: terrainUHazeColor, uHazeMaxAlpha: terrainUHazeMaxAlpha, uHazeColor: terrainUHazeColor,
uDistancePassOn: terrainUDistancePass, uFarPlane: terrainUFarPlane,
uStarAmb: terrainUstarAmb, uStarAmb: terrainUstarAmb,
uTime: terrainUtime, uCloudBot: terrainUcloudbot, uTime: terrainUtime, uCloudBot: terrainUcloudbot,
uCloudTop: terrainUcloudtop, uDensity: terrainUdensity, uCloudTop: terrainUcloudtop, uDensity: terrainUdensity,
@@ -2193,6 +2255,45 @@ function mat4Inverse(out, m) {
var terrainVbo = null; var terrainVbo = null;
var terrainVertCount = 0; var terrainVertCount = 0;
var terrainSample = null; // {h,gw,gh,s,halfX,halfY} — live smoothed grid var terrainSample = null; // {h,gw,gh,s,halfX,halfY} — live smoothed grid
var terrainDistFbo = null;
var terrainDistTex = null;
var terrainDistDepth = null;
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 (terrainDistFbo && terrainDistW === w && terrainDistH === h) return terrainDistOk;
if (terrainDistFbo) {
gl.deleteFramebuffer(terrainDistFbo);
gl.deleteTexture(terrainDistTex);
gl.deleteRenderbuffer(terrainDistDepth);
terrainDistFbo = terrainDistTex = terrainDistDepth = null;
}
terrainDistW = w; terrainDistH = h;
terrainDistTex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, terrainDistTex);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, w, h, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.LINEAR);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
terrainDistDepth = gl.createRenderbuffer();
gl.bindRenderbuffer(gl.RENDERBUFFER, terrainDistDepth);
gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, w, h);
terrainDistFbo = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, terrainDistFbo);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, terrainDistTex, 0);
gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, terrainDistDepth);
terrainDistOk = gl.checkFramebufferStatus(gl.FRAMEBUFFER) === gl.FRAMEBUFFER_COMPLETE;
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.bindTexture(gl.TEXTURE_2D, null);
gl.bindRenderbuffer(gl.RENDERBUFFER, null);
if (!terrainDistOk) {
console.warn('[clouds3d] terrain distance FBO incomplete; front haze disabled');
}
return terrainDistOk;
}
function buildTerrainMesh(gl, state) { function buildTerrainMesh(gl, state) {
if (!elevCanvas || !elevCtx || terrainVbo) return; if (!elevCanvas || !elevCtx || terrainVbo) return;
if (typeof elevCtx.getImageData !== 'function') return; if (typeof elevCtx.getImageData !== 'function') return;
@@ -2601,6 +2702,22 @@ var AIR_HAZE_COLOR = new Float32Array(
? [AIR_HAZE_CFG.color[0], AIR_HAZE_CFG.color[1], AIR_HAZE_CFG.color[2]] ? [AIR_HAZE_CFG.color[0], AIR_HAZE_CFG.color[1], AIR_HAZE_CFG.color[2]]
: [0.58, 0.64, 0.72] : [0.58, 0.64, 0.72]
); );
var FRONT_HAZE_CFG = ATMO.frontHaze || AIR_HAZE_CFG || {};
var FRONT_HAZE_ON = FRONT_HAZE_CFG.enabled !== false;
var FRONT_HAZE_SCALE_HEIGHT_KM = (typeof FRONT_HAZE_CFG.scaleHeightKm === 'number')
? Math.max(1.0, Math.min(30.0, FRONT_HAZE_CFG.scaleHeightKm)) : 8.5;
var FRONT_HAZE_SCALE_HEIGHT_UNITS = FRONT_HAZE_SCALE_HEIGHT_KM / KM_PER_TILE;
var FRONT_HAZE_DENSITY = (typeof FRONT_HAZE_CFG.density === 'number')
? Math.max(0.01, Math.min(5.0, FRONT_HAZE_CFG.density)) : 0.22;
var FRONT_HAZE_GAIN = (typeof FRONT_HAZE_CFG.gain === 'number')
? Math.max(0.1, Math.min(5.0, FRONT_HAZE_CFG.gain)) : 1.0;
var FRONT_HAZE_MAX_ALPHA = (typeof FRONT_HAZE_CFG.maxAlpha === 'number')
? Math.max(0.05, Math.min(1.0, FRONT_HAZE_CFG.maxAlpha)) : 0.65;
var FRONT_HAZE_COLOR = new Float32Array(
Array.isArray(FRONT_HAZE_CFG.color) && FRONT_HAZE_CFG.color.length >= 3
? [FRONT_HAZE_CFG.color[0], FRONT_HAZE_CFG.color[1], FRONT_HAZE_CFG.color[2]]
: [0.58, 0.64, 0.72]
);
var CURV_CFG = (window.CONFIG && window.CONFIG.curvature) || {}; var CURV_CFG = (window.CONFIG && window.CONFIG.curvature) || {};
var CURVATURE_ON = CURV_CFG.enabled !== false; var CURVATURE_ON = CURV_CFG.enabled !== false;
var CURV_EXAG = (typeof CURV_CFG.exaggeration === 'number') var CURV_EXAG = (typeof CURV_CFG.exaggeration === 'number')
@@ -5699,6 +5816,14 @@ window.clouds3dRender = function(target) {
gl.uniform1f(s.uAirHazeGain, AIR_HAZE_GAIN); gl.uniform1f(s.uAirHazeGain, AIR_HAZE_GAIN);
gl.uniform1f(s.uAirHazeMaxAlpha, AIR_HAZE_MAX_ALPHA); gl.uniform1f(s.uAirHazeMaxAlpha, AIR_HAZE_MAX_ALPHA);
gl.uniform3f(s.uAirHazeColor, AIR_HAZE_COLOR[0], AIR_HAZE_COLOR[1], AIR_HAZE_COLOR[2]); gl.uniform3f(s.uAirHazeColor, AIR_HAZE_COLOR[0], AIR_HAZE_COLOR[1], AIR_HAZE_COLOR[2]);
gl.uniform1i(s.uTerrainDistTex, 7);
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);
gl.uniform1f(s.uFrontHazeGain, FRONT_HAZE_GAIN);
gl.uniform1f(s.uFrontHazeMaxAlpha, FRONT_HAZE_MAX_ALPHA);
gl.uniform3f(s.uFrontHazeColor, FRONT_HAZE_COLOR[0], FRONT_HAZE_COLOR[1], FRONT_HAZE_COLOR[2]);
gl.uniform1f(s.uFarPlane, 100.0);
gl.uniform3f(s.uSunColor, _beam[0], _beam[1], _beam[2]); gl.uniform3f(s.uSunColor, _beam[0], _beam[1], _beam[2]);
gl.uniform1f(s.uSunInt, _beamL); gl.uniform1f(s.uSunInt, _beamL);
gl.uniform3f(s.uMoonColor, _mBeam[0], _mBeam[1], _mBeam[2]); gl.uniform3f(s.uMoonColor, _mBeam[0], _mBeam[1], _mBeam[2]);
@@ -6014,6 +6139,23 @@ window.clouds3dRender = function(target) {
gl.enableVertexAttribArray(terr.aColor); gl.enableVertexAttribArray(terr.aColor);
gl.vertexAttribPointer(terr.aColor, 3, gl.FLOAT, false, STRIDE, 32); gl.vertexAttribPointer(terr.aColor, 3, gl.FLOAT, false, STRIDE, 32);
} }
var distPassOk = FRONT_HAZE_ON && ensureTerrainDistFbo(gl, canvas.width, canvas.height);
if (distPassOk) {
gl.bindFramebuffer(gl.FRAMEBUFFER, terrainDistFbo);
gl.viewport(0, 0, terrainDistW, terrainDistH);
gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);
gl.enable(gl.DEPTH_TEST);
gl.depthMask(true);
gl.disable(gl.BLEND);
gl.uniform1f(terr.uDistancePassOn, 1.0);
gl.uniform1f(terr.uFarPlane, 100.0);
gl.drawArrays(gl.TRIANGLES, 0, terr.vertCount);
gl.bindFramebuffer(gl.FRAMEBUFFER, null);
gl.viewport(0, 0, canvas.width, canvas.height);
}
gl.uniform1f(terr.uDistancePassOn, 0.0);
gl.uniform1f(terr.uFarPlane, 100.0);
gl.drawArrays(gl.TRIANGLES, 0, terr.vertCount); gl.drawArrays(gl.TRIANGLES, 0, terr.vertCount);
// Wireframe overlay: draw the mesh edges as bright lines on top // Wireframe overlay: draw the mesh edges as bright lines on top
@@ -6550,6 +6692,22 @@ window.clouds3dRender = function(target) {
gl.drawArrays(gl.TRIANGLES, 0, 3); gl.drawArrays(gl.TRIANGLES, 0, 3);
} }
if (FRONT_HAZE_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, 3);
gl.drawArrays(gl.TRIANGLES, 0, 3);
}
// Restore default state for the next frame. // Restore default state for the next frame.
gl.disable(gl.BLEND); gl.disable(gl.BLEND);
gl.enable(gl.DEPTH_TEST); gl.enable(gl.DEPTH_TEST);