diff --git a/DATA_SOURCES.md b/DATA_SOURCES.md index a46dd1106..c65bafd9e 100644 --- a/DATA_SOURCES.md +++ b/DATA_SOURCES.md @@ -276,17 +276,8 @@ wiring (see below). ### Sky background (`sky/`) -The space behind the globe is drawn from two bundled sources (details, hashes -and build commands 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. +Sources, licences, the changes CC BY asks to state, hashes and build commands +are in [`sky/README.md`](src/data/local_data/sky/README.md). ### DataSF Analysis Neighborhoods (`neighborhoods/`) diff --git a/scripts/format-scope.json b/scripts/format-scope.json index 203fa7ed5..6d93c9e41 100644 --- a/scripts/format-scope.json +++ b/scripts/format-scope.json @@ -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" ] diff --git a/scripts/qa-anime-sky.mjs b/scripts/qa-anime-sky.mjs index ae7ee69dc..b3ac1cf2d 100644 --- a/scripts/qa-anime-sky.mjs +++ b/scripts/qa-anime-sky.mjs @@ -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; diff --git a/scripts/qa-sky-sun-moon.mjs b/scripts/qa-sky-sun-moon.mjs deleted file mode 100644 index 12ec7e780..000000000 --- a/scripts/qa-sky-sun-moon.mjs +++ /dev/null @@ -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(); -} diff --git a/src/sky/starrySky.js b/src/sky/starrySky.js index d538d430d..b31109fde 100644 --- a/src/sky/starrySky.js +++ b/src/sky/starrySky.js @@ -4,30 +4,16 @@ import { governorRequestRender } from '../renderGovernor.js'; import { buildStarVertices, STAR_VERTEX_FLOATS } from './stars.js'; /** - * Space background: catalogue stars drawn as point sprites over a soft Milky - * Way cube map, replacing Cesium's default Tycho-2 sky box (six 1024² JPEGs - * stretched 3–6× on screen, which read as compression blocks around the - * globe). - * - * 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. + * Space background in the `Scene#skyBox` slot: catalogue stars as point + * sprites over a soft Milky Way cube map, both turned by one ICRF→fixed + * rotation at the scene clock (TEME until Cesium's tables load). Scene runs + * the command from `update()` first in the environment pass, so the sky + * stays behind everything without depth. Revalidate on engine upgrades. */ export const STARRY_SKY_CESIUM_VERSION = '1.138.0'; /** Opacity of the Milky Way glow over black; the stars carry the sky. */ 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. */ export const SKY_ROTATION_EPSILON_SECONDS = 1; @@ -118,14 +104,13 @@ void main() `; const STAR_FS = ` -uniform float u_intensity; in vec4 v_color; in float v_sigma; in float v_radius; void main() { 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)); if (alpha < 1.0 / 512.0) discard; 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. */ -export function computeSkyRotation( - time, - result, - transforms = Cesium.Transforms, -) { - const icrf = transforms.computeIcrfToFixedMatrix(time, result); +export function computeSkyRotation(time, result) { + const { Transforms } = Cesium; + const icrf = Transforms.computeIcrfToFixedMatrix(time, result); if (icrf) return { matrix: icrf, precise: true }; return { - matrix: transforms.computeTemeToPseudoFixedMatrix(time, result), + matrix: Transforms.computeTemeToPseudoFixedMatrix(time, result), precise: false, }; } @@ -169,7 +151,6 @@ export class StarrySky { this.milkyWayIntensity = MILKY_WAY_INTENSITY; /** 0..1 multiplier a style sets to hide the glow (see visualEffects.js). */ this.milkyWayVisibility = 1; - this.starIntensity = STAR_INTENSITY; this._requestRender = requestRender; this._rotation = Cesium.Matrix3.clone(Cesium.Matrix3.IDENTITY); this._rotationTime = undefined; @@ -197,10 +178,7 @@ export class StarrySky { u_milkyWay: () => this._milkyWayCubeMap, u_intensity: () => this.milkyWayIntensity * this.milkyWayVisibility, }; - this._starUniforms = { - ...uniforms, - u_intensity: () => this.starIntensity, - }; + this._starUniforms = uniforms; const commands = []; this._commands = commands; // The environment pass reads `derivedCommands` (log depth, HDR, picking) diff --git a/src/sky/starrySky.test.mjs b/src/sky/starrySky.test.mjs index d6488fd1d..c6e318519 100644 --- a/src/sky/starrySky.test.mjs +++ b/src/sky/starrySky.test.mjs @@ -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 icrf = Cesium.Matrix3.fromRotationZ(0.3); const teme = Cesium.Matrix3.fromRotationZ(0.2); - const precise = computeSkyRotation(time, new Cesium.Matrix3(), { - computeIcrfToFixedMatrix: (_, result) => Cesium.Matrix3.clone(icrf, result), - computeTemeToPseudoFixedMatrix: () => assert.fail('TEME not needed'), - }); - assert.equal(precise.precise, true); - assert.ok(Cesium.Matrix3.equals(precise.matrix, icrf)); - const approximate = computeSkyRotation(time, new Cesium.Matrix3(), { - computeIcrfToFixedMatrix: () => undefined, - computeTemeToPseudoFixedMatrix: (_, result) => - Cesium.Matrix3.clone(teme, result), - }); - assert.equal(approximate.precise, false); - assert.ok(Cesium.Matrix3.equals(approximate.matrix, teme)); + const { Transforms } = Cesium; + const original = { ...Transforms }; + 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.ok(Cesium.Matrix3.equals(precise.matrix, icrf)); + Transforms.computeIcrfToFixedMatrix = () => undefined; + Transforms.computeTemeToPseudoFixedMatrix = (_, result) => + Cesium.Matrix3.clone(teme, result); + const approximate = computeSkyRotation(time, new Cesium.Matrix3()); + assert.equal(approximate.precise, false); + 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', () => { @@ -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); 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(); -}); diff --git a/src/sky/stars.js b/src/sky/stars.js index 203e1b2e5..117823eb7 100644 --- a/src/sky/stars.js +++ b/src/sky/stars.js @@ -109,38 +109,17 @@ export function bvToRgb(bv) { return rgb.map((channel) => channel / peak); } -/** - * Decode a catalogue pack into interleaved vertex data, `STAR_VERTEX_FLOATS` - * per star. Malformed rows are skipped, never thrown on: a damaged pack must - * 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); +/** Decode a catalogue pack into interleaved vertex data, `STAR_VERTEX_FLOATS` per star. */ +export function buildStarVertices({ stars }) { + const count = stars.length / 4; const vertices = new Float32Array(count * STAR_VERTEX_FLOATS); - let written = 0; for (let i = 0; i < count; i++) { 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 [r, g, b] = bvToRgb(bv); vertices.set( - [x, y, z, sigmaPx, r, g, b, alpha], - written * STAR_VERTEX_FLOATS, + [...raDecToUnitVector(raDeg, decDeg), sigmaPx, ...bvToRgb(bv), alpha], + i * STAR_VERTEX_FLOATS, ); - written += 1; } - return { - count: written, - vertices: - written === count - ? vertices - : vertices.slice(0, written * STAR_VERTEX_FLOATS), - }; + return { count, vertices }; } diff --git a/src/sky/stars.test.mjs b/src/sky/stars.test.mjs index 6dba55971..2a48c341b 100644 --- a/src/sky/stars.test.mjs +++ b/src/sky/stars.test.mjs @@ -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)); }); -test('vertex data is interleaved per star and skips malformed rows', () => { +test('vertex data is interleaved per star', () => { 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(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[3], starAppearance(1).sigmaPx, 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', () => { diff --git a/src/ui/visualEffects.js b/src/ui/visualEffects.js index b4a3cec0b..f99ef3345 100644 --- a/src/ui/visualEffects.js +++ b/src/ui/visualEffects.js @@ -116,11 +116,8 @@ export class VisualEffects { // a post-process cannot tell the sky from far geometry reliably (near // plane clamps, keyless terrain above the ellipsoid horizon): fade the // glow out with the style instead. Bright stars survive as banded points. - if (stage === this.stages.anime) { - const sky = this.viewer.scene?.skyBox; - if (sky && 'milkyWayVisibility' in sky) - sky.milkyWayVisibility = 1 - value; - } + if (stage === this.stages.anime) + this.viewer.scene.skyBox.milkyWayVisibility = 1 - value; if (stage.enabled && stage.uniforms.time !== undefined) this.startAnimationLoop(); this.requestRender('style-stage'); @@ -228,7 +225,7 @@ export class VisualEffects { 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() { if (this.destroyed) return; this.stop(); @@ -240,11 +237,5 @@ export class VisualEffects { Object.assign(this.bloomStage.uniforms, this.previousBloom.uniforms); 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'); - } } } diff --git a/src/ui/visualEffects.test.mjs b/src/ui/visualEffects.test.mjs index 3d86e3a48..e2abbc35c 100644 --- a/src/ui/visualEffects.test.mjs +++ b/src/ui/visualEffects.test.mjs @@ -37,7 +37,9 @@ function fixture() { }, }; const effects = new VisualEffects({ - viewer: { scene: { postProcessStages: pipeline } }, + viewer: { + scene: { postProcessStages: pipeline, skyBox: { milkyWayVisibility: 1 } }, + }, requestRender: (reason) => requests.push(reason), holdRender: (reason) => holds.add(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); assert.equal(glow(), MILKY_WAY_INTENSITY); 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(); }); -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', () => { const f = fixture(); f.effects.initPostProcess();