Glow the Navara frame with the Display bloom behind an optional render.setBloom capability

postprocessing's BloomEffect runs after the overlays and before the visual styles, follows Cesium's eased strength and dead zone with its threshold set in display terms, and VisualEffects drives it when the engine has the bloom capability.
This commit is contained in:
German Khokhlov committed 2026-11-03 21:11:53 +04:00
1 parent 074f4af10e
commit c392d6fb50
15 files changed
+893 -178

No files matched your search

+15 -169
View File
@@ -11,192 +11,38 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import sharp from 'sharp';
import {
createStrict,
localPreview,
waitForEngine,
} from './qa-strict-harness.mjs';
import {
STILL_VIEW,
capture,
checker,
diff,
freeze,
histogramShift,
pickStyle,
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 VIEW =
'v=2&lat=30.2672&lon=-97.7431&alt=20000&heading=0&pitch=-70&map=osm&l=';
const STYLES = ['retro', 'surveillance', 'thermal', 'anime', 'noir'];
const URL_NAMES = { retro: 'crt', surveillance: 'nvg', thermal: 'flir' };
const pageUrl = (engine, extra = '') =>
`${base.origin}/?engine=${engine}&welcome=0&look=cinematic#${VIEW}${extra}`;
visualPageUrl(base.origin, engine, extra, STILL_VIEW);
const strict = createStrict({ origin: base.origin, layers: [] });
const report = { checks: [] };
const check = (name, pass, detail) => {
report.checks.push({ name, pass: Boolean(pass), detail });
console.log(
pass ? 'PASS' : 'FAIL',
name,
detail ? JSON.stringify(detail) : '',
);
assert.ok(pass, `${name} ${JSON.stringify(detail)}`);
};
const check = checker(report, assert);
// The engine canvas as drawn, downscaled to 160×100 RGBA in the page, read
// inside the engine's postRender (Cesium keeps no drawing buffer after it).
async function capture(page) {
return page.evaluate(async () => {
const { engine } = window.__godsEyeView;
const canvas = document.createElement('canvas');
canvas.width = 160;
canvas.height = 100;
const ctx = canvas.getContext('2d', { willReadFrequently: true });
await new Promise((resolve) => {
const off = engine.frames.postRender.addEventListener(() => {
off();
ctx.drawImage(engine.host.canvas, 0, 0, 160, 100);
resolve();
});
engine.render.request('qa-visual-styles');
});
return Array.from(ctx.getImageData(0, 0, 160, 100).data);
});
}
function stats(pixels) {
let r = 0,
g = 0,
b = 0,
saturation = 0;
const histogram = new Array(16).fill(0);
const count = pixels.length / 4;
for (let i = 0; i < pixels.length; i += 4) {
const [pr, pg, pb] = [pixels[i], pixels[i + 1], pixels[i + 2]];
r += pr;
g += pg;
b += pb;
saturation += Math.max(pr, pg, pb) - Math.min(pr, pg, pb);
histogram[
Math.min(15, Math.floor((0.299 * pr + 0.587 * pg + 0.114 * pb) / 16))
]++;
}
// The vignette: the corners' luma over the centre's (160×100 capture).
const luma = (x0, y0) => {
let sum = 0;
for (let y = y0; y < y0 + 10; y++)
for (let x = x0; x < x0 + 16; x++) {
const i = (y * 160 + x) * 4;
sum +=
0.299 * pixels[i] + 0.587 * pixels[i + 1] + 0.114 * pixels[i + 2];
}
return sum / 160;
};
const corners = (luma(0, 0) + luma(144, 0) + luma(0, 90) + luma(144, 90)) / 4;
return {
vignette: corners / Math.max(1, luma(72, 45)),
r: r / count,
g: g / count,
b: b / count,
saturation: saturation / count,
histogram: histogram.map((n) => n / count),
};
}
function diff(a, b) {
let sum = 0;
let changed = 0;
for (let i = 0; i < a.length; i += 4) {
const d =
Math.abs(a[i] - b[i]) +
Math.abs(a[i + 1] - b[i + 1]) +
Math.abs(a[i + 2] - b[i + 2]);
sum += d / 3;
if (d / 3 > 8) changed++;
}
const count = a.length / 4;
return { mean: sum / count, changedShare: changed / count };
}
// Histogram distance (half the L1 of two normalised histograms, 0..1).
const histogramShift = (a, b) =>
a.histogram.reduce(
(sum, value, i) => sum + Math.abs(value - b.histogram[i]),
0,
) / 2;
async function freeze(page) {
await page.evaluate(async () => {
const a = window.__godsEyeView;
if (window.__GOOGLE_MAPS_API_KEY__ || a.tileset)
throw new Error('Keyless preview required');
a.styleManager.orbitController.stop();
a.engine.camera.cancelFlight();
a.styleManager._setDetectionMode?.('OFF');
for (const [id, row] of a.dataManager.layers)
if (row.enabled) await a.dataManager.setEnabled(id, false);
});
await page.waitForFunction(
() => window.__godsEyeView.engine.surface.tilesLoaded(),
{ timeout: 90000, polling: 250 },
);
await new Promise((resolve) => setTimeout(resolve, 2500));
}
// Picks a style the way the style keys do, then follows the crossfade on
// the engine every frame until VisualEffects has no transition left.
async function pickStyle(page, name, previous) {
return page.evaluate(
async (name, previous) => {
const a = window.__godsEyeView;
const render = a.engine.render;
const drawn = typeof render.setStyle === 'function';
const intensity = (style) =>
!style || style === 'normal'
? 0
: drawn
? render.style(style).intensity
: a.styleManager.stages[style].uniforms.intensity;
const samples = [];
const started = performance.now();
a.styleManager.setStyle(name, { userInitiated: true });
while (performance.now() - started < 3000) {
await new Promise((resolve) => requestAnimationFrame(resolve));
samples.push([intensity(previous), intensity(name)]);
if (a.styleManager.transitions.size === 0) break;
}
return {
ms: performance.now() - started,
pending: a.styleManager.transitions.size,
samples,
active: a.styleManager.activeStyle,
intensities: drawn
? Object.fromEntries(
render.visualStyles.map((style) => [
style,
render.style(style).intensity,
]),
)
: null,
pipeline: a.engine.pipeline?.() ?? null,
};
},
name,
previous,
);
}
const save = (name, pixels) => {
if (!outDir) return;
fs.mkdirSync(outDir, { recursive: true });
void sharp(Buffer.from(pixels), {
raw: { width: 160, height: 100, channels: 4 },
})
.png()
.toFile(path.join(outDir, `${name}.png`));
fs.writeFileSync(
path.join(outDir, `${name}.json`),
JSON.stringify(stats(pixels)),
);
};
const save = (name, pixels) => saveCapture(outDir, name, pixels);
await strict.run(
{ args: ['--use-angle=metal', '--enable-gpu'] },