fix(globe): a daylight sky and no daytime stars in the cinematic look

The atmosphere draws physical radiance scaled for an exposure near 10,
while the baked Google tiles need exposure 1, so the noon sky came out
dark navy with the stars showing through. The cinematic look now scales
the atmosphere's radiance-to-luminance factors (sky, aerial perspective,
clouds) by 3, leaving the tiles alone, and shows its stars only from
space or with the sun below -6 degrees at the camera. Gallery
regenerated.
This commit is contained in:
German Khokhlov committed 2026-10-16 14:29:31 +04:00
1 parent bb8c0b83c4
commit abf16ae8af
7 files changed
+430 -23

No files matched your search

+1
View File
@@ -1760,6 +1760,7 @@
"src/globe/cesium/render.js",
"src/globe/cesium/surface.js",
"src/globe/cesium/engineFromViewer.js",
"src/globe/navara/atmosphereLuminance.js",
"src/globe/navara/cameraMath.js",
"src/globe/navara/cameraModel.js",
"src/globe/navara/createNavaraEngine.js",
+61
View File
@@ -0,0 +1,61 @@
// The cinematic look's brighter atmosphere (viewOptions.js
// ATMOSPHERE_LUMINANCE_SCALE): the radiance-to-luminance factors of the
// atmosphere's materials (sky, aerial perspective, clouds), multiplied by a
// scale. Navara 0.2.1 has no public setting for them; they are uniforms of
// the takram materials its descriptors build, reached through each handle's
// public `ref.raw`, then fields of those objects that are not documented:
//
// - sky: ref.raw (the three Mesh) .material.uniforms;
// - aerial perspective: ref.raw (Navara's Effect) .rawEffect.uniforms (a Map);
// - clouds: ref.raw.rawEffect.cloudsPass.currentMaterial.uniforms (the
// clouds swap that material when their quality changes).
//
// Re-check each path before moving to another Navara version (as for
// internals.js). Upstream ask: a sky luminance or atmosphere exposure that
// leaves unlit content (the baked tiles) alone.
export const LUMINANCE_UNIFORMS = Object.freeze([
'SUN_SPECTRAL_RADIANCE_TO_LUMINANCE',
'SKY_SPECTRAL_RADIANCE_TO_LUMINANCE',
]);
const uniformsOf = {
sky: (raw) => raw?.material?.uniforms,
aerialPerspective: (raw) => raw?.rawEffect?.uniforms,
clouds: (raw) => raw?.rawEffect?.cloudsPass?.currentMaterial?.uniforms,
};
const get = (uniforms, name) =>
uniforms instanceof Map ? uniforms.get(name) : uniforms?.[name];
/**
* Scales the atmosphere's luminance factors to `scale` times the values
* their materials were built with. Idempotent: each factor's vector is
* scaled once (Navara 0.2.1 shares one vector between the sky, the aerial
* perspective and the clouds), and a material Navara rebuilds with a new
* vector is scaled when it first shows.
* @returns {(handles: { sky?: object, aerialPerspective?: object, clouds?: object }) => boolean}
* applies the scale; true when it changed a uniform.
*/
export function createAtmosphereLuminance(scale) {
const scaled = new WeakSet();
return (handles) => {
if (scale === 1) return false;
let changed = false;
for (const [name, read] of Object.entries(uniformsOf)) {
const uniforms = read(handles[name]?.ref?.raw);
if (!uniforms) continue;
for (const key of LUMINANCE_UNIFORMS) {
const uniform = get(uniforms, key);
const value = uniform?.value;
if (!value || typeof value.multiplyScalar !== 'function') continue;
// Navara's materials share one vector per factor: scale it once.
if (scaled.has(value)) continue;
value.multiplyScalar(scale);
scaled.add(value);
changed = true;
}
}
return changed;
};
}
+18 -1
View File
@@ -33,6 +33,21 @@ function preservedCanvas(container) {
return canvas;
}
/** The camera's place in degrees (for the sun's elevation), or null. */
function cameraPlace(camera) {
try {
const place = camera.pose().cartographic;
return place
? {
lat: geo.math.toDegrees(place.latitude),
lng: geo.math.toDegrees(place.longitude),
}
: null;
} catch {
return null;
}
}
/** The camera height, or NaN while the camera has no position yet. */
function cameraHeight(camera) {
try {
@@ -118,7 +133,9 @@ export async function createNavaraGlobe({
engine.frames.preRender.addEventListener(() => {
if (!moving && !groundSettled && engine.surface.tilesLoaded())
readGround();
if (look.frame(cameraHeight(camera), moving, ground))
if (
look.frame(cameraHeight(camera), moving, ground, cameraPlace(camera))
)
engine.render.request('navara-look');
}),
];
+37 -8
View File
@@ -1,13 +1,16 @@
// Builds a Navara look (viewOptions.js navaraScenePlan) on an initialised
// view and keeps it current while the view renders: the sun follows the wall
// clock as the HUD's SUN elevation does; the cinematic look lights the globe
// from the atmosphere when seen from space and shows its clouds only for low
// views over low ground. All of it runs only on frames the view draws
// from the atmosphere when seen from space, shows its clouds only for low
// views over low ground, brightens the atmosphere (atmosphereLuminance.js)
// and shows its stars only at night or from space. All of it runs only on frames the view draws
// anyway, so an idle view stays idle.
import { createAtmosphereLuminance } from './atmosphereLuminance.js';
import {
cloudsVisibleAt,
globeLightingAt,
starsVisibleAt,
sunDateStale,
} from './viewOptions.js';
@@ -15,11 +18,12 @@ import {
* @param {object} view an initialised ThreeView with the default plugin
* @param {object} plugin its DefaultPlugin
* @param {ReturnType<import('./viewOptions.js').navaraScenePlan>} plan
* @returns {{ handles: object[], frame: (height: number, moving?: boolean, ground?: number) => boolean }}
* @returns {{ handles: object[], frame: (height: number, moving?: boolean, ground?: number, place?: { lat: number, lng: number } | null) => boolean }}
* `frame` is called before each frame with the camera height, whether the
* camera is moving and the height of the ground in view (metres above the
* ellipsoid, NaN when unknown); it returns true when it changed the scene,
* so the caller draws one more frame.
* camera is moving, the height of the ground in view (metres above the
* ellipsoid, NaN when unknown) and the camera's place (degrees, for the
* sun's elevation there); it returns true when it changed the scene, so
* the caller draws one more frame.
*/
export function buildNavaraLook(view, plugin, plan, now = () => Date.now()) {
const handles = [];
@@ -37,13 +41,26 @@ export function buildNavaraLook(view, plugin, plan, now = () => Date.now()) {
? null
: handles[plan.meshes.length + plan.lights.length + at];
};
const mesh = (name) => {
const at = plan.meshes.findIndex((desc) => Object.hasOwn(desc, name));
return at < 0 ? null : handles[at];
};
const aerial = plan.globeLighting ? effect('aerialPerspective') : null;
const clouds = effect('clouds');
const stars = plan.daylightHidesStars ? mesh('stars') : null;
const atmosphereHandles = {
sky: mesh('sky'),
aerialPerspective: effect('aerialPerspective'),
clouds,
};
const brighten = createAtmosphereLuminance(plan.atmosphereLuminance ?? 1);
let globeLit = false;
let cloudsShown = true;
let starsShown = true;
const frame = (height, moving = false, ground = Number.NaN) => {
let changed = false;
const frame = (height, moving = false, ground = Number.NaN, place = null) => {
// Every drawn frame: a material Navara rebuilds comes back unscaled.
let changed = brighten(atmosphereHandles);
const atmosphere = view.atmosphere;
if (atmosphere && sunDateStale(atmosphere.date.getTime(), now())) {
atmosphere.date = new Date(now());
@@ -75,6 +92,18 @@ export function buildNavaraLook(view, plugin, plan, now = () => Date.now()) {
changed = true;
}
}
if (stars) {
const elevation =
place && typeof atmosphere?.getSunElevation === 'function'
? atmosphere.getSunElevation(place)
: Number.NaN;
const next = starsVisibleAt(height, elevation, starsShown);
if (next !== starsShown) {
starsShown = next;
stars.update({ visible: next });
changed = true;
}
}
return changed;
};
return { handles, frame };
+217 -10
View File
@@ -6,7 +6,12 @@ import { navaraScenePlan } from './viewOptions.js';
function fakeView(dateMs) {
const added = [];
const handle = (kind, desc) => {
const entry = { kind, desc, updates: [], update: (u) => entry.updates.push(u) };
const entry = {
kind,
desc,
updates: [],
update: (u) => entry.updates.push(u),
};
added.push(entry);
return entry;
};
@@ -35,7 +40,12 @@ const plugin = (calls) => ({
test('the photoreal look is the default plugin scene, untouched', () => {
const calls = [];
const view = fakeView(0);
const look = buildNavaraLook(view, plugin(calls), navaraScenePlan('photoreal'), () => 0);
const look = buildNavaraLook(
view,
plugin(calls),
navaraScenePlan('photoreal'),
() => 0,
);
assert.deepEqual(calls, [[]]);
assert.deepEqual(look.handles, [{ id: 'sky' }]);
assert.equal(look.frame(8_000_000), false, 'no globe lighting');
@@ -58,8 +68,14 @@ test('the cinematic look adds its descriptors in order', () => {
test('from space the globe is lit by the atmosphere; near the ground the tiles keep their own light', () => {
const view = fakeView(0);
const look = buildNavaraLook(view, plugin([]), navaraScenePlan('cinematic'), () => 0);
const effect = (name) => view.added.find(({ desc }) => Object.hasOwn(desc, name));
const look = buildNavaraLook(
view,
plugin([]),
navaraScenePlan('cinematic'),
() => 0,
);
const effect = (name) =>
view.added.find(({ desc }) => Object.hasOwn(desc, name));
assert.equal(look.frame(900), false, 'nothing changes at street level');
assert.equal(look.frame(8_000_000), true);
assert.equal(view.lit, true, 'no tile material recompiles');
@@ -76,13 +92,22 @@ test('from space the globe is lit by the atmosphere; near the ground the tiles k
test('the clouds show only below their tops, switching in motion too', () => {
const view = fakeView(0);
const look = buildNavaraLook(view, plugin([]), navaraScenePlan('cinematic'), () => 0);
const look = buildNavaraLook(
view,
plugin([]),
navaraScenePlan('cinematic'),
() => 0,
);
const clouds = view.added.find(({ desc }) => Object.hasOwn(desc, 'clouds'));
assert.equal(look.frame(900, true), false, 'shown at street level');
assert.equal(look.frame(20_000, true), true);
assert.deepEqual(clouds.updates, [{ visible: false }]);
assert.equal(look.frame(3_800, true), false, 'hysteresis');
assert.equal(view.atmosphere.overlay.value, null, 'no ghost of the last clouds');
assert.equal(
view.atmosphere.overlay.value,
null,
'no ghost of the last clouds',
);
assert.equal(view.atmosphere.shadowLength.value, null);
assert.equal(look.frame(3_000, true), true);
assert.deepEqual(clouds.updates.at(-1), { visible: true });
@@ -94,20 +119,34 @@ test('the clouds show only below their tops, switching in motion too', () => {
test('globe lighting switches on only at rest, and off even in motion', () => {
const view = fakeView(0);
const look = buildNavaraLook(view, plugin([]), navaraScenePlan('cinematic'), () => 0);
const aerial = view.added.find(({ desc }) => Object.hasOwn(desc, 'aerialPerspective'));
const look = buildNavaraLook(
view,
plugin([]),
navaraScenePlan('cinematic'),
() => 0,
);
const aerial = view.added.find(({ desc }) =>
Object.hasOwn(desc, 'aerialPerspective'),
);
look.frame(20_000, true); // the clouds hide on the way up
assert.equal(look.frame(2_300_000, true), false, 'a flight through space');
assert.deepEqual(aerial.updates, []);
assert.equal(look.frame(8_000_000, false), true, 'at rest');
assert.equal(look.frame(500_000, true), true, 'coming down');
assert.deepEqual(aerial.updates.at(-1), { aerialPerspective: { irradiance: false } });
assert.deepEqual(aerial.updates.at(-1), {
aerialPerspective: { irradiance: false },
});
});
test('the sun follows the wall clock, a minute at most behind, only on drawn frames', () => {
let now = 1_000_000;
const view = fakeView(now);
const look = buildNavaraLook(view, plugin([]), navaraScenePlan('photoreal'), () => now);
const look = buildNavaraLook(
view,
plugin([]),
navaraScenePlan('photoreal'),
() => now,
);
now += 30_000;
assert.equal(look.frame(900), false);
assert.equal(view.atmosphere.date.getTime(), 1_000_000);
@@ -116,3 +155,171 @@ test('the sun follows the wall clock, a minute at most behind, only on drawn fra
assert.equal(view.atmosphere.date.getTime(), now);
assert.equal(look.frame(900), false);
});
// A uniform as takram's materials hold it: { value: Vector3-like }.
const vec = (x, y, z) => ({
x,
y,
z,
copy(v) {
Object.assign(this, { x: v.x, y: v.y, z: v.z });
return this;
},
multiplyScalar(k) {
this.x *= k;
this.y *= k;
this.z *= k;
return this;
},
clone() {
return vec(this.x, this.y, this.z);
},
});
const luminanceUniforms = () => ({
SUN_SPECTRAL_RADIANCE_TO_LUMINANCE: { value: vec(1.3, 0.9, 0.9) },
SKY_SPECTRAL_RADIANCE_TO_LUMINANCE: { value: vec(1.5, 0.9, 0.8) },
});
/** Gives the cinematic handles the raw objects Navara 0.2.1 builds. */
function withRawAtmosphere(view) {
const find = (name) =>
view.added.find(({ desc }) => Object.hasOwn(desc, name));
const sky = luminanceUniforms();
const aerial = new Map(Object.entries(luminanceUniforms()));
const clouds = luminanceUniforms();
const stars = luminanceUniforms();
find('sky').ref = { raw: { material: { uniforms: sky } } };
find('stars').ref = { raw: { material: { uniforms: stars } } };
find('aerialPerspective').ref = { raw: { rawEffect: { uniforms: aerial } } };
find('clouds').ref = {
raw: {
rawEffect: { cloudsPass: { currentMaterial: { uniforms: clouds } } },
},
};
return { sky, aerial, clouds, stars };
}
test('the cinematic atmosphere is brighter, the stars and tiles are not', () => {
const view = fakeView(0);
const plan = navaraScenePlan('cinematic');
const look = buildNavaraLook(view, plugin([]), plan, () => 0);
const raw = withRawAtmosphere(view);
assert.equal(look.frame(900), true, 'the first frame applies the scale');
const k = plan.atmosphereLuminance;
const sky = (u) => u.SKY_SPECTRAL_RADIANCE_TO_LUMINANCE.value;
const sun = (u) => u.SUN_SPECTRAL_RADIANCE_TO_LUMINANCE.value;
assert.ok(Math.abs(sky(raw.sky).x - 1.5 * k) < 1e-12);
assert.ok(Math.abs(sun(raw.sky).x - 1.3 * k) < 1e-12);
assert.ok(
Math.abs(
raw.aerial.get('SKY_SPECTRAL_RADIANCE_TO_LUMINANCE').value.x - 1.5 * k,
) < 1e-12,
);
assert.ok(Math.abs(sky(raw.clouds).z - 0.8 * k) < 1e-12);
assert.equal(
sky(raw.stars).x,
1.5,
'only the sky, aerial perspective and clouds are walked',
);
assert.equal(look.frame(900), false, 'applied once, not compounded');
assert.ok(Math.abs(sky(raw.sky).x - 1.5 * k) < 1e-12);
// Navara rebuilds a material (a cloud quality change): the new one is scaled.
const fresh = luminanceUniforms();
view.added.find(({ desc }) =>
Object.hasOwn(desc, 'clouds'),
).ref.raw.rawEffect.cloudsPass.currentMaterial = { uniforms: fresh };
assert.equal(look.frame(900), true);
assert.ok(Math.abs(sky(fresh).x - 1.5 * k) < 1e-12);
});
test('a look without the scale and an atmosphere not built yet are left alone', () => {
const view = fakeView(0);
const look = buildNavaraLook(
view,
plugin([]),
navaraScenePlan('minimal'),
() => 0,
);
const sky = luminanceUniforms();
view.added[0].ref = { raw: { material: { uniforms: sky } } };
assert.equal(look.frame(900), false);
assert.equal(sky.SKY_SPECTRAL_RADIANCE_TO_LUMINANCE.value.x, 1.5);
const cinematic = fakeView(0);
const other = buildNavaraLook(
cinematic,
plugin([]),
navaraScenePlan('cinematic'),
() => 0,
);
assert.equal(other.frame(900), false, 'no raw objects yet: nothing to scale');
});
test('the cinematic stars hide in a daylit sky and show at night and from space', () => {
const view = fakeView(0);
let elevation = 40;
view.atmosphere.getSunElevation = () => elevation;
const look = buildNavaraLook(
view,
plugin([]),
navaraScenePlan('cinematic'),
() => 0,
);
const stars = view.added.find(({ desc }) => Object.hasOwn(desc, 'stars'));
const place = { lat: 40.7, lng: -74 };
assert.equal(look.frame(900, false, 0, place), true);
assert.deepEqual(stars.updates, [{ visible: false }]);
assert.equal(look.frame(900, false, 0, place), false);
elevation = -10;
assert.equal(look.frame(900, false, 0, place), true);
assert.deepEqual(stars.updates.at(-1), { visible: true });
elevation = 40;
look.frame(900, false, 0, place);
assert.equal(look.frame(8_000_000, false, 0, place), true, 'from space');
assert.deepEqual(stars.updates.at(-1), { visible: true });
const count = stars.updates.length;
look.frame(900, false, 0);
assert.equal(stars.updates.length, count, 'no place keeps them');
assert.equal(look.frame(900, false, 0, place), true, 'back down by day');
assert.deepEqual(stars.updates.at(-1), { visible: false });
});
test('the minimal look keeps its stars whatever the sun', () => {
const view = fakeView(0);
view.atmosphere.getSunElevation = () => 40;
const look = buildNavaraLook(
view,
plugin([]),
navaraScenePlan('minimal'),
() => 0,
);
look.frame(900, false, 0, { lat: 0, lng: 0 });
assert.deepEqual(view.added[1].updates, []);
});
test('a factor vector the materials share is scaled once', () => {
const view = fakeView(0);
const plan = navaraScenePlan('cinematic');
const look = buildNavaraLook(view, plugin([]), plan, () => 0);
const shared = luminanceUniforms();
const find = (name) =>
view.added.find(({ desc }) => Object.hasOwn(desc, name));
find('sky').ref = { raw: { material: { uniforms: shared } } };
find('aerialPerspective').ref = {
raw: { rawEffect: { uniforms: new Map(Object.entries(shared)) } },
};
find('clouds').ref = {
raw: {
rawEffect: {
cloudsPass: { currentMaterial: { uniforms: { ...shared } } },
},
},
};
look.frame(900);
look.frame(900);
assert.ok(
Math.abs(
shared.SKY_SPECTRAL_RADIANCE_TO_LUMINANCE.value.x -
1.5 * plan.atmosphereLuminance,
) < 1e-12,
);
});
+54 -3
View File
@@ -94,6 +94,41 @@ export function cloudsVisibleAt(height, ground, current = true) {
: height < CLOUDS_SHOW_BELOW_M && low < CLOUDS_SHOW_OVER_GROUND_M;
}
// Navara's sky, clouds and haze come from the atmosphere model in physical
// radiance, scaled for a tone-mapping exposure near 10, while the Google
// tiles carry baked colours that need exposure 1. At exposure 1 the noon sky
// came out dark navy (sRGB about 39, 58, 90 at Manhattan facing the
// horizon), and the stars showed through it. The cinematic look multiplies
// the atmosphere's radiance-to-luminance factors (sky, clouds, aerial
// perspective; Navara's stars share them) by this scale instead of raising
// the exposure, so the sky turns daylight blue and the tiles keep their
// colours.
export const ATMOSPHERE_LUMINANCE_SCALE = 3;
const STAR_INTENSITY = 40;
// Stars show from space or once the sun is well below the horizon at the
// camera (civil twilight), and hide in a daylit sky. Gaps keep a camera
// near a limit from flickering them.
export const STARS_SHOW_SUN_BELOW_DEG = -6;
export const STARS_HIDE_SUN_ABOVE_DEG = -4;
export const STARS_SPACE_ENTER_M = 100_000;
export const STARS_SPACE_EXIT_M = 80_000;
/**
* Whether the cinematic stars show for a camera at `height` (metres above
* the ellipsoid) with the sun `sunElevation` degrees above its horizon; an
* unknown height or sun elevation keeps `current`.
*/
export function starsVisibleAt(height, sunElevation, current = true) {
if (!Number.isFinite(height)) return current;
if (current ? height > STARS_SPACE_EXIT_M : height >= STARS_SPACE_ENTER_M)
return true;
if (!Number.isFinite(sunElevation)) return current;
return current
? sunElevation <= STARS_HIDE_SUN_ABOVE_DEG
: sunElevation < STARS_SHOW_SUN_BELOW_DEG;
}
/**
* Whether the atmosphere's sun date has drifted from the wall clock (the
* HUD's SUN elevation uses `new Date()`) by more than a minute.
@@ -105,8 +140,17 @@ export function sunDateStale(atmosphereMs, nowMs, toleranceMs = 60_000) {
const cinematic = (mobile) => ({
pluginDefault: false,
// Brighter, larger stars (defaults 10 and 1 px) so the night sky and the
// globe from space keep a visible starfield next to a sunlit Earth.
meshes: [{ sky: {} }, { stars: { intensity: 40, pointSize: 2 } }],
// globe from space keep a visible starfield next to a sunlit Earth: 40,
// divided by the atmosphere scale the stars share.
meshes: [
{ sky: {} },
{
stars: {
intensity: STAR_INTENSITY / ATMOSPHERE_LUMINANCE_SCALE,
pointSize: 2,
},
},
],
// The tiles carry baked sunlight, so the sun casts no shadow maps (CSM
// would cost every frame and darken nothing the tiles show).
lights: [{ skyLightProbe: {} }, { sun: { castShadow: false } }],
@@ -140,6 +184,8 @@ const cinematic = (mobile) => ({
mobile ? { fxaa: {} } : { smaa: {} },
],
globeLighting: true,
atmosphereLuminance: ATMOSPHERE_LUMINANCE_SCALE,
daylightHidesStars: true,
});
const PLANS = {
@@ -149,6 +195,8 @@ const PLANS = {
lights: [{ skyLightProbe: {} }, { sun: {} }],
effects: [],
globeLighting: false,
atmosphereLuminance: 1,
daylightHidesStars: false,
}),
photoreal: () => ({
pluginDefault: true,
@@ -156,6 +204,8 @@ const PLANS = {
lights: [],
effects: [],
globeLighting: false,
atmosphereLuminance: 1,
daylightHidesStars: false,
}),
cinematic,
};
@@ -168,7 +218,8 @@ const PLANS = {
* - `cinematic` (the app's default): photoreal plus volumetric clouds for
* low views over low ground (cloudsVisibleAt), ACES tone mapping, brighter stars, no sun shadow
* maps, and atmosphere lighting of the globe from space (`globeLighting`,
* see globeLightingAt). On a mobile-optimized
* see globeLightingAt), a brighter atmosphere (ATMOSPHERE_LUMINANCE_SCALE)
* and stars only at night or from space (starsVisibleAt). On a mobile-optimized
* view it drops the clouds and the lens flare and uses FXAA.
*/
export function navaraScenePlan(
+42 -1
View File
@@ -16,6 +16,12 @@ import {
navaraViewOptions,
resolveNavaraLook,
sunDateStale,
ATMOSPHERE_LUMINANCE_SCALE,
STARS_HIDE_SUN_ABOVE_DEG,
STARS_SHOW_SUN_BELOW_DEG,
STARS_SPACE_ENTER_M,
STARS_SPACE_EXIT_M,
starsVisibleAt,
} from './viewOptions.js';
test('the product view renders on demand with MSAA 4 at the native pixel ratio', () => {
@@ -55,6 +61,8 @@ test('cinematic is the default look; photoreal is the default plugin scene; mini
lights: [],
effects: [],
globeLighting: false,
atmosphereLuminance: 1,
daylightHidesStars: false,
});
assert.deepEqual(navaraScenePlan('minimal'), {
pluginDefault: false,
@@ -62,6 +70,8 @@ test('cinematic is the default look; photoreal is the default plugin scene; mini
lights: [{ skyLightProbe: {} }, { sun: {} }],
effects: [],
globeLighting: false,
atmosphereLuminance: 1,
daylightHidesStars: false,
});
assert.throws(() => navaraScenePlan('cine'), RangeError);
assert.throws(() => navaraScenePlan('toString'), RangeError);
@@ -74,7 +84,7 @@ test('the cinematic look: photoreal plus clouds and ACES, no sun shadow maps, no
assert.equal(plan.pluginDefault, false);
assert.deepEqual(plan.meshes, [
{ sky: {} },
{ stars: { intensity: 40, pointSize: 2 } },
{ stars: { intensity: 40 / ATMOSPHERE_LUMINANCE_SCALE, pointSize: 2 } },
]);
assert.deepEqual(plan.lights, [
{ skyLightProbe: {} },
@@ -173,3 +183,34 @@ test('the sun date is stale once it drifts a minute from the clock', () => {
assert.equal(sunDateStale(120_000, 0), true, 'either direction');
assert.equal(sunDateStale(Number.NaN, 0), true);
});
test('the cinematic look brightens the atmosphere, not the tiles, and hides stars by day', () => {
const plan = navaraScenePlan('cinematic');
assert.equal(plan.atmosphereLuminance, ATMOSPHERE_LUMINANCE_SCALE);
assert.ok(ATMOSPHERE_LUMINANCE_SCALE > 1);
assert.equal(plan.daylightHidesStars, true);
for (const look of ['minimal', 'photoreal']) {
assert.equal(navaraScenePlan(look).atmosphereLuminance, 1, look);
assert.equal(navaraScenePlan(look).daylightHidesStars, false, look);
}
});
test('stars show at night and from space, not in a daylit sky, with hysteresis', () => {
assert.equal(starsVisibleAt(900, 40, true), false, 'noon');
assert.equal(starsVisibleAt(900, 40, false), false);
assert.equal(starsVisibleAt(900, STARS_SHOW_SUN_BELOW_DEG - 1, false), true);
assert.equal(starsVisibleAt(900, -5, false), false, 'inside the gap');
assert.equal(starsVisibleAt(900, -5, true), true, 'inside the gap');
assert.equal(starsVisibleAt(900, STARS_HIDE_SUN_ABOVE_DEG + 1, true), false);
assert.equal(starsVisibleAt(STARS_SPACE_ENTER_M, 40, false), true, 'space');
assert.equal(starsVisibleAt(90_000, 40, true), true, 'inside the gap');
assert.equal(starsVisibleAt(STARS_SPACE_EXIT_M - 1, 40, true), false);
assert.equal(starsVisibleAt(900, Number.NaN, true), true, 'unknown keeps it');
assert.equal(
starsVisibleAt(Number.NaN, 40, false),
false,
'unknown keeps it',
);
assert.ok(STARS_SHOW_SUN_BELOW_DEG < STARS_HIDE_SUN_ABOVE_DEG);
assert.ok(STARS_SPACE_EXIT_M < STARS_SPACE_ENTER_M);
});