lighting: remove the palette terrain model - physics is the only lighting, themes tint it through THEMES[theme].lighting; debug panel shows the active palette and its tint/gain; credits no longer claim VIIRS

This commit is contained in:
Richard Zink committed 2026-10-11 15:39:42 +02:00
1 parent f26ac6c0ce
commit 4369afd3ef
3 files changed
+405 -478

No files matched your search

+68 -114
View File
@@ -67,7 +67,6 @@ var FRAG_SRC = [
'uniform float uDensity;',
'uniform float uDayProgress;',
'uniform float uSunAlt;',
'uniform float uFogDist;', // world units (=tiles) where ground fog saturates
'uniform float uCloudTop;', // cloud deck upper edge (world Y)
'uniform float uCloudBot;', // cloud deck lower edge (world Y)
'uniform sampler2D uElev;', // terrarium RGB height grid, same UV as uGround
@@ -501,9 +500,6 @@ var TERRAIN_FRAG_SRC = [
'uniform vec2 uGroundSize;',
'uniform vec2 uCorridorRot;',
'uniform vec3 uSunDir;',
'uniform vec3 uLight;',
'uniform vec3 uBg;',
'uniform vec3 uSky;',
'uniform vec3 uMoonDir;', // unit vector towards the moon (world)
'uniform vec3 uSunColor;', // direct beam after atmospheric extinction
'uniform float uSunInt;', // direct beam strength (1 = zenith noon)
@@ -512,7 +508,6 @@ var TERRAIN_FRAG_SRC = [
'uniform float uSkyAmb;', // diffuse skylight
'uniform vec3 uAlbedoTint;', // theme fine-tuning of the surface colour
'uniform vec3 uHorizonColor;', // fog colour taken from the same light model
'uniform float uPhysical;', // 1 = physical lighting, 0 = palette hillshade
'uniform float uStarAmb;', // diffuse starlight + airglow (night only)
'uniform float uTime;', // must match the sky pass: same cloud field
'uniform float uCloudBot;',
@@ -526,17 +521,10 @@ var TERRAIN_FRAG_SRC = [
'uniform float uElevScale;', // metres -> world units (incl. exaggeration)
'uniform float uTerrainShadowOn;',
'uniform float uTerrainShadowFloor;', // brightness a fully shadowed slope keeps
'uniform float uSunAlt;', // sun altitude, for the light switching
'uniform float uLightHi;', // above this altitude no house light is on
'uniform float uLightLo;', // below this altitude every house light is on
'uniform float uLightSoft;',
'uniform vec3 uAirColor;', // Rayleigh airlight (sun-tinted, blue-biased)
'uniform float uAirDensity;', // slant optical depth per world unit
'uniform vec3 uCamPos;', // world-space camera, for the view direction
'uniform float uEdgeFadeTiles;',
'uniform float uFogDist;',
'uniform float uNightFactor;',
'uniform float uNightDim;', // how far the day map is dimmed at night
'uniform vec3 uWireColor;', // non-zero -> debug wireframe overlay (flat colour)
'varying vec3 vNormal;',
'varying vec2 vUV;',
@@ -547,7 +535,7 @@ var TERRAIN_FRAG_SRC = [
'void main() {',
'vec2 gUv = vUV;',
// Edge fade: beyond the canvas the texture is clamped to the
// edge pixel, so we fade toward uBg over the last 5%.
// edge pixel, so the rim dissolves into the airlight instead.
// Distance from the mosaic border in TILES, so the fade band has a
// real width (uEdgeFadeTiles) instead of a fixed 5 % slice - the
// rim dissolves into the airlight over tens of kilometres.
@@ -564,8 +552,7 @@ var TERRAIN_FRAG_SRC = [
// Direct beam (extinction-coloured by the JS air-mass model) +
// hemispherical skylight + moonlight. No hand-picked day/night
// ramp: the sun altitude drives colour and strength continuously.
'if (uPhysical > 0.5) {',
'vec3 alb = texture2D(uGround, gUv).rgb * uAlbedoTint;',
'vec3 alb = texture2D(uGround, gUv).rgb * uAlbedoTint;',
'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
@@ -608,42 +595,10 @@ var TERRAIN_FRAG_SRC = [
' * ambShadow * mix(1.0, tShadow, 0.18);',
'vec3 moon = alb * uMoonColor * max(0.0, dot(n, uMoonDir)) * uMoonInt * ambShadow;',
'col = direct + sky + moon;',
'} else {',
// ---- Hillshade (palette model) ---------------------------------
// Classic shaded-relief: strong lambert term for sun-facing vs
// shadow-facing slopes, plus a sky-ambient term (upward normals
// collect more skylight), plus a subtle warm/cool slope tint.
'float lam = dot(n, uSunDir);',
'float relief = 0.62 + 0.58 * lam;',
// shadowed slopes keep a little bounce light instead of going black
'relief += 0.16 * max(0.0, n.y);',
'relief = clamp(relief, 0.28, 1.5);',
'float warm = clamp(lam, 0.0, 1.0);',
'float cool = clamp(-lam, 0.0, 1.0);',
// sun-facing slopes get a warm cast, shaded slopes a cool cast
'vec3 tint = mix(vec3(1.0), vec3(1.09, 1.03, 0.9), warm * 0.8);',
'tint = mix(tint, vec3(0.82, 0.88, 1.06), cool * 0.6);',
'col *= relief * tint;',
// night: lambert relief against a sun BELOW the horizon would
// crush the map; fade shading back toward the neutral texture
'col = mix(col, texture2D(uGround, gUv).rgb, uNightFactor * 0.7);',
// ---- Day/night cross-fade --------------------------------------
// Starts exactly at sunset and blends over ~the following hour of
// simulated time (uNightFactor: 0 at alt 0, 1 at -0.4 rad, which
// the sun covers in ~1 h at 48 deg N).
'if (uNightFactor > 0.002) {',
// uNightDim is CONFIG.terrain.nightDim - how far to dim the day
// map at full night (0 = pitch black, 1 = unchanged).
'vec3 nightBase = col * uNightDim;',
'col = mix(col, nightBase, uNightFactor);',
'}',
'}',
// ---- Distance fog ----------------------------------------------
// Real view-space distance (uFogDist is in tiles, matching the
// world-unit scale of the mesh), not a height proxy.
// Real view-space distance, not a height proxy.
'float dist = length(vViewPos);',
'vec3 horizonCol = (uPhysical > 0.5) ? uHorizonColor',
' : mix(uBg, uSky, 0.5) * (1.0 - uNightFactor * 0.55);',
'vec3 horizonCol = uHorizonColor;',
// ---- Aerial perspective --------------------------------------
// Physical mode: Beer-Lambert along the view ray. The surface is
// attenuated by exp(-tau) and the light scattered in the same air
@@ -651,19 +606,12 @@ 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.
'if (uPhysical > 0.5) {',
'float tau = uAirDensity * dist * (0.75 + 0.25 * smoothstep(0.4, 0.0, vY));',
'float trans = exp(-tau);',
'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);',
'} else {',
'float fogAmount = clamp(dist / max(uFogDist, 0.001), 0.0, 1.0);',
'fogAmount = pow(fogAmount, 0.8);',
'fogAmount *= 0.75 + 0.25 * smoothstep(0.4, 0.0, vY);',
'col = mix(col, horizonCol, fogAmount);',
'}',
'float tau = uAirDensity * dist * (0.75 + 0.25 * smoothstep(0.4, 0.0, vY));',
'float trans = exp(-tau);',
'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);',
// Edge fade to the same horizon colour
'col = mix(horizonCol, col, edgeFade);',
// Wireframe overlay: when CONFIG.terrain.wireframe is set the
@@ -719,7 +667,6 @@ function init(canvas) {
var uDensity = gl.getUniformLocation(prog, 'uDensity');
var uDayProgress = gl.getUniformLocation(prog, 'uDayProgress');
var uSunAlt = gl.getUniformLocation(prog, 'uSunAlt');
var uFogDist = gl.getUniformLocation(prog, 'uFogDist');
var uCloudTop = gl.getUniformLocation(prog, 'uCloudTop');
var uCloudBot = gl.getUniformLocation(prog, 'uCloudBot');
var uElev = gl.getUniformLocation(prog, 'uElev');
@@ -768,13 +715,6 @@ function init(canvas) {
var terrainUproj = terrainProg ? gl.getUniformLocation(terrainProg, 'uProj') : null;
var terrainUground = terrainProg ? gl.getUniformLocation(terrainProg, 'uGround') : null;
var terrainUsundir = terrainProg ? gl.getUniformLocation(terrainProg, 'uSunDir') : null;
var terrainUlight = terrainProg ? gl.getUniformLocation(terrainProg, 'uLight') : null;
var terrainUbg = terrainProg ? gl.getUniformLocation(terrainProg, 'uBg') : null;
var terrainUsky = terrainProg ? gl.getUniformLocation(terrainProg, 'uSky') : null;
var terrainUfogdist = terrainProg ? gl.getUniformLocation(terrainProg, 'uFogDist') : null;
var terrainUnight = terrainProg ? gl.getUniformLocation(terrainProg, 'uNightFactor') : null;
var terrainUnightdim = terrainProg ? gl.getUniformLocation(terrainProg, 'uNightDim') : null;
var terrainUgroundcenter = terrainProg ? gl.getUniformLocation(terrainProg, 'uGroundCenter') : null;
var terrainUgroundsize = terrainProg ? gl.getUniformLocation(terrainProg, 'uGroundSize') : null;
var terrainUcorridor = terrainProg ? gl.getUniformLocation(terrainProg, 'uCorridorRot') : null;
@@ -788,11 +728,6 @@ function init(canvas) {
var terrainUskyAmb = terrainProg ? gl.getUniformLocation(terrainProg, 'uSkyAmb') : null;
var terrainUalbedoTint = terrainProg ? gl.getUniformLocation(terrainProg, 'uAlbedoTint') : null;
var terrainUhorizon = terrainProg ? gl.getUniformLocation(terrainProg, 'uHorizonColor') : null;
var terrainUphysical = terrainProg ? gl.getUniformLocation(terrainProg, 'uPhysical') : null;
var terrainUsunAlt = terrainProg ? gl.getUniformLocation(terrainProg, 'uSunAlt') : null;
var terrainUlightHi = terrainProg ? gl.getUniformLocation(terrainProg, 'uLightHi') : null;
var terrainUlightLo = terrainProg ? gl.getUniformLocation(terrainProg, 'uLightLo') : null;
var terrainUlightSoft = terrainProg ? gl.getUniformLocation(terrainProg, 'uLightSoft') : null;
var terrainUairColor = terrainProg ? gl.getUniformLocation(terrainProg, 'uAirColor') : null;
var terrainUairDensity = terrainProg ? gl.getUniformLocation(terrainProg, 'uAirDensity') : null;
var terrainUcampos = terrainProg ? gl.getUniformLocation(terrainProg, 'uCamPos') : null;
@@ -811,8 +746,8 @@ function init(canvas) {
var terrainUterrainShadowFloor = terrainProg ? gl.getUniformLocation(terrainProg, 'uTerrainShadowFloor') : null;
// 1x1 black texture: bound wherever a real layer does not exist yet
// (e.g. night lights before the first tile, or nightLights: false)
// so the shaders always sample a valid black.
// (e.g. the basemap before the first tile, or terrain disabled) so the
// shaders always sample a valid black.
var blackTex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, blackTex);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA,
@@ -833,7 +768,7 @@ function init(canvas) {
uSky: uSky, uCloud: uCloud,
uCloudShadow: uCloudShadow, uLight: uLight, uBg: uBg,
uSpeed: uSpeed, uDensity: uDensity, uDayProgress: uDayProgress,
uSunAlt: uSunAlt, uFogDist: uFogDist,
uSunAlt: uSunAlt,
uCloudTop: uCloudTop, uCloudBot: uCloudBot,
uElev: uElev, uElevOn: uElevOn, uElevScale: uElevScale,
uSunDir: uSunDir, uMoonDir: uMoonDir, uMode: uMode,
@@ -847,20 +782,15 @@ function init(canvas) {
prog: terrainProg,
aPos: terrainApos, aNormal: terrainAnormal, aUV: terrainAuv,
uView: terrainUview, uProj: terrainUproj,
uGround: terrainUground, uSunDir: terrainUsundir, uLight: terrainUlight,
uBg: terrainUbg, uSky: terrainUsky, uFogDist: terrainUfogdist, uNightFactor: terrainUnight,
uGround: terrainUground, uSunDir: terrainUsundir,
uGroundCenter: terrainUgroundcenter, uGroundSize: terrainUgroundsize,
uCorridorRot: terrainUcorridor, uCloudsOn: terrainUcloudsOn,
uNightDim: terrainUnightdim,
uElevScale: terrainUelevScale,
uWireColor: terrainUwireColor,
uSunColor: terrainUsunColor, uSunInt: terrainUsunInt,
uMoonColor: terrainUmoonColor, uMoonInt: terrainUmoonInt,
uSkyAmb: terrainUskyAmb, uAlbedoTint: terrainUalbedoTint,
uHorizonColor: terrainUhorizon, uPhysical: terrainUphysical,
uSunAlt: terrainUsunAlt, uLightHi: terrainUlightHi,
uLightLo: terrainUlightLo, uLightSoft: terrainUlightSoft,
uHorizonColor: terrainUhorizon,
uAirColor: terrainUairColor, uAirDensity: terrainUairDensity,
uCamPos: terrainUcampos, uEdgeFadeTiles: terrainUedgeFade,
uStarAmb: terrainUstarAmb,
@@ -1654,13 +1584,6 @@ function buildElevationSystem(glCtx) {
// ---- 3D math helpers (used by the terrain mesh + view-proj matrices) ----
// 4x4 matrix laid out column-major (the same layout gl.uniformMatrix4fv
// expects) as a flat Float32Array of 16 entries.
function mat4Identity(out) {
out[0]=1;out[1]=0;out[2]=0;out[3]=0;
out[4]=0;out[5]=1;out[6]=0;out[7]=0;
out[8]=0;out[9]=0;out[10]=1;out[11]=0;
out[12]=0;out[13]=0;out[14]=0;out[15]=1;
return out;
}
function mat4LookAt(eye, target, up) {
// z axis = (eye - target) normalized (camera looks down -z)
var zx = eye[0]-target[0], zy = eye[1]-target[1], zz = eye[2]-target[2];
@@ -1922,9 +1845,6 @@ function buildTerrainMesh(gl, state) {
// How far the day map is dimmed at full night (0 = pitch black, 1 =
// unchanged). The NASA VIIRS city-lights layer was removed: settlement
// lights come from OSM place nodes (see the settlement lights section).
var NIGHT_CFG = (window.CONFIG && window.CONFIG.terrain) || {};
var NIGHT_DIM = (typeof NIGHT_CFG.nightDim === 'number') ?
Math.max(0, Math.min(1, NIGHT_CFG.nightDim)) : 0.22;
// ---- Motorway car-lights layer ----------------------------------------
// Motorway/trunk roads (Autobahnen) from OSM via Overpass API, fetched at
@@ -1945,7 +1865,6 @@ var ATMO = (window.CONFIG && window.CONFIG.atmosphere) || {};
var ATMO_TURBIDITY = (typeof ATMO.turbidity === 'number') ? Math.max(0.2, ATMO.turbidity) : 1.0;
var ATMO_MOON_GAIN = (typeof ATMO.moonGain === 'number') ? Math.max(0, ATMO.moonGain) : 0.16;
var ATMO_NIGHT_FLOOR = (typeof ATMO.nightFloor === 'number') ? Math.max(0.01, ATMO.nightFloor) : 0.05;
var ATMO_TERRAIN_PHYS = ATMO.terrainLighting !== 'palette';
var ATMO_ALBEDO_GAIN = (typeof ATMO.albedoGain === 'number') ? Math.max(0.1, ATMO.albedoGain) : 1.0;
var ATMO_SKY_GAIN = (typeof ATMO.skyGain === 'number') ? Math.max(0.05, ATMO.skyGain) : 0.55;
@@ -2003,6 +1922,43 @@ function airlightColor(altRad, moonAlt, moonFrac) {
RAYLEIGH[1] * (beam[1] * kDay + mBeam[1] * kMoon),
RAYLEIGH[2] * (beam[2] * kDay + mBeam[2] * kMoon)];
}
// ---- Theme lighting -----------------------------------------------------
// The physical model stays the base in every theme: the theme only pulls
// the beam, the airlight and the moonlight toward its own hue by 'tint'
// (0 = pure physics, 1 = the theme colour), and gain / nightGain scale the
// day and night exposure. Configured per theme in window.THEMES[theme].
// .lighting; a theme without that block is pure physics.
function themeColor(v, fallback) {
if (typeof v === 'number') return hex(v);
if (typeof v === 'string') {
var t = v.replace('#', '');
if (t.length === 3) t = t[0] + t[0] + t[1] + t[1] + t[2] + t[2];
if (t.length !== 6 || isNaN(parseInt(t, 16))) return fallback;
var n = parseInt(t, 16);
return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255];
}
return fallback;
}
function mixRgb(a, b, k) {
return [a[0] + (b[0] - a[0]) * k, a[1] + (b[1] - a[1]) * k, a[2] + (b[2] - a[2]) * k];
}
function themeLighting() {
var name = (window.CONFIG && window.CONFIG.theme) || 'ash';
var th = (window.THEMES && window.THEMES[name]) || null;
var L = th && th.lighting;
if (!L) return null;
var tint = (typeof L.tint === 'number') ? Math.max(0, Math.min(1, L.tint)) : 0.30;
return {
tint: tint,
sun: themeColor(L.sun, [1, 1, 1]),
sky: themeColor(L.sky, [0.55, 0.65, 0.85]),
air: themeColor(L.air, [0.45, 0.60, 0.85]),
moon: themeColor(L.moon, [0.80, 0.85, 0.95]),
gain: (typeof L.gain === 'number') ? Math.max(0.2, L.gain) : 1,
nightGain: (typeof L.nightGain === 'number') ? Math.max(0.2, L.nightGain) : 1
};
}
function lightFactor(altRad) {
var span = LIGHT_HI - LIGHT_LO;
if (span <= 0) return 0;
@@ -2048,10 +2004,6 @@ function skyAmbient(altRad) {
var tw = s > 0 ? 1 : Math.max(ATMO_NIGHT_FLOOR, Math.pow(2, altRad * 57.2957795 / 8.0));
return day * tw;
}
function skyTwilight(altRad) {
if (altRad > 0) return 1;
return Math.max(ATMO_NIGHT_FLOOR, Math.pow(2, altRad * 57.2957795 / 8.0));
}
var CAR_CFG = (window.CONFIG && window.CONFIG.terrain) || {};
var CAR_ON = CAR_CFG.carLights !== false;
var CITY_ON = CAR_CFG.cityLights !== false;
@@ -4824,7 +4776,8 @@ window.clouds3dRender = function(target) {
gl.uniform3f(s.uSunDir, sd[0], sd[1], sd[2]);
var md = window.__currentMoonDir || sd;
gl.uniform3f(s.uMoonDir, md[0], md[1], md[2]);
gl.uniform1f(s.uPhysical, ATMO_TERRAIN_PHYS ? 1.0 : 0.0);
// the physical model is the only lighting model now
gl.uniform1f(s.uPhysical, 1.0);
// Star catalogue sky texture on texture unit 4
gl.activeTexture(gl.TEXTURE4);
@@ -4890,6 +4843,20 @@ window.clouds3dRender = function(target) {
var _moonDiff = ATMO_MOON_GAIN * _moonFrac * _moonUp * 0.9;
var _amb = skyAmbient(sunAlt) + _moonDiff;
var _air = airlightColor(sunAlt, _moonAlt, _moonFrac);
// Theme tint on top of the physics (see themeLighting above). The
// exposure factor ramps from gain (day) to nightGain (night) with the
// same altitude ramp the terrain uses for its night cross-fade.
var _tl = themeLighting();
if (_tl) {
var _nf = Math.max(0, Math.min(1, -sunAlt / 0.4));
var _exp = _tl.gain * (1 - _nf) + _tl.nightGain * _nf;
_beam = mixRgb(_beam, _tl.sun, _tl.tint);
_mBeam = mixRgb(_mBeam, _tl.moon, _tl.tint);
_air = mixRgb(mixRgb(_air, _tl.air, _tl.tint), _tl.sky, _tl.tint * 0.45);
_beamL *= _exp;
_moonI *= _exp;
_amb *= _exp;
}
gl.uniform3f(s.uAirColor, _air[0], _air[1], _air[2]);
gl.uniform3f(s.uSunColor, _beam[0], _beam[1], _beam[2]);
gl.uniform1f(s.uSunInt, _beamL);
@@ -4911,7 +4878,6 @@ window.clouds3dRender = function(target) {
var speed = (typeof themeSpeed === 'number')
? Math.max(0.1, Math.min(3, themeSpeed)) : 1.0;
gl.uniform1f(s.uSpeed, speed);
gl.uniform1f(s.uFogDist, FOG_TILES);
gl.uniform1f(s.uCloudTop, CLOUD_TOP);
gl.uniform1f(s.uCloudBot, CLOUD_BOT);
@@ -5095,7 +5061,6 @@ window.clouds3dRender = function(target) {
gl.uniformMatrix4fv(terr.uView, false, view);
gl.uniformMatrix4fv(terr.uProj, false, proj);
gl.uniform1i(terr.uGround, 1);
gl.uniform1f(terr.uNightDim, NIGHT_DIM);
gl.uniform2f(terr.uGroundCenter, centerWorld.x, centerWorld.y);
gl.uniform2f(terr.uGroundSize, groundTilesX(), groundTilesY());
gl.uniform2f(terr.uCorridorRot, GROUND_TRAVEL_COS, GROUND_TRAVEL_SIN);
@@ -5135,21 +5100,10 @@ window.clouds3dRender = function(target) {
// instead of a separate grey veil.
var _hor = 0.75;
gl.uniform3f(terr.uHorizonColor, _air[0] * _hor, _air[1] * _hor, _air[2] * _hor);
gl.uniform1f(terr.uPhysical, ATMO_TERRAIN_PHYS ? 1.0 : 0.0);
gl.uniform1f(terr.uSunAlt, sunAlt);
gl.uniform1f(terr.uLightHi, LIGHT_HI);
gl.uniform1f(terr.uLightLo, LIGHT_LO);
gl.uniform1f(terr.uLightSoft, LIGHT_SOFT);
gl.uniform3f(terr.uAirColor, _air[0], _air[1], _air[2]);
gl.uniform1f(terr.uAirDensity, -Math.log(ATMO_HAZE_RESIDUAL) / Math.max(FOG_TILES, 0.001));
gl.uniform3f(terr.uCamPos, camX, camY, camZ);
gl.uniform1f(terr.uEdgeFadeTiles, EDGE_FADE_TILES);
gl.uniform3fv(terr.uLight, hex(pal.light));
gl.uniform3fv(terr.uBg, hex(pal.bg));
gl.uniform3fv(terr.uSky, hex(pal.sky));
gl.uniform1f(terr.uFogDist, FOG_TILES);
var nightFac = Math.max(0, Math.min(1, -sunAlt / 0.4));
gl.uniform1f(terr.uNightFactor, nightFac);
gl.bindBuffer(gl.ARRAY_BUFFER, terr.vbo);
var STRIDE = 8 * 4; // 8 floats per vertex