fix(sky): keep the Milky Way out of model lighting, test the sky wiring

Scene reads skyBox._cubeMap as the environment map that lights models, in
the TEME, flipY layout of Cesium's own sky box; the ICRF Milky Way would
light them rotated. Rename the field so models keep Cesium's default.

Test the anime fade on a real StarrySky and that the viewer gets the sky
as an option (skyBox: false would drop the Sun and Moon). Correct three
comments on the star limit and the Milky Way levels.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
German KhokhlovandClaude Opus 5.5 committed 2026-10-20 13:37:43 +04:00
1 parent 86ae5e9cf4
commit d94cc35c81
7 files changed
+54 -17

No files matched your search

+5 -1
View File
@@ -31,7 +31,9 @@
* celestial pole — rotating each direction into galactic coordinates * celestial pole — rotating each direction into galactic coordinates
* with the IAU J2000 matrix. * with the IAU J2000 matrix.
* 6. Map the median sky to black, easing in with a soft knee, and the * 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. * encode with gamma 1/2.2 and ±0.5 LSB dither against banding; WebP.
* *
* Usage: * Usage:
@@ -373,6 +375,8 @@ async function main() {
for (const plane of planes) blurPlane(plane, width, height, sigmaPx); for (const plane of planes) blurPlane(plane, width, height, sigmaPx);
for (const plane of wide) for (const plane of wide)
blurPlane(plane, width, height, (FAINT_BLUR_DEG / 180) * height); 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 black = luminancePercentile(planes, BLACK_PERCENTILE);
const white = luminancePercentile(planes, WHITE_PERCENTILE); const white = luminancePercentile(planes, WHITE_PERCENTILE);
blendByBrightness(planes, wide, (luminance) => blendByBrightness(planes, wide, (luminance) =>
+11
View File
@@ -219,6 +219,17 @@ test('the separate viewer export imports without constructing a browser viewer',
assert.throws(() => createApplicationViewer({}), /containers are required/); 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 } = {}) { function pinchFixture({ zoomEventTypes } = {}) {
const handlers = new Set(); const handlers = new Set();
const relayed = []; const relayed = [];
+14 -6
View File
@@ -103,11 +103,9 @@ export function installTrackpadPinchZoom(
}; };
} }
/** Create the standard globe viewer in caller-owned, visible containers. */ /** Cesium.Viewer options of the standard globe viewer. */
export function createApplicationViewer({ container, creditContainer }) { export function applicationViewerOptions(creditContainer) {
if (!container || !creditContainer) return {
throw new TypeError('Viewer and credit containers are required');
const viewer = new Cesium.Viewer(container, {
timeline: false, timeline: false,
animation: false, animation: false,
baseLayerPicker: false, baseLayerPicker: false,
@@ -127,7 +125,17 @@ export function createApplicationViewer({ container, creditContainer }) {
creditContainer, creditContainer,
msaaSamples: 4, msaaSamples: 4,
contextOptions: { webgl: { preserveDrawingBuffer: true } }, 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 { try {
viewer.targetFrameRate = 60; viewer.targetFrameRate = 60;
// Before any tile builds a draw command: Cesium's per-vertex model // Before any tile builds a draw command: Cesium's per-vertex model
+7 -4
View File
@@ -180,7 +180,10 @@ export class StarrySky {
this._loading = false; this._loading = false;
this._starData = undefined; this._starData = undefined;
this._faceImages = 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._milkyWayCommand = undefined;
this._milkyWayAttributes = undefined; this._milkyWayAttributes = undefined;
this._starCommand = undefined; this._starCommand = undefined;
@@ -191,7 +194,7 @@ export class StarrySky {
}; };
this._milkyWayUniforms = { this._milkyWayUniforms = {
...uniforms, ...uniforms,
u_milkyWay: () => this._cubeMap, u_milkyWay: () => this._milkyWayCubeMap,
u_intensity: () => this.milkyWayIntensity * this.milkyWayVisibility, u_intensity: () => this.milkyWayIntensity * this.milkyWayVisibility,
}; };
this._starUniforms = { this._starUniforms = {
@@ -301,7 +304,7 @@ export class StarrySky {
cubeMap.sampler = new Cesium.Sampler({ cubeMap.sampler = new Cesium.Sampler({
minificationFilter: Cesium.TextureMinificationFilter.LINEAR_MIPMAP_LINEAR, minificationFilter: Cesium.TextureMinificationFilter.LINEAR_MIPMAP_LINEAR,
}); });
this._cubeMap = cubeMap; this._milkyWayCubeMap = cubeMap;
this._faceImages = undefined; this._faceImages = undefined;
const geometry = Cesium.BoxGeometry.createGeometry( const geometry = Cesium.BoxGeometry.createGeometry(
Cesium.BoxGeometry.fromDimensions({ Cesium.BoxGeometry.fromDimensions({
@@ -440,7 +443,7 @@ export class StarrySky {
command.vertexArray?.destroy(); command.vertexArray?.destroy();
command.shaderProgram?.destroy(); command.shaderProgram?.destroy();
} }
this._cubeMap = this._cubeMap?.destroy(); this._milkyWayCubeMap = this._milkyWayCubeMap?.destroy();
this._milkyWayCommand = undefined; this._milkyWayCommand = undefined;
this._starCommand = undefined; this._starCommand = undefined;
this._commands.length = 0; this._commands.length = 0;
+8
View File
@@ -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); 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();
});
+1 -1
View File
@@ -13,7 +13,7 @@
* @module sky/stars * @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; export const STAR_LIMIT_MAGNITUDE = 6.5;
/** Gaussian σ of the faintest star, CSS pixels. */ /** Gaussian σ of the faintest star, CSS pixels. */
export const STAR_MIN_SIGMA_PX = 0.55; export const STAR_MIN_SIGMA_PX = 0.55;
+8 -5
View File
@@ -1,6 +1,7 @@
import assert from 'node:assert/strict'; import assert from 'node:assert/strict';
import test from 'node:test'; import test from 'node:test';
import { VisualEffects } from './visualEffects.js'; import { VisualEffects } from './visualEffects.js';
import { MILKY_WAY_INTENSITY, StarrySky } from '../sky/starrySky.js';
import { import {
GLOBAL_POST_DEFAULTS, GLOBAL_POST_DEFAULTS,
STYLE_PRESET_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', () => { test('the anime stage fades the Milky Way out with its own intensity', () => {
const f = fixture(); 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.viewer.scene.skyBox = sky;
f.effects.initStyles(); f.effects.initStyles();
f.effects.setStageIntensity(f.effects.stages.thermal, 1); 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); 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); 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); 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. // A sky box without the knob (Cesium's own) is left alone.
const plain = {}; const plain = {};
f.effects.viewer.scene.skyBox = plain; f.effects.viewer.scene.skyBox = plain;