mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-12 06:12:18 +00:00
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:
1 parent
d8c63c69d8
commit
f0d38897f4
15 files changed
+1004
-79
No files matched your search
@@ -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),
|
||||
);
|
||||
},
|
||||
);
|
||||
Reference in new issue
Block a user