mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
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:
1 parent
d94cc35c81
commit
b00c7c12c8
10 files changed
+74
-458
No files matched your search
+2
-11
@@ -276,17 +276,8 @@ wiring (see below).
|
|||||||
|
|
||||||
### Sky background (`sky/`)
|
### Sky background (`sky/`)
|
||||||
|
|
||||||
The space behind the globe is drawn from two bundled sources (details, hashes
|
Sources, licences, the changes CC BY asks to state, hashes and build commands
|
||||||
and build commands in [`sky/README.md`](src/data/local_data/sky/README.md)):
|
are in [`sky/README.md`](src/data/local_data/sky/README.md).
|
||||||
|
|
||||||
- **Stars:** the Yale Bright Star Catalogue, 5th Revised Edition (Hoffleit &
|
|
||||||
Warren 1991, CDS catalogue V/50) — public domain; credited as a courtesy.
|
|
||||||
- **Milky Way glow:** ESO image eso0932a, "The Milky Way panorama" — **CC BY
|
|
||||||
4.0, credit "ESO/S. Brunier"**. Modified: stars removed, blurred, reprojected
|
|
||||||
to ICRF cube faces and tone-mapped, as the folder README states.
|
|
||||||
|
|
||||||
NASA SVS "Deep Star Maps 2020" was not used for the glow: its Milky Way layer
|
|
||||||
derives from Gaia DR2, which ESA licenses CC BY-NC 3.0 IGO.
|
|
||||||
|
|
||||||
### DataSF Analysis Neighborhoods (`neighborhoods/`)
|
### DataSF Analysis Neighborhoods (`neighborhoods/`)
|
||||||
|
|
||||||
|
|||||||
@@ -1077,6 +1077,5 @@
|
|||||||
"src/sky/starrySky.test.mjs",
|
"src/sky/starrySky.test.mjs",
|
||||||
"src/tooling/skyAssets.test.mjs",
|
"src/tooling/skyAssets.test.mjs",
|
||||||
"src/tooling/skyAssets.paintOut.test.mjs",
|
"src/tooling/skyAssets.paintOut.test.mjs",
|
||||||
"scripts/qa-sky-sun-moon.mjs",
|
|
||||||
"scripts/qa-anime-sky.mjs"
|
"scripts/qa-anime-sky.mjs"
|
||||||
]
|
]
|
||||||
+32
-231
@@ -1,81 +1,26 @@
|
|||||||
#!/usr/bin/env node
|
#!/usr/bin/env node
|
||||||
/**
|
/**
|
||||||
* qa-anime-sky — the anime style must cel-shade the Earth, and leave the sky
|
* qa-anime-sky — the anime style must leave the sky without bands.
|
||||||
* without bands.
|
|
||||||
*
|
*
|
||||||
* Earlier the anime shader exempted the far plane, judged from the
|
* Anime's luminance bands turn the faint Milky Way into blotches, so
|
||||||
* post-process depth, from its luminance bands. That depth cannot tell the
|
* src/ui/visualEffects.js fades the glow out with the style. Headless against
|
||||||
* sky from far geometry, so the Earth lost its bands. The shader now matches
|
* a Vite dev server, the real anime style looks away from the Earth at the
|
||||||
* main, and src/ui/visualEffects.js fades the Milky Way out with the style.
|
* Galactic Centre: at most 0.5 % of the sky (the stars) may reach the first
|
||||||
* Each case runs headless against a Vite dev server:
|
* band.
|
||||||
*
|
*
|
||||||
* satellites whole globe from 20 000 km with the satellites layer on
|
* Usage: node scripts/qa-anime-sky.mjs [--url http://localhost:4333/]
|
||||||
* (near clamps to 0.1 m): no Earth pixel may skip the bands.
|
* Exit: 0 pass, 1 fail, 2 the anime style was not reached.
|
||||||
* 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.
|
|
||||||
*/
|
*/
|
||||||
import puppeteer from 'puppeteer';
|
import puppeteer from 'puppeteer';
|
||||||
import { readFileSync } from 'node:fs';
|
|
||||||
import { fileURLToPath } from 'node:url';
|
|
||||||
|
|
||||||
const opt = (name, fallback) => {
|
const i = process.argv.indexOf('--url');
|
||||||
const i = process.argv.indexOf(`--${name}`);
|
const URL_ =
|
||||||
return i > 0 && process.argv[i + 1] ? process.argv[i + 1] : fallback;
|
i > 0 && process.argv[i + 1] ? process.argv[i + 1] : 'http://localhost:4333/';
|
||||||
};
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
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({
|
const browser = await puppeteer.launch({
|
||||||
headless: 'new',
|
headless: 'new',
|
||||||
args: ['--no-sandbox', '--ignore-gpu-blocklist'],
|
args: ['--no-sandbox', '--ignore-gpu-blocklist'],
|
||||||
});
|
});
|
||||||
let failed = false,
|
|
||||||
unreached = false;
|
|
||||||
try {
|
try {
|
||||||
const page = await browser.newPage();
|
const page = await browser.newPage();
|
||||||
page.on('pageerror', (e) => console.log('PAGEERR', String(e).slice(0, 300)));
|
page.on('pageerror', (e) => console.log('PAGEERR', String(e).slice(0, 300)));
|
||||||
@@ -87,8 +32,8 @@ try {
|
|||||||
await page.waitForFunction(() => window.__godsEyeView?.viewer?.scene, {
|
await page.waitForFunction(() => window.__godsEyeView?.viewer?.scene, {
|
||||||
timeout: 120000,
|
timeout: 120000,
|
||||||
});
|
});
|
||||||
await sleep(12000);
|
await new Promise((r) => setTimeout(r, 12000));
|
||||||
await page.evaluate(async (fs) => {
|
const r = await page.evaluate(async () => {
|
||||||
const entry = performance
|
const entry = performance
|
||||||
.getEntriesByType('resource')
|
.getEntriesByType('resource')
|
||||||
.map((e) => e.name)
|
.map((e) => e.name)
|
||||||
@@ -97,143 +42,10 @@ try {
|
|||||||
throw new Error(
|
throw new Error(
|
||||||
'run against the Vite dev server (needs .vite/deps/cesium.js)',
|
'run against the Vite dev server (needs .vite/deps/cesium.js)',
|
||||||
);
|
);
|
||||||
const C = (window.__C = await import(entry));
|
const C = await import(entry),
|
||||||
window.__dbg = window.__godsEyeView.viewer.scene.postProcessStages.add(
|
|
||||||
new C.PostProcessStage({ fragmentShader: fs, uniforms: { mode: 1 } }),
|
|
||||||
);
|
|
||||||
}, 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,
|
g = window.__godsEyeView,
|
||||||
v = g.viewer,
|
v = g.viewer,
|
||||||
s = v.scene;
|
s = v.scene;
|
||||||
window.__dbg.enabled = false;
|
|
||||||
g.styleManager.setStyle('anime', { userInitiated: true });
|
g.styleManager.setStyle('anime', { userInitiated: true });
|
||||||
const t = v.clock.currentTime;
|
const t = v.clock.currentTime;
|
||||||
await C.Transforms.preloadIcrfFixed(
|
await C.Transforms.preloadIcrfFixed(
|
||||||
@@ -265,11 +77,7 @@ try {
|
|||||||
);
|
);
|
||||||
s.camera.cancelFlight();
|
s.camera.cancelFlight();
|
||||||
s.camera.setView({
|
s.camera.setView({
|
||||||
destination: C.Cartesian3.multiplyByScalar(
|
destination: C.Cartesian3.multiplyByScalar(look, 3e7, new C.Cartesian3()),
|
||||||
look,
|
|
||||||
3e7,
|
|
||||||
new C.Cartesian3(),
|
|
||||||
),
|
|
||||||
orientation: {
|
orientation: {
|
||||||
direction: look,
|
direction: look,
|
||||||
up: C.Cartesian3.cross(right, look, new C.Cartesian3()),
|
up: C.Cartesian3.cross(right, look, new C.Cartesian3()),
|
||||||
@@ -285,36 +93,29 @@ try {
|
|||||||
gl.readPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, px);
|
gl.readPixels(0, 0, w, h, gl.RGBA, gl.UNSIGNED_BYTE, px);
|
||||||
let banded = 0;
|
let banded = 0;
|
||||||
for (let i = 0; i < px.length; i += 4)
|
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)
|
if (0.299 * px[i] + 0.587 * px[i + 1] + 0.114 * px[i + 2] >= 51) banded++;
|
||||||
banded++;
|
let animeOn = false;
|
||||||
const out = {
|
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,
|
total: px.length / 4,
|
||||||
banded,
|
banded,
|
||||||
visibility: s.skyBox.milkyWayVisibility,
|
visibility: s.skyBox.milkyWayVisibility,
|
||||||
animeOn: (() => {
|
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(
|
const tag = !r.animeOn
|
||||||
'milkyway',
|
? 'UNREACHED'
|
||||||
r.banded <= 0.005 * r.total,
|
: r.banded <= 0.005 * r.total
|
||||||
r.total > 0 && r.animeOn,
|
? 'PASS'
|
||||||
`${r.banded} of ${r.total} sky pixels at or above the first cel band (luma >= 0.2)`,
|
: 'FAIL';
|
||||||
r,
|
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 {
|
} finally {
|
||||||
await browser.close();
|
await browser.close();
|
||||||
}
|
}
|
||||||
process.exitCode = failed ? 1 : unreached ? 2 : 0;
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
+11
-33
@@ -4,30 +4,16 @@ import { governorRequestRender } from '../renderGovernor.js';
|
|||||||
import { buildStarVertices, STAR_VERTEX_FLOATS } from './stars.js';
|
import { buildStarVertices, STAR_VERTEX_FLOATS } from './stars.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Space background: catalogue stars drawn as point sprites over a soft Milky
|
* Space background in the `Scene#skyBox` slot: catalogue stars as point
|
||||||
* Way cube map, replacing Cesium's default Tycho-2 sky box (six 1024² JPEGs
|
* sprites over a soft Milky Way cube map, both turned by one ICRF→fixed
|
||||||
* stretched 3–6× on screen, which read as compression blocks around the
|
* rotation at the scene clock (TEME until Cesium's tables load). Scene runs
|
||||||
* globe).
|
* the command from `update()` first in the environment pass, so the sky
|
||||||
*
|
* stays behind everything without depth. Revalidate on engine upgrades.
|
||||||
* Narrow, version-tested engine adapter. It takes the `Scene#skyBox` slot:
|
|
||||||
* Cesium calls `update(frameState, useHdr)` once per frame and executes the
|
|
||||||
* returned command first in the environment pass, before the atmosphere, sun,
|
|
||||||
* moon and every globe or tile command — so the sky stays behind everything
|
|
||||||
* without depth testing. Only `execute()` and `derivedCommands` of that
|
|
||||||
* command are read. An engine upgrade must revalidate this adapter.
|
|
||||||
*
|
|
||||||
* Both layers share one ICRF→Earth-fixed rotation (IAU 2006 precession,
|
|
||||||
* nutation and Earth rotation at the scene clock), so stars and the Milky
|
|
||||||
* Way agree with each other and with the real sky over the globe. Until
|
|
||||||
* Cesium's ICRF tables load, the TEME approximation the default sky box uses
|
|
||||||
* fills in; the arrival renders one frame through the idle governor.
|
|
||||||
*/
|
*/
|
||||||
export const STARRY_SKY_CESIUM_VERSION = '1.138.0';
|
export const STARRY_SKY_CESIUM_VERSION = '1.138.0';
|
||||||
|
|
||||||
/** Opacity of the Milky Way glow over black; the stars carry the sky. */
|
/** Opacity of the Milky Way glow over black; the stars carry the sky. */
|
||||||
export const MILKY_WAY_INTENSITY = 0.4;
|
export const MILKY_WAY_INTENSITY = 0.4;
|
||||||
/** Overall star brightness multiplier. */
|
|
||||||
export const STAR_INTENSITY = 1;
|
|
||||||
/** Clock change, in seconds, below which the sky rotation is reused. */
|
/** Clock change, in seconds, below which the sky rotation is reused. */
|
||||||
export const SKY_ROTATION_EPSILON_SECONDS = 1;
|
export const SKY_ROTATION_EPSILON_SECONDS = 1;
|
||||||
|
|
||||||
@@ -118,14 +104,13 @@ void main()
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
const STAR_FS = `
|
const STAR_FS = `
|
||||||
uniform float u_intensity;
|
|
||||||
in vec4 v_color;
|
in vec4 v_color;
|
||||||
in float v_sigma;
|
in float v_sigma;
|
||||||
in float v_radius;
|
in float v_radius;
|
||||||
void main()
|
void main()
|
||||||
{
|
{
|
||||||
vec2 offset = (gl_PointCoord - 0.5) * (2.0 * v_radius);
|
vec2 offset = (gl_PointCoord - 0.5) * (2.0 * v_radius);
|
||||||
float alpha = v_color.a * u_intensity *
|
float alpha = v_color.a *
|
||||||
exp(-0.5 * dot(offset, offset) / (v_sigma * v_sigma));
|
exp(-0.5 * dot(offset, offset) / (v_sigma * v_sigma));
|
||||||
if (alpha < 1.0 / 512.0) discard;
|
if (alpha < 1.0 / 512.0) discard;
|
||||||
out_FragColor = vec4(czm_gammaCorrect(v_color.rgb) * alpha, alpha);
|
out_FragColor = vec4(czm_gammaCorrect(v_color.rgb) * alpha, alpha);
|
||||||
@@ -133,15 +118,12 @@ void main()
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
/** Pick the ICRF→fixed rotation for `time`, or TEME while tables load. */
|
/** Pick the ICRF→fixed rotation for `time`, or TEME while tables load. */
|
||||||
export function computeSkyRotation(
|
export function computeSkyRotation(time, result) {
|
||||||
time,
|
const { Transforms } = Cesium;
|
||||||
result,
|
const icrf = Transforms.computeIcrfToFixedMatrix(time, result);
|
||||||
transforms = Cesium.Transforms,
|
|
||||||
) {
|
|
||||||
const icrf = transforms.computeIcrfToFixedMatrix(time, result);
|
|
||||||
if (icrf) return { matrix: icrf, precise: true };
|
if (icrf) return { matrix: icrf, precise: true };
|
||||||
return {
|
return {
|
||||||
matrix: transforms.computeTemeToPseudoFixedMatrix(time, result),
|
matrix: Transforms.computeTemeToPseudoFixedMatrix(time, result),
|
||||||
precise: false,
|
precise: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -169,7 +151,6 @@ export class StarrySky {
|
|||||||
this.milkyWayIntensity = MILKY_WAY_INTENSITY;
|
this.milkyWayIntensity = MILKY_WAY_INTENSITY;
|
||||||
/** 0..1 multiplier a style sets to hide the glow (see visualEffects.js). */
|
/** 0..1 multiplier a style sets to hide the glow (see visualEffects.js). */
|
||||||
this.milkyWayVisibility = 1;
|
this.milkyWayVisibility = 1;
|
||||||
this.starIntensity = STAR_INTENSITY;
|
|
||||||
this._requestRender = requestRender;
|
this._requestRender = requestRender;
|
||||||
this._rotation = Cesium.Matrix3.clone(Cesium.Matrix3.IDENTITY);
|
this._rotation = Cesium.Matrix3.clone(Cesium.Matrix3.IDENTITY);
|
||||||
this._rotationTime = undefined;
|
this._rotationTime = undefined;
|
||||||
@@ -197,10 +178,7 @@ export class StarrySky {
|
|||||||
u_milkyWay: () => this._milkyWayCubeMap,
|
u_milkyWay: () => this._milkyWayCubeMap,
|
||||||
u_intensity: () => this.milkyWayIntensity * this.milkyWayVisibility,
|
u_intensity: () => this.milkyWayIntensity * this.milkyWayVisibility,
|
||||||
};
|
};
|
||||||
this._starUniforms = {
|
this._starUniforms = uniforms;
|
||||||
...uniforms,
|
|
||||||
u_intensity: () => this.starIntensity,
|
|
||||||
};
|
|
||||||
const commands = [];
|
const commands = [];
|
||||||
this._commands = commands;
|
this._commands = commands;
|
||||||
// The environment pass reads `derivedCommands` (log depth, HDR, picking)
|
// The environment pass reads `derivedCommands` (log depth, HDR, picking)
|
||||||
|
|||||||
+15
-17
@@ -25,19 +25,25 @@ test('the ICRF rotation is preferred and TEME fills in while tables load', () =>
|
|||||||
const time = Cesium.JulianDate.fromIso8601('2026-10-19T00:00:00Z');
|
const time = Cesium.JulianDate.fromIso8601('2026-10-19T00:00:00Z');
|
||||||
const icrf = Cesium.Matrix3.fromRotationZ(0.3);
|
const icrf = Cesium.Matrix3.fromRotationZ(0.3);
|
||||||
const teme = Cesium.Matrix3.fromRotationZ(0.2);
|
const teme = Cesium.Matrix3.fromRotationZ(0.2);
|
||||||
const precise = computeSkyRotation(time, new Cesium.Matrix3(), {
|
const { Transforms } = Cesium;
|
||||||
computeIcrfToFixedMatrix: (_, result) => Cesium.Matrix3.clone(icrf, result),
|
const original = { ...Transforms };
|
||||||
computeTemeToPseudoFixedMatrix: () => assert.fail('TEME not needed'),
|
try {
|
||||||
});
|
Transforms.computeIcrfToFixedMatrix = (_, result) =>
|
||||||
|
Cesium.Matrix3.clone(icrf, result);
|
||||||
|
Transforms.computeTemeToPseudoFixedMatrix = () =>
|
||||||
|
assert.fail('TEME not needed');
|
||||||
|
const precise = computeSkyRotation(time, new Cesium.Matrix3());
|
||||||
assert.equal(precise.precise, true);
|
assert.equal(precise.precise, true);
|
||||||
assert.ok(Cesium.Matrix3.equals(precise.matrix, icrf));
|
assert.ok(Cesium.Matrix3.equals(precise.matrix, icrf));
|
||||||
const approximate = computeSkyRotation(time, new Cesium.Matrix3(), {
|
Transforms.computeIcrfToFixedMatrix = () => undefined;
|
||||||
computeIcrfToFixedMatrix: () => undefined,
|
Transforms.computeTemeToPseudoFixedMatrix = (_, result) =>
|
||||||
computeTemeToPseudoFixedMatrix: (_, result) =>
|
Cesium.Matrix3.clone(teme, result);
|
||||||
Cesium.Matrix3.clone(teme, result),
|
const approximate = computeSkyRotation(time, new Cesium.Matrix3());
|
||||||
});
|
|
||||||
assert.equal(approximate.precise, false);
|
assert.equal(approximate.precise, false);
|
||||||
assert.ok(Cesium.Matrix3.equals(approximate.matrix, teme));
|
assert.ok(Cesium.Matrix3.equals(approximate.matrix, teme));
|
||||||
|
} finally {
|
||||||
|
Object.assign(Transforms, original);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the TEME fallback keeps the celestial pole on the Earth axis', () => {
|
test('the TEME fallback keeps the celestial pole on the Earth axis', () => {
|
||||||
@@ -127,11 +133,3 @@ test('the Milky Way glow scales with its visibility, for styles that hide it', (
|
|||||||
assert.equal(sky._milkyWayUniforms.u_intensity(), 0);
|
assert.equal(sky._milkyWayUniforms.u_intensity(), 0);
|
||||||
sky.destroy();
|
sky.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the Milky Way stays out of the environment map that lights models', () => {
|
|
||||||
// Scene#render hands `skyBox._cubeMap` to image-based lighting, which
|
|
||||||
// expects Cesium's own TEME, flipY layout; this ICRF map would be rotated.
|
|
||||||
const sky = new StarrySky({ requestRender: () => {} });
|
|
||||||
assert.equal('_cubeMap' in sky, false);
|
|
||||||
sky.destroy();
|
|
||||||
});
|
|
||||||
+6
-27
@@ -109,38 +109,17 @@ export function bvToRgb(bv) {
|
|||||||
return rgb.map((channel) => channel / peak);
|
return rgb.map((channel) => channel / peak);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** Decode a catalogue pack into interleaved vertex data, `STAR_VERTEX_FLOATS` per star. */
|
||||||
* Decode a catalogue pack into interleaved vertex data, `STAR_VERTEX_FLOATS`
|
export function buildStarVertices({ stars }) {
|
||||||
* per star. Malformed rows are skipped, never thrown on: a damaged pack must
|
const count = stars.length / 4;
|
||||||
* not take the globe down with it.
|
|
||||||
*/
|
|
||||||
export function buildStarVertices(pack) {
|
|
||||||
const stars = Array.isArray(pack?.stars) ? pack.stars : [];
|
|
||||||
const count = Math.floor(stars.length / 4);
|
|
||||||
const vertices = new Float32Array(count * STAR_VERTEX_FLOATS);
|
const vertices = new Float32Array(count * STAR_VERTEX_FLOATS);
|
||||||
let written = 0;
|
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
const [raDeg, decDeg, vmag, bv] = stars.slice(i * 4, i * 4 + 4);
|
const [raDeg, decDeg, vmag, bv] = stars.slice(i * 4, i * 4 + 4);
|
||||||
if (
|
|
||||||
!Number.isFinite(raDeg) ||
|
|
||||||
!Number.isFinite(decDeg) ||
|
|
||||||
!Number.isFinite(vmag)
|
|
||||||
)
|
|
||||||
continue;
|
|
||||||
const [x, y, z] = raDecToUnitVector(raDeg, decDeg);
|
|
||||||
const { sigmaPx, alpha } = starAppearance(vmag);
|
const { sigmaPx, alpha } = starAppearance(vmag);
|
||||||
const [r, g, b] = bvToRgb(bv);
|
|
||||||
vertices.set(
|
vertices.set(
|
||||||
[x, y, z, sigmaPx, r, g, b, alpha],
|
[...raDecToUnitVector(raDeg, decDeg), sigmaPx, ...bvToRgb(bv), alpha],
|
||||||
written * STAR_VERTEX_FLOATS,
|
i * STAR_VERTEX_FLOATS,
|
||||||
);
|
);
|
||||||
written += 1;
|
|
||||||
}
|
}
|
||||||
return {
|
return { count, vertices };
|
||||||
count: written,
|
|
||||||
vertices:
|
|
||||||
written === count
|
|
||||||
? vertices
|
|
||||||
: vertices.slice(0, written * STAR_VERTEX_FLOATS),
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
@@ -82,9 +82,9 @@ test('B−V colours run from blue-white to orange with the peak channel at 1', (
|
|||||||
assert.deepEqual(bvToRgb(9), bvToRgb(2));
|
assert.deepEqual(bvToRgb(9), bvToRgb(2));
|
||||||
});
|
});
|
||||||
|
|
||||||
test('vertex data is interleaved per star and skips malformed rows', () => {
|
test('vertex data is interleaved per star', () => {
|
||||||
const { count, vertices } = buildStarVertices({
|
const { count, vertices } = buildStarVertices({
|
||||||
stars: [0, 0, 1, 0.5, 'bad', 10, 2, 0, 90, 45, 3, null, 10, 20],
|
stars: [0, 0, 1, 0.5, 90, 45, 3, null],
|
||||||
});
|
});
|
||||||
assert.equal(count, 2);
|
assert.equal(count, 2);
|
||||||
assert.equal(vertices.length, 2 * STAR_VERTEX_FLOATS);
|
assert.equal(vertices.length, 2 * STAR_VERTEX_FLOATS);
|
||||||
@@ -93,10 +93,6 @@ test('vertex data is interleaved per star and skips malformed rows', () => {
|
|||||||
close(vertices[STAR_VERTEX_FLOATS + 2], Math.SQRT1_2, 1e-6);
|
close(vertices[STAR_VERTEX_FLOATS + 2], Math.SQRT1_2, 1e-6);
|
||||||
close(vertices[3], starAppearance(1).sigmaPx, 1e-6);
|
close(vertices[3], starAppearance(1).sigmaPx, 1e-6);
|
||||||
close(vertices[7], starAppearance(1).alpha, 1e-6);
|
close(vertices[7], starAppearance(1).alpha, 1e-6);
|
||||||
assert.deepEqual(buildStarVertices(null), {
|
|
||||||
count: 0,
|
|
||||||
vertices: new Float32Array(0),
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test('the committed catalogue holds the Yale Bright Star Catalogue', () => {
|
test('the committed catalogue holds the Yale Bright Star Catalogue', () => {
|
||||||
|
|||||||
+3
-12
@@ -116,11 +116,8 @@ export class VisualEffects {
|
|||||||
// a post-process cannot tell the sky from far geometry reliably (near
|
// a post-process cannot tell the sky from far geometry reliably (near
|
||||||
// plane clamps, keyless terrain above the ellipsoid horizon): fade the
|
// plane clamps, keyless terrain above the ellipsoid horizon): fade the
|
||||||
// glow out with the style instead. Bright stars survive as banded points.
|
// glow out with the style instead. Bright stars survive as banded points.
|
||||||
if (stage === this.stages.anime) {
|
if (stage === this.stages.anime)
|
||||||
const sky = this.viewer.scene?.skyBox;
|
this.viewer.scene.skyBox.milkyWayVisibility = 1 - value;
|
||||||
if (sky && 'milkyWayVisibility' in sky)
|
|
||||||
sky.milkyWayVisibility = 1 - value;
|
|
||||||
}
|
|
||||||
if (stage.enabled && stage.uniforms.time !== undefined)
|
if (stage.enabled && stage.uniforms.time !== undefined)
|
||||||
this.startAnimationLoop();
|
this.startAnimationLoop();
|
||||||
this.requestRender('style-stage');
|
this.requestRender('style-stage');
|
||||||
@@ -228,7 +225,7 @@ export class VisualEffects {
|
|||||||
this.transitions.clear();
|
this.transitions.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Remove owned stages and restore the borrowed bloom stage and sky after callers release them. */
|
/** Remove owned stages and restore the borrowed bloom stage after callers release it. */
|
||||||
destroy() {
|
destroy() {
|
||||||
if (this.destroyed) return;
|
if (this.destroyed) return;
|
||||||
this.stop();
|
this.stop();
|
||||||
@@ -240,11 +237,5 @@ export class VisualEffects {
|
|||||||
Object.assign(this.bloomStage.uniforms, this.previousBloom.uniforms);
|
Object.assign(this.bloomStage.uniforms, this.previousBloom.uniforms);
|
||||||
this.bloomStage.enabled = this.previousBloom.enabled;
|
this.bloomStage.enabled = this.previousBloom.enabled;
|
||||||
}
|
}
|
||||||
// Anime may have faded the Milky Way out; a surviving viewer gets it back.
|
|
||||||
const sky = this.viewer.scene.skyBox;
|
|
||||||
if (sky && 'milkyWayVisibility' in sky) {
|
|
||||||
sky.milkyWayVisibility = 1;
|
|
||||||
this.requestRender('style-stage');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -37,7 +37,9 @@ function fixture() {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
const effects = new VisualEffects({
|
const effects = new VisualEffects({
|
||||||
viewer: { scene: { postProcessStages: pipeline } },
|
viewer: {
|
||||||
|
scene: { postProcessStages: pipeline, skyBox: { milkyWayVisibility: 1 } },
|
||||||
|
},
|
||||||
requestRender: (reason) => requests.push(reason),
|
requestRender: (reason) => requests.push(reason),
|
||||||
holdRender: (reason) => holds.add(reason),
|
holdRender: (reason) => holds.add(reason),
|
||||||
releaseRender: (reason) => holds.delete(reason),
|
releaseRender: (reason) => holds.delete(reason),
|
||||||
@@ -174,32 +176,9 @@ test('the anime stage fades the Milky Way out with its own intensity', () => {
|
|||||||
f.effects.setStageIntensity(f.effects.stages.anime, 0);
|
f.effects.setStageIntensity(f.effects.stages.anime, 0);
|
||||||
assert.equal(glow(), MILKY_WAY_INTENSITY);
|
assert.equal(glow(), MILKY_WAY_INTENSITY);
|
||||||
sky.destroy();
|
sky.destroy();
|
||||||
// A sky box without the knob (Cesium's own) is left alone.
|
|
||||||
const plain = {};
|
|
||||||
f.effects.viewer.scene.skyBox = plain;
|
|
||||||
f.effects.setStageIntensity(f.effects.stages.anime, 1);
|
|
||||||
assert.deepEqual(plain, {});
|
|
||||||
f.effects.destroy();
|
f.effects.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('destroying the effects gives a surviving viewer its Milky Way back', () => {
|
|
||||||
const f = fixture();
|
|
||||||
const sky = { milkyWayVisibility: 1 };
|
|
||||||
f.effects.viewer.scene.skyBox = sky;
|
|
||||||
f.effects.initStyles();
|
|
||||||
f.effects.startTransition('anime', 0, 1);
|
|
||||||
f.tick(250);
|
|
||||||
f.effects.stop();
|
|
||||||
const frozen = sky.milkyWayVisibility;
|
|
||||||
assert.ok(
|
|
||||||
frozen > 0 && frozen < 1,
|
|
||||||
'stop keeps the stages, and the fade, as they are',
|
|
||||||
);
|
|
||||||
f.effects.destroy();
|
|
||||||
assert.equal(sky.milkyWayVisibility, 1);
|
|
||||||
assert.equal(f.requests.at(-1), 'style-stage');
|
|
||||||
});
|
|
||||||
|
|
||||||
test('bloom retains its dead zone, normalized intensity and independent toggle', () => {
|
test('bloom retains its dead zone, normalized intensity and independent toggle', () => {
|
||||||
const f = fixture();
|
const f = fixture();
|
||||||
f.effects.initPostProcess();
|
f.effects.initPostProcess();
|
||||||
|
|||||||
Reference in new issue
Block a user