Taos Engine ▦ Taos: API Documentation

1 — Hello, Geo

Live demo: run this tutorial in your browser — built from src/01_hello_geo.ts in the standalone project.

The geo equivalent of Hello, Cube: a complete, runnable app that streams OpenStreetMap buildings on AWS Terrarium elevation at New York and lets you fly around — using only keyless data sources, so there's no Cesium ion token to set up. Distilled from samples/geo_osm_buildings.ts — the clearest end-to-end keyless reference in the repo.

By the end you'll have a globe streaming real photorealistic-scale data through the same deferred pipeline you used for the cube. The hard parts — f64 floating-origin precision, screen-space-error LOD, frustum culling, tile caching/eviction, attribution — are handled by three types:

  • GeoFrame — a floating origin. It anchors an f64 point on the globe and expresses everything relative to it, so near geometry stays in f32-safe world coordinates. (Tutorial 2 goes deep on this.)
  • GeoScene — the orchestrator. You add tilesets 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 streamed tile draws to frame.opaque / frame.shadowCasters, so the engine's stock geometry and shadow passes render them. You rarely construct it directly — GeoScene.attach registers it (and the floating-origin reanchor) for you.

Frame anchors the world → scene streams tiles → feature feeds them to the renderer.


Prerequisites#

  • You've read Hello, Cube — same project scaffold (package.json, tsconfig.json, vite.config.ts, index.html).
  • No API key. Both datasets we stream are keyless and free: AWS Terrarium elevation (Mapzen/AWS open elevation tiles) and OpenFreeMap vector-tile buildings (OSM-derived). Some other sources do need credentials — Cesium ion wants a Bearer token, Google and MapTiler want a key — but this tutorial avoids all of that. (Tutorial 10's Moon is the one place we still reach for ion.)

The project layout and the four config files are identical to Hello, Cube — only src/main.ts changes. If you're starting fresh, copy that project first.

Want to run it now? This exact app is build-verified at standalone/geo_tutorials/ (entry src/01_hello_geo.ts). cd standalone/geo_tutorials && npm install && npm run dev, then open 01_hello_geo.html. Each tutorial there is self-contained — the engine/camera boilerplate is inlined per file, exactly as the walkthrough below spells it out.


src/main.ts#

import { Vec3 } from 'taos/math/index.js';
import {
  Engine, GameObject, Camera, DirectionalLight, deferredPreset,
} from 'taos/engine/index.js';
import { CameraController } from 'taos/engine/camera_controller.js';
import { GeoFrame, GeoScene } from 'taos/geo/index.js';
import { AWS_TERRARIUM, ESRI_WORLD_IMAGERY, OPENFREEMAP_VECTOR } from 'taos/geo/index.js';

async function main(): Promise<void> {
  const canvas = document.getElementById('canvas') as HTMLCanvasElement;

  // Reversed-Z gives the depth precision a planet-scale far plane needs.
  const engine = await Engine.create({
    canvas,
    contextOptions: { enableErrorHandling: true, reversedZ: true },
  });

  // The deferred renderer preset.
  deferredPreset({
    sky: { kind: 'color', color: [0.52, 0.72, 0.92, 1] },
  })(engine);

  // A sun for the deferred lighting.
  const sunGO = new GameObject({ name: 'Sun' });
  sunGO.addComponent(new DirectionalLight(
    new Vec3(-0.5, -1, -0.35).normalize(), new Vec3(1, 0.97, 0.9), 3));
  engine.scene.add(sunGO);

  // Camera ~2 km from the New York origin, tilted down, with a fly controller.
  // Note the millions-of-meters far plane (paired with 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));
  engine.scene.add(cameraGO);

  // CameraController is a Component: added to the camera, the engine updates it
  // every frame and auto-attaches its mouse/keyboard listeners to the canvas.
  const controller = CameraController.create({
    yaw: 0, pitch: -0.15, speed: 250, sensitivity: 0.002,
  });
  cameraGO.addComponent(controller);

  // The geo scene: floating origin at New York + two keyless streamed sources.
  const geo = new GeoScene(engine.ctx.device, GeoFrame.atLonLat(-74.0060, 40.7128, 0));
  // AWS Terrarium elevation, draped with Esri imagery. Terrain first, so the
  // buildings clamp to its height as they stream in.
  geo.addRasterDemTerrain(AWS_TERRARIUM, { imagery: ESRI_WORLD_IMAGERY });
  // OpenFreeMap vector-tile buildings: OSM footprints extruded by height. Decoding
  // happens off the main thread automatically.
  geo.addVectorTiles(OPENFREEMAP_VECTOR);

  // One call wires the scene into the pipeline: it registers a GeoFeature (just before
  // the stock Shadow + Geometry features, which read frame.opaque / frame.shadowCasters)
  // and installs the floating-origin auto-reanchor for the camera. Sections 4 and 5
  // below unpack both halves.
  geo.attach(engine, { cameraGO });

  engine.run();
}

main().catch((e: unknown) => {
  const msg = e instanceof Error ? (e.stack ?? e.message) : String(e);
  document.body.innerHTML = `<pre style="color:#f88;padding:1rem;font-family:ui-monospace,monospace">${msg}</pre>`;
  console.error(e);
});

No token to paste — just:

npm install
npm run dev

Drag to look, WASD to fly. Tiles stream in over a few seconds and sharpen as you approach — that's screen-space-error LOD pulling finer tiles.


What's new compared to Hello, Cube#

Five things, and only five:

1. Reversed-Z and a giant far plane#

contextOptions: { enableErrorHandling: true, reversedZ: true }
// ...
Camera.createPerspective(60, 1, 5_000_000, aspect)

A planet is ~12,700 km across. A normal depth buffer can't resolve a 1 m building against a 5,000 km far plane — you'd get catastrophic z-fighting. Reversed-Z redistributes depth precision so the near field stays crisp at any far plane. Always pair these two for geo.

2. A GeoFrame as the floating origin#

GeoFrame.atLonLat(-74.0060, 40.7128, 0)   // lon, lat, height(m)

This anchors the world at New York. Engine world-space (0,0,0) is that lat/lon; everything streams in relative to it. Tutorial 2 explains why this matters and what reanchorCamera is doing.

3. A GeoScene holding tilesets#

geo.addRasterDemTerrain(AWS_TERRARIUM, { imagery: ESRI_WORLD_IMAGERY });
geo.addVectorTiles(OPENFREEMAP_VECTOR);

addRasterDemTerrain streams a raster height-map (DEM) and turns it into terrain mesh on the GPU; addVectorTiles streams OSM vector tiles and extrudes the building layer into 3D. Both return immediately with { tileset, ready } — the ready promise resolves when the root tile loads, but it's safe to render before then (the tileset no-ops until it's ready). Tutorial 3 stacks more layers.

The keyed alternatives. If you do have a Cesium ion token, the higher-fidelity swap is geo.addTerrain(await resolveIonAsset(ION_ASSETS.worldTerrain, token)) (quantized-mesh world terrain) and geo.add3DTiles(await resolveIonAsset( ION_ASSETS.osmBuildings, token), { useWorker: () => true }) (the Cesium OSM Buildings 3D Tileset). Same GeoScene API — just a different data source.

4. Wiring it in with geo.attach#

geo.attach(engine, { cameraGO });

attach bundles the two things every geo app does. First, it registers a GeoFeature — the RenderFeature that drives the scene each frame — before the stock shadow and geometry features. That ordering is the single easiest thing to get wrong: GeoFeature appends streamed draws to the frame's draw lists, and those features consume them, so geo must run before them or your tiles render under your scene or not at all. Second, given cameraGO, it installs the floating-origin reanchor (next section).

The screen-space-error LOD needs the camera's vertical FOV; GeoFeature reads it live from the camera each frame, so there's nothing to pass or keep in sync — change the camera's FOV and the LOD follows. (Pass an explicit fovY only to override it, e.g. when you also supply a cullProjection built with a different FOV.)

Prefer to wire it by hand? attach is exactly this — reach for it when you need to order other per-frame work around the reanchor (as tutorials 4, 6 and 8 do):

engine.addFeatureBefore(new GeoFeature({ scene: geo }), ShadowFeature.name);
engine.beforeFrame(() => reanchorCamera(geo.frame, cameraGO));

5. The floating-origin reanchor#

Passing cameraGO to attach installs this in beforeFrame:

engine.beforeFrame(() => {
  reanchorCamera(geo.frame, cameraGO);
});

reanchorCamera rebases the floating origin onto the camera once it drifts past ~10 km, shifting the camera so nothing moves on screen. It runs in beforeFrame, before the engine caches camera matrices. Skip it and far-from-origin coordinates jitter. Tutorial 2 is all about this. (Omit cameraGO — as tutorial 9 does — to pin the origin instead, e.g. to keep baked physics colliders valid.)


Performance#

If the framerate is poor while flying, in priority order:

  • Raise maxSSE to pull fewer/coarser tiles: const { tileset } = geo.addVectorTiles(...); tileset.maxSSE = 24; (default 16; higher = faster, blurrier). The same knob exists on the terrain tileset.
  • Decode tiles off the main thread. The vector-tile builder already decodes in a worker. For ion 3D Tiles (add3DTiles) pass { useWorker: () => true } — without it every streamed glTF/Draco tile is decoded inside the frame loop, hitching the picture the whole time tiles stream over a city.
  • Build for production. npm run dev serves the engine unbundled; npm run build && npm run preview is markedly smoother.
  • Trim the preset. deferredPreset turns on PCSS shadows, SSAO, and TAA by default. Dropping shadow: false or ao: false buys frames on weaker GPUs.

Common mistakes#

  • Tiles invisible / under the ground. GeoFeature ended up after the shadow/geometry features. geo.attach orders it before them for you; if you wire it by hand, use addFeatureBefore(..., ShadowFeature.name).
  • Z-fighting / shimmering surfaces. You forgot reversedZ: true or used a small far plane.
  • Wrong LOD (too blurry or stuttering). Usually a cullProjection whose FOV disagrees with the camera, or an explicit fovY override that's stale. With neither set, GeoFeature reads the camera's live FOV, so this doesn't happen.
  • 401/403 in the console. Only relevant for keyed sources — a bad ion token or Google/MapTiler key. The keyless AWS/OpenFreeMap sources here don't authenticate, so this won't happen with the tutorial as written.
  • Nothing loads but no errors. Streaming is async and budget-bound; await ready only means the root loaded. Give it a few frames.

Next#