diff --git a/config.js b/config.js index 8de18e3..b0b4e7f 100644 --- a/config.js +++ b/config.js @@ -334,13 +334,21 @@ window.CONFIG = { color: [0.58, 0.64, 0.72] }, airHaze: { - enabled: true, + enabled: false, // replaced by stable front haze using terrain distance scaleHeightKm: 8.5, density: 0.16, gain: 1.0, maxAlpha: 0.45, 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: { enabled: false // legacy full atmosphere model remains off }, diff --git a/vendor/clouds3d.js b/vendor/clouds3d.js index bada5f5..fc961a3 100644 --- a/vendor/clouds3d.js +++ b/vendor/clouds3d.js @@ -116,6 +116,14 @@ var FRAG_SRC = [ 'uniform float uAirHazeGain;', 'uniform float uAirHazeMaxAlpha;', '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 float uAirDensity;', // slant optical depth per world unit 'uniform float uCloudHaze;', // clouds sit above most of the haze: much weaker @@ -354,6 +362,21 @@ var FRAG_SRC = [ '}', '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: // 0 = SKY: full-screen pass that paints the sky gradient and // stars into the framebuffer (opaque, alpha = 1). @@ -434,6 +457,21 @@ var FRAG_SRC = [ 'gl_FragColor = vec4(col, alpha);', '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) {', // skyT floor at 0.5: at and below the horizon the sky keeps // the exact horizon colour - the same value the terrain's @@ -751,6 +789,8 @@ var TERRAIN_FRAG_SRC = [ 'uniform float uHazeGain;', 'uniform float uHazeMaxAlpha;', 'uniform vec3 uHazeColor;', + 'uniform float uDistancePassOn;', + 'uniform float uFarPlane;', 'uniform float uEdgeFadeTiles;', 'uniform vec3 uWireColor;', // non-zero -> debug wireframe overlay (flat colour) 'varying vec3 vNormal;', @@ -776,6 +816,11 @@ var TERRAIN_FRAG_SRC = [ 'return column;', '}', '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;', // Edge fade: beyond the canvas the texture is clamped to the // edge pixel, so the rim dissolves into the airlight instead. @@ -987,6 +1032,14 @@ function init(canvas) { var uAirHazeGain = gl.getUniformLocation(prog, 'uAirHazeGain'); var uAirHazeMaxAlpha = gl.getUniformLocation(prog, 'uAirHazeMaxAlpha'); 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 uAirDensity = gl.getUniformLocation(prog, 'uAirDensity'); var uCloudHaze = gl.getUniformLocation(prog, 'uCloudHaze'); @@ -1041,6 +1094,8 @@ function init(canvas) { 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 terrainUDistancePass = terrainProg ? gl.getUniformLocation(terrainProg, 'uDistancePassOn') : null; + 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 terrainUcloudbot = terrainProg ? gl.getUniformLocation(terrainProg, 'uCloudBot') : null; @@ -1096,7 +1151,13 @@ function init(canvas) { uAirHazeOn: uAirHazeOn, uAirHazeDensity: uAirHazeDensity, uAirHazeScaleHeightUnits: uAirHazeScaleHeightUnits, 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, uSkyTex: uSkyTex, uSkyOn: uSkyOn, uStarLon: uStarLon, uPole: uPole, uMoonTex: uMoonTex, uMoonTexOn: uMoonTexOn, uMoonR: uMoonR, @@ -1125,6 +1186,7 @@ function init(canvas) { uHazeOn: terrainUHazeOn, uHazeDensity: terrainUHazeDensity, uHazeScaleHeightUnits: terrainUHazeScale, uHazeGain: terrainUHazeGain, uHazeMaxAlpha: terrainUHazeMaxAlpha, uHazeColor: terrainUHazeColor, + uDistancePassOn: terrainUDistancePass, uFarPlane: terrainUFarPlane, uStarAmb: terrainUstarAmb, uTime: terrainUtime, uCloudBot: terrainUcloudbot, uCloudTop: terrainUcloudtop, uDensity: terrainUdensity, @@ -2193,6 +2255,45 @@ function mat4Inverse(out, m) { var terrainVbo = null; var terrainVertCount = 0; 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) { if (!elevCanvas || !elevCtx || terrainVbo) 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]] : [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 CURVATURE_ON = CURV_CFG.enabled !== false; 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.uAirHazeMaxAlpha, AIR_HAZE_MAX_ALPHA); 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.uniform1f(s.uSunInt, _beamL); gl.uniform3f(s.uMoonColor, _mBeam[0], _mBeam[1], _mBeam[2]); @@ -6014,6 +6139,23 @@ 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); + 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); // 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); } + 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. gl.disable(gl.BLEND); gl.enable(gl.DEPTH_TEST);