Drive the Navara volumetric clouds from the cockpit weather behind an optional render.setCloudCover capability

The cockpit's weather feed moves out of the Cesium cloud pass and serves both: Cesium keeps its own cloud canvas, an engine with its own clouds takes their cover from the same observations, with no second WebGL canvas.
This commit is contained in:
German Khokhlov committed 2026-11-03 21:22:34 +04:00
1 parent d8c63c69d8
commit f0d38897f4
15 files changed
+1004 -79

No files matched your search

+252
View File
@@ -0,0 +1,252 @@
// The cockpit weather clouds on Navara (Stage 6 C3): in cockpit mode with
// the cockpit weather on, Navara's own volumetric clouds take their cover
// from the weather under the camera (clear: none; overcast: a heavy deck),
// with no second canvas or WebGL context; leaving the cockpit gives the look
// its own clouds back. On Cesium the cockpit keeps its own cloud canvas and
// the engine draws none. The weather route is a fixture, the page clock is
// shifted to an Austin midday (the look shows clouds only in daylight), and
// cockpit mode is entered through its event (the cloud controllers listen
// to it; a real cockpit needs a tracked aircraft). Keyless preview under the
// strict default-deny network policy.
//
// node scripts/qa-navara-cockpit-clouds.mjs http://127.0.0.1:<PORT>/ [--out=DIR]
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import {
createStrict,
localPreview,
waitForEngine,
} from './qa-strict-harness.mjs';
import {
capture,
checker,
diff,
freeze,
saveCapture,
stats,
visualPageUrl,
} from './qa/navaraVisual.mjs';
const base = localPreview(process.argv[2]);
const outDir = process.argv
.find((arg) => arg.startsWith('--out='))
?.slice('--out='.length);
const LOW_VIEW =
'v=2&lat=30.2672&lon=-97.7431&alt=2500&heading=90&pitch=-8&map=osm&l=';
const MIDDAY_UTC = '2026-07-15T18:00:00Z';
const WEATHER = {
clear: { weatherCode: 0, cloudCoverPct: 0, windKph: 6, visibilityM: 20000 },
overcast: {
weatherCode: 3,
cloudCoverPct: 100,
windKph: 25,
visibilityM: 15000,
},
};
let weather = WEATHER.clear;
const asked = [];
const strict = createStrict({
origin: base.origin,
layers: [],
routes: [
{
pattern: `${base.origin}/api/weather-effects*`,
respond: (request) => {
asked.push(request.url);
return {
contentType: 'application/json',
body: JSON.stringify({ status: 'live', weather }),
};
},
},
],
});
const report = { checks: [] };
const check = checker(report, assert);
const save = (name, pixels) => saveCapture(outDir, name, pixels);
// Clouds grey the frame: the blue sky and the map's coloured lines go.
const saturation = (pixels) => stats(pixels).saturation;
// A few seconds of continuous frames: the clouds converge over frames.
async function converged(page, name) {
await page.evaluate(async () => {
const { engine } = window.__godsEyeView;
engine.render.setContinuous(true);
await new Promise((resolve) => setTimeout(resolve, 3000));
engine.render.setContinuous(false);
});
const pixels = await capture(page);
save(name, pixels);
return pixels;
}
const cockpit = (page, active) =>
page.evaluate((active) => {
document.body.classList.toggle('cockpit-mode', active);
window.dispatchEvent(
new CustomEvent('gev:cockpit-mode-changed', { detail: { active } }),
);
}, active);
const weatherToggle = (page, enabled) =>
page.evaluate((enabled) => {
window.dispatchEvent(
new CustomEvent('gev:cockpit-weather-toggle', { detail: { enabled } }),
);
}, enabled);
const coverage = (page) =>
page.evaluate(
() => window.__godsEyeView.engine.render.cloudCover?.().coverage,
);
await strict.run(
{ args: ['--use-angle=metal', '--enable-gpu'] },
async (browser) => {
const page = await browser.newPage();
const errors = [];
page.on('pageerror', (error) => errors.push(error.message));
await page.evaluateOnNewDocument((utc) => {
localStorage.setItem('gev:first-run-mission:v1', 'suppressed');
const offset = Date.parse(utc) - Date.now();
const RealDate = Date;
class ShiftedDate extends RealDate {
constructor(...args) {
if (args.length === 0) super(RealDate.now() + offset);
else super(...args);
}
static now() {
return RealDate.now() + offset;
}
}
globalThis.Date = ShiftedDate;
}, MIDDAY_UTC);
await page.setViewport({ width: 1280, height: 800 });
await page.goto(visualPageUrl(base.origin, 'navara', '', LOW_VIEW), {
waitUntil: 'domcontentloaded',
});
await waitForEngine(page, 'navara');
await freeze(page);
const setup = await page.evaluate(() => {
const a = window.__godsEyeView;
return {
cover: a.engine.render.cloudCover?.() ?? null,
canvas: Boolean(document.getElementById('cockpit-cloud-effects')),
canvases: document.querySelectorAll('canvas').length,
};
});
check(
'Navara takes the cockpit cloud cover itself, no cloud canvas',
setup.cover?.coverage === null && !setup.canvas,
setup,
);
const lookOwn = await converged(page, 'look-own');
await weatherToggle(page, true);
await cockpit(page, true);
await page.waitForFunction(
() => window.__godsEyeView.engine.render.cloudCover().coverage === 0,
{ timeout: 10000, polling: 100 },
);
check('clear weather in the cockpit: no cloud cover', asked.length >= 1, {
requests: asked.length,
});
const clear = await converged(page, 'cockpit-clear');
// Overcast 30 km east: the movement makes the weather stale.
weather = WEATHER.overcast;
await page.evaluate(() => {
const { engine } = window.__godsEyeView;
const place = engine.camera.pose().cartographic;
const east =
place.longitude + 30000 / (6378137 * Math.cos(place.latitude));
const { sin, cos } = Math;
// WGS84 Earth-fixed position of (east, latitude, height).
const a = 6378137;
const e2 = 0.00669437999014;
const n = a / Math.sqrt(1 - e2 * sin(place.latitude) ** 2);
const h = place.height;
engine.camera.setView({
destination: {
x: (n + h) * cos(place.latitude) * cos(east),
y: (n + h) * cos(place.latitude) * sin(east),
z: (n * (1 - e2) + h) * sin(place.latitude),
},
orientation: { heading: Math.PI / 2, pitch: -0.14, roll: 0 },
});
});
await page.waitForFunction(
() => window.__godsEyeView.engine.render.cloudCover().coverage > 0.4,
{ timeout: 15000, polling: 100 },
);
const overcastCover = await coverage(page);
const overcast = await converged(page, 'cockpit-overcast');
const greyed = {
lookOwn: saturation(lookOwn),
clear: saturation(clear),
overcast: saturation(overcast),
clearToOvercast: diff(clear, overcast).mean,
};
check(
'overcast weather covers the view with Navara clouds, clear leaves it open',
overcastCover > 0.4 &&
greyed.overcast < greyed.clear * 0.6 &&
greyed.lookOwn < greyed.clear &&
greyed.clearToOvercast > 15,
{ overcastCover, greyed, requests: asked.length },
);
const contexts = await page.evaluate(() => ({
canvas: Boolean(document.getElementById('cockpit-cloud-effects')),
canvases: document.querySelectorAll('canvas').length,
}));
check(
'no second canvas or WebGL context in the cockpit',
!contexts.canvas && contexts.canvases === setup.canvases,
{ before: setup.canvases, after: contexts },
);
await cockpit(page, false);
check(
'leaving the cockpit gives the look its own clouds back',
(await coverage(page)) === null,
);
// Cesium keeps its own cockpit cloud canvas; its engine draws none.
await strict.switchTo(page, 'cesium');
await freeze(page);
weather = WEATHER.overcast;
await cockpit(page, true);
await page.waitForFunction(
() =>
Number(
document.getElementById('cockpit-cloud-effects')?.dataset
.cloudStrength,
) > 0.3,
{ timeout: 15000, polling: 100 },
);
const cesium = await page.evaluate(() => {
const canvas = document.getElementById('cockpit-cloud-effects');
return {
setCloudCover: typeof window.__godsEyeView.engine.render.setCloudCover,
strength: Number(canvas.dataset.cloudStrength),
cockpit: canvas.dataset.cockpit,
};
});
check(
'Cesium: the cockpit draws its own cloud canvas, the engine none',
cesium.setCloudCover === 'undefined' && cesium.cockpit === 'true',
cesium,
);
await cockpit(page, false);
await weatherToggle(page, false);
check('no page errors', errors.length === 0, errors);
if (outDir)
fs.writeFileSync(
path.join(outDir, 'cockpit-clouds.json'),
JSON.stringify(report, null, 2),
);
},
);