diff --git a/scripts/build-milky-way.mjs b/scripts/build-milky-way.mjs index e0703f882..55c7f8225 100644 --- a/scripts/build-milky-way.mjs +++ b/scripts/build-milky-way.mjs @@ -31,7 +31,9 @@ * celestial pole — rotating each direction into galactic coordinates * with the IAU J2000 matrix. * 6. Map the median sky to black, easing in with a soft knee, and the - * brightest 0.01 % to white (on luminance, keeping each pixel's hue), + * brightest 0.01 % to about 97 % white, the knee's offset there (both + * levels taken before the faint-glow blend; on luminance, keeping each + * pixel's hue), * encode with gamma 1/2.2 and ±0.5 LSB dither against banding; WebP. * * Usage: @@ -373,6 +375,8 @@ async function main() { for (const plane of planes) blurPlane(plane, width, height, sigmaPx); for (const plane of wide) blurPlane(plane, width, height, (FAINT_BLUR_DEG / 180) * height); + // Levels from the fine blur, so the blend weight and the output share + // them; the blend moves the median only in the faint sky it smooths. const black = luminancePercentile(planes, BLACK_PERCENTILE); const white = luminancePercentile(planes, WHITE_PERCENTILE); blendByBrightness(planes, wide, (luminance) => diff --git a/src/app/application.test.mjs b/src/app/application.test.mjs index d3ef8c7cc..dcc937c75 100644 --- a/src/app/application.test.mjs +++ b/src/app/application.test.mjs @@ -219,6 +219,17 @@ test('the separate viewer export imports without constructing a browser viewer', assert.throws(() => createApplicationViewer({}), /containers are required/); }); +test('the viewer gets the starry sky as an option, which keeps the Sun and Moon', async () => { + const { applicationViewerOptions } = await import('./viewer.js'); + const { StarrySky } = await import('../sky/starrySky.js'); + const creditContainer = {}; + const options = applicationViewerOptions(creditContainer); + // CesiumWidget skips scene.sun and scene.moon when skyBox is false. + assert.ok(options.skyBox instanceof StarrySky); + assert.equal(options.creditContainer, creditContainer); + options.skyBox.destroy(); +}); + function pinchFixture({ zoomEventTypes } = {}) { const handlers = new Set(); const relayed = []; diff --git a/src/app/viewer.js b/src/app/viewer.js index a09836775..633616fef 100644 --- a/src/app/viewer.js +++ b/src/app/viewer.js @@ -103,11 +103,9 @@ export function installTrackpadPinchZoom( }; } -/** Create the standard globe viewer in caller-owned, visible containers. */ -export function createApplicationViewer({ container, creditContainer }) { - if (!container || !creditContainer) - throw new TypeError('Viewer and credit containers are required'); - const viewer = new Cesium.Viewer(container, { +/** Cesium.Viewer options of the standard globe viewer. */ +export function applicationViewerOptions(creditContainer) { + return { timeline: false, animation: false, baseLayerPicker: false, @@ -127,7 +125,17 @@ export function createApplicationViewer({ container, creditContainer }) { creditContainer, msaaSamples: 4, contextOptions: { webgl: { preserveDrawingBuffer: true } }, - }); + }; +} + +/** Create the standard globe viewer in caller-owned, visible containers. */ +export function createApplicationViewer({ container, creditContainer }) { + if (!container || !creditContainer) + throw new TypeError('Viewer and credit containers are required'); + const viewer = new Cesium.Viewer( + container, + applicationViewerOptions(creditContainer), + ); try { viewer.targetFrameRate = 60; // Before any tile builds a draw command: Cesium's per-vertex model diff --git a/src/sky/starrySky.js b/src/sky/starrySky.js index d77892d6a..d538d430d 100644 --- a/src/sky/starrySky.js +++ b/src/sky/starrySky.js @@ -180,7 +180,10 @@ export class StarrySky { this._loading = false; this._starData = undefined; this._faceImages = undefined; - this._cubeMap = undefined; + // Not `_cubeMap`: Scene reads `skyBox._cubeMap` as the environment map + // that lights models, in the TEME, flipY layout of its own sky box. This + // ICRF map would light them rotated, so models keep Cesium's default. + this._milkyWayCubeMap = undefined; this._milkyWayCommand = undefined; this._milkyWayAttributes = undefined; this._starCommand = undefined; @@ -191,7 +194,7 @@ export class StarrySky { }; this._milkyWayUniforms = { ...uniforms, - u_milkyWay: () => this._cubeMap, + u_milkyWay: () => this._milkyWayCubeMap, u_intensity: () => this.milkyWayIntensity * this.milkyWayVisibility, }; this._starUniforms = { @@ -301,7 +304,7 @@ export class StarrySky { cubeMap.sampler = new Cesium.Sampler({ minificationFilter: Cesium.TextureMinificationFilter.LINEAR_MIPMAP_LINEAR, }); - this._cubeMap = cubeMap; + this._milkyWayCubeMap = cubeMap; this._faceImages = undefined; const geometry = Cesium.BoxGeometry.createGeometry( Cesium.BoxGeometry.fromDimensions({ @@ -440,7 +443,7 @@ export class StarrySky { command.vertexArray?.destroy(); command.shaderProgram?.destroy(); } - this._cubeMap = this._cubeMap?.destroy(); + this._milkyWayCubeMap = this._milkyWayCubeMap?.destroy(); this._milkyWayCommand = undefined; this._starCommand = undefined; this._commands.length = 0; diff --git a/src/sky/starrySky.test.mjs b/src/sky/starrySky.test.mjs index 1d9d2f8a0..d6488fd1d 100644 --- a/src/sky/starrySky.test.mjs +++ b/src/sky/starrySky.test.mjs @@ -127,3 +127,11 @@ 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 b7ef7aa41..203e1b2e5 100644 --- a/src/sky/stars.js +++ b/src/sky/stars.js @@ -13,7 +13,7 @@ * @module sky/stars */ -/** Faintest catalogue magnitude; also the reference for brightness. */ +/** Faintest magnitude drawn distinctly (fainter catalogue stars look like it); the brightness reference. */ export const STAR_LIMIT_MAGNITUDE = 6.5; /** Gaussian σ of the faintest star, CSS pixels. */ export const STAR_MIN_SIGMA_PX = 0.55; diff --git a/src/ui/visualEffects.test.mjs b/src/ui/visualEffects.test.mjs index 21f744985..3d86e3a48 100644 --- a/src/ui/visualEffects.test.mjs +++ b/src/ui/visualEffects.test.mjs @@ -1,6 +1,7 @@ import assert from 'node:assert/strict'; import test from 'node:test'; import { VisualEffects } from './visualEffects.js'; +import { MILKY_WAY_INTENSITY, StarrySky } from '../sky/starrySky.js'; import { GLOBAL_POST_DEFAULTS, STYLE_PRESET_DEFAULTS, @@ -160,17 +161,19 @@ test('direct Cockpit intensity writes are reconciled before rendering', () => { test('the anime stage fades the Milky Way out with its own intensity', () => { const f = fixture(); - const sky = { milkyWayVisibility: 1 }; + const sky = new StarrySky({ requestRender: () => {} }); + const glow = () => sky._milkyWayUniforms.u_intensity(); f.effects.viewer.scene.skyBox = sky; f.effects.initStyles(); f.effects.setStageIntensity(f.effects.stages.thermal, 1); - assert.equal(sky.milkyWayVisibility, 1, 'other styles keep the glow'); + assert.equal(glow(), MILKY_WAY_INTENSITY, 'other styles keep the glow'); f.effects.setStageIntensity(f.effects.stages.anime, 0.25); - assert.equal(sky.milkyWayVisibility, 0.75); + assert.equal(glow(), MILKY_WAY_INTENSITY * 0.75); f.effects.setStageIntensity(f.effects.stages.anime, 1); - assert.equal(sky.milkyWayVisibility, 0); + assert.equal(glow(), 0); f.effects.setStageIntensity(f.effects.stages.anime, 0); - assert.equal(sky.milkyWayVisibility, 1); + 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;