progress logs behind CONFIG.debug, CONFIG.antialias + high-performance context, cheaper cloud sampling (skip far detail octave and redundant self-shading probe), brighter night contrails via CONFIG.flights.contrailNight, window.__flightDbg counters, real sun/moon by default

This commit is contained in:
Richard Zink committed 2026-10-11 18:42:48 +02:00
1 parent 113e4c522d
commit 6d21b37e8f
2 files changed
+98 -37

No files matched your search

+16 -5
View File
@@ -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
},
// ------------------------------------------------------------------
+82 -32
View File
@@ -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));
}
}
}