feat(sky): replace the blocky default sky box with catalogue stars

Cesium's default Tycho-2 sky box is six 1024² JPEGs stretched 3–6× around
the whole-globe view, so space read as an over-compressed JPEG (HAL-627).

The scene now draws its own sky in the Scene#skyBox slot: the 9,096 stars
of the Yale Bright Star Catalogue as point sprites sized in CSS pixels
(magnitude → width and opacity, B−V → colour), over a soft, star-free
Milky Way cube map built from ESO/S. Brunier's panorama (CC BY 4.0). Both
layers share one ICRF→Earth-fixed rotation at the scene clock, with the
TEME rotation filling in until Cesium's IAU 2006 tables load.

Assets total 482 KB, built by scripts/build-bright-stars.mjs and
scripts/build-milky-way.mjs. Frame time and the idle render governor are
unchanged.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
German KhokhlovandClaude Opus 5.5 committed 2026-10-19 11:08:46 +04:00
1 parent 93ea088f45
commit 6549344946
20 files changed
+10644 -2

No files matched your search

+107
View File
@@ -0,0 +1,107 @@
import assert from 'node:assert/strict';
import test from 'node:test';
import {
buildStarPack,
parseBscRecord,
} from '../../scripts/build-bright-stars.mjs';
import {
CUBE_FACES,
cubeFaceDirection,
directionToPlateCarree,
icrfToGalactic,
} from '../../scripts/build-milky-way.mjs';
import { raDecToUnitVector } from '../sky/stars.js';
// Verbatim CDS V/50 records: HR 1, Sirius (HR 2491) and a nova with no J2000
// position. Columns matter, so the padding is part of the fixture.
const HR_1 =
' 1 BD+44 4550 3 36042 46 000001.1+444022000509.9+451345114.44-16.88 6.70 +0.07 +0.08 A1Vn -0.012-0.018 -018 195 4.2 21.6AC 3';
const SIRIUS =
'2491 9Alp CMaBD-16 1591 48915151881 257I 5423 064044.6-163444064508.9-164258227.22-08.88-1.46 0.00 -0.05 -0.03 A1Vm -0.553-1.205 +.375-008SBO 13 10.3 11.2AB 4*';
const NOVA =
' 92 NOVA 1572 B Cas *';
const close = (actual, expected, epsilon = 1e-9) =>
assert.ok(
Math.abs(actual - expected) <= epsilon,
`${actual} is not within ${epsilon} of ${expected}`,
);
test('BSC5 records parse J2000 position, V magnitude and B−V', () => {
const sirius = parseBscRecord(SIRIUS);
assert.equal(sirius.hr, 2491);
close(sirius.raDeg, (6 + 45 / 60 + 8.9 / 3600) * 15);
close(sirius.decDeg, -(16 + 42 / 60 + 58 / 3600));
assert.equal(sirius.vmag, -1.46);
assert.equal(sirius.bv, 0);
const first = parseBscRecord(HR_1);
close(first.raDeg, (5 / 60 + 9.9 / 3600) * 15);
close(first.decDeg, 45 + 13 / 60 + 45 / 3600);
assert.equal(first.vmag, 6.7);
assert.equal(first.bv, 0.07);
assert.equal(parseBscRecord(NOVA), null);
});
test('the star pack is a flat, rounded column array that counts its drops', () => {
const pack = buildStarPack([HR_1, NOVA, SIRIUS, ''].join('\n'), {
fetched: '2026-10-19',
});
assert.deepEqual(
pack.stars,
[1.291, 45.229, 6.7, 0.07, 101.287, -16.716, -1.46, 0],
);
assert.equal(pack.meta.count, 2);
assert.equal(pack.meta.dropped, 1);
assert.equal(pack.meta.fetched, '2026-10-19');
});
test('cube faces use the WebGL major-axis layout', () => {
// Each face centre looks down its own axis …
const axes = {
positiveX: [1, 0, 0],
negativeX: [-1, 0, 0],
positiveY: [0, 1, 0],
negativeY: [0, -1, 0],
positiveZ: [0, 0, 1],
negativeZ: [0, 0, -1],
};
for (const face of CUBE_FACES)
assert.deepEqual(
cubeFaceDirection(face, 0.5, 0.5).map((v) => v + 0),
axes[face],
);
// … and the first texel row and column follow OpenGL ES 3.0 §3.8.10:
// on +X, s grows towards −Z and t towards −Y.
assert.deepEqual(cubeFaceDirection('positiveX', 0, 0), [1, 1, 1]);
assert.deepEqual(cubeFaceDirection('positiveZ', 1, 1), [1, -1, 1]);
assert.deepEqual(cubeFaceDirection('negativeY', 0, 1), [-1, -1, -1]);
assert.throws(() => cubeFaceDirection('up', 0, 0), RangeError);
});
test('plate carrée is centred on longitude 0, growing to the left, north up', () => {
assert.deepEqual(directionToPlateCarree(1, 0, 0), [0.5, 0.5]);
const sixHours = directionToPlateCarree(0, 1, 0);
close(sixHours[0], 0.25);
const eighteenHours = directionToPlateCarree(0, -1, 0);
close(eighteenHours[0], 0.75);
close(directionToPlateCarree(0, 0, 1)[1], 0);
close(directionToPlateCarree(0, 0, -1)[1], 1);
const twelveHours = directionToPlateCarree(-1, 0, 0);
assert.ok(twelveHours[0] === 0 || twelveHours[0] === 1);
});
test('the galactic rotation puts the Galactic Centre and pole where the IAU does', () => {
// Sgr A*-adjacent origin l = b = 0 at RA 266.405°, Dec −28.936° (J2000).
const centre = icrfToGalactic(raDecToUnitVector(266.40499, -28.93617));
close(centre[0], 1, 1e-6);
// North galactic pole at RA 192.85948°, Dec +27.12825°.
const pole = icrfToGalactic(raDecToUnitVector(192.85948, 27.12825));
close(pole[2], 1, 1e-6);
// ESO's panorama: the Large Magellanic Cloud (l ≈ 280.5°, b ≈ −32.9°) sits
// right of and below the centre.
const [u, v] = directionToPlateCarree(
...icrfToGalactic(raDecToUnitVector(80.894, -69.756)),
);
close(u, 0.5 + (360 - 280.47) / 360, 2e-3);
close(v, 0.5 + 32.89 / 180, 2e-3);
});