contrails: implement simple robust contrail ribbon pass

This commit is contained in:
Richard Zink committed 2026-10-12 19:50:02 +02:00
1 parent adad36881a
commit 1da34c913b
1 file changed
+137 -104
+137 -104
View File
@@ -5390,6 +5390,65 @@ var planeV = null, planeVbo = null, planeProg = null, planeU = null, planeA = nu
var ctBuf = null, ctVbo = null, ctVerts = 0; var ctBuf = null, ctVbo = null, ctVerts = 0;
var ctProg = null, ctA = null, ctU = null; var ctProg = null, ctA = null, ctU = null;
var CT2_SEG = 18;
var ct2Buf = null, ct2Vbo = null, ct2Verts = 0;
var ct2Prog = null, ct2A = null, ct2U = null;
function _ct2Vert(x, y, z, u, v, s) {
var i = ct2Verts * 6;
ct2Buf[i] = x; ct2Buf[i + 1] = y; ct2Buf[i + 2] = z;
ct2Buf[i + 3] = u; ct2Buf[i + 4] = v; ct2Buf[i + 5] = s;
ct2Verts++;
}
var CT2_VERT_SRC = [
'attribute vec3 aPos;',
'attribute float aU;',
'attribute float aV;',
'attribute float aS;',
'uniform mat4 uProj;',
'uniform mat4 uView;',
'uniform vec3 uCamPos;',
'uniform float uEarthRadius;',
'uniform float uCurvatureOn;',
'varying float vU;',
'varying float vV;',
'varying float vS;',
'varying vec3 vWorld;',
'void main() {',
'vU = aU; vV = aV; vS = aS;',
'vec3 pos = aPos;',
'if (uCurvatureOn > 0.5) {',
'vec2 d = pos.xz - uCamPos.xz;',
'pos.y -= dot(d, d) / (2.0 * max(1.0, uEarthRadius));',
'}',
'vWorld = pos;',
'gl_Position = uProj * uView * vec4(pos, 1.0);',
'}'
].join('\n');
var CT2_FRAG_SRC = [
'precision mediump float;',
'uniform float uDay;',
'uniform float uNight;',
'varying float vU;',
'varying float vV;',
'varying float vS;',
'varying vec3 vWorld;',
'void main() {',
'float side = abs(vV);',
'float edge = 1.0 - smoothstep(0.25, 1.0, side);',
'float head = 0.35 + 0.65 * vU;',
'float a = clamp(vS * edge * head, 0.0, 0.85);',
'if (a <= 0.004) discard;',
'vec3 dayCol = vec3(1.0, 1.0, 1.0);',
'vec3 nightCol = vec3(0.45, 0.55, 0.75);',
'vec3 col = mix(nightCol, dayCol, clamp(uDay, 0.0, 1.0));',
'col *= 0.35 + 0.65 * clamp(uDay + uNight, 0.0, 1.0);',
'gl_FragColor = vec4(col, a);',
'}'
].join('\n');
function _ctVert(x, y, z, u, v, s, p) { function _ctVert(x, y, z, u, v, s, p) {
var i = ctVerts * 7; var i = ctVerts * 7;
ctBuf[i] = x; ctBuf[i + 1] = y; ctBuf[i + 2] = z; ctBuf[i] = x; ctBuf[i + 1] = y; ctBuf[i + 2] = z;
@@ -6799,138 +6858,113 @@ window.clouds3dRender = function(target) {
gl.depthMask(true); gl.depthMask(true);
} }
} }
// -- contrails -- // -- contrails: simple robust ribbon pass --
// Only CONFIG.flights.contrailChance of the aircraft leave a
// persistent stripe (the ice-humidity window is not everywhere),
// 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 * CT_NIGHT); var tvis = Math.max(dayf, carNight * CT_NIGHT);
if (tvis > 0.02) { if (tvis > 0.01) {
if (!ctBuf) ctBuf = new Float32Array(CT_MAX * CT_SEG * 6 * 7); if (!ct2Buf) ct2Buf = new Float32Array(FLT_MAX * CT2_SEG * 6 * 6);
ctVerts = 0; ct2Verts = 0;
var _cs = { x: 0, z: 0, tx: 1, tz: 0 }; var _cs2 = { x: 0, z: 0, tx: 1, tz: 0 };
var _sk0 = { x: 0, z: 0, px: 0, pz: 0, u: 0 };
var _sk1 = { x: 0, z: 0, px: 0, pz: 0, u: 0 };
for (var cj = 0; cj < fltCount; cj++) { for (var cj = 0; cj < fltCount; cj++) {
if (ctVerts + CT_SEG * 6 > CT_MAX * CT_SEG * 6) break; if (ct2Verts + CT2_SEG * 6 > ct2Buf.length / 6) break;
var stc = fltState[cj]; var stc = fltState[cj];
if (!stc || stc.gnd) continue; if (!stc || stc.gnd) continue;
if (fltRand(cj * 3.7 + 0.77) > CT_CHANCE) continue; if (fltRand(cj * 3.7 + 0.77) > CT_CHANCE) continue;
// formation gate: above the configured trail altitude, smooth var gt = (stc.alt - (CT_START_FRAC - 0.10)) / 0.18;
var gt = (stc.alt - (CT_START_FRAC - 0.06)) / 0.12; gt = Math.max(0, Math.min(1, gt));
gt = gt < 0 ? 0 : (gt > 1 ? 1 : gt); if (gt < 0.03) continue;
gt = gt * gt * (3 - 2 * gt); var tl = Math.min(Math.max(1.0, CT_T * fltData[cj].v), stc.s);
if (gt < 0.05) continue; if (tl < 0.2) continue;
var tl = Math.min(CT_T * fltData[cj].v, stc.s);
if (tl < 0.05) continue;
fltLegAt(stc.leg, stc.s - tl * 0.5, _cs);
var cdx = _cs.x - camX, cdy = stc.y - camY, cdz = _cs.z - camZ;
var distc = Math.sqrt(cdx * cdx + cdy * cdy + cdz * cdz);
// distance fade: full out to 35% of contrailFadeKm, gone at
// contrailFadeKm. Trails that thin only at 200-340 km were
// the remaining shimmer at 4x pixel scale.
var dfc = 1 - (distc - CT_FADE_START) / (CT_FADE_END - CT_FADE_START);
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 * CT_NIGHT); var lit = Math.max(dayf, carNight * CT_NIGHT);
var strg = gt * lit * dfc; var strg = Math.max(0.30, gt * lit);
if (strg < 0.18) strg = 0.18;
// 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
var hw = CT_HALF_W + distc * 0.0025;
var ph = fltData[cj].phase;
var hy = stc.y; var hy = stc.y;
var prev = null, cur = _sk0; var prev = null;
for (var sk = 0; sk <= CT_SEG; sk++) { var cur = { x: 0, z: 0, px: 0, pz: 0, u: 0 };
var dd = stc.s - tl + tl * (sk / CT_SEG); for (var sk = 0; sk <= CT2_SEG; sk++) {
var dd = stc.s - tl + tl * (sk / CT2_SEG);
if (dd < 0) dd = 0; if (dd < 0) dd = 0;
fltLegAt(stc.leg, dd, _cs); fltLegAt(stc.leg, dd, _cs2);
cur.x = _cs.x; cur.z = _cs.z; var dx = _cs2.x - camX, dz = _cs2.z - camZ, dy = hy - camY;
cur.px = _cs.tz * hw; cur.pz = -_cs.tx * hw; var dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
cur.u = sk / CT_SEG; var hw = CT_HALF_W + dist * 0.004;
var tx = _cs2.tx, tz = _cs2.tz;
var sl = Math.hypot(tx, tz) || 1;
tx /= sl; tz /= sl;
cur.x = _cs2.x; cur.z = _cs2.z;
cur.px = tz * hw; cur.pz = -tx * hw;
cur.u = sk / CT2_SEG;
if (prev) { if (prev) {
_ctVert(prev.x + prev.px, hy, prev.z + prev.pz, prev.u, 1, strg, ph); _ct2Vert(prev.x + prev.px, hy, prev.z + prev.pz, prev.u, 1, strg);
_ctVert(prev.x - prev.px, hy, prev.z - prev.pz, prev.u, -1, strg, ph); _ct2Vert(prev.x - prev.px, hy, prev.z - prev.pz, prev.u, -1, strg);
_ctVert(cur.x - cur.px, hy, cur.z - cur.pz, cur.u, -1, strg, ph); _ct2Vert(cur.x - cur.px, hy, cur.z - cur.pz, cur.u, -1, strg);
_ctVert(prev.x + prev.px, hy, prev.z + prev.pz, prev.u, 1, strg, ph); _ct2Vert(prev.x + prev.px, hy, prev.z + prev.pz, prev.u, 1, strg);
_ctVert(cur.x - cur.px, hy, cur.z - cur.pz, cur.u, -1, strg, ph); _ct2Vert(cur.x - cur.px, hy, cur.z - cur.pz, cur.u, -1, strg);
_ctVert(cur.x + cur.px, hy, cur.z + cur.pz, cur.u, 1, strg, ph); _ct2Vert(cur.x + cur.px, hy, cur.z + cur.pz, cur.u, 1, strg);
} }
prev = cur; cur = (cur === _sk0) ? _sk1 : _sk0; prev = cur;
cur = { x: 0, z: 0, px: 0, pz: 0, u: 0 };
} }
} }
if (ctVerts > 0) { if (ct2Verts > 0) {
if (!ctVbo) ctVbo = gl.createBuffer(); if (!ct2Vbo) ct2Vbo = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, ctVbo); gl.bindBuffer(gl.ARRAY_BUFFER, ct2Vbo);
gl.bufferData(gl.ARRAY_BUFFER, ctBuf.subarray(0, ctVerts * 7), gl.STREAM_DRAW); gl.bufferData(gl.ARRAY_BUFFER, ct2Buf.subarray(0, ct2Verts * 6), gl.STREAM_DRAW);
if (!ctProg) { if (!ct2Prog) {
var cpm = makeProgram(gl, CT_VERT_SRC, CT_FRAG_SRC); var c2p = makeProgram(gl, CT2_VERT_SRC, CT2_FRAG_SRC);
if (cpm) { if (c2p) {
ctProg = cpm; ct2Prog = c2p;
ctA = { pos: gl.getAttribLocation(cpm, 'aPos'), ct2A = {
u: gl.getAttribLocation(cpm, 'aU'), pos: gl.getAttribLocation(c2p, 'aPos'),
v: gl.getAttribLocation(cpm, 'aV'), u: gl.getAttribLocation(c2p, 'aU'),
s: gl.getAttribLocation(cpm, 'aS'), v: gl.getAttribLocation(c2p, 'aV'),
p: gl.getAttribLocation(cpm, 'aP') }; s: gl.getAttribLocation(c2p, 'aS')
ctU = { proj: gl.getUniformLocation(cpm, 'uProj'), };
view: gl.getUniformLocation(cpm, 'uView'), ct2U = {
camPos: gl.getUniformLocation(cpm, 'uCamPos'), proj: gl.getUniformLocation(c2p, 'uProj'),
earthRadius: gl.getUniformLocation(cpm, 'uEarthRadius'), view: gl.getUniformLocation(c2p, 'uView'),
curvatureOn: gl.getUniformLocation(cpm, 'uCurvatureOn'), camPos: gl.getUniformLocation(c2p, 'uCamPos'),
sunDir: gl.getUniformLocation(cpm, 'uSunDir'), earthRadius: gl.getUniformLocation(c2p, 'uEarthRadius'),
sunColor: gl.getUniformLocation(cpm, 'uSunColor'), curvatureOn: gl.getUniformLocation(c2p, 'uCurvatureOn'),
sunInt: gl.getUniformLocation(cpm, 'uSunInt'), day: gl.getUniformLocation(c2p, 'uDay'),
moonColor: gl.getUniformLocation(cpm, 'uMoonColor'), night: gl.getUniformLocation(c2p, 'uNight')
moonInt: gl.getUniformLocation(cpm, 'uMoonInt'), };
skyAmb: gl.getUniformLocation(cpm, 'uSkyAmb'),
starAmb: gl.getUniformLocation(cpm, 'uStarAmb'),
nightBoost: gl.getUniformLocation(cpm, 'uNightBoost') };
} }
} }
if (ctProg) { if (ct2Prog) {
gl.useProgram(ctProg); gl.useProgram(ct2Prog);
gl.uniformMatrix4fv(ctU.proj, false, proj); gl.uniformMatrix4fv(ct2U.proj, false, proj);
gl.uniformMatrix4fv(ctU.view, false, view); gl.uniformMatrix4fv(ct2U.view, false, view);
gl.uniform3f(ctU.camPos, camX, camY, camZ); gl.uniform3f(ct2U.camPos, camX, camY, camZ);
gl.uniform1f(ctU.earthRadius, EARTH_RADIUS_UNITS); gl.uniform1f(ct2U.earthRadius, EARTH_RADIUS_UNITS);
gl.uniform1f(ctU.curvatureOn, CURVATURE_ON ? 1.0 : 0.0); gl.uniform1f(ct2U.curvatureOn, CURVATURE_ON ? 1.0 : 0.0);
gl.uniform3f(ctU.sunDir, sd[0], sd[1], sd[2]); gl.uniform1f(ct2U.day, dayf);
gl.uniform3f(ctU.sunColor, _beam[0], _beam[1], _beam[2]); gl.uniform1f(ct2U.night, carNight);
gl.uniform1f(ctU.sunInt, _beamL); gl.enableVertexAttribArray(ct2A.pos);
gl.uniform3f(ctU.moonColor, _mBeam[0], _mBeam[1], _mBeam[2]); gl.vertexAttribPointer(ct2A.pos, 3, gl.FLOAT, false, 24, 0);
gl.uniform1f(ctU.moonInt, _moonI); gl.enableVertexAttribArray(ct2A.u);
gl.uniform1f(ctU.skyAmb, _amb * ATMO_SKY_GAIN); gl.vertexAttribPointer(ct2A.u, 1, gl.FLOAT, false, 24, 12);
gl.uniform1f(ctU.starAmb, _star); gl.enableVertexAttribArray(ct2A.v);
gl.uniform1f(ctU.nightBoost, 1.0 + carNight * (CT_NIGHT_BOOST - 1.0)); gl.vertexAttribPointer(ct2A.v, 1, gl.FLOAT, false, 24, 16);
gl.enableVertexAttribArray(ctA.pos); gl.enableVertexAttribArray(ct2A.s);
gl.vertexAttribPointer(ctA.pos, 3, gl.FLOAT, false, 28, 0); gl.vertexAttribPointer(ct2A.s, 1, gl.FLOAT, false, 24, 20);
gl.enableVertexAttribArray(ctA.u);
gl.vertexAttribPointer(ctA.u, 1, gl.FLOAT, false, 28, 12);
gl.enableVertexAttribArray(ctA.v);
gl.vertexAttribPointer(ctA.v, 1, gl.FLOAT, false, 28, 16);
gl.enableVertexAttribArray(ctA.s);
gl.vertexAttribPointer(ctA.s, 1, gl.FLOAT, false, 28, 20);
gl.enableVertexAttribArray(ctA.p);
gl.vertexAttribPointer(ctA.p, 1, gl.FLOAT, false, 28, 24);
gl.disable(gl.DEPTH_TEST); gl.disable(gl.DEPTH_TEST);
gl.depthMask(false); gl.depthMask(false);
gl.enable(gl.BLEND); gl.enable(gl.BLEND);
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA); gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);
gl.drawArrays(gl.TRIANGLES, 0, ctVerts); gl.drawArrays(gl.TRIANGLES, 0, ct2Verts);
gl.disable(gl.BLEND); gl.disable(gl.BLEND);
gl.depthMask(true); gl.depthMask(true);
gl.enable(gl.DEPTH_TEST); gl.enable(gl.DEPTH_TEST);
} }
} }
}
// Diagnostic snapshot: how many aircraft are in range, how many // Diagnostic snapshot: how many aircraft are in range, how many
// meshes were actually drawn and how much contrail geometry made // meshes were actually drawn and how much contrail geometry made
// it past the distance gates. // it past the distance gates.
window.__flightDbg = { window.__flightDbg = {
aircraft: fltCount, aircraft: fltCount,
meshDrawn: _meshDrawn, meshDrawn: _meshDrawn,
ctVerts: ctVerts, ctVerts: ct2Verts,
day: +dayf.toFixed(2), day: +dayf.toFixed(2),
night: +carNight.toFixed(2), night: +carNight.toFixed(2),
meshRangeKm: Math.round(16 * KM_PER_TILE) meshRangeKm: Math.round(16 * KM_PER_TILE)
@@ -6939,7 +6973,6 @@ window.clouds3dRender = function(target) {
_flightDbgT = now; _flightDbgT = now;
dbgLog('[clouds3d] flights:', JSON.stringify(window.__flightDbg)); dbgLog('[clouds3d] flights:', JSON.stringify(window.__flightDbg));
} }
}
} }
// Restore default state for the next frame. // Restore default state for the next frame.