@poopdeck.gl/cesium
A CesiumJS backend for SpatioTemporal Tiles archives. It renders STT on a real WGS84 globe using CesiumJS's own scene, camera, and picking — no deck.gl or MapLibre dependency, and no Cesium ion access token (CesiumJS itself is Apache-2.0; nothing here talks to ion).
The package is intentionally small. It is the first green-field backend built
against the shared render kernel in @poopdeck.gl/core — every layer is an
SttRenderNode plus a BackendDescriptor that declares what it supports, a
ViewState⇄Cesium camera bridge, and a render-loop clock hook. Positions,
categorical colour, time-filter alpha, trip interpolation, and OD endpoint
derivation are not reimplemented here — they come straight from
@poopdeck.gl/core/{geo,style,time-filter,trips,geometry}, the same modules
the deck.gl, Three.js, and MapLibre backends use.
It renders all 23 frozen LayerKinds natively — the movement catalog,
core geometry, the AV kinds, the summary tiers and the flow family alike;
cesiumBackend.layerKinds declares no fallback and no unsupported kind. The
deviations that remain are behavioural, not catalog gaps — see
Limitations.
Install#
@poopdeck.gl/cesium is not published to npm. The package is
private: true in the workspace and there is no registry release to install;
consume it from a checkout of this repo ("@poopdeck.gl/cesium": "workspace:*"), as examples/showcase does.
cesium is a peer dependency, pinned ^1 (developed and tested against
1.144.0). Rendering STT tiles needs no Cesium ion access token, but CesiumJS
still needs its static asset bundle (workers, widget CSS) reachable at
runtime — point window.CESIUM_BASE_URL at the npm package's
Build/Cesium/ output or a CDN copy before constructing a Viewer:
window.CESIUM_BASE_URL ='https://cdn.jsdelivr.net/npm/cesium@1.144.0/Build/Cesium/';
Exports#
Every layer class carries the STT prefix, matching @poopdeck.gl/maplibre
and @poopdeck.gl/three, so one layer kind has one spelling on every backend
and the import path (not a word inside the symbol) says which renderer you are
on. The camera and clock bridges (viewStateToCesiumView, attachCesiumClock,
CesiumView) are named after CesiumJS concepts, not STT layer kinds. The class
rows below run in family order — core geometry, motion, AV, summary tiers,
flow — and cover all 23 LayerKinds between them.
| Export | Kind | Description |
|---|---|---|
STTPointLayer | class | The point SttRenderNode — builds a PointPrimitiveCollection from decoded tiles and drives per-point alpha off the shared time-filter oracle |
STTPathLayer | class | Animated LineStrings (path and OD line — an OD line is a 2-vertex LineString); batched Primitive + per-instance colour animation |
STTArcLayer | class | OD flow arcs — endpoints via the kernel's deriveSourceTargetPositions, swept into raised great-circle polylines (same parametrization as three's globe arc material) |
STTTripsLayer | class | Vehicle trails — per-frame CPU trail trim (core/trips trimTrail) into a PolylineCollection, arc-length tail fade material |
STTTripHeadsLayer | class | Moving head-dots — per-frame sampleHead interpolation (core/trips) onto PointPrimitives |
STTPolygonLayer | class | Filled — optionally extruded — polygon areas on the ellipsoid; per-feature window fade, height from a numeric column |
STTIconLayer | class | Billboard sprites cut from one caller-supplied atlas texture (icon), per-feature heading/size/tint |
STTColumnLayer | class | One extruded n-sided prism per point (column), radius in true metres; carries the space-time-cube lift (lib/columns.ts timeHeightLiftMeters) |
STTMeshLayer | class | One posed glTF model per tracked object at the playhead (mesh) — the thing the detection cuboid is around |
STTIsoLayer | class | Iso-contour polylines (isoLines) with a level ramp over STTPathLayer's polyline machinery |
STTBoundingBoxLayer | class | One oriented 3-D cuboid per tracked object at the playhead (boundingBox), interpolated between keyframes |
STTPointCloudLayer | class | Lit 3-D point clouds (pointCloud) — per-point elevation plus an optional surface normal (lambertShade) |
STTSurfelLayer | class | Oriented anisotropic surface elements (surfel) from baked quaternion/scale/rgba columns |
STTTextLayer | class | One screen-space Label per feature (text), anchored at the feature's absolute f64 ECEF position |
STTEgoLayer | class | The single ego-vehicle cuboid + its full trajectory (ego), sampled from a one-track pose stream |
STTH3SummaryLayer | class | Summary-tier H3 cells (h3Summary) as ramp-coloured, optionally extruded plates; takes an injected cellToBoundary |
STTQuadbinSummaryLayer | class | Summary-tier CARTO Quadbin cells (quadbinSummary), same ramp/extrusion path as H3 |
STTHexbinLayer | class | Runtime hexbin (hexbin) over the raw point tier — binned in the browser over the playhead's slice |
STTHeatmapLayer | class | A geodetic RectangleGeometry textured with a CPU-computed density raster (heatmap; see lib/heatmap-field.ts) |
STTFlowCorridorLayer | class | Static route network whose per-segment colour breathes off a per-vertex x per-bucket volume matrix (flowCorridor) |
STTFlowStrokeLayer | class | The twin-ribbon sibling of the corridor — the same matrix drives WIDTH rather than colour (flowStroke) |
STTFlowmapLayer | class | flowmap.gl-style tapered OD arrows with heads (flowmap); runtime KDEEB bundling via core's shared bundleEdges |
STTBatchedPolylineLayer | class | The shared batched-Primitive machinery behind the path/arc layers (advanced use) |
buildPathPolylines / buildArcPolylines / sampleGreatCircleArc / lineStringTimeOrigin | functions | The pure (Cesium-free, unit-tested) geometry builders behind the polyline layers (lineStringTimeOrigin = their shared scene-wide time origin) |
buildPointEntries / collectPointLayers | functions | The pure (Cesium-free, unit-tested) point builders behind STTPointLayer — CPU assembly of per-feature ECEF points |
featureColor | function | Per-feature constant/categorical/ramp colour dispatch over core/style scalar lookups |
cesiumBackend | BackendDescriptor | This backend's declared capabilities / layer-kind support, against @poopdeck.gl/core/capabilities |
viewStateToCesiumView | function | Pure ViewState → Cesium camera-parameter math (no Cesium runtime import) |
cesiumViewToViewState | function | Pure inverse of viewStateToCesiumView |
applyViewStateToCamera | function | Drives a live Cesium Camera from a ViewState |
attachCesiumClock | function | Binds a governor-owned playback clock to scene.preRender |
There is no shared base layer or archive-owning helper like MapLibre's
STTBaseLayer — the app wires STTArchive + SpatioTemporalTileset itself
and feeds tiles to the layer (see How it works). Every layer
class has the same surface: setTiles(tiles), setTime(absoluteMs),
pick(cssX, cssY), dispose().
Pure point builders#
STTPointLayer is a thin Cesium shell over a pure CPU core, exactly like the
polyline layers. buildPointEntries does the per-feature ECEF assembly behind
setTiles; collectPointLayers gathers the non-empty Point layers it walks.
Both are Cesium-free and unit-tested. lineStringTimeOrigin (exported alongside
the polyline builders) returns the first animatable LineString layer's
timeOffset — the scene-wide origin STTTripsLayer shares.
// Every non-empty Point layer across the tiles, in tile/layer order.function collectPointLayers(tiles: Tile[]): BinaryFeatures[];// One ECEF point per Point feature; times rebased to the first Point layer's// timeOffset. Empty build ({ points: [], timeOrigin: 0 }) when no Point features.function buildPointEntries(tiles: Tile[], opts?: PointBuildOptions): PointBuild;// First animatable LineString layer's timeOffset (0 when none) — shared by STTTripsLayer.function lineStringTimeOrigin(tiles: Tile[]): number;interface PointBuildOptions {colorProperty?: string; // categorical property to colour bycolorMapping?: Record<string, RGBA255>; // category → colourcolorMappingDefault?: RGBA255; // unmapped/absent (0–255) — @default opaque grey}interface PointBuild {points: FeaturePoint[];timeOrigin: number; // absolute ms all start/end are relative to}interface FeaturePoint {x: number;y: number;z: number; // absolute ECEF position (metres)r: number;g: number;b: number;a: number; // base colour, pre-normalized to 0..1start: number;end: number; // active window, relative to timeOrigin (ms)lon: number;lat: number; // source degrees — the picking coordinatebinary: BinaryFeatures; // picking provenancefeatureIndex: number;}
These mirror the polyline builders (FeaturePolyline / PolylineBuild).
Factoring them out is a pure refactor — STTPointLayer and STTTripsLayer
options and behaviour are unchanged.
Quick start#
import { Viewer } from 'cesium';import 'cesium/Build/Cesium/Widgets/widgets.css';import { STTArchive, SpatioTemporalTileset } from '@poopdeck.gl/core';import { makeTilesetCallbacks } from '@poopdeck.gl/core/tileset-adapter';import { STTPointLayer, applyViewStateToCamera } from '@poopdeck.gl/cesium';window.CESIUM_BASE_URL ='https://cdn.jsdelivr.net/npm/cesium@1.144.0/Build/Cesium/';const viewer = new Viewer(document.getElementById('cesiumContainer')!, {baseLayer: false, // no imagery provider — no ion token neededrequestRenderMode: true, // render on demand; see the clock section below});viewer.clock.shouldAnimate = false; // Cesium's own clock must not compete with the STT playheadconst layer = new STTPointLayer(viewer.scene, {id: 'earthquakes',mode: 'window',timeFilter: { windowHalf: 12 * 60 * 60 * 1000 }, // 12h half-windowpixelSize: 6,});applyViewStateToCamera(viewer.camera, {longitude: -122.4,latitude: 37.7,zoom: 6,});const archive = new STTArchive({ url: '/data/earthquakes/manifest.json' });const meta = await archive.getMetadata();const tileset = new SpatioTemporalTileset({minZoom: meta.minZoom,maxZoom: meta.maxZoom,temporalBucketMs: meta.temporalBucketMs,...makeTilesetCallbacks(archive),onTileLoad: () => layer.setTiles(tileset.getVisibleTiles()),onTileUnload: () => layer.setTiles(tileset.getVisibleTiles()),});const now = Date.now();tileset.update({ bounds: meta.bounds, zoom: 6, time: now, timeWindow: 24 * 60 * 60 * 1000 },true,);layer.setTime(now);
Driving the playhead from Cesium's render loop#
attachCesiumClock reads a governor-owned clock on every drawn frame
(scene.preRender) instead of pushing time through React state, so animation
stays synced to the actual draw frame:
import { attachCesiumClock } from '@poopdeck.gl/cesium';// `timeController` is any object shaped like @poopdeck.gl/playback's// TimeController — getTime() + on('tick'|'playState', ...). No import needed.const detach = attachCesiumClock(viewer.scene,timeController,(t) => {layer.setTime(t);tileset.update({ bounds, zoom, time: t, timeWindow }, true);},{ requestRender: true }, // required when requestRenderMode:true — see Limitations);// Before viewer.destroy():detach();
attachCesiumClock never advances the clock itself — it is read-only, so it
cannot double-drive a controller that already owns its own requestAnimationFrame.
STTPointLayer#
Constructor#
new STTPointLayer(scene: Scene, options?: STTPointLayerOptions)
Adds a PointPrimitiveCollection to scene.primitives immediately; no tiles
are drawn until setTiles is called.
Options (STTPointLayerOptions)#
| Field | Type | Default | Description |
|---|---|---|---|
id | string | 'stt-cesium-points' | Layer id, stamped onto each primitive's pick id so pick() can filter hits to this layer |
mode | TimeFilterMode | 'window' | One of 'window' | 'wake' | 'cumulative' | 'trail' | 'none' — see Time Filter Extension for the shared semantics |
timeFilter | TimeFilterParams | {} | Mode parameters (windowHalf, fadeIn, fadeOut, wakeLength, trailLength, trailFade) — all relative milliseconds |
colorProperty | string | — | Categorical property to colour by. Omit it and every point uses colorMappingDefault |
colorMapping | Record<string, RGBA255> | — | Category → colour lookup for colorProperty |
colorMappingDefault | RGBA255 | [200, 205, 215, 255] | Colour for unmapped/absent categories, and for every point when colorProperty is unset |
pixelSize | number | 6 | Point size in pixels — one constant for the whole layer; there is no per-feature radius property (unlike the deck.gl/MapLibre point layers) |
Methods#
| Method | Description |
|---|---|
setTiles(tiles: Tile[]) | Rebuilds the point collection from decoded tiles. Clears and re-adds every primitive; rebases all feature [startTime, endTime] pairs onto one scene-wide timeOrigin (the first tile layer's timeOffset). Non-point layers in the tile set are silently skipped |
setTime(absoluteMs: number) | Recomputes per-point alpha via the shared timeFilterAlpha oracle and writes it into each primitive's colour. Skips the write when a point's alpha is unchanged since the last call, and reuses one scratch Color — no allocations in the steady state |
pick(cssX: number, cssY: number) | scene.pick() at the given CSS pixel, filtered to this layer's own primitives, returning a shared SttPickResult (object from getFeatureProperties, index, layerId, coordinate: [lon, lat], screen) or null on a miss |
dispose() | Removes the point collection from scene.primitives and drops all entries |
STTPointLayer implements the shared SttRenderNode interface (id,
setTime, pick, dispose) but does not implement the optional
setViewState hook — camera control goes through the camera bridge
functions below, not through the layer.
The layer catalog#
Every other layer class shares STTPointLayer's lifecycle (setTiles →
setTime per drawn frame → pick → dispose) and its scene-wide
timeOrigin rebasing. All colour options take a FeatureColorMode —
{ type: 'constant', color }, { type: 'categorical', property, colorMapping?, fallback }, or { type: 'ramp', property, domain, range, fallback } — resolved per feature through core/style. The four movement
kinds are detailed below; the rest are listed in Exports, each
with the LayerKind it backs.
STTPathLayer (path + line)#
new STTPathLayer(scene, { id?, mode?, timeFilter?, color?, width?, zLift?, arcType? })
One batched Primitive of PolylineGeometry instances with per-instance
ColorGeometryInstanceAttributes — a colour write is a batch-table texel
update, so per-frame time-filter animation stays one draw-call bucket.
Geometry z is honoured when the tile is 3-D (satellite tracks fly at
altitude). arcType ('none' default | 'geodesic' | 'rhumb') picks the
vertex-to-vertex interpolation; use 'geodesic' for sparse ground-hugging
lines. An OD line dataset needs no special handling — each 2-vertex
LineString renders as a (geodesic-capable) polyline.
STTArcLayer (arc)#
new STTArcLayer(scene, { id?, mode?, timeFilter?, color?, height?, samples?, width?, zLift? })
Each feature collapses to source/target endpoints
(core/geometry deriveSourceTargetPositions) and sweeps a raised
great-circle polyline (sampleGreatCircleArc, samples default 33): slerp of
the two ECEF direction vectors, radius lerped between the endpoint radii,
radial parabolic lift height · chord · 4·t·(1−t) — the SAME parametrization
as three's globe arc material, so a backend toggle shows the same arc.
height: 0 hugs the great circle.
STTTripsLayer (trips)#
new STTTripsLayer(scene, { id?, trailLength?, color?, width?, fadeTrail? })
Cesium's stock polyline has no per-vertex shader hook, so the trail is
GEOMETRY, not alpha: every drawn frame each active trip is trimmed to
[t − trailLength, t] by core/trips trimTrail (interpolated head + tail
vertices) and written into a PolylineCollection polyline. fadeTrail
(default true) applies a tiny shared polyline material that ramps alpha along
the trimmed line's arc length — a geometric approximation of deck's
per-vertex time fade. Trips sharing a colour share one material instance, so
the collection batches by colour. Per-frame cost tracks the number of ACTIVE
trips.
STTTripHeadsLayer (tripHeads)#
new STTTripHeadsLayer(scene, { id?, color?, pixelSize? })
One PointPrimitive per trip, show-toggled; every drawn frame the head
position is interpolated by core/trips sampleHead (binary search + lerp
along per-vertex times — the tile's vertexTimestamps column when present,
else distance-synthesized). The trip index is built at 'f64' precision so
globe-spanning data doesn't quantize.
Camera bridge#
STT's cross-backend camera vocabulary is a ViewState
({ longitude, latitude, zoom, pitch?, bearing?, roll?, altitude? }) — the
same shape deck.gl, Three.js, and MapLibre share, so a renderer toggle keeps
one view. Cesium is a 3-DOF camera (it has roll, where deck/MapLibre don't),
and it's height-driven rather than zoom-driven, so the bridge does a bit more
conversion work than the other backends' equivalents.
Convention differences the bridge absorbs:
- Pitch. STT
pitchis 0 = top-down; Cesiumpitchis-90°= straight down,0°= horizon.cesiumPitch = viewPitch - 90. - Heading / bearing.
heading = bearing, both compass degrees. - Zoom ⇄ height. Cesium's camera is positioned by altitude in metres, not
a mercator zoom level. The bridge reuses the framework-free
core/geoGlobeProjection+worldUnitsPerPixel/zoomForWorldUnitsPerPixelhelpers (WGS84, no Cesium import) to convert between the two given a viewport height and vertical field of view — the same math a deckGlobeViewwould use for ground resolution. An explicitViewState.altitudeoverrides the derived height outright.
viewStateToCesiumView(v, opts?) / cesiumViewToViewState(view, opts?)#
Pure functions — no Cesium runtime import, safe to unit test in Node.
export interface CesiumViewOptions {/** Viewport height in CSS px — sets the zoom→height scale. @default 800 */viewportHeight?: number;/** Vertical field of view, radians. @default 60° */fovRadians?: number;}interface CesiumView {// NOTE: longitude/latitude are the LOOK-AT TARGET, not the camera position.longitude: number;latitude: number;range: number; // camera→target distance, metres (HeadingPitchRange.range)height: number; // camera altitude above the target's tangent plane = range × cos(pitch)headingRad: number;pitchRad: number;rollRad: number;}
cesiumViewToViewState returns a ResolvedViewState — every ViewState
field present (longitude, latitude, zoom, pitch, bearing, roll),
so the two functions round-trip.
applyViewStateToCamera(camera, v, opts?)#
The one function in the package that touches a live Cesium Camera — it
converts v via viewStateToCesiumView and calls
camera.lookAt(target, new HeadingPitchRange(heading, pitch, range)), then
releases the camera back to the normal globe controls with
camera.lookAtTransform(Matrix4.IDENTITY).
It deliberately does not use camera.setView({ destination }). In Cesium
destination is the CAMERA POSITION, never a look-at target, so passing
ViewState.longitude/latitude there framed ground 0.5–2 camera-heights away
from the requested point on every pitched view — the whole dataset sat off to
one side. lookAt is what makes ViewState.longitude/latitude mean the same
thing here as on the other three backends. See
tile-loading-3d-2026-07.md RC6.
Render-loop clock#
attachCesiumClock(scene, clock, apply, options?)#
| Field | Type | Default | Description |
|---|---|---|---|
scene | Scene | — | The live Cesium scene to hook |
clock | PlayheadClock | — | Anything shaped like { getTime(): number; on('tick', cb): () => void; on('playState', cb): () => void } — @poopdeck.gl/playback's TimeController satisfies this structurally, with no import |
apply | (timeMs: number) => void | — | Called with the clock's absolute time on every drawn frame |
options.requestRender | boolean | false | Also pump scene.requestRender() on 'tick' and 'playState', so a Scene with requestRenderMode: true keeps animating while playing and goes idle (zero renders) when paused |
Returns a disposer that removes every listener it added — call it before
viewer.destroy().
apply is wired to scene.preRender, which fires immediately before
Cesium's primitive-update + draw pass, so a colour/uniform write from apply
lands in the same frame it's read. The hook is read-only: it calls
clock.getTime() but never advances the clock, so it structurally cannot
double-drive a controller that runs its own requestAnimationFrame loop.
requestRenderMode: true on the Viewer/Scene and
attachCesiumClock(..., { requestRender: true }) are an atomic pair —
turning on the former without the latter silently freezes a playing
animation, because nothing else will ask Cesium to redraw a new frame.
Backend descriptor#
cesiumBackend is a BackendDescriptor (from @poopdeck.gl/core/capabilities)
declaring what this backend supports:
| Trait / capability | Value |
|---|---|
globe | true — Cesium's native frame is a WGS84 globe |
picking | true — scene.pick |
extrude3d | true |
metricSizing | true — ECEF metres |
gpuHeatmap | false — STTHeatmapLayer's density field is computed on the CPU |
liveBundling | true — KDEEB at runtime through core's shared bundleEdges, on the CPU schedule (a bundle is static geometry, recomputed when the edge set changes, never per frame) |
timeAsHeight | true — lib/columns.ts timeHeightLiftMeters raises each prism's base along local up |
interleavedBasemap | true — STT primitives share Cesium's scene + depth buffer |
userExtensions | true — lib/extensions.ts per-frame value hooks over the oracle's RESOLVED alpha/colour |
cameraRoll | true — Cesium's camera has heading/pitch/roll |
projectsOnCpu | true — via core/geo GlobeProjection(wgs84) → Cartesian3 |
tilesetOwnership | shared |
pickMechanism | host — scene.pick |
basemapProjection | globe |
layerKinds() marks every one of the 23 LayerKinds { supported: true } —
there is no fallback and no unsupported kind to declare. See the generated
docs/spec/backend-capabilities.md for the
full cross-backend matrix (regenerated by scripts/gen-capabilities-doc.mjs —
don't hand-edit it).
How it works#
- The
STTPointLayerconstructor adds an (initially empty)PointPrimitiveCollectiontoscene.primitives. There's no archive-owning base class — the app constructs its ownSTTArchiveandSpatioTemporalTileset(wired viamakeTilesetCallbacksfrom@poopdeck.gl/core/tileset-adapter), exactly as it would for any other STT backend. - Whenever the tileset's resident tile set changes, the app calls
layer.setTiles(tileset.getVisibleTiles()). Each point feature's[longitude, latitude, altitude?]is projected once throughcore/geo'sGlobeProjection({ datum: 'wgs84' })into ECEFCartesian3— Cesium's native frame, so the projected output drops straight intoPointPrimitiveCollection.add()with no further conversion. Categorical colour, if configured, is expanded once per tile viacore/style'sexpandCategoricalColors. - Camera sync goes through the
ViewStatebridge, not through the layer:applyViewStateToCamerafor one-shot moves, or read the camera back each frame viacesiumViewToViewState(as the showcase's Cesium renderer does to drive tileset streaming fromscene.camera.changed/moveEnd). attachCesiumClocksubscribes toscene.preRenderand callssetTime(plus, typically, atileset.update(...)) on every drawn frame — so animation is paced by Cesium's actual draw cadence, not React's UI clock.setTimewalks the flat entry list built bysetTilesand asks the sharedtimeFilterAlpha(mode, …)oracle for each point's alpha, skipping the GPU colour write when the value hasn't changed since the last frame.pick(cssX, cssY)callsscene.pick, checks the returned primitive's id belongs to this layer, and joins the hit back to feature properties via the sharedgetFeatureProperties(binary, featureIndex)helper — the same join every backend's picking result uses.
Limitations#
- One colour per feature. The batch-table animation path has no
per-vertex colour, so deck's OD endpoint gradients
(
getSourceColor/getTargetColor) collapse to the source colour, per-vertex trip gradients collapse to a per-trip ramp, and the trips tail fade is arc-length-based rather than per-vertex-time-based. - No shared archive/tileset-owning base class. Unlike MapLibre's
STTBaseLayer(which ownsonAdd/streaming/buffer-change forwarding), the Cesium package gives you the primitives (STTPointLayer,attachCesiumClock, the camera bridge) and expects the host app to wireSTTArchive+SpatioTemporalTileset+makeTilesetCallbacksitself, as shown in Quick start. - CPU-side time filtering — this backend ships no time-filter shader.
setTimeloops every feature on the CPU and writes a colour per changed feature (pointColors, polyline batch-table texels), so large feature counts pay a per-frame JS loop (mitigated by the unchanged-alpha skip, not eliminated). A GPU-Appearancepath would fix this, and would start from the AST: no backend compiles fromALPHA_EXPR— deck, maplibre and three each hand-write their shader and are pinned to the shared oracle by conformance tests (see Render kernel §core/shader-codegen). Trips additionally rewrite active polylines' positions each frame (the trail trim). - One constant pixel size / width per layer. There is no per-feature
radius or width property (
radiusProperty, property-namepathWidth) —pixelSize/widthapply to every feature in a layer. requestRenderMode+attachCesiumClock({ requestRender: true })are a matched pair. Turning onrequestRenderModewithout also passingrequestRender: true(or otherwise callingscene.requestRender()yourself) freezes a playing animation — Cesium simply never redraws.- Cesium's own clock must be silenced.
viewer.clock.shouldAnimateneeds to befalse, or Cesium's built-in clock competes with the STT-driven playhead for scene updates. - Zoom↔height is an approximation.
viewStateToCesiumView/cesiumViewToViewStatederive Cesium's height-driven camera from STT's zoom-drivenViewStateusing a fixed viewport-height/FOV model; it isn't pixel-identical to Cesium's own frustum math at extreme pitches or very high latitudes. - CesiumJS is a large runtime dependency (workers, static assets, WebGL2 requirement) compared to the other STT backends — reach for it only when you need a true 3D WGS84 globe with camera roll.
Live demo#
The showcase app has a dedicated Cesium route,
/cesium/:datasetId (CesiumDemoPage → CesiumRenderer →
buildCesiumLayer), which streams a dataset through the matching Cesium
layer on a real globe using the same playback clock as the other renderer
demos. Run pnpm dev from examples/showcase and navigate to
/cesium/<datasetId> for any dataset whose kind cesiumBackend declares —
the route gates on CESIUM_SUPPORTED_TYPES, read straight from the
descriptor, so adding a layer class moves the route without a showcase edit
(other types redirect home).