Taos Engine · Geo Tutorials

Geo Tutorials · Step 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:

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


Prerequisites#

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#

TypeScript
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: { 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:

Shell
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#

TypeScript
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#

TypeScript
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#

TypeScript
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#

TypeScript
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):

TypeScript
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:

TypeScript
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:

Common mistakes#


Next#