Taos Engine ▦ Taos: API Documentation

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 — a RenderFeature that drives the scene each frame and appends the streamed tile draws to frame.opaque / frame.shadowCasters so 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 by geo.waterDraws.
  • GeoWireframeFeature — a debug edge overlay; construct with { drawItems: () => geo.opaqueDraws, color }.

Samples#


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 the GeoFeature before 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. attach installs it from cameraGO; doing it by hand, call reanchorCamera in beforeFrame. Skip it entirely and far-from-origin coordinates jitter.
  • The LOD FOV tracks the camera automaticallyGeoFeature reads the camera's live fov each frame. Only an explicit fovY override (or a mismatched cullProjection) 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 to useWorker: () => 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 ready only means the root loaded.
  • Lower maxSSE = sharper but heavier. It's per-tileset; tune per dataset.

See also#