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

+19 -75
View File
@@ -9,9 +9,10 @@
<script>
(function() {
var theme = (window.CONFIG && window.CONFIG.theme) || 'ash';
var cssFile = window.THEMES && window.THEMES[theme];
var entry = window.THEMES && window.THEMES[theme];
var cssFile = (entry && entry.css) || (window.THEMES && window.THEMES.ash && window.THEMES.ash.css) || 'ash.css';
var css = document.getElementById('theme-css');
if (css) css.href = cssFile || (window.THEMES && window.THEMES.ash) || 'ash.css';
if (css) css.href = cssFile;
// standalone build (build.py): themes are embedded in
// window.__inlineThemeCss and the external link is disabled
if (window.__inlineThemeCss && window.__inlineThemeCss[theme]) {
@@ -928,13 +929,6 @@ window.sunPalettes = [
}
}
function adjustColor(color, factor) {
var r = Math.min(255, Math.floor(((color >> 16) & 255) * factor));
var g = Math.min(255, Math.floor(((color >> 8) & 255) * factor));
var b = Math.min(255, Math.floor((color & 255) * factor));
return (r << 16) | (g << 8) | b;
}
function shiftColor(color, delta) {
var r = Math.max(0, Math.min(255, ((color >> 16) & 255) + delta));
var g = Math.max(0, Math.min(255, ((color >> 8) & 255) + delta));
@@ -942,50 +936,8 @@ window.sunPalettes = [
return (r << 16) | (g << 8) | b;
}
function getWeatherModifiers(code) {
// Returns {cloudDark: factor, cloudLight: factor, skyShift: shift, bgShift: shift}
// code is WMO weather code from open-meteo
if (code === 0) {
// Clear sky - lighter clouds, brighter sky
return { cloudDark: 0.7, cloudLight: 1.3, skyShift: 15, bgShift: 15 };
} else if (code >= 1 && code <= 3) {
// Mainly clear to overcast - neutral
return { cloudDark: 0.95, cloudLight: 1.0, skyShift: 0, bgShift: -5 };
} else if (code === 45 || code === 48) {
// Fog - cool, washed out
return { cloudDark: 1.1, cloudLight: 0.85, skyShift: 10, bgShift: 10 };
} else if ((code >= 51 && code <= 57) || (code >= 61 && code <= 67)) {
// Drizzle / Rain - darker, slightly cooler
return { cloudDark: 1.3, cloudLight: 0.7, skyShift: -15, bgShift: -20 };
} else if ((code >= 71 && code <= 77) || (code >= 85 && code <= 86)) {
// Snow - brighter (light reflection)
return { cloudDark: 0.7, cloudLight: 1.5, skyShift: 25, bgShift: 25 };
} else if (code >= 80 && code <= 82) {
// Rain showers - darker
return { cloudDark: 1.4, cloudLight: 0.6, skyShift: -20, bgShift: -25 };
} else if (code >= 95) {
// Thunderstorm - very dark, dramatic
return { cloudDark: 1.6, cloudLight: 0.4, skyShift: -30, bgShift: -35 };
}
return { cloudDark: 1.0, cloudLight: 1.0, skyShift: 0, bgShift: 0 };
}
var currentWeatherCode = -1;
function applyWeatherModifiers(c, code) {
var m = getWeatherModifiers(code);
return {
sky: c.sky,
cloud: adjustColor(c.cloud, m.cloudLight),
cloudShadow: adjustColor(c.cloudShadow, m.cloudDark),
sun: adjustColor(c.sun, m.cloudLight),
sunGlare: adjustColor(c.sunGlare, m.cloudLight),
sunLight: adjustColor(c.sunLight, m.cloudLight),
bg: shiftColor(c.bg, m.bgShift),
sunVis: c.sunVis
};
}
// ============ Clock & Sun/Moon Calculations ============
var hourHand = document.getElementById('hour-hand');
@@ -1038,10 +990,6 @@ window.sunPalettes = [
return (h < 10 ? '0' : '') + h + ':' + (m < 10 ? '0' : '') + m;
}
function formatTimeHMS(h, m, s) {
return (h < 10 ? '0' : '') + h + ':' + (m < 10 ? '0' : '') + m + ':' + (s < 10 ? '0' : '') + s;
}
function pad2(n) { return n < 10 ? '0' + n : '' + n; }
function pad3(n) { return n < 10 ? '00' + n : n < 100 ? '0' + n : '' + n; }
@@ -1145,12 +1093,6 @@ window.sunPalettes = [
return result;
}
function formatDateShort(date) {
var d = date.getDate();
var mo = date.getMonth() + 1;
return (d < 10 ? '0' : '') + d + '.' + (mo < 10 ? '0' : '') + mo + '.';
}
function formatDateLong(date) {
var days = ['So.', 'Mo.', 'Di.', 'Mi.', 'Do.', 'Fr.', 'Sa.'];
var d = date.getDate();
@@ -1679,11 +1621,11 @@ window.weatherThemes = {
var t = cfg.terrain || {};
if (t.enabled !== false) {
lines.push('Terrain: Copernicus DEM via Mapzen Terrarium');
if (t.nightLights !== false) lines.push('Night lights: NASA GIBS VIIRS');
if (t.carLights !== false || t.cityLights !== false) lines.push('Roads: \u00a9 OpenStreetMap contributors (ODbL)');
var fc = cfg.flights;
if (!fc || fc.enabled !== false) lines.push('Airliner model: Grab3D (CC0)');
}
var fc = cfg.flights;
if (!fc || fc.enabled !== false) lines.push('Airliner model: Grab3D (CC0)');
lines.push('Stars: Yale Bright Star Catalogue');
lines.push('Weather: Open-Meteo');
el.innerHTML = lines.join('<br>');
})();
@@ -1713,12 +1655,6 @@ window.weatherThemes = {
};
}
function getCurrentDayProgress() {
var now = new Date();
var startOfDay = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0);
return (now.getTime() - startOfDay.getTime()) / 86400000;
}
function toggleDebugPanel() {
var content = document.getElementById('debug-content');
var title = document.querySelector('.debug-title');
@@ -1814,7 +1750,8 @@ window.weatherThemes = {
}
function setDebugTheme(theme) {
var cssFile = window.THEMES && window.THEMES[theme];
var entry = window.THEMES && window.THEMES[theme];
var cssFile = (entry && entry.css) || 'ash.css';
var css = document.getElementById('theme-css');
if (window.__inlineThemeCss && window.__inlineThemeCss[theme]) {
if (css) css.media = 'not all';
@@ -1939,7 +1876,17 @@ window.weatherThemes = {
'orange', 'yellow', 'red', 'brown', 'pink', 'green', 'blue', 'blue-light', 'purple'
];
var dbgRoot = getComputedStyle(document.documentElement);
var colorHtml = '<div class="theme-table-header">Palette</div>';
// Header names the active theme and its lighting block, so the
// palette table also shows how the theme tints the physical model.
var __thName = (window.CONFIG && window.CONFIG.theme) || 'ash';
var __th = (window.THEMES && window.THEMES[__thName]) || {};
var __lt = __th.lighting || null;
var colorHtml = '<div class="theme-table-header">Palette (' + __thName + ')</div>';
colorHtml += '<div class="palette-table-row">' +
'<span class="theme-table-name">' +
(__lt ? ('lighting tint ' + __lt.tint + ' · gain ' + __lt.gain +
'/' + __lt.nightGain) : 'lighting: physical (no tint)') +
'</span></div>';
for (var k = 0; k < colorKeys.length; k++) {
var ck = colorKeys[k];
var cv = ASH[ck] || dbgRoot.getPropertyValue('--' + ck).trim();
@@ -2205,9 +2152,6 @@ window.weatherThemes = {
}
// Get timezone offset based on user's local time
function getTimezoneOffsetMinutes() {
return new Date().getTimezoneOffset();
}
var weatherData = null;
var WMO_ICONS = {
+318 -289
View File
@@ -1,347 +1,376 @@
/*
* Wallpaper background config.
* animated_wallpaper - configuration
*
* Properties:
* theme - Active palette theme. One of the keys in window.THEMES
* (default 'ash'). The matching CSS file is loaded at
* startup.
* language - UI / i18n language code (default 'en').
* timeFormat - Format string for the clock using the token set below
* (Moment.js-like names, own mini-formatter).
* debug - If 1, the right-side debug panel is shown.
* units - Object with 'temperature' ('C' or 'F').
* location - Geographic coordinates for sun/moon calculations. Used
* by SunCalc (https://github.com/mourner/suncalc). Object
* with 'lat' (latitude, -90 to +90) and 'lon' (longitude,
* -180 to +180). Positive lat = north, positive lon = east.
* Optionally add 'label' (string) to override the
* reverse-geocoded city name.
* Note: the height grid is axis-aligned (N up); terrain
* matches the square ground mosaic and straight flights
* (prefetch direction via #fetch=N). Heights: Copernicus
* DEM via terrarium tiles (z9, ~200 m).
* terrain - 3D ground from real elevation data. Object:
* enabled - true (default) / false = flat plane.
* exaggeration - vertical scale factor on the DEM
* (default 8; 1 would be physically
* true and nearly invisible at this
* horizontal scale).
* weatherSim - If true, ignore open-meteo and cycle through
* ['sunny','cloudy','stormy','night'] automatically every
* weatherSimInterval ms. Useful for A/B testing every
* theme without waiting on the API.
* weatherSimInterval - Sim cycle period in ms (default 30000).
* orbit - Figure-eight flight (Bernoulli lemniscate) around
* CONFIG.location instead of a straight drift. The
* eight's self-crossing sits exactly on the configured
* coordinate. Each time the aircraft crosses that point
* a new random orientation is picked and blended in
* over one lobe (spline through the crossing). Object:
* radiusKm - lobe tip reach from the centre in km
* (~13 km per tile at z11/48deg N).
* Clamped (~190 km) so tips + fog horizon
* fit inside the 43x43 tile mosaic.
* clockwise - travel direction along the eight.
* reorient - random re-aim at every crossing
* (default true; false keeps the bearing).
* Set to null (or remove) for the straight-line flight.
* fog - Ground fog / distance haze. Object with 'distanceKm'
* (visibility range at which terrain fully blends into
* a milky haze; default 120). false/0 disables fog.
* clouds - Vertical extent of the cloud deck in world units
* (1 unit = ~12.9 km at zoom 11; camera band 15-35 km,
* i.e. y ~ 1.16-2.70). Default puts the deck top (~11.6
* km) below the lowest flight altitude.
* tiles - All downloads (street tiles, elevation grid, last
* weather payload, geo label) live permanently in
* IndexedDB and are never re-fetched once cached; a
* warm cache = fully offline rendering. Set
* expireDays (number > 0) to re-fetch street tiles
* periodically (e.g. 90).
* Everything the scene reads at runtime lives here. Values are plain
* JavaScript so the file can also be edited while the page is open
* (window.CONFIG is read every frame for most knobs).
*
* Time format tokens (subset):
* YYYY 4-digit year (2026)
* MMMM full month name (October)
* MM month with leading zero (10)
* DD day with leading zero (07)
* HH 24-hour hour with zero (18)
* mm minute with leading zero (14)
* ss second with leading zero (25)
* a am/pm lowercase (pm)
* A AM/PM uppercase (PM)
* dddd full weekday name (lowercase!)
* Any other character is emitted verbatim.
* TOP LEVEL
* theme active palette, a key of window.THEMES (default 'ash')
* language UI language code for i18n.js ('en', 'de', ...)
* timeFormat clock format string, token list at the bottom
* debug 1 = show the debug panel (bottom left: weather, shader
* counters, theme palette). 0 (default) = hidden
* credits true = attribution line at the bottom of the info box
* units { temperature: 'C' | 'F' }
* location { lat, lon, label? } - sun/moon geometry, traffic side,
* tile/DEM/Overpass origin, figure-eight centre
* renderScale internal 3D resolution (0.2..1) with blocky upscale
* maxFps scene frame cap (0 = free running)
* weatherSim ignore Open-Meteo and cycle the weather themes
* tiles basemap/elevation cache behaviour + debug overlays
* ground basemap style
* orbit flight path (figure eight or straight line)
* fog distance haze
* clouds cloud deck height + per-weather coverage
* flights civil aviation layer (aircraft + contrails)
* terrain 3D ground, car lights, settlement lights
* carLights tuning of the vehicle light layer
* lights twilight switching of artificial lights
* atmosphere physical light model + shadow/haze knobs
* stars star catalogue tint and brightness
* day simulated day cycle or the real sun
*
* WORLD SCALE
* 1 world unit = 1 zoom-11 tile ~ 12.9 km at 48.6 deg N.
* World axes: +X = west, +Y = up, +Z = north (verified against the
* basemap and the star frame). Camera flies a 15-35 km band (y ~ 1.16-2.70).
*
* TIME FORMAT TOKENS (own mini formatter, Moment.js-like names)
* YYYY 4-digit year MMMM full month name MM month 01-12
* DD day 01-31 HH hour 00-23 mm minute
* ss second a am/pm A AM/PM
* dddd full weekday name DDD short weekday name
* any other character is copied verbatim
*/
window.CONFIG = {
theme: 'ash',
language: 'en',
// ------------------------------------------------------------------
// Identity and UI
// ------------------------------------------------------------------
theme: 'ash', // 'ash' | 'nord' | 'gruvbox' (see window.THEMES)
language: 'en', // 'en', 'de' - keys of the i18n.js dictionaries
timeFormat: 'DD. MMMM, YYYY ‒ HH:mm:SS',
debug: 0,
units: {
temperature: 'C'
},
// Data attribution at the bottom of the info box (automatic text
// based on the enabled layers). false = hidden.
credits: true,
// Internal 3D resolution scale (0.3..1, default 1). Lower values
// render the clouds/map shader at reduced resolution and upscale
// via CSS - use it if fps is low (GPU-bound) despite a quiet CPU.
// 1 = native. 0.5 = a quarter of the pixels (half width, half height),
// upscaled 2x as hard blocks; the DOM UI on top stays crisp.
debug: 0, // 1 = debug panel with the theme palette
credits: true, // false = hide the attribution line
units: { temperature: 'C' }, // 'F' for Fahrenheit
// ------------------------------------------------------------------
// Rendering budget
// ------------------------------------------------------------------
// Internal 3D resolution. The value is snapped to an exact integer
// divisor of the CSS size (0.5 -> 2x blocks, 0.33 -> 3x, 0.25 -> 4x)
// and upscaled with image-rendering: pixelated, so it reads as pixel
// art instead of a blurry rescale. The DOM UI stays crisp at any value.
// 1 = native resolution (sharpest, most expensive).
renderScale: 0.5,
// WebGL scene frame cap (default 30; 0 = free-running/vsync). All
// scene motion is time-based, so the cap only changes smoothness.
// Scene frame cap. All motion is time-based, so this only changes
// smoothness, never speed. 0 = free running (vsync).
maxFps: 30,
// ------------------------------------------------------------------
// Location
// ------------------------------------------------------------------
// Drives sun/moon positions, the traffic side lookup, the tile/DEM
// origin, the Overpass grid and the figure-eight centre.
location: {
// Latitude and longitude for sun/moon calculations
lat: 48.5667,
lon: 13.4319
// label: 'Passau' // overrides the reverse-geocoded name
},
// weatherSim: true = Szenario-Wetter statt Open-Meteo (A/B-Durchlauf
// aller Wetterstufen alle weatherSimInterval ms). false = echtes
// Wetter von Open-Meteo.
// ------------------------------------------------------------------
// Weather
// ------------------------------------------------------------------
// false = live Open-Meteo conditions (also used for the theme choice).
// true = cycle sunny -> cloudy -> stormy -> night, useful to compare
// all weather looks without waiting on the API.
weatherSim: false,
weatherSimInterval: 30000,
// All downloads (basemap tiles, elevation grid, weather JSON, geo
// label) stay in IndexedDB permanently and are never re-fetched once
// cached. expireDays (days, > 0) re-enables periodic basemap refresh.
weatherSimInterval: 30000, // ms per weather step when weatherSim is true
// ------------------------------------------------------------------
// Tiles and caches
// ------------------------------------------------------------------
// Basemap tiles, the elevation grid, Overpass cells, the star catalogue
// and the last weather payload live permanently in IndexedDB
// ('background-tiles' / store 'tiles') and are never re-fetched once
// cached: a warm cache renders fully offline.
tiles: {
expireDays: null,
// true = draw tile borders + N/S + x/y coords into the ground
// texture (debug orientation). Off = clean map.
debug: true
expireDays: null, // e.g. 90 = re-fetch basemap tiles every 90 days
debug: false // true = tile borders + N/S + x/y labels baked
// into the ground texture (orientation debug)
},
// Ground imagery: 'satellite' = ESRI World_Imagery (aerial, no
// labels), 'streets' = ESRI World_Street_Map (with place names).
// Both cached permanently (separate cache prefix per style).
ground: {
style: 'streets'
// 'satellite' = ESRI World_Imagery (aerial, no labels)
// 'streets' = ESRI World_Street_Map (with place names)
// Both are cached separately (cache id prefix 'sat/' vs plain).
style: 'satellite'
},
// Figure-eight course around location (lat/lon); the crossing point
// sits exactly on the coordinate. null/removed = straight flight.
// ------------------------------------------------------------------
// Flight path
// ------------------------------------------------------------------
// Figure eight (Bernoulli lemniscate) whose crossing sits exactly on
// CONFIG.location. At every crossing a new bearing is picked and blended
// over one lobe. Set orbit: null for a straight drift.
orbit: {
radiusKm: 60, // lobe tip: max distance from the centre
clockwise: true,
reorient: true, // random heading at every crossing pass
// Slow vertical bobbing inside the 15-35 km flight band:
// centre 25 km (~1.93 world units), amp = half the band
// (10 km ~ 0.77); hard clamp at the band limits.
// periodS = seconds per cycle, tilt = camera nose reaction to
// climb/descent rate. enabled: false = constant 25 km.
radiusKm: 60, // lobe tip reach (clamped ~190 km so the
// tips plus the fog dome fit the mosaic)
clockwise: true, // travel direction along the eight
reorient: true, // false keeps the bearing across crossings
// Vertical bobbing inside the 15-35 km band: centre 25 km, amp is
// half the band (10 km ~ 0.77 world units), tilt is how much the
// camera nose follows the climb/descent rate.
altBob: { enabled: true, amp: 0.77, periodS: 90, tilt: 0.3 }
// altBob: { enabled: false } // constant 25 km, no bobbing
},
// Ground fog: distance haze. distanceKm = visibility until fully
// saturated haze (false/0 = off).
// ------------------------------------------------------------------
// Distance haze
// ------------------------------------------------------------------
fog: {
distanceKm: 120
distanceKm: 120 // fully saturated haze at this range; 0/false = off
},
// ---- Civil aviation (CONFIG.flights) ---------------------------------
// Low-poly jets (CC0 model, vendor/plane3d.js) with randomized target
// airports: the destination is rolled at every take-off (hubs weighted
// via wHub/wField/wStrip). Paths are gentle Bezier curves resampled to
// arc length (constant ground speed) flown as a full profile: take-off
// and climb, cruise, descent and landing, a short ground hold, then a
// fresh destination. By day sun-lit silhouettes; by night white
// strobing position lights with a red beacon. Contrails form above
// contrailStartKm on contrailChance of the aircraft and stay visible
// for contrailLifeS. All distances in km (world scale: 1 unit = one
// z11 tile ~ 12.9 km at 48.6 deg N).
// ------------------------------------------------------------------
// Cloud deck
// ------------------------------------------------------------------
// World units (1 unit ~ 12.9 km). Keep 'top' below the lowest flight
// altitude so aircraft fly above the deck.
clouds: {
top: 0.9, // ~11.6 km
bottom: -0.3, // ~-3.9 km (deck base below the 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).
density: { stormy: 0.80 }
// density: { sunny: 0.05, cloudy: 0.9, stormy: 1.0, night: 0.2 }
},
// ------------------------------------------------------------------
// Civil aviation
// ------------------------------------------------------------------
// Low-poly jets (CC0 model in vendor/plane3d.js) with a destination
// rolled at every take-off (hubs weighted by wHub/wField/wStrip).
// Paths are Bezier curves resampled to arc length, flown as a full
// profile: climb, cruise, descent, landing, ground hold, new leg.
flights: {
enabled: true,
altitudeKm: 14, // cruise altitude
climbKm: 100, // climb/descent run to/from cruise altitude
count: 21, // max simultaneous aircraft
speedKmh: 850, // mean ground speed
speedVar: 0.18, // +/- deviation (0..0.5)
minLegKm: 60, // shortest leg (no taxi hops)
holdMinS: 50, // ground hold at destination (min..max)
altitudeKm: 14, // cruise altitude
climbKm: 100, // climb/descent run to and from cruise
count: 21, // simultaneous aircraft (max 240)
speedKmh: 850, // mean ground speed
speedVar: 0.18, // +/- speed deviation (0..0.5)
minLegKm: 60, // shortest leg (no taxi hops)
holdMinS: 50, // ground hold at the destination
holdMaxS: 150,
wHub: 4, // destination weights: hub / airfield / strip
wHub: 4, // destination weights
wField: 2,
wStrip: 1,
contrailChance: 0.85, // share of aircraft with a persistent trail
contrailStartKm: 6, // trail formation altitude
contrailLifeS: 300, // trail visibility duration
contrailWidthM: 620, // full trail width
// trails fade out completely by this distance (start is 35% of it)
contrailFadeKm: 100,
// Night strobes. Size shrinks with distance but never below
// lightPx, and never grows past lightMaxPx for a jet right above.
// The white strobe is a double flash: two equal flashes
// blinkGapS apart, then blinkPeriodS - blinkGapS - blinkFlashS
// of darkness (1.4 - 0.16 - 0.08 = 1.16 s pause).
// Contrails: contrailChance of the aircraft leaves a trail once
// above contrailStartKm, visible for contrailLifeS seconds.
contrailChance: 0.85,
contrailStartKm: 6,
contrailLifeS: 300,
contrailWidthM: 620, // full trail width
contrailFadeKm: 100, // fully faded at this distance (ramp starts at 35 %)
// Night strobes. Point size shrinks with distance but never below
// lightPx and never above lightMaxPx for a jet directly overhead.
// The white strobe is a double flash: two flashes blinkFlashS long,
// blinkGapS apart, then the rest of blinkPeriodS is dark.
lightPx: 7,
lightMaxPx: 16,
lightBoost: 9,
blinkPeriodS: 1.4,
blinkGapS: 0.16,
blinkFlashS: 0.08
// flights: false // remove the aviation layer completely
},
// 3D ground from real elevation data (Copernicus DEM, terrarium
// tiles). enabled: false = flat plane. exaggeration: vertical
// elevation scale (1 = real, nearly invisible; default 8).
// wireframe: true = overlay mesh edges as orange lines (debug).
// carLights: moving lights on the real road network (OSM/Overpass
// curves, additive over the terrain at night). JSON once, cached.
// cityLights: settlement lights - OSM place nodes (city/town/
// village/hamlet) as slightly yellow glow dots from dusk on;
// bigger settlements (place class + population tag) get more and
// brighter dots. One Overpass query, cached permanently.
// Data (c) OpenStreetMap contributors, ODbL.
// ------------------------------------------------------------------
// 3D ground
// ------------------------------------------------------------------
// Copernicus DEM via terrarium tiles (z10), mesh built from the same
// height field the terrain shadows are cast against.
terrain: {
// soft rim at the mosaic border, in tiles (1 tile ~ 13 km)
edgeFadeTiles: 4,
enabled: true,
exaggeration: 8,
wireframe: false,
// How far the day map is dimmed at full night: 0 = pitch black,
// 1 = unchanged. Only affects night; the day map is untouched.
// (The NASA VIIRS city-lights layer was removed; city lights come
// from OSM place nodes via cityLights.)
nightDim: 0.22,
carLights: true,
cityLights: true
enabled: true, // false = flat plane (no DEM, no terrain shadows)
exaggeration: 8, // vertical scale (1 = real, nearly invisible here)
edgeFadeTiles: 4, // soft rim at the mosaic border, in tiles (~13 km)
carLights: true, // moving vehicle lights on the real road network
cityLights: true, // settlement lights from OSM place nodes
wireframe: false // true = draw the mesh edges (orientation debug)
},
// Car lights on the real road network (on/off stays in terrain.carLights).
// Two layers now: a thin road line (bandPx / bandBrightness) and one
// round point sprite per vehicle (minPx..maxPx), same idea as the city
// dots' gl_PointSize floor. laneM is the real distance between the two
// traffic directions, so they split close to the camera and merge into
// one line far away instead of drifting apart with the camera distance.
// dashM: mean vehicle spacing (secondary roads use dashM * 2.8).
// speed: multiplier on the real driving speed (motorway ~120 km/h,
// secondary ~70 km/h at dashM spacing).
// ------------------------------------------------------------------
// Vehicle lights
// ------------------------------------------------------------------
// Roads come from Overpass (motorway/trunk for the A-net, primary/
// secondary for the side layer), cached per grid cell.
carLights: {
roadWidthM: 24, // real carriageway width of the road line
bandPx: 1.2, // pixel floor for the road line
bandBrightness: 0.10, // road line glow (points are the main layer)
minPx: 3.6, // guaranteed vehicle light diameter
maxPx: 10, // ceiling for a light right above the camera
boost: 5, // extra light size close to the camera
laneM: 8, // real distance between the two directions
lanePx: 2.6, // screen floor for that distance, so the two
// directions never collapse into one stream
// Traffic lives on a disc around the configured location: roads
// outside rangeKm carry no vehicles. fadeKm / fadeSpanKm are the
// distance fade belonging to that disc (start, and how far past it
// until nothing is left) - keep them at or beyond rangeKm so the
// region never ends in a hard cut.
roadWidthM: 24, // real carriageway width of the road line
bandPx: 1.2, // pixel floor of the road line
bandBrightness: 0.10, // road line glow (the points are the main layer)
minPx: 3.6, // guaranteed vehicle light diameter
maxPx: 10, // ceiling for a light right above the camera
boost: 5, // extra light size close to the camera
laneM: 8, // real distance between the two directions
lanePx: 2.6, // screen floor for that distance, so the two
// streams never collapse into one line
// Traffic lives on a disc around CONFIG.location. Roads outside
// rangeKm carry no vehicles; fadeKm/fadeSpanKm are the matching
// distance fade (start, and how far past it nothing is left) - keep
// them at or beyond rangeKm so the disc never ends in a hard cut.
rangeKm: 200,
fadeKm: 150,
fadeSpanKm: 60,
// 'auto' = side of the road taken from location (lat/lon): left in
// the UK, Japan, Australia, India, Southern Africa, ... and right
// everywhere else. Set 'left' or 'right' to force it.
// 'auto' derives the driving side from the location (left in the UK,
// Japan, Australia, India, southern Africa, ...), 'left'/'right' forces it.
trafficSide: 'auto',
dashM: 620, // mean vehicle spacing on motorways
// Traffic spread around the motorway network (gaussian): peak on the
// motorway itself, thinning with distance to the nearest one.
// spreadKm = sigma, spreadFloor = share that far side roads keep.
spreadKm: 45,
spreadFloor: 0.30,
speed: 1.0, // flow speed multiplier
density: 1.0, // vehicle density multiplier
brightness: 1.3 // vehicle light intensity
dashM: 620, // mean vehicle spacing on motorways
// (side roads use dashM * 2.8)
spreadKm: 45, // gaussian sigma around the motorway network
spreadFloor: 0.30, // share that far side roads keep
speed: 1.0, // flow speed multiplier
density: 1.0, // vehicle density multiplier
brightness: 1.3 // vehicle light intensity
},
// Headlights and house lights do not ignite at once: every single
// light gets its own sun-altitude threshold between offDeg (nothing
// lit) and onDeg (everything lit), drawn per vehicle / per city block.
// offDeg +5 deg is roughly 30 min before sunset here, onDeg -6 deg is
// civil twilight. In the morning the same thresholds switch the lights
// off again as the sun climbs.
// ------------------------------------------------------------------
// Artificial light switching
// ------------------------------------------------------------------
// Every single light (vehicle, city dot, ground block) gets its own
// switch altitude inside this window, so a city does not ignite as one
// piece. offDeg +5 deg is about 30 min before sunset here, onDeg -6 deg
// is civil twilight. In the morning the same thresholds switch off again.
lights: {
offDeg: 5,
onDeg: -6,
softDeg: 1.2
softDeg: 1.2 // width of the individual transition
},
// Physical lighting: direct beam from a Kasten-Young air mass with
// Rayleigh + ozone + aerosol extinction, diffuse skylight and moonlight
// from the same model. terrainLighting: 'palette' switches back to the
// old hand-painted hillshade. turbidity scales the aerosol load,
// moonGain the moonlight strength, nightFloor the darkest skylight
// (airglow + starlight), albedoTint is the theme's share on surfaces.
// ------------------------------------------------------------------
// Physical light model
// ------------------------------------------------------------------
// Direct beam through a Kasten-Young air mass with Rayleigh + ozone +
// aerosol extinction; diffuse skylight, moonlight, starlight floor and
// aerial perspective all come from the same model, and sky, clouds,
// terrain, contrails and aircraft share one light state per frame.
atmosphere: {
terrainLighting: 'physical',
turbidity: 1.0,
// moonlight strength; scales the direct term AND the diffuse sky
// lift, both with the illuminated fraction (new moon -> none)
moonGain: 0.60,
// stylised starlight + airglow floor on a dark night (real
// integrated starlight is ~1e-6 of daylight and would be invisible)
starGain: 0.18,
// extra share on the ground. The product is the starlit floor and
// must stay below the skylight at sunset, otherwise the night would
// be brighter than the moment of sunset. Moonlight (moonGain) is
// what lifts a night above that floor.
starGainGround: 0.4,
nightFloor: 0.10,
// cloud albedo in physical mode: theme hue kept, brightness comes
// from the light model (the night palette alone would double-darken)
cloudBrightness: 1.0,
// moving cloud patches darken the ground beneath them (the terrain
// samples the same cloud field as the sky pass)
turbidity: 1.0, // aerosol load of the extinction
moonGain: 0.60, // moonlight, scaled by the illuminated fraction
starGain: 0.18, // stylised starlight + airglow (real integrated
// starlight would be invisible)
starGainGround: 0.4, // extra share on the ground; the product must
// stay below the skylight at sunset
nightFloor: 0.10, // darkest skylight before starlight takes over
albedoGain: 1.0, // theme share on surface colours
skyGain: 0.45, // diffuse skylight share on surfaces
airGain: 0.55, // Rayleigh airlight strength (blue-biased)
hazeResidual: 0.30, // surface colour surviving at the full fog distance
cloudBrightness: 1.0, // cloud albedo (theme hue, brightness from the model)
cloudHaze: 0.22, // share of the ground haze applied to the deck
// Cloud shadows: the terrain samples the same cloud field as the sky pass.
cloudShadows: true,
cloudShadowDensity: 1.8, // optical depth through the deck
cloudShadowFloor: 0.06, // darkest shade under a thick cloud
cloudShadowAmbient: 0.65, // share of the skylight a cloud also blocks
// ridges occlude the sun for the ground behind them (march over the
// same height field the mesh was built from)
cloudShadowDensity: 1.8, // optical depth through the deck
cloudShadowFloor: 0.06, // darkest shade under a thick cloud
cloudShadowAmbient: 0.65, // share of the skylight a deck also blocks
// Terrain self-shadowing over the DEM height field.
terrainShadows: true,
// brightness a fully shadowed slope keeps (0 = pitch black, 1 = off)
terrainShadowFloor: 0.42,
// how much of the ground haze applies to the cloud deck (clouds sit
// above most of the air that causes it)
cloudHaze: 0.22,
albedoGain: 1.0,
// share of the daylight column that the distance fog / horizon takes,
// and the diffuse skylight share on surfaces
skyGain: 0.45,
// Aerial perspective in physical mode: the terrain is attenuated by
// exp(-tau) and the scattered light of the same air column is added.
// airGain scales the Rayleigh airlight (blue-biased), hazeResidual
// is how much of the surface survives at the full fog distance
// (0.3 = distant ground keeps 30 % of its own colour).
airGain: 0.55,
hazeResidual: 0.30
terrainShadowFloor: 0.42, // brightness a fully shadowed slope keeps (0 = black)
// Optional variants (leave commented out to use the defaults):
// turbidity: 3.0 // hazy, warm, strongly reddened low sun
// moonGain: 1.2 // bright full-moon nights
// starGain: 0.30 // stronger starcast on clouds and contrails
// hazeResidual: 0.10 // flatter, milkier distance haze
// cloudShadows: false // no moving cloud shade on the ground
// terrainShadows: false // no ridge shadows
},
// Star colour: every catalogue star gets its real blackbody tint, mixed
// towards white by this factor. 0 = pure white points, 1 = full colour.
// ------------------------------------------------------------------
// Star catalogue
// ------------------------------------------------------------------
stars: {
tint: 0.10, // 90 % white + 10 % of the star's real colour
brightness: 1.6 // overall glow of the catalogue points
tint: 0.10, // 0 = pure white points, 1 = full blackbody colour
brightness: 1.6 // overall glow of the catalogue points
},
// Day simulation: true = a full 24 h cycle runs in cycleSeconds
// (sun rises east, culminates south, sets west).
// false = real sun/moon positions via SunCalc.
// fixedTime: 'HH:MM' (or a number 0-24) freezes the sim clock at
// that time of day (moving objects, clouds, cars and the weather
// sim keep running unaffected); null/omitted = the clock advances.
// ------------------------------------------------------------------
// Day cycle
// ------------------------------------------------------------------
// simulation: true runs a full 24 h cycle in cycleSeconds (sun rises
// east, culminates south, sets west) and the clock plus the sun/moon
// tables follow the simulated date. false uses the real SunCalc positions.
day: {
// simulation: true runs a full 24 h cycle in cycleSeconds.
// 60 s = one simulated day per minute of wall time (one simulated
// hour every 2.5 s). The analog clock and the sun/moon tables follow
// the simulated date.
simulation: true,
cycleSeconds: 60,
fixedTime: null
},
// Cloud deck in world units (1 unit ~ 12.9 km at zoom 11; flight
// band 15-35 km = y ~ 1.16-2.70). Keep the top below the lowest
// flight altitude.
clouds: {
top: 0.9,
bottom: -0.3,
// per weather level (0 = clear, 1 = overcast); overrides the built-in
// sunny 0.20 / cloudy 0.70 / stormy 0.80 / night 0.45
density: { stormy: 0.80 }
cycleSeconds: 60, // 60 s = one simulated day per minute
fixedTime: null // 'HH:MM' or 0-24 freezes the sim clock
// (clouds, traffic and flights keep moving)
// day: { simulation: false } // real sun and moon
},
// ------------------------------------------------------------------
// Debug helpers (off by default, documented so they are findable)
// ------------------------------------------------------------------
// debug: 1 // show the debug panel (weather, shader
// counters, theme palette, theme lighting)
// tiles: { debug: true } // bake tile borders and x/y labels into the map
// terrain: { wireframe: true } // draw the mesh edges
// ground: { style: 'streets' } // labelled street map instead of satellite
};
// List of available themes. The key is the theme id (used in config.theme),
// the value is the CSS file name. Add a new theme by adding a new entry here
// and dropping the corresponding CSS file in the same directory.
/*
* Themes.
*
* Each entry is an object:
* css stylesheet loaded for this theme (also the palette source:
* the scene reads the CSS custom properties of the active theme)
* lighting optional tint of the physical light model. The physics stays
* the base; the theme only pulls the colours toward its own hue
* by 'tint' (0 = pure physics, 1 = theme colour) and scales the
* exposure with gain (day) and nightGain (night).
* sun/moon/air/sky accept '#rgb' or '#rrggbb'.
* Omit 'lighting' entirely for a purely physical theme.
*
* Add a theme by adding an entry here and dropping the CSS file next to
* config.js - build.py inlines every stylesheet referenced here.
*/
window.THEMES = {
'ash': 'ash.css',
'gruvbox': 'gruvbox.css',
'nord': 'nord.css'
};
'ash': {
css: 'ash.css',
lighting: {
tint: 0.28,
sun: '#ffd9a6', // warm amber beam
sky: '#5b6f96', // cool slate skylight
air: '#7d93bd', // haze and horizon colour
moon: '#c9d6ea',
gain: 1.00,
nightGain: 1.00
}
},
'nord': {
css: 'nord.css',
lighting: {
tint: 0.45,
sun: '#f2e3c0',
sky: '#5e81ac',
air: '#81a1c1',
moon: '#d8e3ee',
gain: 0.95,
nightGain: 1.12 // nord nights read a little brighter
}
},
'gruvbox': {
css: 'gruvbox.css',
lighting: {
tint: 0.50,
sun: '#fabd7f',
sky: '#83a598',
air: '#b8a27f',
moon: '#e6d5ae',
gain: 1.05,
nightGain: 1.05
}
}
// A theme without a lighting block keeps the pure physical colours:
// 'mono': { css: 'mono.css' }
};
+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