meteor showers: add debug shower and known radiant streaks

This commit is contained in:
Richard Zink committed 2026-10-13 08:10:55 +02:00
1 parent 22e2bae5bd
commit 0f22aad650
2 files changed
+351

No files matched your search

+52
View File
@@ -147,6 +147,58 @@ window.CONFIG = {
// emergencyVehicles: { colorVariation: 0.25, ledColor: [0.02, 0.18, 1.0], halogenColor: [0.70, 0.82, 1.0] }
},
// ------------------------------------------------------------------
// Meteor showers
// ------------------------------------------------------------------
// Known meteor showers are rendered as short night-sky streaks whose
// backward extension points toward the shower radiant. The debug shower
// is a temporary test shower for a fixed local start time.
meteorShowers: {
enabled: true,
brightness: 1.0,
maxActive: 12,
rate: 0.25, // meteors per second across all active showers
speedDegPerSec: 14, // apparent angular speed
lengthDeg: 18, // trail length in degrees
trailPoints: 14, // point sprites per trail
pointSize: 10, // base point size in pixels
nightOnly: true,
colors: [
[1.00, 0.96, 0.88],
[0.72, 1.00, 0.86],
[1.00, 0.72, 0.42]
],
debug: {
enabled: true,
name: 'Debug Shower',
startAt: '2026-10-13T07:00:00',
durationMin: 10,
rate: 1.2,
maxActive: 18,
radiant: { raH: 3.2, decDeg: 58 },
lengthDeg: 22,
speedDegPerSec: 18,
brightness: 2.4,
nightOnly: false
},
showers: [
{ name: 'Quadrantids', startMonth: 1, startDay: 1, peakMonth: 1, peakDay: 3, endMonth: 1, endDay: 15, zhr: 110, raH: 15.4, decDeg: 49 },
{ name: 'Lyrids', startMonth: 4, startDay: 16, peakMonth: 4, peakDay: 22, endMonth: 4, endDay: 25, zhr: 18, raH: 18.1, decDeg: 34 },
{ name: 'Eta Aquariids', startMonth: 4, startDay: 19, peakMonth: 5, peakDay: 6, endMonth: 5, endDay: 28, zhr: 50, raH: 22.5, decDeg: -1 },
{ name: 'Delta Aquariids', startMonth: 7, startDay: 15, peakMonth: 7, peakDay: 30, endMonth: 8, endDay: 18, zhr: 25, raH: 22.8, decDeg: -16 },
{ name: 'Perseids', startMonth: 7, startDay: 17, peakMonth: 8, peakDay: 12, endMonth: 8, endDay: 24, zhr: 100, raH: 3.2, decDeg: 58 },
{ name: 'Draconids', startMonth: 10, startDay: 6, peakMonth: 10, peakDay: 8, endMonth: 10, endDay: 10, zhr: 10, raH: 17.5, decDeg: 54 },
{ name: 'Orionids', startMonth: 10, startDay: 2, peakMonth: 10, peakDay: 21, endMonth: 11, endDay: 7, zhr: 20, raH: 6.4, decDeg: 15 },
{ name: 'Taurids', startMonth: 10, startDay: 10, peakMonth: 11, peakDay: 5, endMonth: 12, endDay: 10, zhr: 5, raH: 3.9, decDeg: 21 },
{ name: 'Leonids', startMonth: 11, startDay: 6, peakMonth: 11, peakDay: 17, endMonth: 11, endDay: 30, zhr: 15, raH: 10.2, decDeg: 22 },
{ name: 'Geminids', startMonth: 12, startDay: 4, peakMonth: 12, peakDay: 14, endMonth: 12, endDay: 17, zhr: 120, raH: 7.5, decDeg: 33 },
{ name: 'Ursids', startMonth: 12, startDay: 17, peakMonth: 12, peakDay: 22, endMonth: 12, endDay: 26, zhr: 10, raH: 14.5, decDeg: 75 }
]
// meteorShowers: { enabled: false }
// meteorShowers: { debug: { enabled: false } }
// meteorShowers: { debug: { startAt: '2026-10-13T07:00:00', durationMin: 10, rate: 2.0 } }
},
// ------------------------------------------------------------------
// Weather effects
// ------------------------------------------------------------------
+299
View File
@@ -6255,6 +6255,300 @@ function starSidereal() {
var _skyLat = ((window.CONFIG && window.CONFIG.location && window.CONFIG.location.lat) || 51.5074) * Math.PI / 180;
var SKY_SIN_LAT = Math.sin(_skyLat), SKY_COS_LAT = Math.cos(_skyLat);
var METEOR_CFG = (window.CONFIG && window.CONFIG.meteorShowers) || {};
var METEORS_ON = METEOR_CFG.enabled !== false;
var meteorProg = null, meteorVbo = null, meteorUniforms = null, meteorCount = 0;
var meteorList = [], meteorSpawnAcc = 0, meteorLastT = null, meteorAllowDay = false;
var METEOR_BRIGHT = (typeof METEOR_CFG.brightness === 'number') ? Math.max(0, METEOR_CFG.brightness) : 1.0;
var METEOR_MAX_ACTIVE = (typeof METEOR_CFG.maxActive === 'number') ? Math.max(1, Math.min(80, METEOR_CFG.maxActive)) : 12;
var METEOR_RATE = (typeof METEOR_CFG.rate === 'number') ? Math.max(0, Math.min(8, METEOR_CFG.rate)) : 0.25;
var METEOR_SPEED = (typeof METEOR_CFG.speedDegPerSec === 'number') ? Math.max(1, Math.min(80, METEOR_CFG.speedDegPerSec)) : 14;
var METEOR_LENGTH = (typeof METEOR_CFG.lengthDeg === 'number') ? Math.max(2, Math.min(90, METEOR_CFG.lengthDeg)) : 18;
var METEOR_TRAIL_POINTS = (typeof METEOR_CFG.trailPoints === 'number') ? Math.max(2, Math.min(40, Math.round(METEOR_CFG.trailPoints))) : 14;
var METEOR_POINT_SIZE = (typeof METEOR_CFG.pointSize === 'number') ? Math.max(1, Math.min(40, METEOR_CFG.pointSize)) : 10;
var METEOR_NIGHT_ONLY = METEOR_CFG.nightOnly !== false;
var METEOR_COLORS = Array.isArray(METEOR_CFG.colors) && METEOR_CFG.colors.length ? METEOR_CFG.colors : [
[1.00, 0.96, 0.88], [0.72, 1.00, 0.86], [1.00, 0.72, 0.42]
];
var METEOR_DEBUG = METEOR_CFG.debug || {};
var METEOR_SHOWERS = Array.isArray(METEOR_CFG.showers) ? METEOR_CFG.showers : [];
var METEOR_VERT = [
'attribute vec4 aMpos;',
'attribute vec4 aMcol;',
'uniform mat4 uMVP;',
'uniform float uSizeScale;',
'varying vec4 vC;',
'varying float vSeed;',
'void main() {',
'gl_Position = uMVP * vec4(aMpos.xyz, 1.0);',
'gl_PointSize = clamp(aMcol.w * uSizeScale, 1.0, 34.0);',
'vC = aMcol;',
'vSeed = aMpos.w;',
'}'
].join('\n');
var METEOR_FRAG = [
'precision mediump float;',
'varying vec4 vC;',
'varying float vSeed;',
'void main() {',
'vec2 pc = gl_PointCoord - 0.5;',
'float d = length(pc) * 2.0;',
'float g = exp(-d * d * 3.1);',
'float flicker = 0.88 + 0.12 * sin(vSeed * 628.0);',
'float a = vC.w * g * flicker;',
'gl_FragColor = vec4(vC.rgb * a, 1.0);',
'}'
].join('\n');
function meteorNum(v, def, min, max) {
return (typeof v === 'number' && isFinite(v)) ? Math.max(min, Math.min(max, v)) : def;
}
function meteorDot(a, b) { return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; }
function meteorCross(a, b) {
return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]];
}
function meteorNorm(a) {
var l = Math.sqrt(meteorDot(a, a)) || 1;
return [a[0] / l, a[1] / l, a[2] / l];
}
function meteorLocalDir(raH, decDeg, lst) {
var ra = (raH || 0) * 15 * DEG;
var dec = (decDeg || 0) * DEG;
var H = lst - ra;
var cd = Math.cos(dec), sd = Math.sin(dec);
var sh = Math.sin(H), ch = Math.cos(H);
return [cd * sh, SKY_SIN_LAT * sd + SKY_COS_LAT * cd * ch, SKY_COS_LAT * sd - SKY_SIN_LAT * cd * ch];
}
function meteorActiveOnDate(s, date) {
var key = (date.getMonth() + 1) * 100 + date.getDate();
var start = s.startMonth * 100 + s.startDay;
var end = s.endMonth * 100 + s.endDay;
if (start <= end) return key >= start && key <= end;
return key >= start || key <= end;
}
function meteorDayDiffToPeak(date, peakMonth, peakDay) {
var y = date.getFullYear();
var cur = Date.UTC(y, date.getMonth(), date.getDate());
var best = Infinity;
for (var yy = y - 1; yy <= y + 1; yy++) {
var peak = Date.UTC(yy, peakMonth - 1, peakDay);
var diff = (cur - peak) / 86400000;
if (Math.abs(diff) < Math.abs(best)) best = diff;
}
return best;
}
function meteorShowerIntensity(s, date) {
if (!meteorActiveOnDate(s, date)) return 0;
var diff = meteorDayDiffToPeak(date, s.peakMonth, s.peakDay);
var sigma = 4.0;
var g = Math.exp(-0.5 * (diff / sigma) * (diff / sigma));
var zhr = (typeof s.zhr === 'number') ? s.zhr : 10;
return Math.max(0, Math.min(1, g * Math.min(1.2, zhr / 100)));
}
function meteorDebugActive(sceneDate, realDate) {
if (!METEOR_DEBUG.enabled) return null;
var start = METEOR_DEBUG.startAt ? new Date(METEOR_DEBUG.startAt) : null;
var dur = (typeof METEOR_DEBUG.durationMin === 'number') ? METEOR_DEBUG.durationMin * 60000 : 600000;
var s = start ? start.getTime() : 0;
var e = start ? s + dur : Infinity;
var t1 = sceneDate ? sceneDate.getTime() : NaN;
var t2 = realDate ? realDate.getTime() : NaN;
var active = !start || (t1 >= s && t1 <= e) || (t2 >= s && t2 <= e);
if (!active) return null;
var rad = METEOR_DEBUG.radiant || {};
return {
name: METEOR_DEBUG.name || 'Debug Shower',
intensity: 1,
raH: (typeof rad.raH === 'number') ? rad.raH : 3.2,
decDeg: (typeof rad.decDeg === 'number') ? rad.decDeg : 58,
rate: meteorNum(METEOR_DEBUG.rate, 1.2, 0, 8),
maxActive: (typeof METEOR_DEBUG.maxActive === 'number') ? Math.max(1, Math.min(80, Math.round(METEOR_DEBUG.maxActive))) : 18,
lengthDeg: meteorNum(METEOR_DEBUG.lengthDeg, 22, 2, 90),
speedDegPerSec: meteorNum(METEOR_DEBUG.speedDegPerSec, 18, 1, 80),
brightness: meteorNum(METEOR_DEBUG.brightness, 2.4, 0, 8),
nightOnly: METEOR_DEBUG.nightOnly === true,
debug: true
};
}
function meteorActiveShowers(sceneDate, realDate) {
var out = [];
var dbg = meteorDebugActive(sceneDate, realDate);
if (dbg) out.push(dbg);
for (var i = 0; i < METEOR_SHOWERS.length; i++) {
var s = METEOR_SHOWERS[i];
var intensity = meteorShowerIntensity(s, sceneDate || realDate || new Date());
if (intensity < 0.02) continue;
out.push({
name: s.name || 'Shower',
intensity: intensity,
raH: s.raH,
decDeg: s.decDeg,
rate: METEOR_RATE * intensity,
maxActive: METEOR_MAX_ACTIVE,
lengthDeg: METEOR_LENGTH,
speedDegPerSec: METEOR_SPEED,
brightness: METEOR_BRIGHT * intensity,
nightOnly: METEOR_NIGHT_ONLY,
debug: false
});
}
return out;
}
function meteorSpawn(shower, now) {
var lst = starSidereal();
if (lst === null) lst = ((window.__currentDayProgress || 0) * 6.2831853);
var R = meteorLocalDir(shower.raH, shower.decDeg, lst);
if (R[1] < -0.25) return null;
var helper = (Math.abs(R[1]) < 0.8) ? [0, 1, 0] : [1, 0, 0];
var u = meteorNorm(meteorCross(R, helper));
var v = meteorNorm(meteorCross(R, u));
var P = null;
for (var attempt = 0; attempt < 10; attempt++) {
var phi = Math.random() * 6.2831853;
var theta = 0.12 + Math.random() * 1.15;
var ct = Math.cos(theta), st = Math.sin(theta);
P = meteorNorm([
R[0] * ct + u[0] * st * Math.cos(phi) + v[0] * st * Math.sin(phi),
R[1] * ct + u[1] * st * Math.cos(phi) + v[1] * st * Math.sin(phi),
R[2] * ct + u[2] * st * Math.cos(phi) + v[2] * st * Math.sin(phi)
]);
if (P[1] > 0.06) break;
}
if (!P || P[1] <= 0.06) return null;
var radial = meteorDot(P, R);
var dir = meteorNorm([P[0] - R[0] * radial, P[1] - R[1] * radial, P[2] - R[2] * radial]);
if (Math.abs(dir[0]) + Math.abs(dir[1]) + Math.abs(dir[2]) < 1e-4) dir = u;
var angularLen = shower.lengthDeg * DEG * (0.7 + Math.random() * 0.6);
var speed = shower.speedDegPerSec * DEG * (0.7 + Math.random() * 0.6);
var seed = Math.random();
var color = METEOR_COLORS[Math.floor(seed * METEOR_COLORS.length) % METEOR_COLORS.length];
return {
start: now,
lifetime: angularLen / speed,
P: P,
dir: dir,
angularLen: angularLen,
speed: speed,
seed: seed,
color: color,
brightness: shower.brightness,
shower: shower.name
};
}
function updateMeteorShower(gl, sceneDate, realDate, now) {
if (!gl || !METEORS_ON) return;
if (meteorLastT === null) meteorLastT = now;
var elapsed = Math.max(0, Math.min(0.25, now - meteorLastT));
meteorLastT = now;
var active = meteorActiveShowers(sceneDate, realDate);
var maxActive = METEOR_MAX_ACTIVE;
meteorAllowDay = !METEOR_NIGHT_ONLY;
for (var i = 0; i < active.length; i++) {
if (active[i].maxActive > maxActive) maxActive = active[i].maxActive;
if (active[i].nightOnly === false) meteorAllowDay = true;
}
for (var j = meteorList.length - 1; j >= 0; j--) {
if (now - meteorList[j].start > meteorList[j].lifetime) meteorList.splice(j, 1);
}
if (active.length && meteorList.length < maxActive) {
var totalRate = 0;
for (var k = 0; k < active.length; k++) totalRate += active[k].rate * active[k].intensity;
meteorSpawnAcc += elapsed * totalRate;
while (meteorSpawnAcc >= 1 && meteorList.length < maxActive) {
meteorSpawnAcc -= 1;
var r = Math.random() * totalRate;
var chosen = active[0];
for (var a = 0; a < active.length; a++) {
r -= active[a].rate * active[a].intensity;
if (r <= 0) { chosen = active[a]; break; }
}
var m = meteorSpawn(chosen, now);
if (m) meteorList.push(m);
}
}
var maxPoints = maxActive * METEOR_TRAIL_POINTS;
var data = new Float32Array(maxPoints * 8);
var di = 0, count = 0;
for (var mi = 0; mi < meteorList.length; mi++) {
var mm = meteorList[mi];
var age = now - mm.start;
if (age < 0) continue;
var progress = age / mm.lifetime;
if (progress > 1) continue;
var headAngle = age * mm.speed;
var trailLen = Math.min(headAngle, mm.angularLen);
for (var p = 0; p < METEOR_TRAIL_POINTS; p++) {
var s = (METEOR_TRAIL_POINTS === 1) ? 0 : p / (METEOR_TRAIL_POINTS - 1);
var angle = headAngle - s * trailLen;
var ca = Math.cos(angle), sa = Math.sin(angle);
var x = mm.P[0] * ca + mm.dir[0] * sa;
var y = mm.P[1] * ca + mm.dir[1] * sa;
var z = mm.P[2] * ca + mm.dir[2] * sa;
var fade = Math.pow(1 - s, 1.35) * Math.pow(1 - progress, 0.35);
var intensity = fade * mm.brightness * (0.72 + 0.28 * Math.sin(mm.seed * 628.0 + age * 7.0));
if (intensity <= 0.01) continue;
data[di++] = x * 88; data[di++] = y * 88; data[di++] = z * 88; data[di++] = mm.seed;
data[di++] = mm.color[0]; data[di++] = mm.color[1]; data[di++] = mm.color[2];
data[di++] = intensity * METEOR_POINT_SIZE;
count++;
}
}
if (!meteorVbo) meteorVbo = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, meteorVbo);
gl.bufferData(gl.ARRAY_BUFFER, data.subarray(0, di), gl.STREAM_DRAW);
meteorCount = count;
window.__meteorDbg = {
enabled: METEORS_ON,
active: active.map(function (x) { return { name: x.name, intensity: +x.intensity.toFixed(2), rate: +x.rate.toFixed(2), debug: !!x.debug }; }),
meteors: meteorList.length,
points: count,
spawnAcc: +meteorSpawnAcc.toFixed(2),
allowDay: meteorAllowDay
};
}
function ensureMeteorProgram(gl) {
if (meteorProg) return true;
var prog = makeProgram(gl, METEOR_VERT, METEOR_FRAG);
if (!prog) return false;
meteorProg = prog;
meteorUniforms = {
mvp: gl.getUniformLocation(prog, 'uMVP'),
sizeScale: gl.getUniformLocation(prog, 'uSizeScale'),
a0: gl.getAttribLocation(prog, 'aMpos'),
a1: gl.getAttribLocation(prog, 'aMcol')
};
return true;
}
function drawMeteorShower(gl, proj, view, sunAlt) {
if (!meteorCount || !meteorVbo || !ensureMeteorProgram(gl)) return;
var night = 1 - Math.max(0, Math.min(1, (sunAlt + 0.05) / 0.25));
if (METEOR_NIGHT_ONLY && !meteorAllowDay && night < 0.02) return;
var viewR = new Float32Array(16);
viewR[0] = view[0]; viewR[1] = view[1]; viewR[2] = view[2];
viewR[4] = view[4]; viewR[5] = view[5]; viewR[6] = view[6];
viewR[8] = view[8]; viewR[9] = view[9]; viewR[10] = view[10];
viewR[12] = 0; viewR[13] = 0; viewR[14] = 0; viewR[15] = 1;
var mvp = new Float32Array(16);
mat4Multiply(mvp, proj, viewR);
gl.useProgram(meteorProg);
gl.bindBuffer(gl.ARRAY_BUFFER, meteorVbo);
gl.enableVertexAttribArray(meteorUniforms.a0);
gl.vertexAttribPointer(meteorUniforms.a0, 4, gl.FLOAT, false, 32, 0);
gl.enableVertexAttribArray(meteorUniforms.a1);
gl.vertexAttribPointer(meteorUniforms.a1, 4, gl.FLOAT, false, 32, 16);
gl.uniformMatrix4fv(meteorUniforms.mvp, false, mvp);
gl.uniform1f(meteorUniforms.sizeScale, canvas.height / 900);
gl.disable(gl.DEPTH_TEST);
gl.depthMask(false);
gl.enable(gl.BLEND);
gl.blendFunc(gl.ONE, gl.ONE);
gl.drawArrays(gl.POINTS, 0, meteorCount);
gl.disable(gl.BLEND);
gl.enable(gl.DEPTH_TEST);
gl.depthMask(true);
}
function kelvinToRGB(k) {
// crude blackbody tint
if (k < 3400) return [0.98, 0.84, 0.70];
@@ -7262,6 +7556,11 @@ window.clouds3dRender = function(target) {
gl.depthMask(true);
}
if (METEORS_ON) {
updateMeteorShower(gl, window.__currentSceneDate || new Date(), new Date(), now);
drawMeteorShower(gl, proj, view, sunAlt);
}
// ---- Terrain mesh pass (real 3D heightfield) ---------------------
// The mesh VBO is built once by buildTerrainMesh() after the
// elevation grid finishes loading. Depth test+write on so it