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
+404 -477

No files matched your search

+19 -75
View File
@@ -9,9 +9,10 @@
<script> <script>
(function() { (function() {
var theme = (window.CONFIG && window.CONFIG.theme) || 'ash'; 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'); 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 // standalone build (build.py): themes are embedded in
// window.__inlineThemeCss and the external link is disabled // window.__inlineThemeCss and the external link is disabled
if (window.__inlineThemeCss && window.__inlineThemeCss[theme]) { 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) { function shiftColor(color, delta) {
var r = Math.max(0, Math.min(255, ((color >> 16) & 255) + 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)); var g = Math.max(0, Math.min(255, ((color >> 8) & 255) + delta));
@@ -942,50 +936,8 @@ window.sunPalettes = [
return (r << 16) | (g << 8) | b; 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; 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 ============ // ============ Clock & Sun/Moon Calculations ============
var hourHand = document.getElementById('hour-hand'); var hourHand = document.getElementById('hour-hand');
@@ -1038,10 +990,6 @@ window.sunPalettes = [
return (h < 10 ? '0' : '') + h + ':' + (m < 10 ? '0' : '') + m; 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 pad2(n) { return n < 10 ? '0' + n : '' + n; }
function pad3(n) { return n < 10 ? '00' + n : n < 100 ? '0' + n : '' + n; } function pad3(n) { return n < 10 ? '00' + n : n < 100 ? '0' + n : '' + n; }
@@ -1145,12 +1093,6 @@ window.sunPalettes = [
return result; 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) { function formatDateLong(date) {
var days = ['So.', 'Mo.', 'Di.', 'Mi.', 'Do.', 'Fr.', 'Sa.']; var days = ['So.', 'Mo.', 'Di.', 'Mi.', 'Do.', 'Fr.', 'Sa.'];
var d = date.getDate(); var d = date.getDate();
@@ -1679,11 +1621,11 @@ window.weatherThemes = {
var t = cfg.terrain || {}; var t = cfg.terrain || {};
if (t.enabled !== false) { if (t.enabled !== false) {
lines.push('Terrain: Copernicus DEM via Mapzen Terrarium'); 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)'); 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'); lines.push('Weather: Open-Meteo');
el.innerHTML = lines.join('<br>'); 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() { function toggleDebugPanel() {
var content = document.getElementById('debug-content'); var content = document.getElementById('debug-content');
var title = document.querySelector('.debug-title'); var title = document.querySelector('.debug-title');
@@ -1814,7 +1750,8 @@ window.weatherThemes = {
} }
function setDebugTheme(theme) { 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'); var css = document.getElementById('theme-css');
if (window.__inlineThemeCss && window.__inlineThemeCss[theme]) { if (window.__inlineThemeCss && window.__inlineThemeCss[theme]) {
if (css) css.media = 'not all'; if (css) css.media = 'not all';
@@ -1939,7 +1876,17 @@ window.weatherThemes = {
'orange', 'yellow', 'red', 'brown', 'pink', 'green', 'blue', 'blue-light', 'purple' 'orange', 'yellow', 'red', 'brown', 'pink', 'green', 'blue', 'blue-light', 'purple'
]; ];
var dbgRoot = getComputedStyle(document.documentElement); 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++) { for (var k = 0; k < colorKeys.length; k++) {
var ck = colorKeys[k]; var ck = colorKeys[k];
var cv = ASH[ck] || dbgRoot.getPropertyValue('--' + ck).trim(); var cv = ASH[ck] || dbgRoot.getPropertyValue('--' + ck).trim();
@@ -2205,9 +2152,6 @@ window.weatherThemes = {
} }
// Get timezone offset based on user's local time // Get timezone offset based on user's local time
function getTimezoneOffsetMinutes() {
return new Date().getTimezoneOffset();
}
var weatherData = null; var weatherData = null;
var WMO_ICONS = { var WMO_ICONS = {
+317 -288
View File
@@ -1,347 +1,376 @@
/* /*
* Wallpaper background config. * animated_wallpaper - configuration
* *
* Properties: * Everything the scene reads at runtime lives here. Values are plain
* theme - Active palette theme. One of the keys in window.THEMES * JavaScript so the file can also be edited while the page is open
* (default 'ash'). The matching CSS file is loaded at * (window.CONFIG is read every frame for most knobs).
* 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).
* *
* Time format tokens (subset): * TOP LEVEL
* YYYY 4-digit year (2026) * theme active palette, a key of window.THEMES (default 'ash')
* MMMM full month name (October) * language UI language code for i18n.js ('en', 'de', ...)
* MM month with leading zero (10) * timeFormat clock format string, token list at the bottom
* DD day with leading zero (07) * debug 1 = show the debug panel (bottom left: weather, shader
* HH 24-hour hour with zero (18) * counters, theme palette). 0 (default) = hidden
* mm minute with leading zero (14) * credits true = attribution line at the bottom of the info box
* ss second with leading zero (25) * units { temperature: 'C' | 'F' }
* a am/pm lowercase (pm) * location { lat, lon, label? } - sun/moon geometry, traffic side,
* A AM/PM uppercase (PM) * tile/DEM/Overpass origin, figure-eight centre
* dddd full weekday name (lowercase!) * renderScale internal 3D resolution (0.2..1) with blocky upscale
* Any other character is emitted verbatim. * 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 = { 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', timeFormat: 'DD. MMMM, YYYY ‒ HH:mm:SS',
debug: 0, debug: 0, // 1 = debug panel with the theme palette
units: { credits: true, // false = hide the attribution line
temperature: 'C' units: { temperature: 'C' }, // 'F' for Fahrenheit
},
// Data attribution at the bottom of the info box (automatic text // ------------------------------------------------------------------
// based on the enabled layers). false = hidden. // Rendering budget
credits: true, // ------------------------------------------------------------------
// Internal 3D resolution scale (0.3..1, default 1). Lower values // Internal 3D resolution. The value is snapped to an exact integer
// render the clouds/map shader at reduced resolution and upscale // divisor of the CSS size (0.5 -> 2x blocks, 0.33 -> 3x, 0.25 -> 4x)
// via CSS - use it if fps is low (GPU-bound) despite a quiet CPU. // and upscaled with image-rendering: pixelated, so it reads as pixel
// 1 = native. 0.5 = a quarter of the pixels (half width, half height), // art instead of a blurry rescale. The DOM UI stays crisp at any value.
// upscaled 2x as hard blocks; the DOM UI on top stays crisp. // 1 = native resolution (sharpest, most expensive).
renderScale: 0.5, renderScale: 0.5,
// WebGL scene frame cap (default 30; 0 = free-running/vsync). All // Scene frame cap. All motion is time-based, so this only changes
// scene motion is time-based, so the cap only changes smoothness. // smoothness, never speed. 0 = free running (vsync).
maxFps: 30, maxFps: 30,
// ------------------------------------------------------------------
// Location
// ------------------------------------------------------------------
// Drives sun/moon positions, the traffic side lookup, the tile/DEM
// origin, the Overpass grid and the figure-eight centre.
location: { location: {
// Latitude and longitude for sun/moon calculations
lat: 48.5667, lat: 48.5667,
lon: 13.4319 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, weatherSim: false,
weatherSimInterval: 30000, weatherSimInterval: 30000, // ms per weather step when weatherSim is true
// 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. // 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: { tiles: {
expireDays: null, expireDays: null, // e.g. 90 = re-fetch basemap tiles every 90 days
// true = draw tile borders + N/S + x/y coords into the ground debug: false // true = tile borders + N/S + x/y labels baked
// texture (debug orientation). Off = clean map. // into the ground texture (orientation debug)
debug: true
}, },
// 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: { 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: { orbit: {
radiusKm: 60, // lobe tip: max distance from the centre radiusKm: 60, // lobe tip reach (clamped ~190 km so the
clockwise: true, // tips plus the fog dome fit the mosaic)
reorient: true, // random heading at every crossing pass clockwise: true, // travel direction along the eight
// Slow vertical bobbing inside the 15-35 km flight band: reorient: true, // false keeps the bearing across crossings
// centre 25 km (~1.93 world units), amp = half the band // Vertical bobbing inside the 15-35 km band: centre 25 km, amp is
// (10 km ~ 0.77); hard clamp at the band limits. // half the band (10 km ~ 0.77 world units), tilt is how much the
// periodS = seconds per cycle, tilt = camera nose reaction to // camera nose follows the climb/descent rate.
// climb/descent rate. enabled: false = constant 25 km.
altBob: { enabled: true, amp: 0.77, periodS: 90, tilt: 0.3 } 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: { 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 // Cloud deck
// via wHub/wField/wStrip). Paths are gentle Bezier curves resampled to // ------------------------------------------------------------------
// arc length (constant ground speed) flown as a full profile: take-off // World units (1 unit ~ 12.9 km). Keep 'top' below the lowest flight
// and climb, cruise, descent and landing, a short ground hold, then a // altitude so aircraft fly above the deck.
// fresh destination. By day sun-lit silhouettes; by night white clouds: {
// strobing position lights with a red beacon. Contrails form above top: 0.9, // ~11.6 km
// contrailStartKm on contrailChance of the aircraft and stay visible bottom: -0.3, // ~-3.9 km (deck base below the terrain)
// for contrailLifeS. All distances in km (world scale: 1 unit = one // Coverage per weather level, 0 = clear, 1 = overcast. Only the
// z11 tile ~ 12.9 km at 48.6 deg N). // 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: { flights: {
enabled: true, enabled: true,
altitudeKm: 14, // cruise altitude altitudeKm: 14, // cruise altitude
climbKm: 100, // climb/descent run to/from cruise altitude climbKm: 100, // climb/descent run to and from cruise
count: 21, // max simultaneous aircraft count: 21, // simultaneous aircraft (max 240)
speedKmh: 850, // mean ground speed speedKmh: 850, // mean ground speed
speedVar: 0.18, // +/- deviation (0..0.5) speedVar: 0.18, // +/- speed deviation (0..0.5)
minLegKm: 60, // shortest leg (no taxi hops) minLegKm: 60, // shortest leg (no taxi hops)
holdMinS: 50, // ground hold at destination (min..max) holdMinS: 50, // ground hold at the destination
holdMaxS: 150, holdMaxS: 150,
wHub: 4, // destination weights: hub / airfield / strip wHub: 4, // destination weights
wField: 2, wField: 2,
wStrip: 1, wStrip: 1,
contrailChance: 0.85, // share of aircraft with a persistent trail // Contrails: contrailChance of the aircraft leaves a trail once
contrailStartKm: 6, // trail formation altitude // above contrailStartKm, visible for contrailLifeS seconds.
contrailLifeS: 300, // trail visibility duration contrailChance: 0.85,
contrailWidthM: 620, // full trail width contrailStartKm: 6,
// trails fade out completely by this distance (start is 35% of it) contrailLifeS: 300,
contrailFadeKm: 100, contrailWidthM: 620, // full trail width
// Night strobes. Size shrinks with distance but never below contrailFadeKm: 100, // fully faded at this distance (ramp starts at 35 %)
// lightPx, and never grows past lightMaxPx for a jet right above. // Night strobes. Point size shrinks with distance but never below
// The white strobe is a double flash: two equal flashes // lightPx and never above lightMaxPx for a jet directly overhead.
// blinkGapS apart, then blinkPeriodS - blinkGapS - blinkFlashS // The white strobe is a double flash: two flashes blinkFlashS long,
// of darkness (1.4 - 0.16 - 0.08 = 1.16 s pause). // blinkGapS apart, then the rest of blinkPeriodS is dark.
lightPx: 7, lightPx: 7,
lightMaxPx: 16, lightMaxPx: 16,
lightBoost: 9, lightBoost: 9,
blinkPeriodS: 1.4, blinkPeriodS: 1.4,
blinkGapS: 0.16, blinkGapS: 0.16,
blinkFlashS: 0.08 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). // 3D ground
// wireframe: true = overlay mesh edges as orange lines (debug). // ------------------------------------------------------------------
// carLights: moving lights on the real road network (OSM/Overpass // Copernicus DEM via terrarium tiles (z10), mesh built from the same
// curves, additive over the terrain at night). JSON once, cached. // height field the terrain shadows are cast against.
// 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.
terrain: { terrain: {
// soft rim at the mosaic border, in tiles (1 tile ~ 13 km) enabled: true, // false = flat plane (no DEM, no terrain shadows)
edgeFadeTiles: 4, exaggeration: 8, // vertical scale (1 = real, nearly invisible here)
enabled: true, edgeFadeTiles: 4, // soft rim at the mosaic border, in tiles (~13 km)
exaggeration: 8, carLights: true, // moving vehicle lights on the real road network
wireframe: false, cityLights: true, // settlement lights from OSM place nodes
// How far the day map is dimmed at full night: 0 = pitch black, wireframe: false // true = draw the mesh edges (orientation debug)
// 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
}, },
// 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 // Vehicle lights
// dots' gl_PointSize floor. laneM is the real distance between the two // ------------------------------------------------------------------
// traffic directions, so they split close to the camera and merge into // Roads come from Overpass (motorway/trunk for the A-net, primary/
// one line far away instead of drifting apart with the camera distance. // secondary for the side layer), cached per grid cell.
// 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).
carLights: { carLights: {
roadWidthM: 24, // real carriageway width of the road line roadWidthM: 24, // real carriageway width of the road line
bandPx: 1.2, // pixel floor for the road line bandPx: 1.2, // pixel floor of the road line
bandBrightness: 0.10, // road line glow (points are the main layer) bandBrightness: 0.10, // road line glow (the points are the main layer)
minPx: 3.6, // guaranteed vehicle light diameter minPx: 3.6, // guaranteed vehicle light diameter
maxPx: 10, // ceiling for a light right above the camera maxPx: 10, // ceiling for a light right above the camera
boost: 5, // extra light size close to the camera boost: 5, // extra light size close to the camera
laneM: 8, // real distance between the two directions laneM: 8, // real distance between the two directions
lanePx: 2.6, // screen floor for that distance, so the two lanePx: 2.6, // screen floor for that distance, so the two
// directions never collapse into one stream // streams never collapse into one line
// Traffic lives on a disc around the configured location: roads // Traffic lives on a disc around CONFIG.location. Roads outside
// outside rangeKm carry no vehicles. fadeKm / fadeSpanKm are the // rangeKm carry no vehicles; fadeKm/fadeSpanKm are the matching
// distance fade belonging to that disc (start, and how far past it // distance fade (start, and how far past it nothing is left) - keep
// until nothing is left) - keep them at or beyond rangeKm so the // them at or beyond rangeKm so the disc never ends in a hard cut.
// region never ends in a hard cut.
rangeKm: 200, rangeKm: 200,
fadeKm: 150, fadeKm: 150,
fadeSpanKm: 60, fadeSpanKm: 60,
// 'auto' = side of the road taken from location (lat/lon): left in // 'auto' derives the driving side from the location (left in the UK,
// the UK, Japan, Australia, India, Southern Africa, ... and right // Japan, Australia, India, southern Africa, ...), 'left'/'right' forces it.
// everywhere else. Set 'left' or 'right' to force it.
trafficSide: 'auto', trafficSide: 'auto',
dashM: 620, // mean vehicle spacing on motorways dashM: 620, // mean vehicle spacing on motorways
// Traffic spread around the motorway network (gaussian): peak on the // (side roads use dashM * 2.8)
// motorway itself, thinning with distance to the nearest one. spreadKm: 45, // gaussian sigma around the motorway network
// spreadKm = sigma, spreadFloor = share that far side roads keep. spreadFloor: 0.30, // share that far side roads keep
spreadKm: 45, speed: 1.0, // flow speed multiplier
spreadFloor: 0.30, density: 1.0, // vehicle density multiplier
speed: 1.0, // flow speed multiplier brightness: 1.3 // vehicle light intensity
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. // Artificial light switching
// 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 // Every single light (vehicle, city dot, ground block) gets its own
// off again as the sun climbs. // 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: { lights: {
offDeg: 5, offDeg: 5,
onDeg: -6, 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 // Physical light model
// old hand-painted hillshade. turbidity scales the aerosol load, // ------------------------------------------------------------------
// moonGain the moonlight strength, nightFloor the darkest skylight // Direct beam through a Kasten-Young air mass with Rayleigh + ozone +
// (airglow + starlight), albedoTint is the theme's share on surfaces. // 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: { atmosphere: {
terrainLighting: 'physical', turbidity: 1.0, // aerosol load of the extinction
turbidity: 1.0, moonGain: 0.60, // moonlight, scaled by the illuminated fraction
// moonlight strength; scales the direct term AND the diffuse sky starGain: 0.18, // stylised starlight + airglow (real integrated
// lift, both with the illuminated fraction (new moon -> none) // starlight would be invisible)
moonGain: 0.60, starGainGround: 0.4, // extra share on the ground; the product must
// stylised starlight + airglow floor on a dark night (real // stay below the skylight at sunset
// integrated starlight is ~1e-6 of daylight and would be invisible) nightFloor: 0.10, // darkest skylight before starlight takes over
starGain: 0.18, albedoGain: 1.0, // theme share on surface colours
// extra share on the ground. The product is the starlit floor and skyGain: 0.45, // diffuse skylight share on surfaces
// must stay below the skylight at sunset, otherwise the night would airGain: 0.55, // Rayleigh airlight strength (blue-biased)
// be brighter than the moment of sunset. Moonlight (moonGain) is hazeResidual: 0.30, // surface colour surviving at the full fog distance
// what lifts a night above that floor. cloudBrightness: 1.0, // cloud albedo (theme hue, brightness from the model)
starGainGround: 0.4, cloudHaze: 0.22, // share of the ground haze applied to the deck
nightFloor: 0.10, // Cloud shadows: the terrain samples the same cloud field as the sky pass.
// 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)
cloudShadows: true, cloudShadows: true,
cloudShadowDensity: 1.8, // optical depth through the deck cloudShadowDensity: 1.8, // optical depth through the deck
cloudShadowFloor: 0.06, // darkest shade under a thick cloud cloudShadowFloor: 0.06, // darkest shade under a thick cloud
cloudShadowAmbient: 0.65, // share of the skylight a cloud also blocks cloudShadowAmbient: 0.65, // share of the skylight a deck also blocks
// ridges occlude the sun for the ground behind them (march over the // Terrain self-shadowing over the DEM height field.
// same height field the mesh was built from)
terrainShadows: true, terrainShadows: true,
// brightness a fully shadowed slope keeps (0 = pitch black, 1 = off) terrainShadowFloor: 0.42, // brightness a fully shadowed slope keeps (0 = black)
terrainShadowFloor: 0.42, // Optional variants (leave commented out to use the defaults):
// how much of the ground haze applies to the cloud deck (clouds sit // turbidity: 3.0 // hazy, warm, strongly reddened low sun
// above most of the air that causes it) // moonGain: 1.2 // bright full-moon nights
cloudHaze: 0.22, // starGain: 0.30 // stronger starcast on clouds and contrails
albedoGain: 1.0, // hazeResidual: 0.10 // flatter, milkier distance haze
// share of the daylight column that the distance fog / horizon takes, // cloudShadows: false // no moving cloud shade on the ground
// and the diffuse skylight share on surfaces // terrainShadows: false // no ridge shadows
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
}, },
// 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: { stars: {
tint: 0.10, // 90 % white + 10 % of the star's real colour tint: 0.10, // 0 = pure white points, 1 = full blackbody colour
brightness: 1.6 // overall glow of the catalogue points 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. // Day cycle
// fixedTime: 'HH:MM' (or a number 0-24) freezes the sim clock at // ------------------------------------------------------------------
// that time of day (moving objects, clouds, cars and the weather // simulation: true runs a full 24 h cycle in cycleSeconds (sun rises
// sim keep running unaffected); null/omitted = the clock advances. // east, culminates south, sets west) and the clock plus the sun/moon
// tables follow the simulated date. false uses the real SunCalc positions.
day: { 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, simulation: true,
cycleSeconds: 60, cycleSeconds: 60, // 60 s = one simulated day per minute
fixedTime: null fixedTime: null // 'HH:MM' or 0-24 freezes the sim clock
}, // (clouds, traffic and flights keep moving)
// Cloud deck in world units (1 unit ~ 12.9 km at zoom 11; flight // day: { simulation: false } // real sun and moon
// 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 }
}, },
// ------------------------------------------------------------------
// 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 * Themes.
// and dropping the corresponding CSS file in the same directory. *
* 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 = { window.THEMES = {
'ash': 'ash.css', 'ash': {
'gruvbox': 'gruvbox.css', css: 'ash.css',
'nord': 'nord.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 uDensity;',
'uniform float uDayProgress;', 'uniform float uDayProgress;',
'uniform float uSunAlt;', 'uniform float uSunAlt;',
'uniform float uFogDist;', // world units (=tiles) where ground fog saturates
'uniform float uCloudTop;', // cloud deck upper edge (world Y) 'uniform float uCloudTop;', // cloud deck upper edge (world Y)
'uniform float uCloudBot;', // cloud deck lower edge (world Y) 'uniform float uCloudBot;', // cloud deck lower edge (world Y)
'uniform sampler2D uElev;', // terrarium RGB height grid, same UV as uGround 'uniform sampler2D uElev;', // terrarium RGB height grid, same UV as uGround
@@ -501,9 +500,6 @@ var TERRAIN_FRAG_SRC = [
'uniform vec2 uGroundSize;', 'uniform vec2 uGroundSize;',
'uniform vec2 uCorridorRot;', 'uniform vec2 uCorridorRot;',
'uniform vec3 uSunDir;', 'uniform vec3 uSunDir;',
'uniform vec3 uLight;',
'uniform vec3 uBg;',
'uniform vec3 uSky;',
'uniform vec3 uMoonDir;', // unit vector towards the moon (world) 'uniform vec3 uMoonDir;', // unit vector towards the moon (world)
'uniform vec3 uSunColor;', // direct beam after atmospheric extinction 'uniform vec3 uSunColor;', // direct beam after atmospheric extinction
'uniform float uSunInt;', // direct beam strength (1 = zenith noon) 'uniform float uSunInt;', // direct beam strength (1 = zenith noon)
@@ -512,7 +508,6 @@ var TERRAIN_FRAG_SRC = [
'uniform float uSkyAmb;', // diffuse skylight 'uniform float uSkyAmb;', // diffuse skylight
'uniform vec3 uAlbedoTint;', // theme fine-tuning of the surface colour 'uniform vec3 uAlbedoTint;', // theme fine-tuning of the surface colour
'uniform vec3 uHorizonColor;', // fog colour taken from the same light model '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 uStarAmb;', // diffuse starlight + airglow (night only)
'uniform float uTime;', // must match the sky pass: same cloud field 'uniform float uTime;', // must match the sky pass: same cloud field
'uniform float uCloudBot;', 'uniform float uCloudBot;',
@@ -526,17 +521,10 @@ var TERRAIN_FRAG_SRC = [
'uniform float uElevScale;', // metres -> world units (incl. exaggeration) 'uniform float uElevScale;', // metres -> world units (incl. exaggeration)
'uniform float uTerrainShadowOn;', 'uniform float uTerrainShadowOn;',
'uniform float uTerrainShadowFloor;', // brightness a fully shadowed slope keeps '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 vec3 uAirColor;', // Rayleigh airlight (sun-tinted, blue-biased)
'uniform float uAirDensity;', // slant optical depth per world unit 'uniform float uAirDensity;', // slant optical depth per world unit
'uniform vec3 uCamPos;', // world-space camera, for the view direction 'uniform vec3 uCamPos;', // world-space camera, for the view direction
'uniform float uEdgeFadeTiles;', '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) 'uniform vec3 uWireColor;', // non-zero -> debug wireframe overlay (flat colour)
'varying vec3 vNormal;', 'varying vec3 vNormal;',
'varying vec2 vUV;', 'varying vec2 vUV;',
@@ -547,7 +535,7 @@ var TERRAIN_FRAG_SRC = [
'void main() {', 'void main() {',
'vec2 gUv = vUV;', 'vec2 gUv = vUV;',
// Edge fade: beyond the canvas the texture is clamped to the // 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 // Distance from the mosaic border in TILES, so the fade band has a
// real width (uEdgeFadeTiles) instead of a fixed 5 % slice - the // real width (uEdgeFadeTiles) instead of a fixed 5 % slice - the
// rim dissolves into the airlight over tens of kilometres. // 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) + // Direct beam (extinction-coloured by the JS air-mass model) +
// hemispherical skylight + moonlight. No hand-picked day/night // hemispherical skylight + moonlight. No hand-picked day/night
// ramp: the sun altitude drives colour and strength continuously. // 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));', 'float ndl = max(0.0, dot(n, uSunDir));',
// Cloud shadow: march from the surface point towards the sun // Cloud shadow: march from the surface point towards the sun
// through the cloud layer and accumulate optical depth. Same // through the cloud layer and accumulate optical depth. Same
@@ -608,42 +595,10 @@ var TERRAIN_FRAG_SRC = [
' * ambShadow * mix(1.0, tShadow, 0.18);', ' * ambShadow * mix(1.0, tShadow, 0.18);',
'vec3 moon = alb * uMoonColor * max(0.0, dot(n, uMoonDir)) * uMoonInt * ambShadow;', 'vec3 moon = alb * uMoonColor * max(0.0, dot(n, uMoonDir)) * uMoonInt * ambShadow;',
'col = direct + sky + moon;', '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 ---------------------------------------------- // ---- Distance fog ----------------------------------------------
// Real view-space distance (uFogDist is in tiles, matching the // Real view-space distance, not a height proxy.
// world-unit scale of the mesh), not a height proxy.
'float dist = length(vViewPos);', 'float dist = length(vViewPos);',
'vec3 horizonCol = (uPhysical > 0.5) ? uHorizonColor', 'vec3 horizonCol = uHorizonColor;',
' : mix(uBg, uSky, 0.5) * (1.0 - uNightFactor * 0.55);',
// ---- Aerial perspective -------------------------------------- // ---- Aerial perspective --------------------------------------
// Physical mode: Beer-Lambert along the view ray. The surface is // Physical mode: Beer-Lambert along the view ray. The surface is
// attenuated by exp(-tau) and the light scattered in the same air // 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, // veil but into the airlight itself - blue-biased (Rayleigh,
// 1/lambda^4) and brighter towards the sun through the (1+cos^2)/2 // 1/lambda^4) and brighter towards the sun through the (1+cos^2)/2
// phase function. // phase function.
'if (uPhysical > 0.5) {', 'float tau = uAirDensity * dist * (0.75 + 0.25 * smoothstep(0.4, 0.0, vY));',
'float tau = uAirDensity * dist * (0.75 + 0.25 * smoothstep(0.4, 0.0, vY));', 'float trans = exp(-tau);',
'float trans = exp(-tau);', 'vec3 vd = normalize(vWorld - uCamPos);',
'vec3 vd = normalize(vWorld - uCamPos);', 'float cosT = dot(vd, uSunDir);',
'float cosT = dot(vd, uSunDir);', 'float rphase = 0.5 + 0.5 * cosT * cosT;',
'float rphase = 0.5 + 0.5 * cosT * cosT;', 'col = col * trans + uAirColor * rphase * (1.0 - trans);',
'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);',
'}',
// Edge fade to the same horizon colour // Edge fade to the same horizon colour
'col = mix(horizonCol, col, edgeFade);', 'col = mix(horizonCol, col, edgeFade);',
// Wireframe overlay: when CONFIG.terrain.wireframe is set the // Wireframe overlay: when CONFIG.terrain.wireframe is set the
@@ -719,7 +667,6 @@ function init(canvas) {
var uDensity = gl.getUniformLocation(prog, 'uDensity'); var uDensity = gl.getUniformLocation(prog, 'uDensity');
var uDayProgress = gl.getUniformLocation(prog, 'uDayProgress'); var uDayProgress = gl.getUniformLocation(prog, 'uDayProgress');
var uSunAlt = gl.getUniformLocation(prog, 'uSunAlt'); var uSunAlt = gl.getUniformLocation(prog, 'uSunAlt');
var uFogDist = gl.getUniformLocation(prog, 'uFogDist');
var uCloudTop = gl.getUniformLocation(prog, 'uCloudTop'); var uCloudTop = gl.getUniformLocation(prog, 'uCloudTop');
var uCloudBot = gl.getUniformLocation(prog, 'uCloudBot'); var uCloudBot = gl.getUniformLocation(prog, 'uCloudBot');
var uElev = gl.getUniformLocation(prog, 'uElev'); var uElev = gl.getUniformLocation(prog, 'uElev');
@@ -768,13 +715,6 @@ function init(canvas) {
var terrainUproj = terrainProg ? gl.getUniformLocation(terrainProg, 'uProj') : null; var terrainUproj = terrainProg ? gl.getUniformLocation(terrainProg, 'uProj') : null;
var terrainUground = terrainProg ? gl.getUniformLocation(terrainProg, 'uGround') : null; var terrainUground = terrainProg ? gl.getUniformLocation(terrainProg, 'uGround') : null;
var terrainUsundir = terrainProg ? gl.getUniformLocation(terrainProg, 'uSunDir') : 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 terrainUgroundcenter = terrainProg ? gl.getUniformLocation(terrainProg, 'uGroundCenter') : null;
var terrainUgroundsize = terrainProg ? gl.getUniformLocation(terrainProg, 'uGroundSize') : null; var terrainUgroundsize = terrainProg ? gl.getUniformLocation(terrainProg, 'uGroundSize') : null;
var terrainUcorridor = terrainProg ? gl.getUniformLocation(terrainProg, 'uCorridorRot') : 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 terrainUskyAmb = terrainProg ? gl.getUniformLocation(terrainProg, 'uSkyAmb') : null;
var terrainUalbedoTint = terrainProg ? gl.getUniformLocation(terrainProg, 'uAlbedoTint') : null; var terrainUalbedoTint = terrainProg ? gl.getUniformLocation(terrainProg, 'uAlbedoTint') : null;
var terrainUhorizon = terrainProg ? gl.getUniformLocation(terrainProg, 'uHorizonColor') : 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 terrainUairColor = terrainProg ? gl.getUniformLocation(terrainProg, 'uAirColor') : null;
var terrainUairDensity = terrainProg ? gl.getUniformLocation(terrainProg, 'uAirDensity') : null; var terrainUairDensity = terrainProg ? gl.getUniformLocation(terrainProg, 'uAirDensity') : null;
var terrainUcampos = terrainProg ? gl.getUniformLocation(terrainProg, 'uCamPos') : null; var terrainUcampos = terrainProg ? gl.getUniformLocation(terrainProg, 'uCamPos') : null;
@@ -811,8 +746,8 @@ function init(canvas) {
var terrainUterrainShadowFloor = terrainProg ? gl.getUniformLocation(terrainProg, 'uTerrainShadowFloor') : null; var terrainUterrainShadowFloor = terrainProg ? gl.getUniformLocation(terrainProg, 'uTerrainShadowFloor') : null;
// 1x1 black texture: bound wherever a real layer does not exist yet // 1x1 black texture: bound wherever a real layer does not exist yet
// (e.g. night lights before the first tile, or nightLights: false) // (e.g. the basemap before the first tile, or terrain disabled) so the
// so the shaders always sample a valid black. // shaders always sample a valid black.
var blackTex = gl.createTexture(); var blackTex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, blackTex); gl.bindTexture(gl.TEXTURE_2D, blackTex);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA,
@@ -833,7 +768,7 @@ function init(canvas) {
uSky: uSky, uCloud: uCloud, uSky: uSky, uCloud: uCloud,
uCloudShadow: uCloudShadow, uLight: uLight, uBg: uBg, uCloudShadow: uCloudShadow, uLight: uLight, uBg: uBg,
uSpeed: uSpeed, uDensity: uDensity, uDayProgress: uDayProgress, uSpeed: uSpeed, uDensity: uDensity, uDayProgress: uDayProgress,
uSunAlt: uSunAlt, uFogDist: uFogDist, uSunAlt: uSunAlt,
uCloudTop: uCloudTop, uCloudBot: uCloudBot, uCloudTop: uCloudTop, uCloudBot: uCloudBot,
uElev: uElev, uElevOn: uElevOn, uElevScale: uElevScale, uElev: uElev, uElevOn: uElevOn, uElevScale: uElevScale,
uSunDir: uSunDir, uMoonDir: uMoonDir, uMode: uMode, uSunDir: uSunDir, uMoonDir: uMoonDir, uMode: uMode,
@@ -847,20 +782,15 @@ function init(canvas) {
prog: terrainProg, prog: terrainProg,
aPos: terrainApos, aNormal: terrainAnormal, aUV: terrainAuv, aPos: terrainApos, aNormal: terrainAnormal, aUV: terrainAuv,
uView: terrainUview, uProj: terrainUproj, uView: terrainUview, uProj: terrainUproj,
uGround: terrainUground, uSunDir: terrainUsundir, uLight: terrainUlight, uGround: terrainUground, uSunDir: terrainUsundir,
uBg: terrainUbg, uSky: terrainUsky, uFogDist: terrainUfogdist, uNightFactor: terrainUnight,
uGroundCenter: terrainUgroundcenter, uGroundSize: terrainUgroundsize, uGroundCenter: terrainUgroundcenter, uGroundSize: terrainUgroundsize,
uCorridorRot: terrainUcorridor, uCloudsOn: terrainUcloudsOn, uCorridorRot: terrainUcorridor, uCloudsOn: terrainUcloudsOn,
uNightDim: terrainUnightdim,
uElevScale: terrainUelevScale, uElevScale: terrainUelevScale,
uWireColor: terrainUwireColor, uWireColor: terrainUwireColor,
uSunColor: terrainUsunColor, uSunInt: terrainUsunInt, uSunColor: terrainUsunColor, uSunInt: terrainUsunInt,
uMoonColor: terrainUmoonColor, uMoonInt: terrainUmoonInt, uMoonColor: terrainUmoonColor, uMoonInt: terrainUmoonInt,
uSkyAmb: terrainUskyAmb, uAlbedoTint: terrainUalbedoTint, uSkyAmb: terrainUskyAmb, uAlbedoTint: terrainUalbedoTint,
uHorizonColor: terrainUhorizon, uPhysical: terrainUphysical, uHorizonColor: terrainUhorizon,
uSunAlt: terrainUsunAlt, uLightHi: terrainUlightHi,
uLightLo: terrainUlightLo, uLightSoft: terrainUlightSoft,
uAirColor: terrainUairColor, uAirDensity: terrainUairDensity, uAirColor: terrainUairColor, uAirDensity: terrainUairDensity,
uCamPos: terrainUcampos, uEdgeFadeTiles: terrainUedgeFade, uCamPos: terrainUcampos, uEdgeFadeTiles: terrainUedgeFade,
uStarAmb: terrainUstarAmb, uStarAmb: terrainUstarAmb,
@@ -1654,13 +1584,6 @@ function buildElevationSystem(glCtx) {
// ---- 3D math helpers (used by the terrain mesh + view-proj matrices) ---- // ---- 3D math helpers (used by the terrain mesh + view-proj matrices) ----
// 4x4 matrix laid out column-major (the same layout gl.uniformMatrix4fv // 4x4 matrix laid out column-major (the same layout gl.uniformMatrix4fv
// expects) as a flat Float32Array of 16 entries. // 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) { function mat4LookAt(eye, target, up) {
// z axis = (eye - target) normalized (camera looks down -z) // 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]; 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 = // How far the day map is dimmed at full night (0 = pitch black, 1 =
// unchanged). The NASA VIIRS city-lights layer was removed: settlement // unchanged). The NASA VIIRS city-lights layer was removed: settlement
// lights come from OSM place nodes (see the settlement lights section). // 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 car-lights layer ----------------------------------------
// Motorway/trunk roads (Autobahnen) from OSM via Overpass API, fetched at // 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_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_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_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_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; 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[1] * (beam[1] * kDay + mBeam[1] * kMoon),
RAYLEIGH[2] * (beam[2] * kDay + mBeam[2] * 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) { function lightFactor(altRad) {
var span = LIGHT_HI - LIGHT_LO; var span = LIGHT_HI - LIGHT_LO;
if (span <= 0) return 0; 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)); var tw = s > 0 ? 1 : Math.max(ATMO_NIGHT_FLOOR, Math.pow(2, altRad * 57.2957795 / 8.0));
return day * tw; 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_CFG = (window.CONFIG && window.CONFIG.terrain) || {};
var CAR_ON = CAR_CFG.carLights !== false; var CAR_ON = CAR_CFG.carLights !== false;
var CITY_ON = CAR_CFG.cityLights !== 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]); gl.uniform3f(s.uSunDir, sd[0], sd[1], sd[2]);
var md = window.__currentMoonDir || sd; var md = window.__currentMoonDir || sd;
gl.uniform3f(s.uMoonDir, md[0], md[1], md[2]); 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 // Star catalogue sky texture on texture unit 4
gl.activeTexture(gl.TEXTURE4); gl.activeTexture(gl.TEXTURE4);
@@ -4890,6 +4843,20 @@ window.clouds3dRender = function(target) {
var _moonDiff = ATMO_MOON_GAIN * _moonFrac * _moonUp * 0.9; var _moonDiff = ATMO_MOON_GAIN * _moonFrac * _moonUp * 0.9;
var _amb = skyAmbient(sunAlt) + _moonDiff; var _amb = skyAmbient(sunAlt) + _moonDiff;
var _air = airlightColor(sunAlt, _moonAlt, _moonFrac); 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.uAirColor, _air[0], _air[1], _air[2]);
gl.uniform3f(s.uSunColor, _beam[0], _beam[1], _beam[2]); gl.uniform3f(s.uSunColor, _beam[0], _beam[1], _beam[2]);
gl.uniform1f(s.uSunInt, _beamL); gl.uniform1f(s.uSunInt, _beamL);
@@ -4911,7 +4878,6 @@ window.clouds3dRender = function(target) {
var speed = (typeof themeSpeed === 'number') var speed = (typeof themeSpeed === 'number')
? Math.max(0.1, Math.min(3, themeSpeed)) : 1.0; ? Math.max(0.1, Math.min(3, themeSpeed)) : 1.0;
gl.uniform1f(s.uSpeed, speed); gl.uniform1f(s.uSpeed, speed);
gl.uniform1f(s.uFogDist, FOG_TILES);
gl.uniform1f(s.uCloudTop, CLOUD_TOP); gl.uniform1f(s.uCloudTop, CLOUD_TOP);
gl.uniform1f(s.uCloudBot, CLOUD_BOT); gl.uniform1f(s.uCloudBot, CLOUD_BOT);
@@ -5095,7 +5061,6 @@ window.clouds3dRender = function(target) {
gl.uniformMatrix4fv(terr.uView, false, view); gl.uniformMatrix4fv(terr.uView, false, view);
gl.uniformMatrix4fv(terr.uProj, false, proj); gl.uniformMatrix4fv(terr.uProj, false, proj);
gl.uniform1i(terr.uGround, 1); gl.uniform1i(terr.uGround, 1);
gl.uniform1f(terr.uNightDim, NIGHT_DIM);
gl.uniform2f(terr.uGroundCenter, centerWorld.x, centerWorld.y); gl.uniform2f(terr.uGroundCenter, centerWorld.x, centerWorld.y);
gl.uniform2f(terr.uGroundSize, groundTilesX(), groundTilesY()); gl.uniform2f(terr.uGroundSize, groundTilesX(), groundTilesY());
gl.uniform2f(terr.uCorridorRot, GROUND_TRAVEL_COS, GROUND_TRAVEL_SIN); gl.uniform2f(terr.uCorridorRot, GROUND_TRAVEL_COS, GROUND_TRAVEL_SIN);
@@ -5135,21 +5100,10 @@ window.clouds3dRender = function(target) {
// instead of a separate grey veil. // instead of a separate grey veil.
var _hor = 0.75; var _hor = 0.75;
gl.uniform3f(terr.uHorizonColor, _air[0] * _hor, _air[1] * _hor, _air[2] * _hor); 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.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.uniform1f(terr.uAirDensity, -Math.log(ATMO_HAZE_RESIDUAL) / Math.max(FOG_TILES, 0.001));
gl.uniform3f(terr.uCamPos, camX, camY, camZ); gl.uniform3f(terr.uCamPos, camX, camY, camZ);
gl.uniform1f(terr.uEdgeFadeTiles, EDGE_FADE_TILES); 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); gl.bindBuffer(gl.ARRAY_BUFFER, terr.vbo);
var STRIDE = 8 * 4; // 8 floats per vertex var STRIDE = 8 * 4; // 8 floats per vertex