Geo (Geospatial Globe Streaming)
The geo module streams planet-scale geospatial data — photorealistic 3D Tiles, OSM buildings, quantized-mesh terrain, imagery — and renders it through the standard Taos pipeline. The hard parts (floating-origin f64 precision, LOD by screen-space error, frustum culling, tile caching/eviction, attribution) are handled for you; you supply a data source and a camera.
New to the module? The Geo Tutorials are a guided, step-by-step path — start with Hello, Geo and work through anchors, layers, atmosphere, splats, clipping, and more. This page is the reference they build on.
It's the largest subsystem. Source lives in src/geo/. The
three types you touch most:
import { GeoFrame } from 'taos/geo/geo.js';
import { GeoScene } from 'taos/geo/geo_scene.js';
import { GeoFeature, reanchorCamera } from 'taos/geo/geo_feature.js';
import { resolveIonAsset, ION_ASSETS } from 'taos/geo/cesium_ion.js';
(Everything is also re-exported from the taos/geo/index.js barrel.)
The mental model#
GeoFrame— a floating origin. Geospatial coordinates (ECEF, ~6.3 million meters) would shred f32 precision, so the frame keeps an f64 origin anchored at some lat/lon and expresses everything relative to it. Near geometry stays in small, f32-safe world coordinates.GeoScene— the orchestrator. You add tilesets (3D Tiles, terrain) to it; each frame it traverses them against the camera, streams tiles within a memory budget, and produces pooled draw lists.GeoFeature— aRenderFeaturethat drives the scene each frame and appends the streamed tile draws toframe.opaque/frame.shadowCastersso the engine's stock geometry/shadow passes render them.- Data sources (
IonAsset,GoogleTilesSource,MapTilerTerrainSource) — resolve a provider + credentials into a root tileset URL.
So: frame anchors the world → scene streams tiles → feature feeds them to the renderer.
Minimal app#
The complete samples/geo_minimal.ts — OSM buildings on Cesium World Terrain at New York — is essentially this:
// Reversed-Z gives the depth precision a planet-scale far plane needs.
const engine = await Engine.create({
canvas,
contextOptions: { enableErrorHandling: true, reversedZ: true },
});
deferredPreset({
sky: { kind: 'color', color: [0.52, 0.72, 0.92, 1] },
ao: 'ssao',
// aerial perspective is opt-in (`lighting: { enableAerial: true }`); it's tuned
// for an atmosphere sky, so leave it off with this flat color.
})(engine);
// Sun + camera (note the huge far plane and reversed-Z above).
const cameraGO = new GameObject({ name: 'Camera' });
cameraGO.setPosition(0, 400, 2000);
cameraGO.addComponent(Camera.createPerspective(60, 1, 5_000_000, engine.ctx.width / engine.ctx.height));
// Fly controller as a component: the engine updates it and attaches its input.
cameraGO.addComponent(CameraController.create({ pitch: -0.15, speed: 250 }));
engine.scene.add(cameraGO);
// Floating origin at New York + two streamed ion tilesets.
const geo = new GeoScene(engine.ctx.device, GeoFrame.atLonLat(-74.0060, 40.7128, 0));
geo.addTerrain(await resolveIonAsset(ION_ASSETS.worldTerrain, token));
geo.add3DTiles(await resolveIonAsset(ION_ASSETS.osmBuildings, token));
// `attach` registers a GeoFeature (which appends to frame.opaque / frame.shadowCasters)
// BEFORE the stock Shadow + Geometry features that read those lists, and — given the
// camera — installs the floating-origin auto-reanchor that keeps world coords f32-safe.
geo.attach(engine, { cameraGO });
engine.run();
GeoFrame — the floating origin#
GeoFrame.atLonLat(lonDeg, latDeg, height = 0); // anchor on the WGS84 ellipsoid
GeoFrame.atEcef(originEcef, up); // anchor anywhere (Moon, other bodies)
It exposes the transforms between ECEF (f64) and engine world space:
frame.worldFromEcefPoint(p); // ECEF → world position
frame.worldFromEcefDir(d); // ECEF → world direction (rotation only)
frame.ecefFromWorldPoint(w); // world → ECEF
frame.reanchor(newOriginEcef); // move the origin, keep the tangent basis fixed
Reanchoring. As the camera roams away from the origin, world coordinates
grow until f32 precision suffers. Call reanchorCamera(frame, cameraGO, reanchorDist = 10_000) from beforeFrame (before the engine caches camera
matrices). When the camera passes reanchorDist meters from the origin, it
rebases the frame and shifts the camera so nothing moves on screen.
Geodesy helpers (geodeticToEcef, lonLatToEcef, ecefToGeodetic, the WGS84
constants) live in geo.ts.
GeoScene#
const geo = new GeoScene(device, frame, opts?);
GeoSceneOptions: budgetBytes (GPU budget per tileset, default 512 MiB),
budgetTris (~30M), maxConcurrent (in-flight loads, 16), retainFrames
(eviction grace window, 240), evictHard (soft-overage multiplier, 2).
Add datasets — both return immediately with the tileset and a ready promise
(it's safe to update() before ready resolves; the tileset no-ops):
const { tileset, ready } = geo.add3DTiles(asset, {
// GeoTilesetOptions
lit: true, // light unlit materials (default true)
collision: true, // retain CPU triangle soup for physics (TileContent.collision)
albedoBoost: 1.0, // brighten dark content
bodyOffset, // ECEF offset for non-Earth bodies
regionEllipsoid: { a, b }, // ellipsoid for region-bound tiles
maxRenderDistance: 5000, // render-distance cap (m); default 5 km, Infinity = draw to horizon
});
await ready;
tileset.maxSSE = 12; // target screen-space error in pixels (default 16; lower = sharper, costlier)
const terr = geo.addTerrain(asset, { imagery: true }); // quantized-mesh terrain
Other methods:
geo.addStatic(content); geo.removeStatic(content); // one-off baked content (see anchors)
geo.remove(tileset); geo.clear();
geo.heightAt(lonRad, latRad); // terrain height (e.g. to clamp buildings); null if unloaded
geo.setBudget(bytes, tris);
geo.cacheStats();
geo.lastResult; // the previous frame's GeoFrameResult (copyrights, triangles, …)
You normally don't call geo.update() yourself — GeoFeature does it. Its
GeoFrameResult carries opaque, shadowCasters, water, copyrights,
triangles, renderedLodLevels, terrainContents, and tileContents.
GeoFeature — and geo.attach#
GeoScene.attach is the one-call wiring used above: it constructs a GeoFeature,
registers it in the right slot, and (given a camera) installs the floating-origin
reanchor.
const feature = geo.attach(engine, {
cameraGO, // installs autoOriginShift; omit to pin the origin
reanchorDist: 10_000, // reanchor threshold (m) for the auto origin shift
// ...any GeoFeature option below also passes through...
});
The underlying GeoFeature options:
engine.addFeatureBefore(new GeoFeature({
scene: geo,
fovY: undefined, // screen-space-error FOV; defaults to the camera's live `fov`
// (read every frame) — pass a number only to override it
cull: () => true, // live frustum-cull toggle
dt: (f) => f.dt, // LOD cross-fade dt; return 0 to disable the fade
draw: () => true, // stream but emit nothing when false
shadows: () => true,
materialOverride: (lod) => null, // LOD-debug coloring
}), ShadowFeature.name);
Order is critical: GeoFeature appends to frame.opaque /
frame.shadowCasters, so it must run before ShadowFeature and the geometry
feature that consume those lists — hence addFeatureBefore(..., ShadowFeature.name)
(which attach does for you).
geo.autoOriginShift(engine, cameraGO, reanchorDist?) is the reanchor half on its
own — attach calls it when you pass cameraGO; call it directly if you wire the
GeoFeature by hand but still want automatic origin shifting.
Data sources#
| Provider | How to get a source | Credential |
|---|---|---|
| Cesium ion | await resolveIonAsset(assetId, ionToken) → IonAsset |
Bearer token |
| Google 3D Tiles (direct) | resolveGoogleTiles(apiKey) → GoogleTilesSource |
API key in URL |
| MapTiler terrain | resolveMapTilerTerrain(apiKey) → MapTilerTerrainSource |
API key in URL |
ION_ASSETS has the well-known ion asset IDs:
ION_ASSETS.googlePhotorealistic; // 2275207 — Google Photorealistic 3D Tiles via ion
ION_ASSETS.osmBuildings; // 96188
ION_ASSETS.worldTerrain; // 1 — Cesium World Terrain (quantized-mesh)
ION_ASSETS.bingAerial; // 2
ION_ASSETS.cesiumMoon; // 2684829 — Moon terrain (3D Tiles)
All implement the GeoDataSource interface
(kind, label, rootUrl, attributions, fetchJson, fetchArrayBuffer).
Anchors — placing models on the globe#
Drop a glTF model at a geodetic position and add it as static content:
import { loadAnchor } from 'taos/geo/anchor.js';
const content = await loadAnchor(device, geoFrame, '/models/duck.glb', {
lonDeg: -122.4194, latDeg: 37.7749, height: 50,
scale: 10, headingDeg: 45, // rotation about local up (0 = north)
// bodyCenter, ellipsoid — for non-Earth bodies
});
geo.addStatic(content);
Water & wireframe overlays#
GeoScene exposes waterDraws (quantized-mesh watermask surfaces) and
opaqueDraws, which feed two optional features:
GeoWaterFeature— a translucent water pass driven bygeo.waterDraws.GeoWireframeFeature— a debug edge overlay; construct with{ drawItems: () => geo.opaqueDraws, color }.
Samples#
- samples/geo_minimal.ts — the clearest end-to-end reference (OSM + terrain at New York).
- samples/planet_explorer.ts — the full explorer: ion/Google/MapTiler sources, terrain + buildings, height clamping, data-source logging.
- samples/geo_moon.ts —
GeoFrame.atEcef, a custom sphere ellipsoid, and a non-Earth body offset. - samples/geo_physics.ts — terrain colliders + Jolt balls (also see the Physics guide).
- samples/geo_photo.ts — anchors and pixel→geo ray casting.
Gotchas#
- Use reversed-Z and a huge far plane. Create the engine with
contextOptions: { reversedZ: true }and the camera with a far plane in the millions; planet scale demands the depth precision. - Wire the scene in with
geo.attach(engine, { cameraGO })— it registers theGeoFeaturebefore the stock features (which read the draw lists it appends to; add it after and tiles render under your scene or not at all) and installs the reanchor. - Reanchoring happens every frame, before the engine caches camera matrices.
attachinstalls it fromcameraGO; doing it by hand, callreanchorCamerainbeforeFrame. Skip it entirely and far-from-origin coordinates jitter. - The LOD FOV tracks the camera automatically —
GeoFeaturereads the camera's livefoveach frame. Only an explicitfovYoverride (or a mismatchedcullProjection) can desync it. - Credentials are yours to supply. ion needs a Bearer token; Google/MapTiler put the API key in the URL. Don't ship a hard-coded production token.
- Decode tiles off the main thread.
add3DTiles/add3DTiles-style options default touseWorker: () => false, which decodes streamed glTF/Draco tiles on the render thread — the most common cause of poor framerate while flying over a city. Pass{ useWorker: () => true }to move decode to a worker. (Quantized-mesh terrain already bakes in a worker pool.) - Streaming is async and budget-bound. Tiles stream in over several frames
and evict under the memory budget;
await readyonly means the root loaded. - Lower
maxSSE= sharper but heavier. It's per-tileset; tune per dataset.
See also#
- Quick-Start Guide — the engine, features, and reversed-Z context option
- Physics guide — pairing terrain colliders with rigid bodies