refactor(sky): cut what the sky does not need

- qa-anime-sky keeps only the Milky Way case; the satellites and keyless
  cases checked an anime shader this branch no longer changes.
- Drop qa-sky-sun-moon.mjs: the viewer-options unit test covers it.
- VisualEffects sets the sky's visibility directly: the viewer always
  carries a StarrySky and never outlives the effects.
- Drop the unused star intensity knob, the malformed-row tolerance for
  our own pack, the test-only Transforms parameter and a private-name test.
- DATA_SOURCES points to sky/README.md instead of repeating it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
German KhokhlovandClaude Opus 5.5 committed 2026-10-20 13:45:36 +04:00
1 parent d94cc35c81
commit b00c7c12c8
10 files changed
+131 -515

No files matched your search

-1
View File
@@ -1077,6 +1077,5 @@
"src/sky/starrySky.test.mjs",
"src/tooling/skyAssets.test.mjs",
"src/tooling/skyAssets.paintOut.test.mjs",
"scripts/qa-sky-sun-moon.mjs",
"scripts/qa-anime-sky.mjs"
]
+85 -284
View File
@@ -1,81 +1,26 @@
#!/usr/bin/env node
/**
* qa-anime-sky — the anime style must cel-shade the Earth, and leave the sky
* without bands.
* qa-anime-sky — the anime style must leave the sky without bands.
*
* Earlier the anime shader exempted the far plane, judged from the
* post-process depth, from its luminance bands. That depth cannot tell the
* sky from far geometry, so the Earth lost its bands. The shader now matches
* main, and src/ui/visualEffects.js fades the Milky Way out with the style.
* Each case runs headless against a Vite dev server:
* Anime's luminance bands turn the faint Milky Way into blotches, so
* src/ui/visualEffects.js fades the glow out with the style. Headless against
* a Vite dev server, the real anime style looks away from the Earth at the
* Galactic Centre: at most 0.5 % of the sky (the stars) may reach the first
* band.
*
* satellites whole globe from 20 000 km with the satellites layer on
* (near clamps to 0.1 m): no Earth pixel may skip the bands.
* keyless keyless stack, Denver 1700 m looking west: no terrain pixel
* above the ellipsoid horizon may skip the bands.
* milkyway the real anime style, looking away from the Earth at the
* Galactic Centre: at most 0.5 % of the sky (the stars) may
* reach the first band.
*
* The first two cases compile the checkout's anime shader with a debug
* output (R = pixel skipped the bands, G = raw depth 1.0) as an extra stage.
*
* Usage: node scripts/qa-anime-sky.mjs [--url http://localhost:4333/] [--case satellites|keyless|milkyway|all]
* Exit: 0 all pass, 1 a case failed, 2 a scenario was not reached or the case is unknown.
* Usage: node scripts/qa-anime-sky.mjs [--url http://localhost:4333/]
* Exit: 0 pass, 1 fail, 2 the anime style was not reached.
*/
import puppeteer from 'puppeteer';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
const opt = (name, fallback) => {
const i = process.argv.indexOf(`--${name}`);
return i > 0 && process.argv[i + 1] ? process.argv[i + 1] : fallback;
};
const URL_ = opt('url', 'http://localhost:4333/');
const ALL_CASES = ['satellites', 'keyless', 'milkyway'];
const requested = opt('case', 'all');
const CASES = requested === 'all' ? ALL_CASES : [requested];
if (!CASES.every((c) => ALL_CASES.includes(c))) {
console.error(
`unknown case ${requested}; expected ${ALL_CASES.join('|')}|all`,
);
process.exit(2);
}
const i = process.argv.indexOf('--url');
const URL_ =
i > 0 && process.argv[i + 1] ? process.argv[i + 1] : 'http://localhost:4333/';
let fs = readFileSync(
fileURLToPath(new URL('../src/styles/anime.js', import.meta.url)),
'utf8',
);
fs = fs
.slice(fs.indexOf('/* glsl */ `') + 12, fs.lastIndexOf('`'))
.replace('uniform float intensity;', 'const float intensity = 1.0;')
.replace('uniform float saturation;', 'const float saturation = 1.0;')
.replace('uniform float edgeThick;', 'const float edgeThick = 0.5;');
if (!/uniform\s+sampler2D\s+depthTexture/.test(fs))
fs = fs.replace(
'uniform sampler2D colorTexture;',
'uniform sampler2D colorTexture;\n uniform sampler2D depthTexture;',
);
if (!/float bands\b/.test(fs))
throw new Error(
'anime.js has no `bands` variable: adapt the debug line of this script',
);
fs = fs
.replace(
/out_FragColor = [^;]+;\s*}\s*$/,
`out_FragColor = mode < 0.5 ? texture(colorTexture, uv)
: vec4(bands > 255.0 ? 1.0 : 0.0, texture(depthTexture, uv).r >= 1.0 ? 1.0 : 0.0, 0.0, 1.0);
}`,
)
.replace('void main()', 'uniform float mode;\n void main()');
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const browser = await puppeteer.launch({
headless: 'new',
args: ['--no-sandbox', '--ignore-gpu-blocklist'],
});
let failed = false,
unreached = false;
try {
const page = await browser.newPage();
page.on('pageerror', (e) => console.log('PAGEERR', String(e).slice(0, 300)));
@@ -87,8 +32,8 @@ try {
await page.waitForFunction(() => window.__godsEyeView?.viewer?.scene, {
timeout: 120000,
});
await sleep(12000);
await page.evaluate(async (fs) => {
await new Promise((r) => setTimeout(r, 12000));
const r = await page.evaluate(async () => {
const entry = performance
.getEntriesByType('resource')
.map((e) => e.name)
@@ -97,224 +42,80 @@ try {
throw new Error(
'run against the Vite dev server (needs .vite/deps/cesium.js)',
);
const C = (window.__C = await import(entry));
window.__dbg = window.__godsEyeView.viewer.scene.postProcessStages.add(
new C.PostProcessStage({ fragmentShader: fs, uniforms: { mode: 1 } }),
const C = await import(entry),
g = window.__godsEyeView,
v = g.viewer,
s = v.scene;
g.styleManager.setStyle('anime', { userInitiated: true });
const t = v.clock.currentTime;
await C.Transforms.preloadIcrfFixed(
new C.TimeInterval({
start: C.JulianDate.addDays(t, -1, new C.JulianDate()),
stop: C.JulianDate.addDays(t, 1, new C.JulianDate()),
}),
);
}, fs);
const setView = (v) =>
page.evaluate((v) => {
const C = window.__C,
s = window.__godsEyeView.viewer.scene;
s.camera.cancelFlight();
s.camera.setView({
destination: C.Cartesian3.fromDegrees(v.lon, v.lat, v.h),
orientation: {
heading: C.Math.toRadians(v.heading ?? 0),
pitch: C.Math.toRadians(v.pitch),
roll: 0,
},
});
s.requestRender();
}, v);
// returns per-pixel {exempt, raw1, ground?} counts
const measure = (truth) =>
page.evaluate((truth) => {
const s = window.__godsEyeView.viewer.scene;
const read = () => {
s.requestRender();
s.render();
const gl = s.context._gl,
w = gl.drawingBufferWidth,
h = gl.drawingBufferHeight,
px = new Uint8Array(w * h * 4);
gl.readPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, px);
return px;
};
window.__dbg.uniforms.mode = 1;
const px = read();
let g;
if (truth) {
const keep = [s.skyAtmosphere, s.skyBox, s.sun, s.moon].map(
(o) => o && o.show,
);
[s.skyAtmosphere, s.skyBox, s.sun, s.moon].forEach(
(o) => o && (o.show = false),
);
window.__dbg.uniforms.mode = 0;
g = read();
[s.skyAtmosphere, s.skyBox, s.sun, s.moon].forEach(
(o, i) => o && (o.show = keep[i]),
);
window.__dbg.uniforms.mode = 1;
}
const r = {
total: px.length / 4,
near: s._view.frustumCommandsList[0]?.near ?? null,
frusta: s._view.frustumCommandsList.length,
geom: 0,
geomExempt: 0,
raw1: 0,
raw1Exempt: 0,
ground: 0,
groundRaw1: 0,
groundExempt: 0,
clearGlobeDepth: s._environmentState.clearGlobeDepth,
};
for (let i = 0; i < px.length; i += 4) {
const ex = px[i] > 127,
raw1 = px[i + 1] > 127;
if (raw1) {
r.raw1++;
if (ex) r.raw1Exempt++;
} else {
r.geom++;
if (ex) r.geomExempt++;
}
if (g && g[i] + g[i + 1] + g[i + 2] > 6) {
r.ground++;
if (raw1) r.groundRaw1++;
if (ex) r.groundExempt++;
}
}
return r;
}, truth);
const verdict = (name, ok, reached, msg, r) => {
const tag = !reached ? 'UNREACHED' : ok ? 'PASS' : 'FAIL';
if (!reached) unreached = true;
else if (!ok) failed = true;
console.log(`${tag} ${name}: ${msg}\n ${JSON.stringify(r)}`);
};
for (const c of CASES) {
if (c === 'satellites') {
await page.evaluate(() =>
window.__godsEyeView.dataManager.setEnabled('satellites', true, {
origin: 'user',
}),
);
await sleep(20000);
await setView({ lon: 10, lat: 20, h: 2.0e7, pitch: -90 });
await sleep(5000);
const r = await measure(false);
verdict(
'satellites',
r.geomExempt <= 0.01 * r.geom,
r.frusta > 0 && r.near !== null && r.near < 1 && r.geom > 50000,
`${r.geomExempt} of ${r.geom} Earth pixels exempt from cel-shading (near ${r.near} m)`,
r,
);
await page.evaluate(() =>
window.__godsEyeView.dataManager.setEnabled('satellites', false, {
origin: 'user',
}),
);
await sleep(5000);
} else if (c === 'keyless') {
await setView({
lon: -104.99,
lat: 39.74,
h: 1700,
heading: 270,
pitch: 0,
});
await sleep(25000);
const r = await measure(true);
verdict(
'keyless',
r.groundExempt <= 0.01 * r.ground,
r.clearGlobeDepth && r.groundRaw1 > 1000,
`${r.groundExempt} of ${r.ground} terrain pixels exempt (${r.groundRaw1} terrain pixels have raw depth 1.0)`,
r,
);
} else if (c === 'milkyway') {
const r = await page.evaluate(async () => {
const C = window.__C,
g = window.__godsEyeView,
v = g.viewer,
s = v.scene;
window.__dbg.enabled = false;
g.styleManager.setStyle('anime', { userInitiated: true });
const t = v.clock.currentTime;
await C.Transforms.preloadIcrfFixed(
new C.TimeInterval({
start: C.JulianDate.addDays(t, -1, new C.JulianDate()),
stop: C.JulianDate.addDays(t, 1, new C.JulianDate()),
}),
);
const m = C.Transforms.computeIcrfToFixedMatrix(t);
const ra = C.Math.toRadians(266.4),
dec = C.Math.toRadians(-29);
const look = C.Matrix3.multiplyByVector(
m,
new C.Cartesian3(
Math.cos(dec) * Math.cos(ra),
Math.cos(dec) * Math.sin(ra),
Math.sin(dec),
),
new C.Cartesian3(),
);
const north = C.Matrix3.multiplyByVector(
m,
C.Cartesian3.UNIT_Z,
new C.Cartesian3(),
);
const right = C.Cartesian3.normalize(
C.Cartesian3.cross(look, north, new C.Cartesian3()),
new C.Cartesian3(),
);
s.camera.cancelFlight();
s.camera.setView({
destination: C.Cartesian3.multiplyByScalar(
look,
3e7,
new C.Cartesian3(),
),
orientation: {
direction: look,
up: C.Cartesian3.cross(right, look, new C.Cartesian3()),
},
});
await new Promise((res) => setTimeout(res, 2500));
s.requestRender();
s.render();
const gl = s.context._gl,
w = gl.drawingBufferWidth,
h = gl.drawingBufferHeight,
px = new Uint8Array(w * h * 4);
gl.readPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, px);
let banded = 0;
for (let i = 0; i < px.length; i += 4)
if (0.299 * px[i] + 0.587 * px[i + 1] + 0.114 * px[i + 2] >= 51)
banded++;
const out = {
total: px.length / 4,
banded,
visibility: s.skyBox.milkyWayVisibility,
animeOn: (() => {
const stages = s.postProcessStages;
for (let i = 0; i < stages.length; i++) {
const stage = stages.get(i);
if (stage.name === 'godsEyeView_anime')
return stage.enabled && stage.uniforms.intensity > 0.99;
}
return false;
})(),
};
g.styleManager.setStyle('normal', { userInitiated: true });
window.__dbg.enabled = true;
return out;
});
verdict(
'milkyway',
r.banded <= 0.005 * r.total,
r.total > 0 && r.animeOn,
`${r.banded} of ${r.total} sky pixels at or above the first cel band (luma >= 0.2)`,
r,
);
const m = C.Transforms.computeIcrfToFixedMatrix(t);
const ra = C.Math.toRadians(266.4),
dec = C.Math.toRadians(-29);
const look = C.Matrix3.multiplyByVector(
m,
new C.Cartesian3(
Math.cos(dec) * Math.cos(ra),
Math.cos(dec) * Math.sin(ra),
Math.sin(dec),
),
new C.Cartesian3(),
);
const north = C.Matrix3.multiplyByVector(
m,
C.Cartesian3.UNIT_Z,
new C.Cartesian3(),
);
const right = C.Cartesian3.normalize(
C.Cartesian3.cross(look, north, new C.Cartesian3()),
new C.Cartesian3(),
);
s.camera.cancelFlight();
s.camera.setView({
destination: C.Cartesian3.multiplyByScalar(look, 3e7, new C.Cartesian3()),
orientation: {
direction: look,
up: C.Cartesian3.cross(right, look, new C.Cartesian3()),
},
});
await new Promise((res) => setTimeout(res, 2500));
s.requestRender();
s.render();
const gl = s.context._gl,
w = gl.drawingBufferWidth,
h = gl.drawingBufferHeight,
px = new Uint8Array(w * h * 4);
gl.readPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, px);
let banded = 0;
for (let i = 0; i < px.length; i += 4)
if (0.299 * px[i] + 0.587 * px[i + 1] + 0.114 * px[i + 2] >= 51) banded++;
let animeOn = false;
for (let i = 0; i < s.postProcessStages.length; i++) {
const stage = s.postProcessStages.get(i);
if (stage.name === 'godsEyeView_anime')
animeOn = stage.enabled && stage.uniforms.intensity > 0.99;
}
}
return {
total: px.length / 4,
banded,
visibility: s.skyBox.milkyWayVisibility,
animeOn,
};
});
const tag = !r.animeOn
? 'UNREACHED'
: r.banded <= 0.005 * r.total
? 'PASS'
: 'FAIL';
console.log(
`${tag}: ${r.banded} of ${r.total} sky pixels at or above the first cel band (luma >= 0.2)\n ${JSON.stringify(r)}`,
);
process.exitCode = tag === 'PASS' ? 0 : tag === 'FAIL' ? 1 : 2;
} finally {
await browser.close();
}
process.exitCode = failed ? 1 : unreached ? 2 : 0;
-96
View File
@@ -1,96 +0,0 @@
#!/usr/bin/env node
/**
* qa-sky-sun-moon — the custom sky must keep Cesium's Sun and Moon.
*
* CesiumWidget creates `scene.sun` and `scene.moon` only when the `skyBox`
* option is not `false`, so replacing the sky box the wrong way silently
* removes the Sun disc and its bloom. Points the camera at the Sun from
* 22 000 km and fails unless both bodies exist; saves a screenshot.
*
* Usage: node scripts/qa-sky-sun-moon.mjs [--url http://localhost:4317] [--out qa-shots/sky-sun.png]
*/
import puppeteer from 'puppeteer';
import { mkdirSync } from 'node:fs';
import path from 'node:path';
const argv = process.argv;
const option = (name, fallback) =>
argv.includes(name) ? argv[argv.indexOf(name) + 1] : fallback;
const url = option('--url', 'http://localhost:4317');
const out = option('--out', 'qa-shots/sky-sun.png');
mkdirSync(path.dirname(out), { recursive: true });
const browser = await puppeteer.launch({
headless: 'new',
args: ['--no-sandbox', '--ignore-gpu-blocklist'],
});
try {
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 800 });
await page.evaluateOnNewDocument(() =>
localStorage.setItem('gev:first-run-mission:v1', 'suppressed'),
);
await page.goto(url, { waitUntil: 'domcontentloaded' });
await page.waitForFunction(() => window.__godsEyeView?.viewer?.scene, {
timeout: 90_000,
});
await new Promise((r) => setTimeout(r, 10_000));
const info = await page.evaluate(async () => {
// The page's own Cesium instance, as Vite served it.
const entry = performance
.getEntriesByType('resource')
.map((e) => e.name)
.find((n) => /\.vite\/deps\/cesium\.js/.test(n));
const C = await import(entry);
const { viewer } = window.__godsEyeView;
const scene = viewer.scene;
scene.camera.cancelFlight();
const time = viewer.clock.currentTime;
const sun = C.Cartesian3.normalize(
C.Matrix3.multiplyByVector(
C.Transforms.computeTemeToPseudoFixedMatrix(time),
C.Simon1994PlanetaryPositions.computeSunPositionInEarthInertialFrame(
time,
),
new C.Cartesian3(),
),
new C.Cartesian3(),
);
// Off to the side of the Earth, looking straight at the Sun.
const side = C.Cartesian3.normalize(
C.Cartesian3.cross(sun, C.Cartesian3.UNIT_Z, new C.Cartesian3()),
new C.Cartesian3(),
);
const destination = C.Cartesian3.add(
C.Cartesian3.multiplyByScalar(side, 2.2e7, new C.Cartesian3()),
C.Cartesian3.multiplyByScalar(sun, -0.6e7, new C.Cartesian3()),
new C.Cartesian3(),
);
const up = C.Cartesian3.normalize(
C.Cartesian3.cross(
C.Cartesian3.cross(sun, C.Cartesian3.UNIT_Z, new C.Cartesian3()),
sun,
new C.Cartesian3(),
),
new C.Cartesian3(),
);
scene.camera.setView({ destination, orientation: { direction: sun, up } });
scene.requestRender();
return {
sun: Boolean(scene.sun),
moon: Boolean(scene.moon),
skyBox: scene.skyBox?.constructor?.name,
};
});
await new Promise((r) => setTimeout(r, 4_000));
await page.evaluate(() => window.__godsEyeView.viewer.scene.requestRender());
await new Promise((r) => setTimeout(r, 1_500));
await page.screenshot({ path: out });
console.log(JSON.stringify({ url, ...info, screenshot: out }));
if (!info.sun || !info.moon) {
console.error('FAIL: scene.sun and scene.moon must both exist');
process.exitCode = 1;
}
} finally {
await browser.close();
}