Anchor Navara inspection sunlight to the restored view

This commit is contained in:
German Khokhlov committed 2026-11-02 17:26:34 +04:00
1 parent 0d15e80e28
commit 3a2a53aef9
5 files changed
+80 -12

No files matched your search

+5
View File
@@ -199,6 +199,7 @@ export async function createNavaraScene({
const { engine } = globe; const { engine } = globe;
navaraViews.set(engine, { navaraViews.set(engine, {
view: globe.view, view: globe.view,
alignDaylightToRestoredView: globe.alignDaylightToRestoredView,
MeshDesc: globe.MeshDesc, MeshDesc: globe.MeshDesc,
EffectDesc: globe.EffectDesc, EffectDesc: globe.EffectDesc,
NavaraEffect: globe.NavaraEffect, NavaraEffect: globe.NavaraEffect,
@@ -264,6 +265,10 @@ export function createViewerlessTools({
defer, defer,
}) { }) {
const { dataManager } = data; const { dataManager } = data;
void styleManager.initialRestorePromise?.then(() => {
if (!signal.aborted)
navaraViews.get(engine)?.alignDaylightToRestoredView?.();
});
const recentImagery = dataManager.layers.get('recent-imagery')?.module; const recentImagery = dataManager.layers.get('recent-imagery')?.module;
if (recentImagery) { if (recentImagery) {
const tool = initNavaraImageryBoxTool({ const tool = initNavaraImageryBoxTool({
+9 -7
View File
@@ -109,6 +109,7 @@ export async function createNavaraGlobe({
let view; let view;
let engine; let engine;
let overlay = null; let overlay = null;
let look;
let removeLook = null; let removeLook = null;
let removePostSampling = null; let removePostSampling = null;
try { try {
@@ -123,13 +124,9 @@ export async function createNavaraGlobe({
}); });
// The sun follows the engine clock (one time source for layers and the // The sun follows the engine clock (one time source for layers and the
// look); the look only runs in preRender, after the engine exists. // look); the look only runs in preRender, after the engine exists.
const look = buildNavaraLook( look = buildNavaraLook(view, plugin, plan, () => engine.clock.frameTime(), {
view, forEachLuminanceUniform,
plugin, });
plan,
() => engine.clock.frameTime(),
{ forEachLuminanceUniform },
);
// The look's shadowed sun leaked a light per frame (internals.js). // The look's shadowed sun leaked a light per frame (internals.js).
keepCascadedLightIds(view); keepCascadedLightIds(view);
removePostSampling = createPostSampling(view); removePostSampling = createPostSampling(view);
@@ -213,6 +210,11 @@ export async function createNavaraGlobe({
NavaraEffect, NavaraEffect,
DrapedMesh, DrapedMesh,
setupMaterialForDrape, setupMaterialForDrape,
// Startup can fly from the engine's placeholder camera into a shared view.
alignDaylightToRestoredView() {
look.resetDaylight();
engine.render.request('restored-daylight');
},
setRenderLoop: (enabled) => setRenderLoopEnabled(view, enabled), setRenderLoop: (enabled) => setRenderLoopEnabled(view, enabled),
destroy() { destroy() {
if (destroyed) return; if (destroyed) return;
+34 -3
View File
@@ -86,6 +86,9 @@ export function buildNavaraLook(
let cloudsShown = true; let cloudsShown = true;
let cloudsSunlit = true; let cloudsSunlit = true;
let landscape = false; let landscape = false;
let daylightTime = null;
let fillLight;
let fill = 0;
const tone = effect('toneMapping'); const tone = effect('toneMapping');
let starsShown = true; let starsShown = true;
@@ -100,13 +103,35 @@ export function buildNavaraLook(
let changed = brighten(!landscapeRequested); let changed = brighten(!landscapeRequested);
if (landscapeRequested !== landscape && tone) { if (landscapeRequested !== landscape && tone) {
landscape = landscapeRequested; landscape = landscapeRequested;
daylightTime = null;
view.toneMappingExposure = landscape ? 10 : 1; view.toneMappingExposure = landscape ? 10 : 1;
tone.update({ toneMapping: { mode: landscape ? AGX : ACES_FILMIC } }); tone.update({ toneMapping: { mode: landscape ? AGX : ACES_FILMIC } });
changed = true; changed = true;
} }
// In orbit, retain some of the underlying imagery in the atmospheric
// shadow. Fade up with altitude so ground views keep their original light.
const nextFill =
landscape && Number.isFinite(height)
? Math.min(1, Math.max(0, (height - 50_000) / 250_000))
: 0;
if (nextFill !== fill) {
fill = nextFill;
if (fillLight) fillLight.update({ ambient: { intensity: fill } });
else fillLight = view.addLight({ ambient: { intensity: fill } });
changed = true;
}
const opacity = aerial?.ref?.raw?.rawEffect?.blendMode.opacity;
const strength = 1 - 0.2 * fill;
if (opacity && opacity.value !== strength) {
opacity.value = strength;
changed = true;
}
const atmosphere = view.atmosphere; const atmosphere = view.atmosphere;
const time = // Choose inspection daylight once. Rotating the camera must not rotate
landscape && place ? landscapeSunTime(now(), place.lng) : now(); // the terminator across the geography.
if (landscape && place && daylightTime === null)
daylightTime = landscapeSunTime(now(), place.lng);
const time = daylightTime ?? now();
if (atmosphere && sunDateStale(atmosphere.date.getTime(), time)) { if (atmosphere && sunDateStale(atmosphere.date.getTime(), time)) {
atmosphere.date = new Date(time); atmosphere.date = new Date(time);
changed = true; changed = true;
@@ -170,7 +195,13 @@ export function buildNavaraLook(
} }
return changed; return changed;
}; };
return { handles, frame }; return {
handles,
frame,
resetDaylight() {
daylightTime = null;
},
};
} }
const IRRADIANCE_ON = Object.freeze({ const IRRADIANCE_ON = Object.freeze({
+30
View File
@@ -167,20 +167,50 @@ test('daylight inspection restores live exposure, tone mapping and time when lef
const tone = view.added.find(({ desc }) => const tone = view.added.find(({ desc }) =>
Object.hasOwn(desc, 'toneMapping'), Object.hasOwn(desc, 'toneMapping'),
); );
const aerial = view.added.find(({ desc }) =>
Object.hasOwn(desc, 'aerialPerspective'),
);
const opacity = { value: 1 };
aerial.ref = { raw: { rawEffect: { blendMode: { opacity } } } };
const fuji = { lat: 35.2599, lng: 138.6574 }; const fuji = { lat: 35.2599, lng: 138.6574 };
look.frame(6508, false, 2600, fuji, true); look.frame(6508, false, 2600, fuji, true);
assert.equal(view.toneMappingExposure, 10); assert.equal(view.toneMappingExposure, 10);
assert.equal(opacity.value, 1);
assert.equal(tone.updates.at(-1).toneMapping.mode, 7); assert.equal(tone.updates.at(-1).toneMapping.mode, 7);
assert.ok( assert.ok(
Math.abs(view.atmosphere.date.getTime() / 3_600_000 + fuji.lng / 15 - 13) < Math.abs(view.atmosphere.date.getTime() / 3_600_000 + fuji.lng / 15 - 13) <
1e-6, 1e-6,
); );
look.frame(750_149, false, 0, fuji, true);
assert.ok(opacity.value < 1);
const ambient = view.added.find(({ desc }) => Object.hasOwn(desc, 'ambient'));
assert.equal(ambient.desc.ambient.intensity, 1);
look.frame(6508, false, 2600, fuji, false); look.frame(6508, false, 2600, fuji, false);
assert.equal(ambient.updates.at(-1).ambient.intensity, 0);
assert.equal(view.toneMappingExposure, 1); assert.equal(view.toneMappingExposure, 1);
assert.equal(opacity.value, 1);
assert.equal(tone.updates.at(-1).toneMapping.mode, 6); assert.equal(tone.updates.at(-1).toneMapping.mode, 6);
assert.equal(view.atmosphere.date.getTime(), 0); assert.equal(view.atmosphere.date.getTime(), 0);
}); });
test('inspection sunlight stays fixed through orbit, zoom and move end', () => {
const view = fakeView(0);
const look = warmCinematic(view);
const origin = { lat: -45.9847, lng: 67.8795 };
const rotated = { lat: -20, lng: -158 };
look.frame(50_151_111, false, 0, origin, true);
const sunDate = view.atmosphere.date;
look.frame(50_151_111, true, 0, rotated, true);
look.frame(750_149, false, 0, rotated, true);
assert.equal(view.atmosphere.date, sunDate);
look.resetDaylight();
look.frame(750_149, false, 0, rotated, true);
assert.notEqual(view.atmosphere.date.getTime(), sunDate.getTime());
const restored = view.atmosphere.date;
look.frame(50_151_111, false, 0, origin, true);
assert.equal(view.atmosphere.date, restored);
});
test('the sun follows the wall clock, a minute at most behind, only on drawn frames', () => { test('the sun follows the wall clock, a minute at most behind, only on drawn frames', () => {
let now = 1_000_000; let now = 1_000_000;
const view = fakeView(now); const view = fakeView(now);
+2 -2
View File
@@ -52,8 +52,8 @@ export function resolveNavaraLook(search = '') {
export const ACES_FILMIC = 6; export const ACES_FILMIC = 6;
export const AGX = 7; export const AGX = 7;
// The Sentinel-2 landscape uses early afternoon at the camera longitude, // Pick early afternoon at the starting landscape longitude. The look keeps
// independently of the telemetry clock. Only the displayed sunlight moves. // this timestamp while the user explores; telemetry uses its own clock.
export function landscapeSunTime(now, longitude) { export function landscapeSunTime(now, longitude) {
const hour = 3_600_000; const hour = 3_600_000;
const day = 24 * hour; const day = 24 * hour;