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/three

A Three.js renderer for SpatioTemporal Tiles archives, built on Three's node-material system (TSL — Three Shading Language) so it runs on WebGPURenderer's WebGPU backend and falls back to its own WebGL2 backend transparently. It consumes the exact same decoded tiles as @poopdeck.gl/layers (via @poopdeck.gl/core) and the same playback clock from @poopdeck.gl/playback, so it is a drop-in alternative renderer rather than a separate data pipeline. It ships two surfaces: a framework-agnostic engine core (STTScene, createSTTRenderer, individual STTLayers) and a declarative react-three-fiber binding at the @poopdeck.gl/three/r3f subpath (<STTCanvas> + layer components). It covers a first-class local-metric (ENU) frame for the AV LIDAR cockpit — oriented Gaussian surfels included — alongside mercator and globe projections, viewport streaming, and the complete 23-kind layer catalog. See System overview for where it sits in the stack and renderer-architecture.md for the deck-parity design rationale.

Install#

pnpm add @poopdeck.gl/three three
# for the react-three-fiber binding:
pnpm add @react-three/fiber @react-three/drei react react-dom
# only for <STTTiles3D> / <STTAtmosphere>:
pnpm add 3d-tiles-renderer @takram/three-atmosphere

three is a peer dependency at >=0.183.0 <0.190.0 — an upper-bounded range, not an open floor, because the TSL node API still moves between minors. Every other peer is optional: @react-three/fiber (>=9), @react-three/drei (>=10) and react (>=19) are needed only if you import from the @poopdeck.gl/three/r3f subpath (the base package has no React dependency), and 3d-tiles-renderer (^0.5.2) / @takram/three-atmosphere (^0.19.1) only by the <STTTiles3D> / <STTAtmosphere> components.

Needs WebGPU or WebGL2. TSL node materials only compile on Three's WebGPURenderer (which transparently falls back to its own WebGL2 backend when the browser has no WebGPU adapter); the classic WebGLRenderer cannot run them. There is no WebGL1 fallback — isWebGPUAvailable() / canRenderGpu()-style feature detection is worth gating on (the r3f <STTCanvas> does this for you, see below).

Renderer bootstrap#

import { createSTTRenderer, isWebGPUAvailable } from '@poopdeck.gl/three';
const { renderer, backend } = await createSTTRenderer({
canvas: document.querySelector('canvas')!,
antialias: true,
alpha: true, // transparent clear, lets a basemap show through
});
console.log(backend); // 'webgpu' | 'webgl2'

createSTTRenderer builds and init()s a WebGPURenderer — always await it before the first render. It also pre-requests a WebGPU device with its buffer-size limits raised to the adapter maximum (createHighLimitDevice), because Three's default device caps a single buffer at the WebGPU spec default (256 MB) and a dense LIDAR sweep's merged vertex buffer can exceed that; forceWebGL: true skips this (the WebGL2 backend has no such cap).

FunctionDescription
createSTTRenderer(opts?)Builds + init()s a WebGPURenderer. Returns { renderer, backend }.
isWebGPUAvailable()true if the page can request a WebGPU adapter (navigator.gpu present). Does not guarantee adapter/device acquisition succeeds.
resolveBackend(renderer, forceWebGL?)Inspects a live renderer to report which backend init() actually chose.
createHighLimitDevice(powerPreference?)Pre-creates a GPUDevice with raised buffer-size limits; used internally by createSTTRenderer and the r3f binding's gl factory. Returns undefined on WebGL2 / on failure.

CreateRendererOptions#

FieldTypeDefaultDescription
canvasHTMLCanvasElementnew canvasTarget canvas (the r3f binding supplies its own).
antialiasbooleantrueMSAA.
alphabooleantrueTransparent clear, so a DOM/basemap layer underneath shows through.
forceWebGLbooleanfalsePin the WebGL2 backend even when WebGPU is available (no compute shaders; maximum-uniformity mode).
powerPreference'high-performance' | 'low-power' | 'default''high-performance'GPU power hint.

Non-React mount#

For apps without React there is no viewer wrapper — you own the camera and the loop, and the package gives you the two halves: createSTTRenderer() (which awaits renderer.init() before you ever call render(), so the "render() before the backend is initialized" warning cannot happen) and STTScene, whose root is a plain Group you add to any Three scene.

import { Scene, PerspectiveCamera, Group } from 'three';
import { STTScene, STTPointLayer, createSTTRenderer } from '@poopdeck.gl/three';
const { renderer } = await createSTTRenderer({
canvas: document.getElementById('viewport') as HTMLCanvasElement,
});
const stt = new STTScene({
anchor: { longitude: -122.4, latitude: 37.77 },
timeOrigin: Date.now(),
});
stt.addLayer(
new STTPointLayer({ id: 'points', colorProperty: 'category' }),
'/data/points/manifest.json',
);
await stt.load();
const scene = new Scene();
scene.add(stt.root);
const camera = new PerspectiveCamera(60, 16 / 9, 0.1, 10_000);
camera.up.set(0, 0, 1); // the engine's frames are Z-up
renderer.setAnimationLoop(() => {
stt.setTime(Date.now());
renderer.render(scene, camera);
});

For a camera rig, orbit controls and a follow-ego mode, use the r3f binding (<STTCanvas>) below rather than hand-rolling them.

Projections#

Projection (re-exported from @poopdeck.gl/core/geo) is the pluggable seam every layer's buffer builder projects lon/lat/alt through — it decouples the GPU layers from any one coordinate scheme.

ClassFrameUseNotes
LocalEnuProjectionLocal East-North-Up, metres, Z-upAV cockpit / any small local scene1 world unit = 1 metre; metersPerWorldUnit = 1. Anchored at a GeoAnchor { longitude, latitude }.
MercatorProjectionEPSG:3857, Z-up plane (ground XY, altitude +Z)Flat-map geographic demosmetersPerWorldUnit is cos(lat)-scaled; MAX_MERCATOR_LAT clamps the usual ±85.05°.
GlobeProjectionECEF (Earth-Centred, Earth-Fixed)Globe geographic demosReal 3D sphere coords — a standard MVP renders it with real depth/occlusion (not deck's in-shader vertex-warp). datum: 'sphere' (default, byte-identical to the original implementation) or datum: 'wgs84' (ellipsoid, matches Cesium/real-globe hosts to the metre instead of the sphere's ~20 km mid-latitude mismatch). radius is configurable (default EARTH_RADIUS).

Every Projection also exposes metersPerWorldUnit(lon, lat) (a sizing scale for metric layers like columns/surfels) and localFrame(lon, lat) (a per-position east/north/up basis — how a column stands up straight on a globe, or a box orients on a mercator plane).

Coordinates project through RTC (relative-to-center): projectPositions returns f32-relative vertices plus a per-build f64 origin, which each layer writes to its Object3D.position — so large mercator/globe magnitudes stay in the CPU-side f64 transform instead of losing precision in an f32 vertex buffer. (projectPositionsToEnu is the ENU-only precursor, kept for back-compat.)

View state#

import {
viewStateToCamera,
cameraToViewState,
MercatorProjection,
} from '@poopdeck.gl/three';
const proj = new MercatorProjection();
const target = viewStateToCamera(
proj,
{ longitude: -74, latitude: 40.7, zoom: 12, pitch: 45, bearing: 0 },
camera,
);
// ...user drags the camera via OrbitControls/MapControls...
const viewState = cameraToViewState(proj, camera); // round-trips back to {longitude, latitude, zoom, pitch, bearing}

viewStateToCamera/cameraToViewState bridge deck.gl-shaped {longitude, latitude, zoom, pitch, bearing} view state and a Three PerspectiveCamera, so a showcase page can drive a deck view and a three view from the same state object (used by the /drive deck↔three toggle). frameGlobe/setGlobeClip (from scene/globe-camera) fit a camera to a globe scene with planet-aware near/far clipping.

Layer catalog#

Every layer implements the small STTLayer contract (setTiles(tiles, ctx), setTime(absoluteMs), dispose()) and owns one Three Object3D; a layer merges every resident tile into one InstancedMesh/indexed mesh per layer (not one draw call per tile, unlike the maplibre adapter), and per-frame animation is a uniform write — no rebuild. The kind→class map is in the renderer-architecture.md appendix.

Every class carries the STT prefix, matching @poopdeck.gl/maplibre and @poopdeck.gl/cesium, so one layer kind has one spelling on every backend (and nothing shadows deck's own exports in an app importing both); the deck column below keeps deck's own Animated* names.

ClassGeometryDeck equivalentNotes
STTPointLayerPointAnimatedPointLayerwindow/wake/cumulative modes, soft-Gaussian splat, categorical/RGB-column/continuous-ramp colour, metre or pixel sizing, GPU id-colour picking (opt-in, browser-verify).
STTSurfelLayerPoint (oriented surfel)SplatLayer / SplatPrimitiveLayerOriented anisotropic Gaussian surfels (surface splatting) — the AV LIDAR hero mode. Reads --surfel-baked quaternion/scale/rgba columns. ENU-only; no globe port.
STTWideLineLayerLineStringAnimatedPathLayer / AnimatedLineLayer / AnimatedTripsLayerScreen-pixel-width instanced ribbon over createWideLineMaterial; mode: 'window' | 'trail' | 'none'. STTPathGeoLayer subclasses it directly; STTOdLineLayer/STTTripsLayer/STTFlowCorridorLayer are siblings reusing the same material + segment-quad geometry with their own buffer builders.
STTPathGeoLayerLineStringAnimatedPathLayerSTTWideLineLayer subclass pinned to mode: 'window' with path-shaped option names.
STTStaticPathLayerLineString— (AV map_line)Flat, static hairline path (no width/time) for AV map-line overlays.
STTOdLineLayerLineString → 2-point segmentAnimatedLineLayerCollapses each feature to its first→last vertex (a straight OD flow).
STTTripsLayerLineString (trail)AnimatedTripsLayerPer-vertex trail times, trailing fade over [cur - trailLength, cur].
STTTripHeadsLayerLineString → pointAnimatedTripHeadsLayerCPU-interpolates a moving dot at the head of every active trip each frame (sub-ms; only active trips are re-uploaded).
STTArcLayerLineString → OD arcAnimatedArcLayerRaised parabolic or spherical great-circle source→target arcs, per-endpoint colour, per-feature height.
STTIconLayerPointAnimatedIconLayerDirectional billboard markers from a host-supplied atlas texture; per-feature heading/size/tint; pixel sizing.
STTColumnLayerPoint → prismAnimatedColumnLayerExtruded 3D disk-prism bars, oriented to the local ground frame (stands up straight on a globe too); categorical/ramp/constant colour.
STTPolygonLayerPolygonAnimatedPolygonLayerProjected-space earcut (or pre-baked triangles), window-mode time fade, optional extrusion to a 3D prism. STTStaticPolygonLayer is a flat, static, categorically-coloured preset for AV map_poly overlays.
STTIsoLayerLineString (contours)— (AV lidarIso/lidarIso3d)Animated density iso-contour lines; window-filtered, optional per-ring altitude for iso3d.
STTBoundingBoxLayerPoint (keyframed) → oriented boxAnimatedBoundingBoxLayerCPU track pooling + binary-search interpolation per frame; draws 12-edge wireframe boxes + optional velocity arrows; supports ray-OBB picking.
STTEgoLayerPoint (keyframed) → box + trail— (AV ego vehicle)Static full trajectory line + an interpolated marker box; the source of the follow-camera target.
STTFlowmapLayerPoint pairs + value matrixFlowmapLayerflowmap.gl-style tapered OD arrows sized by per-bucket trip volume + node circles sized by incident flow; re-expands at ~5 Hz, not per frame.
STTFlowCorridorLayerLineString + value matrixFlowCorridorLayerStatic route network geometry, ridership-over-time from a vertexValueMatrix baked into a linear-filtered DataTexture (GPU does the two-bucket lerp — no CPU re-expand per sub-step).
STTH3SummaryLayerH3 cell (summary tier)H3SummaryLayerDecodes summary-tier u64 cell ids to H3 boundary rings; static (built once).
STTQuadbinSummaryLayerQuadbin cell (summary tier)QuadbinSummaryLayerDecodes summary-tier u64 cell ids to CARTO quadbin quads; static.
STTHeatmapLayerPoint (density field)AnimatedHeatmapLayerPer-pixel density heatmap — additive splat pass, then a ramp-resolve pass. This is the gpuHeatmap capability.
STTFlowStrokeLayerLineString + value matrixFlowStrokeLayerExtends STTFlowCorridorLayer: twin offset ribbons whose WIDTH (not colour) breathes with the active bucket.
STTTextLayerPoint → glyph runAnimatedTextLayerOne billboard-quad instance per character over a caller-supplied SDF/bitmap font atlas, sampled the way STTIconLayer samples icons.
STTMeshLayerPoint (keyframed) → modelAnimatedMeshLayer / AnimatedScenegraphLayerRecognizable glTF models on the same pooled-track motion as STTBoundingBoxLayer — the mesh analogue of the detection cuboid.
STTPointCloudLayerPoint (lit, with normals)AnimatedPointCloudLayerPhong-lit 3-D points with optional surface normals — between flat STTPointLayer dots and oriented STTSurfelLayer disks.
STTHexbinLayerPoint → hex prismAnimatedHexagonLayerRuntime hexbin over the raw point tier: world-space hex lattice, one instanced prism per occupied cell, coloured/extruded by aggregate weight.

Colour management#

Three is colour-managed and deck is not. Every STT colour — colorMapping values, ramp stops, palette textures, the r,g,b/point_rgba columns — is authored as sRGB bytes, the same numbers deck writes straight to an unmanaged canvas. WebGPURenderer.outputColorSpace defaults to SRGBColorSpace (and r3f re-asserts it unless <Canvas linear>), so Three's output pass runs the linear→sRGB OETF over whatever the fragment stage produced. Handing it a value that is already sRGB encodes it twice: mid-tones lift by ~50/255 and saturated colours go pastel — the app cyan [31,186,214] reaches the screen as [98,222,236].

So a material's colorNode must be wrapped in srgbToWorking() (exported from the package root, tsl/color-space.ts), which runs the matching EOTF in the fragment stage so the output pass returns the authored byte exactly. Two rules bind every caller:

  • Convert colour only — never alpha, never an id material. Alpha is linear already (opacityNode is not part of the transfer function), and the GPU-pick pass renders into a RenderTarget that stays in the working space with no output encode, so its 24-bit indices must reach the readback bit-exact.
  • Convert last, on the final fragment colour — after the gradient mix()es, after the column shade term, after the icon atlas × tint product. deck does all of those on sRGB values, so interpolating the varying and converting per-fragment reproduces deck's result; converting per-vertex first would interpolate in a different space.

srgbToLinear in lib/color.ts is the CPU mirror, for the few layers that shade through a classic vertexColors material rather than a TSL graph (the H3 / Quadbin summaries, the AV map paths and bounding boxes).

Time-window vocabulary#

Every layer accepts ThreeTimeWindowOptions, which bridges deck/maplibre's full-width timeWindow (ms) + fadeInDuration/fadeOutDuration onto the three-native half-width windowHalf + fadeIn/fadeOut (windowHalf = timeWindow / 2). Both forms are accepted on every layer; if both are supplied for the same knob, the lower-level three-native name wins (windowHalf over timeWindow, fadeIn over fadeInDuration, fadeOut over fadeOutDuration).

deck / maplibre (full-width)three (half-width)Resolved as
timeWindowwindowHalfwindowHalf = timeWindow / 2 when windowHalf is unset
fadeInDurationfadeInfadeIn = fadeInDuration when fadeIn is unset
fadeOutDurationfadeOutfadeOut = fadeOutDuration when fadeOut is unset

A deck demo's timeWindow: 86_400_000 therefore ports onto a three layer directly; the half-width names win only when set explicitly.

Streaming model#

Two tile sources cover the two shapes of dataset this renderer targets:

SourceStrategyUse
STTTileSourceEager — loads every tile of an archive once (optionally the union of every zoom level under lodMode: 'additive'), hands the layer the full set, and lets the GPU time-filter cull per frame. No viewport reselection, no per-frame rebuild.Small, local archives (the AV cockpit's ~20 s scenes).
StreamingTileSourceWraps the core SpatioTemporalTileset (the same selection/prefetch/eviction machinery the deck renderer uses). A camera-derived {bounds, zoom, time} viewport (via cameraToViewport, which casts a grid of NDC rays against the projection's reference surface — the ground plane on mercator, the sphere on globe — clamps grazing rays at the horizon so a near-horizontal view cannot claim the world, and takes the zoom from cameraToViewState) drives tileset.update, and onTilesChanged fires with the fresh resident tile set only when it actually changes (residentSetEqual).Heavy multi-km / wide-area datasets that can't be loaded eagerly.

TilesetBufferSource is the real playback BufferSource for a streaming dataset — it delegates buffered-runway / ranges / cost / ETA straight to the tileset's coverage index, replacing the always-complete:true createCompleteBufferSource used for eagerly-loaded (AV) sources, so the PlaybackGovernor gates honestly on how much sim-time is actually buffered.

react-three-fiber (@poopdeck.gl/three/r3f)#

<STTCanvas> owns the WebGPURenderer, a Z-up camera, MapControls (left-drag pans, matching the deck MapController gesture), the ground, and an optional follow-ego rig; layer components compose inside it declaratively. r3f's reconciler drives the lifecycle — mounting a layer adds it to the scene, unmounting disposes it — and tile loading is coordinated through React Suspense (each layer suspends on its archive load via useSTTTiles, with a bounded LRU across mount/unmount cycles).

import { STTCanvas, STTPointLayer } from '@poopdeck.gl/three/r3f';
function Viewport({ getTime }: { getTime: () => number }) {
return (
<STTCanvas
anchor={{ longitude: -122.4, latitude: 37.77 }}
timeOrigin={Date.now()}
getTime={getTime}
>
<STTPointLayer
url="/data/points/manifest.json"
colorProperty="category"
/>
</STTCanvas>
);
}

Every Stt*Layer component (STTSurfelLayer, STTPointLayer, STTPointCloudLayer, STTBoundingBoxLayer, STTMapPolygonLayer/STTPolygonLayer, STTMapLineLayer/STTPathLayer, STTOdLineLayer, STTArcLayer, STTIconLayer, STTTextLayer, STTMeshLayer, STTColumnLayer, STTTripsLayer, STTTripHeadsLayer, STTQuadbinLayer, STTH3Layer, STTHexbinLayer, STTHeatmapLayer, STTFlowmapLayer, STTFlowCorridorLayer, STTFlowStrokeLayer, STTIsoLayer, STTEgoLayer) takes the corresponding engine layer's options plus a url (archive manifest) and an optional lodMode/sourceRequired. STTGlobeBasemap mounts a static earth-sphere mesh for globe scenes; <STTAtmosphere> and <STTTiles3D> add a scattering atmosphere and a 3D-Tiles tileset to one (they are the reason @takram/three-atmosphere and 3d-tiles-renderer are optional peers).

STTCanvasProps#

FieldTypeDefaultDescription
anchorGeoAnchorlon/lat mapped to the world origin (the LocalEnuProjection anchor).
timeOriginnumberCommon time base (epoch-ms) every layer rebases to.
getTime() => numberPlayback clock — absolute playhead each frame.
registrySTTSourceRegistryPlayback governor registry; when set, each mounted layer registers a BufferSource so the transport's buffered bar / Auto-speed / ETA reflect this scene.
timeRange{ start, end }Reported to the governor as the buffered span.
followEgobooleanfalseCamera chases the STTEgoLayer pose with an exponential filter.
topDownbooleanfalseSteeper framing pitch.
pitchDeg / headingDegnumberExplicit initial camera pitch / heading (degrees), overriding the framing defaults.
backgroundstring'#05070d'Canvas background (CSS color string).
forceWebGLbooleanfalsePin the WebGL2 backend.
pixelRationumber | [number, number]clamped device ratio [1, 2]Device-pixel-ratio cap — the single biggest perf lever for fill-bound clouds on retina.
reducedMotionbooleanfalseSnap the camera instead of easing/damping (prefers-reduced-motion).
groundGroundOptions | false{}Metric reference grid, or false to omit.
onPick(info: STTPickInfo | null) => voidClick-to-inspect callback over registered pickable layers (boxes + ego); omit to disable picking.
renderFallbackReactNodebuilt-in messageShown when WebGPU/WebGL2 is unavailable or the canvas subtree errors.
fallbackReactNodenullSuspense fallback while layer archives load.

gl factory and the render loop#

The gl prop is an async factory (WebGPURenderer.init() before r3f's first render — no blank frame, no "render before init" warning). Because the STT layers read the playback TimeController in useFrame rather than reacting to React state, and r3f's frameloop="always" only repaints on demand under an async gl factory (camera/control events, not clock ticks), <STTCanvas> runs frameloop="never" and drives its own requestAnimationFrame pump (advance(now) every frame) so the scene tracks the external clock.

Picking#

Picking is hybrid — two mechanisms, one of them the declared one:

  • GPU id-colour picking (GpuPicker, encodeId/decodeId/buildIdColors, and STTPointLayer.pick()) — an opt-in off-screen id-buffer render pass + readback for merged-instance point clouds, resolved back to a feature via the InstanceProvenance merged-buffer identity contract (resolvePointPick). This is the mechanism the backend descriptor declares (pickMechanism: 'gpu-id', matching deck for the identical technique). Unit-tested on the resolve half; the live GPU pass is browser-verify-only (needs a device-backed harness).
  • CPU ray-OBB (pickBoxes/rayObbHit, wired by default in <STTCanvas> via onPick) — the complement covering the box/ego path: it hit-tests a pointer click against every registered pickable layer's boxes (objects + ego), which number in the tens. Picking here is genuinely hybrid; the descriptor has one pickMechanism slot and no 'hybrid' member, so it declares the id-buffer half.

Compared to @poopdeck.gl/layers (deck.gl)#

Feature@poopdeck.gl/three@poopdeck.gl/layers
RendererSingle WebGPURenderer (WebGPU, WebGL2 fallback), TSL node materialsWebGL2 (deck.gl)
Mercator projection
Globe projection✓ (ECEF mesh — real 3D sphere/ellipsoid, standard MVP depth)✓ (GlobeView — in-shader vertex warp)
Local metric (ENU) projection✓ (native — the AV cockpit frame)
Viewport streaming✓ (StreamingTileSource wraps the shared SpatioTemporalTileset)
GPU time filtering (window/wake/cumulative/trail)✓ (tsl/time-filter.ts, parity across all 4 modes)✓ (TimeFilterExtension)
BasemapHost-owned maplibre overlay canvas, camera-synced (not interleaved — WebGPU/WebGL contexts can't share a GL context)Interleaved (interleaved: true) or overlay
GPU heatmap aggregation✓ (STTHeatmapLayer — additive splat, then ramp resolve)✓ (AnimatedHeatmapLayer)
Live edge bundling✓ (lib/edge-bundler.ts over core/edge-bundling's shared bundleEdges)✓ (BundledFlowmapLayer)
Category-color GPU palette texture✓ (tsl/palette.ts + lib/palette.ts — stable slot, recolour = one texture swap)✓ (CategoryColorExtension)
Surfel / oriented-splat rendering✓ (STTSurfelLayer, ENU-only)✓ (SplatLayer/SplatPrimitiveLayer)
PickingGPU id-buffer (declared) + CPU ray-OBB for the box/ego pathGPU id-colour (Deck.pickObject)
fp64 precisionNot needed — RTC (relative-to-center f32 + f64 CPU origin) instead of an in-shader fp64 splitfp64 attribute split
16-attribute WebGL2 budgetNot applicable (WebGPU/TSL has no such ceiling)NoPickingPathLayer workaround needed for some layers

See backend-capabilities.md for the machine-generated, drift-guarded capability matrix across all four backends (deck / three / maplibre / cesium).

Limitations#

  • No WebGL1 fallback. WebGPURenderer requires WebGPU or WebGL2; older browsers/devices render nothing (<STTCanvas> shows a "needs WebGPU or WebGL2" fallback by default).
  • Basemap is a separate overlay canvas, never interleaved. TSL only compiles on WebGPURenderer, and WebGL/WebGPU contexts are non-interoperable — the maplibre basemap sits on its own camera-synced canvas underneath, so there is no per-pixel depth-weaving between 3D basemap content (extruded buildings, terrain) and STT layers; three content always composites on top.
  • STTSurfelLayer is ENU-only — the surfel orientation quaternions are baked at build time in the local-ENU render basis; there is no mercator/globe surfel port.
  • GlobeProjection defaults to a sphere, not the WGS84 ellipsoid — a sphere mis-registers geometry against a true ellipsoidal frame (e.g. Cesium's) by up to ~20 km at mid-latitudes. Pass datum: 'wgs84' explicitly when ellipsoid accuracy matters.
  • GPU id-colour picking is browser-verify-only. The pure index→feature resolve path (resolvePointPick) is unit-tested; the live off-screen render-and-readback pass needs a real WebGPU device and has not been verified end-to-end outside manual browser testing.
  • Per-tile-group time origin under streaming is not yet wired — all resident tiles currently rebase to one scene-wide timeOrigin, which is exact for AV-scale (second-to-minute) spans but can lose f32 precision for a streaming dataset spanning many days/years within one resident set.
  • Showcase wiring covers the geo cases, not the whole demo catalog. SttThreeGeoViewer is the geographic analog of the AV cockpit's AvThreeViewer and mirrors point, tripHeads, path, trips (including the flowMatrix / flowStroke corridor variants, which route to STTFlowCorridorLayer), arc, column, h3Summary, quadbinSummary, flowmap, flowmap-bundled, polygon and heatmap. The showcase's own composite types (radar, weather, lightning, worlds) and av never reach it: datasetSupportsThree keeps the three toggle off those demos, and av has the cockpit's own viewer. The remaining kinds the three backend itself declares — line, icon, boundingBox, surfel, text, mesh, pointCloud, hexbin, isoLines, ego, plus flowCorridor / flowStroke as standalone dataset types rather than trips variants — have no case in the viewer's switch and would render nothing; no shipped demo uses one (see renderer-architecture.md §5.2).

Live demo#

The AV cockpit (/drive/:sceneId in the showcase app) ships a live deck.gl ↔ Three.js + TSL toggle — the "TSL · WebGPU" button in the cockpit chrome swaps AvDeck for AvThreeViewer (@poopdeck.gl/three/r3f) against the same dataset, playback clock, and governor registry. Run pnpm dev from examples/showcase and open any Argoverse 2 or nuScenes drive scene.