diff --git a/config.js b/config.js index 58abc46..6639456 100644 --- a/config.js +++ b/config.js @@ -149,12 +149,25 @@ window.CONFIG = { dryMinutes: 30, // roads dry out after this many dry minutes roadGain: 0.8, // extra road-ribbon glow when wet lightGain: 0.35 // extra halo on vehicle lights when wet + }, + snowCover: { + enabled: true, + meltMinutes: 90, // snow melts after this many warm minutes + baseLineM: 900, // snowline at about +5 C + tempLineMPerC: 80, // snowline moves down as temperature drops + minLineM: 0, // full snowline in deep cold + maxLineM: 2500, // no snow above this line when warm + transitionM: 120, // soft transition band around the snowline + slopeMax: 0.72, // steep slopes keep less snow + gain: 0.85 // maximum whiteness of the snow albedo } // weatherEffects: { enabled: false } // weatherEffects: { rain: { maxAlpha: 0.55, speed: 1.4 }, snow: { enabled: false } } // weatherEffects: { lightning: { gain: 2.0, minDelayS: 3, maxDelayS: 12 } } // weatherEffects: { wetRoads: { enabled: false } } // weatherEffects: { wetRoads: { dryMinutes: 10, roadGain: 1.4, lightGain: 0.7 } } + // weatherEffects: { snowCover: { baseLineM: 600, tempLineMPerC: 120, gain: 1.0 } } + // weatherEffects: { snowCover: { enabled: false } } }, // ------------------------------------------------------------------ diff --git a/vendor/clouds3d.js b/vendor/clouds3d.js index e402b68..7272653 100644 --- a/vendor/clouds3d.js +++ b/vendor/clouds3d.js @@ -896,6 +896,11 @@ var TERRAIN_FRAG_SRC = [ 'uniform float uCloudShadowAmbient;', // how much of the skylight a cloud also blocks 'uniform sampler2D uElev;', // height field, same grid the mesh was built from 'uniform float uElevScale;', // metres -> world units (incl. exaggeration) + 'uniform float uSnowCover;', // accumulated snow cover 0..1 + 'uniform float uSnowLineM;', // dynamic snowline in metres + 'uniform float uSnowTransitionM;', // soft transition band around the snowline + 'uniform float uSnowSlopeMax;', // slope cutoff for snow retention + 'uniform float uSnowGain;', // maximum whiteness 'uniform float uTerrainShadowOn;', 'uniform float uTerrainShadowFloor;', // brightness a fully shadowed slope keeps 'uniform vec3 uAirColor;', // Rayleigh airlight (sun-tinted, blue-biased) @@ -969,6 +974,15 @@ var TERRAIN_FRAG_SRC = [ // ramp: the sun altitude drives colour and strength continuously. 'float outsideMask = clamp(outside, 0.0, 1.0);', 'vec3 alb = mix(texture2D(uGround, gUv).rgb, vColor, outsideMask) * uAlbedoTint;', + 'if (uSnowCover > 0.001) {', + 'float elevM = (vY + earthDrop(vWorld.xz)) / max(0.000001, uElevScale);', + 'float slope = 1.0 - clamp(n.y, 0.0, 1.0);', + 'float snowMask = smoothstep(uSnowLineM + uSnowTransitionM, uSnowLineM - uSnowTransitionM, elevM);', + 'snowMask *= smoothstep(uSnowSlopeMax, uSnowSlopeMax * 0.45, slope);', + 'snowMask *= 0.82 + 0.18 * hash3(floor(vWorld * 80.0));', + 'snowMask *= uSnowCover * uSnowGain;', + 'alb = mix(alb, vec3(0.92, 0.95, 1.0), clamp(snowMask, 0.0, 1.0));', + '}', 'float ndl = max(0.0, dot(n, uSunDir));', // Cloud shadow: march from the surface point towards the sun // through the cloud layer and accumulate optical depth. Same @@ -1237,6 +1251,11 @@ function init(canvas) { var terrainUelev = terrainProg ? gl.getUniformLocation(terrainProg, 'uElev') : null; var terrainUterrainShadow = terrainProg ? gl.getUniformLocation(terrainProg, 'uTerrainShadowOn') : null; var terrainUterrainShadowFloor = terrainProg ? gl.getUniformLocation(terrainProg, 'uTerrainShadowFloor') : null; + var terrainUsnowCover = terrainProg ? gl.getUniformLocation(terrainProg, 'uSnowCover') : null; + var terrainUsnowLineM = terrainProg ? gl.getUniformLocation(terrainProg, 'uSnowLineM') : null; + var terrainUsnowTransitionM = terrainProg ? gl.getUniformLocation(terrainProg, 'uSnowTransitionM') : null; + var terrainUsnowSlopeMax = terrainProg ? gl.getUniformLocation(terrainProg, 'uSnowSlopeMax') : null; + var terrainUsnowGain = terrainProg ? gl.getUniformLocation(terrainProg, 'uSnowGain') : null; // 1x1 black texture: bound wherever a real layer does not exist yet // (e.g. the basemap before the first tile, or terrain disabled) so the @@ -1325,7 +1344,10 @@ function init(canvas) { uCloudShadowFloor: terrainUcsFloor, uCloudShadowAmbient: terrainUcsAmb, uElev: terrainUelev, uTerrainShadowOn: terrainUterrainShadow, - uTerrainShadowFloor: terrainUterrainShadowFloor + uTerrainShadowFloor: terrainUterrainShadowFloor, + uSnowCover: terrainUsnowCover, uSnowLineM: terrainUsnowLineM, + uSnowTransitionM: terrainUsnowTransitionM, uSnowSlopeMax: terrainUsnowSlopeMax, + uSnowGain: terrainUsnowGain // vbo / vertCount / wireVbo / wireVertCount are read from the // module-level terrainVbo/terrainVertCount/terrainWireVbo/ // terrainWireVertCount (set by buildTerrainMesh). @@ -6073,6 +6095,43 @@ function updateRoadWetness(now) { return roadWetness; } +var SNOW_COVER_CFG = WEATHER_FX_CFG.snowCover || {}; +var SNOW_COVER_ON = SNOW_COVER_CFG.enabled !== false; +var SNOW_COVER_MELT_MINUTES = (typeof SNOW_COVER_CFG.meltMinutes === 'number') ? Math.max(1, SNOW_COVER_CFG.meltMinutes) : 90; +var SNOW_COVER_BASE_LINE = (typeof SNOW_COVER_CFG.baseLineM === 'number') ? Math.max(0, SNOW_COVER_CFG.baseLineM) : 900; +var SNOW_COVER_TEMP_LINE = (typeof SNOW_COVER_CFG.tempLineMPerC === 'number') ? Math.max(0, SNOW_COVER_CFG.tempLineMPerC) : 80; +var SNOW_COVER_MIN_LINE = (typeof SNOW_COVER_CFG.minLineM === 'number') ? Math.max(0, SNOW_COVER_CFG.minLineM) : 0; +var SNOW_COVER_MAX_LINE = (typeof SNOW_COVER_CFG.maxLineM === 'number') ? Math.max(SNOW_COVER_MIN_LINE, SNOW_COVER_CFG.maxLineM) : 2500; +var SNOW_COVER_TRANSITION = (typeof SNOW_COVER_CFG.transitionM === 'number') ? Math.max(1, SNOW_COVER_CFG.transitionM) : 120; +var SNOW_COVER_SLOPE_MAX = (typeof SNOW_COVER_CFG.slopeMax === 'number') ? Math.max(0.1, Math.min(1.2, SNOW_COVER_CFG.slopeMax)) : 0.72; +var SNOW_COVER_GAIN = (typeof SNOW_COVER_CFG.gain === 'number') ? Math.max(0, Math.min(1.2, SNOW_COVER_CFG.gain)) : 0.85; +var snowCoverAmount = 0, lastSnowT = null, snowLineM = SNOW_COVER_MAX_LINE; + +function updateSnowCover(now) { + if (!SNOW_COVER_ON) { + snowCoverAmount = 0; + snowLineM = SNOW_COVER_MAX_LINE; + lastSnowT = null; + return 0; + } + if (lastSnowT === null) { lastSnowT = now; return snowCoverAmount; } + var dt = Math.max(0, Math.min(5, now - lastSnowT)); + lastSnowT = now; + var wx = window.__currentWeather || {}; + var tempC = Number(wx.tempC || 0); + var snow = Number(wx.snowMm || 0); + var precip = Number(wx.precipMm || 0); + var coldSnow = (tempC < 2.0 ? 1.0 : 0.0); + var input = Math.min(1.0, (snow + (tempC < 1.0 ? precip * 0.55 : 0.0)) * 0.85) * coldSnow; + var melt = dt / (SNOW_COVER_MELT_MINUTES * 60.0); + if (tempC > 2.0) melt *= 1.0 + Math.min(4.0, tempC - 2.0) * 0.35; + snowCoverAmount = Math.max(0, Math.min(1, snowCoverAmount + input * dt * 0.22 - melt)); + var line = SNOW_COVER_BASE_LINE - (5.0 - tempC) * SNOW_COVER_TEMP_LINE; + line = Math.max(SNOW_COVER_MIN_LINE, Math.min(SNOW_COVER_MAX_LINE, line)); + snowLineM = line; + return snowCoverAmount; +} + function updateLightning(now) { if (!WEATHER_LIGHTNING_ON) { lightningFlash = 0; @@ -6733,6 +6792,11 @@ window.clouds3dRender = function(target) { gl.uniform3f(terr.uMoonColor, _mBeam[0], _mBeam[1], _mBeam[2]); gl.uniform1f(terr.uMoonInt, _moonI); gl.uniform1f(terr.uSkyAmb, _amb * ATMO_SKY_GAIN); + gl.uniform1f(terr.uSnowCover, _snowCover); + gl.uniform1f(terr.uSnowLineM, snowLineM); + gl.uniform1f(terr.uSnowTransitionM, SNOW_COVER_TRANSITION); + gl.uniform1f(terr.uSnowSlopeMax, SNOW_COVER_SLOPE_MAX); + gl.uniform1f(terr.uSnowGain, SNOW_COVER_GAIN); gl.uniform1f(terr.uStarAmb, _star * ATMO_STAR_GROUND); gl.uniform3f(terr.uAlbedoTint, _pl[0] / _pm * ATMO_ALBEDO_GAIN, _pl[1] / _pm * ATMO_ALBEDO_GAIN, _pl[2] / _pm * ATMO_ALBEDO_GAIN); @@ -6827,6 +6891,7 @@ window.clouds3dRender = function(target) { // aircraft): the mean over the per-light thresholds. var carNight = lightFactor(window.__currentSunAlt || 0); var _roadWet = updateRoadWetness(now); + var _snowCover = updateSnowCover(now); if (carProg && (carVboM || carVboB) && carNight > 0.01) { var carPxScale = Math.max(0.45, Math.min(1.25, gl.drawingBufferHeight / 1080)); var tCurve = Math.max(0, Math.min(1, (carNight - 0.10) / 0.40)); @@ -7357,6 +7422,8 @@ window.clouds3dRender = function(target) { snow: +_weather[1].toFixed(2), lightning: +lightningFlash.toFixed(2), wet: +roadWetness.toFixed(2), + snowCover: +snowCoverAmount.toFixed(2), + snowLineM: Math.round(snowLineM), code: Number((window.__currentWeather || {}).code || 0) }; var _rainI = _weather[0];