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

+2 -11
View File
@@ -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/`)
-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();
}
+11 -33
View File
@@ -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)
+19 -21
View File
@@ -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();
});
+6 -27
View File
@@ -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 };
}
+2 -6
View File
@@ -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', () => {
+3 -12
View File
@@ -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');
}
}
}
+3 -24
View File
@@ -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();