diff --git a/config.js b/config.js index c84d469..45e2610 100644 --- a/config.js +++ b/config.js @@ -17,6 +17,7 @@ * tile/DEM/Overpass origin, figure-eight centre * renderScale internal 3D resolution (0.2..1) with blocky upscale * maxFps scene frame cap (0 = free running) + * antialias MSAA on the WebGL framebuffer (true/false) * weatherSim ignore Open-Meteo and cycle the weather themes * tiles basemap/elevation cache behaviour + debug overlays * ground basemap style @@ -51,7 +52,7 @@ window.CONFIG = { 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, // 1 = debug panel with the theme palette + debug: 0, // 1 = debug panel + progress logs in the console credits: true, // false = hide the attribution line units: { temperature: 'C' }, // 'F' for Fahrenheit @@ -67,6 +68,10 @@ window.CONFIG = { // Scene frame cap. All motion is time-based, so this only changes // smoothness, never speed. 0 = free running (vsync). maxFps: 30, + // MSAA on the WebGL framebuffer. Costs fill rate, and with renderScale + // below 1 the pixel blocks dominate anyway - set false if the scene is + // GPU bound. Read once at context creation, so a reload is needed. + antialias: true, // ------------------------------------------------------------------ // Location @@ -169,11 +174,16 @@ window.CONFIG = { wStrip: 1, // Contrails: contrailChance of the aircraft leaves a trail once // above contrailStartKm, visible for contrailLifeS seconds. - contrailChance: 0.85, + contrailChance: 1.00, contrailStartKm: 6, contrailLifeS: 300, contrailWidthM: 620, // full trail width contrailFadeKm: 100, // fully faded at this distance (ramp starts at 35 %) + // Night contrast: a physically scaled contrail is a faint grey thread + // that disappears at 4x pixel scale, so this is the night share of its + // opacity (0.34 was the old value) and it also drives the gain on the + // moon/star light. 0 = night contrails off, 1 = strongest. + contrailNight: 0.85, // 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, @@ -301,11 +311,12 @@ window.CONFIG = { // 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, - cycleSeconds: 60, // 60 s = one simulated day per minute + simulation: false, // false = the real sun and moon (default) + cycleSeconds: 60, // only used when simulation is true: + // 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 + // day: { simulation: true, cycleSeconds: 60 } // accelerated day }, // ------------------------------------------------------------------ diff --git a/vendor/clouds3d.js b/vendor/clouds3d.js index 0edebcc..e7af4ff 100644 --- a/vendor/clouds3d.js +++ b/vendor/clouds3d.js @@ -10,6 +10,13 @@ (function() { +// Progress and build diagnostics are only interesting while the debug panel +// is on; a wall display must not fill the console with them. +var DEBUG_LOG = !!(window.CONFIG && window.CONFIG.debug); +function dbgLog() { + if (DEBUG_LOG) console.log.apply(console, arguments); +} + function makeShader(gl, type, src) { var s = gl.createShader(type); gl.shaderSource(s, src); @@ -147,8 +154,13 @@ var FRAG_SRC = [ // the fine structure that cannot be resolved disappears. 'float cloudDensity(vec3 p, float baseThresh, float soft) {', 'float n = fbm3(p * 0.35 + vec3(0.0, 0.0, uTime * 0.06));', - 'float d = fbm3(p * 1.4 + vec3(uTime * 0.04, 7.3, uTime * 0.03));', - 'n = n + d * mix(0.20, 0.0, soft);', + // The detail octave weighs at most 0.20 and fades to zero with + // distance; past soft = 0.95 it contributes under 1 %, so the whole + // 4-octave lookup is skipped for the (numerous) far samples. + 'if (soft < 0.95) {', + 'float d = fbm3(p * 1.4 + vec3(uTime * 0.04, 7.3, uTime * 0.03));', + 'n = n + d * mix(0.20, 0.0, soft);', + '}', // Cloud layer between uCloudBot and uCloudTop, densest in the // middle (bell curve). 'float h = clamp((p.y - uCloudBot) / (uCloudTop - uCloudBot), 0.0, 1.0);', @@ -343,7 +355,10 @@ var FRAG_SRC = [ 'vec3 albC = uCloudAlbedo * mix(0.78, 1.0, lit);', // Self-shading: probe the density towards the sun, so // thick cloud interior stays dark and thin edges light up. - 'float thick = cloudDensity(p + uSunDir * 2.2, baseThresh, cSoft);', + // d > 0.85 already clamps selfLit to its floor, so the + // second density lookup (a whole fbm) is skipped there. + 'float thick = (d > 0.85) ? 1.0 :', + 'cloudDensity(p + uSunDir * 2.2, baseThresh, cSoft);', 'float selfLit = clamp(1.0 - thick * 1.3, 0.12, 1.0);', // Forward scattering - clouds brighten towards the sun // (silver lining) instead of a flat day/night ramp. @@ -484,8 +499,13 @@ var CLOUD_SAMPLER_GLSL = [ '}', 'float cloudDensity(vec3 p, float baseThresh, float soft) {', 'float n = fbm3(p * 0.35 + vec3(0.0, 0.0, uTime * 0.06));', - 'float d = fbm3(p * 1.4 + vec3(uTime * 0.04, 7.3, uTime * 0.03));', - 'n = n + d * mix(0.20, 0.0, soft);', + // The detail octave weighs at most 0.20 and fades to zero with + // distance; past soft = 0.95 it contributes under 1 %, so the whole + // 4-octave lookup is skipped for the (numerous) far samples. + 'if (soft < 0.95) {', + 'float d = fbm3(p * 1.4 + vec3(uTime * 0.04, 7.3, uTime * 0.03));', + 'n = n + d * mix(0.20, 0.0, soft);', + '}', 'float h = clamp((p.y - uCloudBot) / (uCloudTop - uCloudBot), 0.0, 1.0);', 'float profile = 4.0 * h * (1.0 - h);', 'profile = max(profile, 0.0);', @@ -627,8 +647,12 @@ var TERRAIN_FRAG_SRC = [ ].join('\n'); function init(canvas) { - var gl = canvas.getContext('webgl2', { antialias: false, alpha: false }) - || canvas.getContext('webgl', { antialias: false, alpha: false }); + // CONFIG.antialias (default on) asks for MSAA on the default framebuffer. + // It costs fill rate, and at renderScale < 1 the pixel blocks dominate + // anyway - set it to false if the scene is GPU bound. + var AA_ON = !(window.CONFIG && window.CONFIG.antialias === false); + var glOpts = { antialias: AA_ON, alpha: false, powerPreference: 'high-performance' }; + var gl = canvas.getContext('webgl2', glOpts) || canvas.getContext('webgl', glOpts); if (!gl) { console.error('clouds3d: WebGL not available'); return null; @@ -1195,7 +1219,7 @@ function tileProbeProxy() { var type = (r.headers.get('Content-Type') || '').toLowerCase(); if (r.ok && type.indexOf('image/') === 0) { apply(); - console.log('[clouds3d] tiles: using dev proxy ' + path); + dbgLog('[clouds3d] tiles: using dev proxy ' + path); } }, function () {}); } @@ -2151,7 +2175,7 @@ function opProbeProxy() { }).then(function (r) { if (r.status === 200) { CAR_ENDPOINTS = OP_PUBLIC.concat(OP_PROXY); - console.log('[clouds3d] overpass: dev proxy available'); + dbgLog('[clouds3d] overpass: dev proxy available'); } }, function () {}); } @@ -2433,11 +2457,11 @@ function fetchMotorways() { var fresh = 0; // cells that needed a real fetch this cycle var fpParts = []; // "id:savedAt" of every raw cell -> fp() var doneCells = 0, totalCells = CAR_QUERIES.length; - console.log('[clouds3d] road cells: starting 0/' + totalCells); + dbgLog('[clouds3d] road cells: starting 0/' + totalCells); function progCell(cached) { doneCells++; if (doneCells % 10 === 0 || doneCells === totalCells) { - console.log('[clouds3d] road cells: ' + doneCells + '/' + totalCells + + dbgLog('[clouds3d] road cells: ' + doneCells + '/' + totalCells + (cached ? ' (cached)' : ' (fresh ' + fresh + ')')); } } @@ -2535,14 +2559,14 @@ function fetchMotorways() { // be replayed forever and never grow. if (newest > rec.savedAt) return false; if (missing > Math.max(1, total * 0.1)) { - console.log('[clouds3d] car layer: ' + missing + '/' + total + + dbgLog('[clouds3d] car layer: ' + missing + '/' + total + ' cells missing - rebuilding instead of replaying the cache'); return false; } carGeo = { motorway: { cnts: Int32Array.from(rec.mCnts), pts: b64ToF32(rec.mData) }, secondary: { cnts: Int32Array.from(rec.bCnts), pts: b64ToF32(rec.bData) } }; kickCarBuild(false); - console.log('[clouds3d] car layer: ' + carGeo.motorway.cnts.length + + dbgLog('[clouds3d] car layer: ' + carGeo.motorway.cnts.length + ' motorway + ' + carGeo.secondary.cnts.length + ' chains (from cache' + (missing ? ', ' + missing + ' cells unavailable' : '') + ')'); return true; @@ -2558,7 +2582,7 @@ function fetchMotorways() { carPersistFp = geoFp(fpParts); carCycleFp = carPersistFp; kickCarBuild(true); - console.log('[clouds3d] car layer: ' + carRaw.motorway.length + + dbgLog('[clouds3d] car layer: ' + carRaw.motorway.length + ' motorway + ' + carRaw.secondary.length + ' secondary polylines queued (' + fresh + ' cells re-fetched)'); }); @@ -2594,7 +2618,7 @@ function fetchSettlements() { function progCell(cached) { doneCells++; if (doneCells % 5 === 0 || doneCells === totalCells) { - console.log('[clouds3d] place cells: ' + doneCells + '/' + totalCells + + dbgLog('[clouds3d] place cells: ' + doneCells + '/' + totalCells + (cached ? ' (cached)' : ' (fresh ' + fresh + ')')); } } @@ -2623,7 +2647,7 @@ function fetchSettlements() { } } totalCells = cells.length; - console.log('[clouds3d] place cells: starting 0/' + totalCells); + dbgLog('[clouds3d] place cells: starting 0/' + totalCells); var list = []; var seenB = {}; // bucket -> index in list: dedupe node/way/rel of one place function parse(data) { @@ -2688,7 +2712,7 @@ function fetchSettlements() { var before = list.length; parse(obj); if (list.length > before) - console.log('[clouds3d] place cell ' + entry.id + ': +' + + dbgLog('[clouds3d] place cell ' + entry.id + ': +' + (list.length - before) + ' places (total ' + list.length + ')'); if (!entry._cached) fresh++; // dot geometry is generated in the geo worker; the debounce @@ -2715,7 +2739,7 @@ function fetchSettlements() { cityGeo = b64ToF32(rec.data); kickCityBuild(); if (carGeo) kickCarBuild(false); - console.log('[clouds3d] settlements: ' + rec.cnt + ' places (from cache)'); + dbgLog('[clouds3d] settlements: ' + rec.cnt + ' places (from cache)'); return true; }); }); @@ -2731,7 +2755,7 @@ function fetchSettlements() { tileDbPut(db, { id: 'geo/placecache_v1', fp: geoFp(fpParts), cnt: list.length, data: f32ToB64(packPlaces(list)), savedAt: Date.now() }); }); - console.log('[clouds3d] settlements: ' + list.length + ' places queued for build' + + dbgLog('[clouds3d] settlements: ' + list.length + ' places queued for build' + (terrainSample ? '' : ' (terrain pending)')); }); }); @@ -3511,7 +3535,7 @@ function geoWorkerGet(lane) { w.onmessage = function (e) { var res = e.data || {}; if (res.progress) { - console.log('[clouds3d] ' + lane + ' worker ' + res.kind + ': ' + + dbgLog('[clouds3d] ' + lane + ' worker ' + res.kind + ': ' + res.done + '/' + res.total); return; } @@ -3698,13 +3722,13 @@ function runCarBuild(done) { if (res.joins) carJoinCount = res.joins; if (res.maxChainKm) carMaxChainKm = res.maxChainKm; applyCarBuild(gl, res); - console.log('[clouds3d] car build: ' + carCountM + ' + ' + carCountB + + dbgLog('[clouds3d] car build: ' + carCountM + ' + ' + carCountB + ' ribbon verts, ' + carGeo.motorway.cnts.length + '+' + carGeo.secondary.cnts.length + ' chains, ' + carPtsCountM + ' motorway + ' + carPtsCountB + ' side lights, longest ~' + Math.round(carMaxChainKm) + ' km, ' + Math.round(performance.now() - t0) + ' ms off-thread'); - console.log('[clouds3d] car slots: motorway ' + (carPtsCountM / 2) + + dbgLog('[clouds3d] car slots: motorway ' + (carPtsCountM / 2) + ' slots on ' + Math.round(res.mLen * KM_PER_TILE) + ' km (want ' + Math.round(res.mLen / CAR_M_DASH) + ', dash ' + Math.round(CAR_M_DASH * KM_PER_TILE * 1000) + ' m) | side ' + (carPtsCountB / 2) + @@ -3815,20 +3839,20 @@ function runCityBuild(done) { var gl = elevGl || (clouds3dState && clouds3dState.gl); if (!gl || !terrainSample || !cityGeo) { // silent skip made the layer look dead: say what is still missing - console.log('[clouds3d] city build skipped - ' + + dbgLog('[clouds3d] city build skipped - ' + (!gl ? 'no gl context' : '') + (!gl || terrainSample ? '' : ', no elevation sample yet') + (cityGeo ? '' : ', no place nodes fetched yet')); done(); return; } - console.log('[clouds3d] city build: ' + (cityGeo.length / 4) + ' places queued'); + dbgLog('[clouds3d] city build: ' + (cityGeo.length / 4) + ' places queued'); var t0 = performance.now(); geoRun({ op: 'city', places: cityGeo, terrain: terrainMsg(), cfg: cityCfg() }, function (res) { if (!res || res.error) { done(); return; } cityBuildMs = Math.round(performance.now() - t0); applyCityBuild(gl, res); - console.log('[clouds3d] settlements: ' + cityCount + ' lights from ' + + dbgLog('[clouds3d] settlements: ' + cityCount + ' lights from ' + (cityGeo.length / 4) + ' places, ' + cityBuildMs + ' ms off-thread'); done(); }); @@ -4021,6 +4045,11 @@ var CT_T = Math.max(10, fltNum(FLT_CFG.contrailLifeS, 300)); var CT_HALF_W = Math.max(0.002, fltNum(FLT_CFG.contrailWidthM, 310) / 2000 / KM_PER_TILE); // world units: trails are gone by contrailFadeKm, ramping from 35% of it var CT_FADE_END = Math.max(1, fltNum(FLT_CFG.contrailFadeKm, 100) / KM_PER_TILE); +// Night readability. A physically scaled contrail on a starlit night is a +// faint grey thread that the pixel grid removes entirely, so the night share +// of its opacity and a gain on the moon/star light are configurable. +var CT_NIGHT = Math.max(0, Math.min(1, fltNum(FLT_CFG.contrailNight, 0.85))); +var CT_NIGHT_BOOST = 1.0 + CT_NIGHT * 2.2; var CT_FADE_START = CT_FADE_END * 0.35; var CT_MAX = FLT_MAX; var CT_SEG = 8; // curve resolution per trail @@ -4218,7 +4247,7 @@ function fetchAirports() { }); if (!airports.length) return; buildFlights(airports); - console.log('[clouds3d] flights: ' + airports.length + ' airfields -> ' + + dbgLog('[clouds3d] flights: ' + airports.length + ' airfields -> ' + fltData.length + ' aircraft (destinations rolled per take-off)'); }); } @@ -4388,6 +4417,7 @@ var CT_FRAG_SRC = [ 'uniform float uMoonInt;', 'uniform float uSkyAmb;', 'uniform float uStarAmb;', + 'uniform float uNightBoost;', // extra gain on the moon/star terms 'uniform vec3 uSunDir;', 'uniform vec3 uCamPos;', 'varying float vU;', @@ -4426,8 +4456,9 @@ var CT_FRAG_SRC = [ // Same multipliers as the cloud deck, so the contrail/cloud // brightness ratio at night is the same as in daylight. 'vec3 light = uSunColor * uSunInt * phase * 1.25', - ' + uMoonColor * uMoonInt * 3.0', - ' + max(vec3(uSkyAmb), uStarAmb * vec3(0.75, 0.85, 1.0)) * 1.7;', + ' + (uMoonColor * uMoonInt * 3.0', + ' + max(vec3(uSkyAmb), uStarAmb * vec3(0.75, 0.85, 1.0)) * 1.7)', + ' * uNightBoost;', 'gl_FragColor = vec4(vec3(0.95, 0.96, 1.0) * light, a);', '}' ].join('\n'); @@ -4616,7 +4647,7 @@ function buildSkyTexture(stars) { var b = 0.7 + 0.3 * Math.min(1, Math.pow(2.512, -mag * 0.7) * 1.3); data[di++] = b; } - console.log('[clouds3d] stars: ' + stars.length + ' points, mean rgb ' + + dbgLog('[clouds3d] stars: ' + stars.length + ' points, mean rgb ' + (mr / stars.length).toFixed(3) + '/' + (mg / stars.length).toFixed(3) + '/' + (mb / stars.length).toFixed(3)); var prog = makeProgram(gl, SKYPTS_VERT, SKYPTS_FRAG); @@ -5513,6 +5544,7 @@ window.clouds3dRender = function(target) { // CT_HALF_W doubled with the trail width, so the factor // halves: the aircraft silhouette keeps its old size. var S0 = CT_HALF_W * 1.0; + var _meshDrawn = 0; for (var dj = 0; dj < fltCount; dj++) { var briJ = fltBuf[dj * 6 + 3]; if (briJ <= 0) continue; @@ -5535,6 +5567,7 @@ window.clouds3dRender = function(target) { gl.uniform4f(planeU.orit, Math.cos(yaw), Math.sin(yaw), pitch, 0); gl.uniform1f(planeU.s, scl); gl.drawArrays(gl.TRIANGLES, 0, planeN); + _meshDrawn++; } gl.depthMask(true); } @@ -5545,7 +5578,7 @@ window.clouds3dRender = function(target) { // and the stripe follows the curved leg: walked backwards from // the aircraft in fixed steps. Visible by day (sun-lit) AND by // night as dimmed moonlit streaks. - var tvis = Math.max(dayf, carNight * 0.42); + var tvis = Math.max(dayf, carNight * CT_NIGHT); if (tvis > 0.02) { if (!ctBuf) ctBuf = new Float32Array(CT_MAX * CT_SEG * 6 * 7); ctVerts = 0; @@ -5574,7 +5607,7 @@ window.clouds3dRender = function(target) { if (dfc > 1) dfc = 1; else if (dfc <= 0.02) continue; // sun-lit by day, moonlit after dusk. dayf alone made the // stripes vanish completely at night although the pass ran. - var lit = Math.max(dayf, carNight * 0.34); + var lit = Math.max(dayf, carNight * CT_NIGHT); var strg = gt * lit * dfc; // widen with distance: a fixed 310 m ribbon is sub-pixel at // 150 km and blends away; grow it to hold ~2-4 screen px @@ -5622,7 +5655,8 @@ window.clouds3dRender = function(target) { moonColor: gl.getUniformLocation(cpm, 'uMoonColor'), moonInt: gl.getUniformLocation(cpm, 'uMoonInt'), skyAmb: gl.getUniformLocation(cpm, 'uSkyAmb'), - starAmb: gl.getUniformLocation(cpm, 'uStarAmb') }; + starAmb: gl.getUniformLocation(cpm, 'uStarAmb'), + nightBoost: gl.getUniformLocation(cpm, 'uNightBoost') }; } } if (ctProg) { @@ -5637,6 +5671,7 @@ window.clouds3dRender = function(target) { gl.uniform1f(ctU.moonInt, _moonI); gl.uniform1f(ctU.skyAmb, _amb * ATMO_SKY_GAIN); gl.uniform1f(ctU.starAmb, _star); + gl.uniform1f(ctU.nightBoost, 1.0 + carNight * (CT_NIGHT_BOOST - 1.0)); gl.enableVertexAttribArray(ctA.pos); gl.vertexAttribPointer(ctA.pos, 3, gl.FLOAT, false, 28, 0); gl.enableVertexAttribArray(ctA.u); @@ -5655,6 +5690,21 @@ window.clouds3dRender = function(target) { gl.depthMask(true); } } + // Diagnostic snapshot: how many aircraft are in range, how many + // meshes were actually drawn and how much contrail geometry made + // it past the distance gates. + window.__flightDbg = { + aircraft: fltCount, + meshDrawn: _meshDrawn, + ctVerts: ctVerts, + day: +dayf.toFixed(2), + night: +carNight.toFixed(2), + meshRangeKm: Math.round(16 * KM_PER_TILE) + }; + if (DEBUG_LOG && now - _flightDbgT > 5000) { + _flightDbgT = now; + dbgLog('[clouds3d] flights:', JSON.stringify(window.__flightDbg)); + } } }