Some visualizations animate, pulse, and flash. If you're sensitive to motion or flashing, turn on Reduce motion in your system settings.

poopdeck.gl
Renderer Backends

@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.

ExportKindDescription
STTPointLayerclassThe point SttRenderNode — builds a PointPrimitiveCollection from decoded tiles and drives per-point alpha off the shared time-filter oracle
STTPathLayerclassAnimated LineStrings (path and OD line — an OD line is a 2-vertex LineString); batched Primitive + per-instance colour animation
STTArcLayerclassOD flow arcs — endpoints via the kernel's deriveSourceTargetPositions, swept into raised great-circle polylines (same parametrization as three's globe arc material)
STTTripsLayerclassVehicle trails — per-frame CPU trail trim (core/trips trimTrail) into a PolylineCollection, arc-length tail fade material
STTTripHeadsLayerclassMoving head-dots — per-frame sampleHead interpolation (core/trips) onto PointPrimitives
STTPolygonLayerclassFilled — optionally extruded — polygon areas on the ellipsoid; per-feature window fade, height from a numeric column
STTIconLayerclassBillboard sprites cut from one caller-supplied atlas texture (icon), per-feature heading/size/tint
STTColumnLayerclassOne extruded n-sided prism per point (column), radius in true metres; carries the space-time-cube lift (lib/columns.ts timeHeightLiftMeters)
STTMeshLayerclassOne posed glTF model per tracked object at the playhead (mesh) — the thing the detection cuboid is around
STTIsoLayerclassIso-contour polylines (isoLines) with a level ramp over STTPathLayer's polyline machinery
STTBoundingBoxLayerclassOne oriented 3-D cuboid per tracked object at the playhead (boundingBox), interpolated between keyframes
STTPointCloudLayerclassLit 3-D point clouds (pointCloud) — per-point elevation plus an optional surface normal (lambertShade)
STTSurfelLayerclassOriented anisotropic surface elements (surfel) from baked quaternion/scale/rgba columns
STTTextLayerclassOne screen-space Label per feature (text), anchored at the feature's absolute f64 ECEF position
STTEgoLayerclassThe single ego-vehicle cuboid + its full trajectory (ego), sampled from a one-track pose stream
STTH3SummaryLayerclassSummary-tier H3 cells (h3Summary) as ramp-coloured, optionally extruded plates; takes an injected cellToBoundary
STTQuadbinSummaryLayerclassSummary-tier CARTO Quadbin cells (quadbinSummary), same ramp/extrusion path as H3
STTHexbinLayerclassRuntime hexbin (hexbin) over the raw point tier — binned in the browser over the playhead's slice
STTHeatmapLayerclassA geodetic RectangleGeometry textured with a CPU-computed density raster (heatmap; see lib/heatmap-field.ts)
STTFlowCorridorLayerclassStatic route network whose per-segment colour breathes off a per-vertex x per-bucket volume matrix (flowCorridor)
STTFlowStrokeLayerclassThe twin-ribbon sibling of the corridor — the same matrix drives WIDTH rather than colour (flowStroke)
STTFlowmapLayerclassflowmap.gl-style tapered OD arrows with heads (flowmap); runtime KDEEB bundling via core's shared bundleEdges
STTBatchedPolylineLayerclassThe shared batched-Primitive machinery behind the path/arc layers (advanced use)
buildPathPolylines / buildArcPolylines / sampleGreatCircleArc / lineStringTimeOriginfunctionsThe pure (Cesium-free, unit-tested) geometry builders behind the polyline layers (lineStringTimeOrigin = their shared scene-wide time origin)
buildPointEntries / collectPointLayersfunctionsThe pure (Cesium-free, unit-tested) point builders behind STTPointLayer — CPU assembly of per-feature ECEF points
featureColorfunctionPer-feature constant/categorical/ramp colour dispatch over core/style scalar lookups
cesiumBackendBackendDescriptorThis backend's declared capabilities / layer-kind support, against @poopdeck.gl/core/capabilities
viewStateToCesiumViewfunctionPure ViewState → Cesium camera-parameter math (no Cesium runtime import)
cesiumViewToViewStatefunctionPure inverse of viewStateToCesiumView
applyViewStateToCamerafunctionDrives a live Cesium Camera from a ViewState
attachCesiumClockfunctionBinds 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 by
colorMapping?: Record<string, RGBA255>; // category → colour
colorMappingDefault?: 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..1
start: number;
end: number; // active window, relative to timeOrigin (ms)
lon: number;
lat: number; // source degrees — the picking coordinate
binary: BinaryFeatures; // picking provenance
featureIndex: 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 needed
requestRenderMode: true, // render on demand; see the clock section below
});
viewer.clock.shouldAnimate = false; // Cesium's own clock must not compete with the STT playhead
const layer = new STTPointLayer(viewer.scene, {
id: 'earthquakes',
mode: 'window',
timeFilter: { windowHalf: 12 * 60 * 60 * 1000 }, // 12h half-window
pixelSize: 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)#

FieldTypeDefaultDescription
idstring'stt-cesium-points'Layer id, stamped onto each primitive's pick id so pick() can filter hits to this layer
modeTimeFilterMode'window'One of 'window' | 'wake' | 'cumulative' | 'trail' | 'none' — see Time Filter Extension for the shared semantics
timeFilterTimeFilterParams{}Mode parameters (windowHalf, fadeIn, fadeOut, wakeLength, trailLength, trailFade) — all relative milliseconds
colorPropertystringCategorical property to colour by. Omit it and every point uses colorMappingDefault
colorMappingRecord<string, RGBA255>Category → colour lookup for colorProperty
colorMappingDefaultRGBA255[200, 205, 215, 255]Colour for unmapped/absent categories, and for every point when colorProperty is unset
pixelSizenumber6Point size in pixels — one constant for the whole layer; there is no per-feature radius property (unlike the deck.gl/MapLibre point layers)

Methods#

MethodDescription
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 (setTilessetTime per drawn frame → pickdispose) 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 pitch is 0 = top-down; Cesium pitch is -90° = straight down, = 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/geo GlobeProjection + worldUnitsPerPixel/zoomForWorldUnitsPerPixel helpers (WGS84, no Cesium import) to convert between the two given a viewport height and vertical field of view — the same math a deck GlobeView would use for ground resolution. An explicit ViewState.altitude overrides 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?)#

FieldTypeDefaultDescription
sceneSceneThe live Cesium scene to hook
clockPlayheadClockAnything 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) => voidCalled with the clock's absolute time on every drawn frame
options.requestRenderbooleanfalseAlso 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 / capabilityValue
globetrue — Cesium's native frame is a WGS84 globe
pickingtruescene.pick
extrude3dtrue
metricSizingtrue — ECEF metres
gpuHeatmapfalseSTTHeatmapLayer's density field is computed on the CPU
liveBundlingtrue — 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)
timeAsHeighttruelib/columns.ts timeHeightLiftMeters raises each prism's base along local up
interleavedBasemaptrue — STT primitives share Cesium's scene + depth buffer
userExtensionstruelib/extensions.ts per-frame value hooks over the oracle's RESOLVED alpha/colour
cameraRolltrue — Cesium's camera has heading/pitch/roll
projectsOnCputrue — via core/geo GlobeProjection(wgs84)Cartesian3
tilesetOwnershipshared
pickMechanismhostscene.pick
basemapProjectionglobe

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#

  1. The STTPointLayer constructor adds an (initially empty) PointPrimitiveCollection to scene.primitives. There's no archive-owning base class — the app constructs its own STTArchive and SpatioTemporalTileset (wired via makeTilesetCallbacks from @poopdeck.gl/core/tileset-adapter), exactly as it would for any other STT backend.
  2. 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 through core/geo's GlobeProjection({ datum: 'wgs84' }) into ECEF Cartesian3 — Cesium's native frame, so the projected output drops straight into PointPrimitiveCollection.add() with no further conversion. Categorical colour, if configured, is expanded once per tile via core/style's expandCategoricalColors.
  3. Camera sync goes through the ViewState bridge, not through the layer: applyViewStateToCamera for one-shot moves, or read the camera back each frame via cesiumViewToViewState (as the showcase's Cesium renderer does to drive tileset streaming from scene.camera.changed/moveEnd).
  4. attachCesiumClock subscribes to scene.preRender and calls setTime (plus, typically, a tileset.update(...)) on every drawn frame — so animation is paced by Cesium's actual draw cadence, not React's UI clock.
  5. setTime walks the flat entry list built by setTiles and asks the shared timeFilterAlpha(mode, …) oracle for each point's alpha, skipping the GPU colour write when the value hasn't changed since the last frame.
  6. pick(cssX, cssY) calls scene.pick, checks the returned primitive's id belongs to this layer, and joins the hit back to feature properties via the shared getFeatureProperties(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 owns onAdd/streaming/buffer-change forwarding), the Cesium package gives you the primitives (STTPointLayer, attachCesiumClock, the camera bridge) and expects the host app to wire STTArchive + SpatioTemporalTileset + makeTilesetCallbacks itself, as shown in Quick start.
  • CPU-side time filtering — this backend ships no time-filter shader. setTime loops every feature on the CPU and writes a colour per changed feature (point Colors, polyline batch-table texels), so large feature counts pay a per-frame JS loop (mitigated by the unchanged-alpha skip, not eliminated). A GPU-Appearance path would fix this, and would start from the AST: no backend compiles from ALPHA_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-name pathWidth) — pixelSize/width apply to every feature in a layer.
  • requestRenderMode + attachCesiumClock({ requestRender: true }) are a matched pair. Turning on requestRenderMode without also passing requestRender: true (or otherwise calling scene.requestRender() yourself) freezes a playing animation — Cesium simply never redraws.
  • Cesium's own clock must be silenced. viewer.clock.shouldAnimate needs to be false, or Cesium's built-in clock competes with the STT-driven playhead for scene updates.
  • Zoom↔height is an approximation. viewStateToCesiumView/ cesiumViewToViewState derive Cesium's height-driven camera from STT's zoom-driven ViewState using 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 (CesiumDemoPageCesiumRendererbuildCesiumLayer), 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).