diff --git a/config.js b/config.js index d32fae5..778ffb6 100644 --- a/config.js +++ b/config.js @@ -335,6 +335,14 @@ window.CONFIG = { start: 0.35, // world units: clouds fade out very close to camera end: 2.5 // full density from this distance onward }, + // Terrain-following cloud base. The DEM height grid is sampled in + // the cloud shader, so the cloud bottom rises locally to keep a + // clearance above the visible ground. 'bottom' remains the global + // floor for areas without terrain data. + groundFollow: { + enabled: true, // false = flat cloud deck + clearanceM: 50 // metres above terrain, exaggerated with terrain + }, // Coverage per weather level, 0 = clear, 1 = overcast. Only the // levels listed here override the built-in defaults // (sunny 0.20, cloudy 0.70, stormy 0.80, night 0.45). Live weather diff --git a/vendor/clouds3d.js b/vendor/clouds3d.js index 8074cef..30e0ece 100644 --- a/vendor/clouds3d.js +++ b/vendor/clouds3d.js @@ -85,6 +85,10 @@ var FRAG_SRC = [ 'uniform float uSunAlt;', 'uniform float uCloudTop;', // cloud deck upper edge (world Y) 'uniform float uCloudBot;', // cloud deck lower edge (world Y) + 'uniform float uCloudGroundFollow;', + 'uniform float uCloudClearanceUnits;', + 'float terrElev(vec2 xz);', + 'float cloudBaseY(vec2 xz);', 'uniform float uCloudNearFadeStart;', 'uniform float uCloudNearFadeEnd;', 'uniform sampler2D uElev;', // terrarium RGB height grid, same UV as uGround @@ -230,9 +234,9 @@ var FRAG_SRC = [ 'float d = fbm3(cp * 1.80 + vec3(uTime * 0.015, 7.3, uTime * 0.012));', 'n = n + d * mix(0.22, 0.0, soft / 0.75);', '}', - // Cloud layer between uCloudBot and uCloudTop, densest in the - // middle (bell curve). The layer follows the Earth curvature. - 'float h = clamp((py - uCloudBot) / (uCloudTop - uCloudBot), 0.0, 1.0);', + // Cloud layer between the terrain-following base and uCloudTop. + 'float bot = cloudBaseY(p.xz);', + 'float h = clamp((py - bot) / max(0.01, uCloudTop - bot), 0.0, 1.0);', 'float profile = 4.0 * h * (1.0 - h); // bell curve', 'profile = max(profile, 0.0);', 'return smoothstep(baseThresh, baseThresh + 0.34 + soft * 0.28, n) * profile;', @@ -266,6 +270,14 @@ var FRAG_SRC = [ 'vec3 t = texture2D(uElev, euv).rgb;', 'return (t.r * 256.0 + t.g + t.b / 256.0) * 255.0 - 32768.0;', '}', + 'float cloudBaseY(vec2 xz) {', + 'float base = uCloudBot;', + 'if (uCloudGroundFollow > 0.5 && uElevOn > 0.5) {', + 'float terrainY = terrElev(xz) * uElevScale;', + 'base = max(base, terrainY + uCloudClearanceUnits);', + '}', + 'return min(base, uCloudTop - 0.05);', + '}', 'vec2 curvedLayerRoots(float layerY, vec3 ro, vec3 rd) {', 'float R = max(1.0, uEarthRadius);', 'float a = (uCurvatureOn > 0.5) ? dot(rd.xz, rd.xz) / (2.0 * R) : 0.0;', @@ -421,7 +433,8 @@ var FRAG_SRC = [ 'float d = fbm3(q * 1.80 + vec3(uTime * 0.015, 7.3, uTime * 0.012));', '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 bot = cloudBaseY(p.xz);', + 'float h = clamp((alt - bot) / max(0.01, uCloudTop - bot), 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;', @@ -725,7 +738,8 @@ var FRAG_SRC = [ '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);', + 'float botHere = cloudBaseY(p.xz);', + 'float lit = clamp((alt - botHere) / max(0.01, uCloudTop - botHere), 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;', @@ -834,6 +848,14 @@ var CLOUD_SAMPLER_GLSL = [ 'vec3 t = texture2D(uElev, euv).rgb;', 'return (t.r * 256.0 + t.g + t.b / 256.0) * 255.0 - 32768.0;', '}', + 'float cloudBaseYT(vec2 xz) {', + 'float base = uCloudBot;', + 'if (uCloudGroundFollow > 0.5) {', + 'float terrainY = terrElevT(xz) * uElevScale;', + 'base = max(base, terrainY + uCloudClearanceUnits);', + '}', + 'return min(base, uCloudTop - 0.05);', + '}', // Terrain self-shadow: march from the surface towards the sun over the // height field and keep the smallest clearance between ray and ground. // The clearance (not a hard hit test) gives a soft penumbra, and the @@ -887,7 +909,8 @@ var CLOUD_SAMPLER_GLSL = [ 'float d = fbm3(cp * 1.80 + vec3(uTime * 0.015, 7.3, uTime * 0.012));', 'n = n + d * mix(0.22, 0.0, soft / 0.75);', '}', - 'float h = clamp((py - uCloudBot) / (uCloudTop - uCloudBot), 0.0, 1.0);', + 'float bot = cloudBaseYT(p.xz);', + 'float h = clamp((py - bot) / max(0.01, uCloudTop - bot), 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;', @@ -914,6 +937,8 @@ var TERRAIN_FRAG_SRC = [ 'uniform vec3 uWind;', // cloud advection vector, world units / s 'uniform float uCloudBot;', 'uniform float uCloudTop;', + 'uniform float uCloudGroundFollow;', + 'uniform float uCloudClearanceUnits;', 'uniform float uDensity;', // cloud coverage, same as the sky pass 'uniform float uCloudShadowOn;', 'uniform float uCloudShadowDensity;', // optical depth per density sample @@ -1145,6 +1170,8 @@ function init(canvas) { var uSunAlt = gl.getUniformLocation(prog, 'uSunAlt'); var uCloudTop = gl.getUniformLocation(prog, 'uCloudTop'); var uCloudBot = gl.getUniformLocation(prog, 'uCloudBot'); + var uCloudGroundFollow = gl.getUniformLocation(prog, 'uCloudGroundFollow'); + var uCloudClearanceUnits = gl.getUniformLocation(prog, 'uCloudClearanceUnits'); var uElev = gl.getUniformLocation(prog, 'uElev'); var uElevOn = gl.getUniformLocation(prog, 'uElevOn'); var uElevScale = gl.getUniformLocation(prog, 'uElevScale'); @@ -1268,6 +1295,8 @@ function init(canvas) { var terrainUwind = terrainProg ? gl.getUniformLocation(terrainProg, 'uWind') : null; var terrainUcloudbot = terrainProg ? gl.getUniformLocation(terrainProg, 'uCloudBot') : null; var terrainUcloudtop = terrainProg ? gl.getUniformLocation(terrainProg, 'uCloudTop') : null; + var terrainUcloudfollow = terrainProg ? gl.getUniformLocation(terrainProg, 'uCloudGroundFollow') : null; + var terrainUcloudclearance = terrainProg ? gl.getUniformLocation(terrainProg, 'uCloudClearanceUnits') : null; var terrainUdensity = terrainProg ? gl.getUniformLocation(terrainProg, 'uDensity') : null; var terrainUcloudshadow = terrainProg ? gl.getUniformLocation(terrainProg, 'uCloudShadowOn') : null; var terrainUcsDens = terrainProg ? gl.getUniformLocation(terrainProg, 'uCloudShadowDensity') : null; @@ -1307,6 +1336,7 @@ function init(canvas) { uSpeed: uSpeed, uDensity: uDensity, uDayProgress: uDayProgress, uSunAlt: uSunAlt, uCloudTop: uCloudTop, uCloudBot: uCloudBot, + uCloudGroundFollow: uCloudGroundFollow, uCloudClearanceUnits: uCloudClearanceUnits, uCloudNearFadeStart: gl.getUniformLocation(prog, 'uCloudNearFadeStart'), uCloudNearFadeEnd: gl.getUniformLocation(prog, 'uCloudNearFadeEnd'), uElev: uElev, uElevOn: uElevOn, uElevScale: uElevScale, uTerrainExag: uTerrainExag, @@ -1372,6 +1402,7 @@ function init(canvas) { uStarAmb: terrainUstarAmb, uTime: terrainUtime, uWind: terrainUwind, uCloudBot: terrainUcloudbot, uCloudTop: terrainUcloudtop, uDensity: terrainUdensity, + uCloudGroundFollow: terrainUcloudfollow, uCloudClearanceUnits: terrainUcloudclearance, uCloudShadowOn: terrainUcloudshadow, uCloudShadowDensity: terrainUcsDens, uCloudShadowFloor: terrainUcsFloor, @@ -1553,6 +1584,11 @@ var TERRAIN_EXAG = (typeof TERRAIN_CFG.exaggeration === 'number' && TERRAIN_CFG.exaggeration > 0) ? TERRAIN_CFG.exaggeration : 8; // metres -> world units, including the vertical exaggeration var ELEV_SCALE_UNITS = TERRAIN_EXAG / (KM_PER_TILE * 1000.0); +var CLOUD_GROUND_FOLLOW_CFG = (window.CONFIG && window.CONFIG.clouds && window.CONFIG.clouds.groundFollow) || {}; +var CLOUD_GROUND_FOLLOW = CLOUD_GROUND_FOLLOW_CFG.enabled === true ? 1.0 : 0.0; +var CLOUD_CLEARANCE_M = (typeof CLOUD_GROUND_FOLLOW_CFG.clearanceM === 'number') + ? Math.max(0, CLOUD_GROUND_FOLLOW_CFG.clearanceM) : 50; +var CLOUD_CLEARANCE_UNITS = CLOUD_CLEARANCE_M * ELEV_SCALE_UNITS; var ORBIT_CFG = (window.CONFIG && window.CONFIG.orbit) || null; var ORBIT_R = 0; // figure-8 half-diagonal (lobe tip reach) in tiles @@ -7384,6 +7420,8 @@ window.clouds3dRender = function(target) { gl.uniform1f(s.uCloudBot, CLOUD_BOT); gl.uniform1f(s.uCloudNearFadeStart, CLOUD_NEAR_FADE_START); gl.uniform1f(s.uCloudNearFadeEnd, CLOUD_NEAR_FADE_END); + gl.uniform1f(s.uCloudGroundFollow, CLOUD_GROUND_FOLLOW); + gl.uniform1f(s.uCloudClearanceUnits, CLOUD_CLEARANCE_UNITS); // Ground mosaic uniforms + periodic re-fetch trigger. // uGroundCenter is the world position (X, Z) of the centre tile's @@ -7656,6 +7694,8 @@ window.clouds3dRender = function(target) { gl.uniform3f(terr.uWind, _cloudWind[0], _cloudWind[1], _cloudWind[2]); gl.uniform1f(terr.uCloudBot, CLOUD_BOT); gl.uniform1f(terr.uCloudTop, CLOUD_TOP); + gl.uniform1f(terr.uCloudGroundFollow, CLOUD_GROUND_FOLLOW); + gl.uniform1f(terr.uCloudClearanceUnits, CLOUD_CLEARANCE_UNITS); gl.uniform1f(terr.uDensity, density); gl.uniform1f(terr.uCloudShadowOn, ATMO_CLOUD_SHADOWS ? 1.0 : 0.0); gl.uniform1f(terr.uCloudShadowDensity, ATMO_CS_DENSITY);