meteor showers: add debug shower and known radiant streaks
This commit is contained in:
1 parent
22e2bae5bd
commit
0f22aad650
2 files changed
+351
No files matched your search
Vendored
+299
@@ -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
|
||||
|
||||
Reference in new issue
Block a user