orbit-studio / src /modules /createViewer.ts
moncefem's picture
Deploy Orbit Studio propagator
9f21d0a
Raw
History Blame Contribute Delete
5.13 kB
// The one module that names a DOM container and asks for a WebGL context.
//
// Split out of CesiumController's constructor, which used to do this itself
// against a hardcoded element id. Nothing else in the app constructs a Viewer,
// so this is the only file that cannot run outside a browser — and everything
// downstream of it takes the viewer as an argument instead of making one.
import { Tonemapper } from "@cesium/engine";
import { Viewer } from "@cesium/widgets";
/**
* A viewer with this app's widget selection and scene defaults already applied.
*
* No `animation` and no `timeline` on any device: the clock deck replaces both
* (`ClockDeck.vue`), and neither widget fits a phone's width.
*
* `minimalUI` is the caller's answer, not this module's, and decides only the
* fullscreen button: nothing to do in an iframe, no meaning on iOS. Read once
* (`DeviceDetect.minimalUI`) rather than re-derived per consumer.
*/
export function createViewer(container: string | Element, options: { minimalUI: boolean }): Viewer {
const { minimalUI } = options;
const viewer = new Viewer(container, {
animation: false,
// No base layer here: the store's layer stack is the only default, and it
// arrives through sceneSync's immediate watcher a tick later. Naming one here
// as well meant two defaults that could drift.
baseLayer: false,
baseLayerPicker: false,
fullscreenButton: !minimalUI,
fullscreenElement: document.body,
geocoder: false,
homeButton: false,
infoBox: false,
navigationHelpButton: false,
navigationInstructionsInitiallyVisible: false,
sceneModePicker: false,
selectionIndicator: false,
timeline: false,
contextOptions: {
webgl: {
alpha: true,
},
},
});
// Cesium's own wording for the lightbox link is "Data attribution", which is 28 px
// of a credit line the clock deck has to share the bottom row with — and the two
// breakpoints that decide where that line goes are measured off its width
// (`useClockDeckChrome`, and the corner rule in main.css). "Attribution" says the
// same thing about the same lightbox.
//
// Written once, because CreditDisplay sets this text in its constructor and never
// rewrites it: only the credit list beside it is rebuilt as credits come and go.
const expandLink = viewer.container.querySelector(".cesium-credit-expand-link");
if (expandLink) {
expandLink.textContent = "Attribution";
}
viewer.clock.shouldAnimate = true;
viewer.scene.globe.enableLighting = true;
viewer.scene.highDynamicRange = true;
viewer.scene.maximumRenderTimeChange = 1 / 30;
viewer.scene.requestRenderMode = true;
// Away from Cesium's PBR_NEUTRAL default, because that curve is wrong for a
// scene this dark.
//
// Its black-point term subtracts min(r,g,b) from every channel while that
// minimum is under linear 0.08 (see czm_pbrNeutralTonemapping). On a near-black
// pixel the minimum is nearly the whole signal, so the subtraction strips the
// achromatic part and leaves only the channel imbalance — which czm_inverseGamma
// then stretches. A neutral sRGB grey of (18,17,16) comes out around (9,7,2).
//
// Measured, not assumed. It renders the faint sky as olive mottling at any star
// map resolution (src/config/starMaps.ts), and it is not confined to the sky:
// over Manhattan with OSM Buildings, 37% of the frame sits under luminance 40,
// and there it claims a mean saturation of 0.598 against ACES's 0.260. Shadowed
// building faces get the same treatment as the sky.
//
// ACES costs about 5% luminance and 6% saturation on properly lit surfaces, and
// agrees with PBR_NEUTRAL to within 6% in the 80-140 luminance band, with a mean
// absolute channel delta under 11 everywhere — the globe and the satellite models
// are visually unchanged. Same pass count either way, so no frame-time change.
viewer.scene.postProcessStages.tonemapper = Tonemapper.ACES;
// Otherwise render-on-demand and Cesium's "wait for the data sources" rule
// deadlock each other.
//
// A static geometry primitive is built asynchronously and only advances a
// state per render. While it is unfinished `DataSourceDisplay.update` reports
// false, and by default the Viewer answers that by clearing `clock.canAnimate`
// — so simulation time stops. But `maximumRenderTimeChange` above is what asks
// for the next frame, and it asks based on simulation time having moved. Time
// cannot move until the primitive is ready, the primitive cannot become ready
// without frames, and no frame is requested: the clock stops dead the first
// time a ground-track corridor is switched on and never restarts.
//
// Nothing here needs the guarantee it gives up. Suspending animation exists so
// a CZML clip does not run ahead of geometry still streaming in; every
// geometry in this app is generated locally from a propagator, and a swath
// corridor arriving a frame late is not worth stopping time over.
viewer.allowDataSourcesToSuspendAnimation = false;
return viewer;
}