Taos Engine ▦ Taos: API Documentation

8 — Projection Textures

Live demo: run this tutorial in your browser — built from src/08_projection_textures.ts. Toggle the projector with T (mode), B (blend), L (lit).

A projector casts an image onto whatever surfaces are already in front of it — terrain, buildings, anything in the G-buffer — like a slide projector or a map overlay draped over the relief. It reconstructs each shaded pixel's world position from depth, tests it against the projector's frustum, and composites the image where it lands. Sources: samples/geo_osm_projection.ts, src/engine/components/projector.ts.

Turning the feature on#

Projectors are an opt-in pair of render features. Pass projectors: true to the deferred preset (the geo tutorials wrap it in createGeoEngine):

const { engine, device, cameraGO, controller } = await createGeoEngine(
  canvas, { projectors: true }, { y: 600, z: 1500, pitch: -0.35 },
);

That adds two features: the unlit ProjectorFeature (composites the image after lighting, a flat overlay) and the lit ProjectorDecalFeature (writes the image into the G-buffer albedo before lighting, so it picks up scene light and shadow like paint on the surface). A Projector is routed to one or the other by its lit flag.

The Projector component#

A Projector is a Component. Its GameObject's world transform supplies the projector's position and forward (local −Z); the shape/range fields define the frustum the image is cast through.

import { Projector } from 'taos/engine/index.js';

const projGO = new GameObject({ name: 'CityProjector' });
const projector = projGO.addComponent(new Projector());
projector.source = { kind: 'texture', texture: myGpuTexture };
projector.blend = 'alpha';      // 'alpha' | 'add' | 'multiply'
projector.edgeFalloff = 0.05;   // soften the frustum edge
projector.near = 1;
projector.far = 6000;
engine.scene.add(projGO);

Key fields:

Field Meaning
source { kind: 'texture' | 'atlas' | 'video', ... } — what gets projected
shape 'rect' (orthographic box) or 'perspective' (slide-projector frustum)
orthoWidth / orthoHeight footprint size in meters when shape === 'rect'
fovY / focalLength vertical FOV (or a physical lens) when shape === 'perspective'
aspect width/height of the projected image
near / far frustum depth range; surfaces beyond far get nothing
blend 'alpha' (sticker), 'add' (light), 'multiply' (stain/gobo)
opacity, tint, crop, edgeFalloff strength, color, sub-rect, edge softening
lit false = unlit overlay; true = painted into albedo, lit + shadowed

The source here is a plain GPUTexture you own — in the sample it's a small translucent "target" drawn into a 2D canvas, so the transparent background lets the alpha blend read as a sticker on the city.

Top-down vs. perspective#

The two shapes give two very different looks:

  • 'rect' — an orthographic box. Point it straight down and it drapes the image over the relief with no perspective foreshortening: a map overlay.
  • 'perspective' — a real frustum cast from the projector's position, like a slide projector throwing the image across the scene from an angle.

Anchoring to the ground through reanchors#

Because the floating origin shifts as you fly (Tutorial 2), a projector placed once in world space would drift off the terrain. Anchor it in ECEF instead and re-derive its world transform every frame from the live GeoFrame:

const anchorEcef = geodeticToEcef(LON * DEG2RAD, LAT * DEG2RAD, GROUND_M);

function placeProjector(): void {
  const fr = geo.frame;
  const a = fr.worldFromEcefPoint(anchorEcef);            // ground in world space now
  const ground = new Vec3(a.x, a.y, a.z);
  const ud = fr.worldFromEcefDir(fr.up);
  const up = new Vec3(ud.x, ud.y, ud.z).normalize();      // local "up" at the anchor

  // Top-down map drape: hang above the ground, look straight down.
  projGO.setPosition(...ground.add(up.scale(RIG_UP)).toArray());
  projector.shape = 'rect';
  projector.orthoWidth = projector.orthoHeight = FOOTPRINT;
  projGO.rotation = lookRotation(up.scale(-1));
}

Call placeProjector() in beforeFrame so the image stays locked to the city no matter how far you roam. geo.attach(engine, { cameraGO }) installs the floating-origin reanchor in beforeFrame; register placeProjector after it so it re-derives the projector transform against the freshly reanchored frame.

geo.attach(engine, { cameraGO });   // installs the floating-origin reanchor

engine.beforeFrame(() => {
  placeProjector();
});

Lit vs. unlit#

Flip projector.lit to move the same projector between the two features:

  • lit = false (default): a flat, self-lit overlay composited on top of the lit scene — good for HUD markers, reticles, map labels.
  • lit = true: the image is written into the albedo before lighting, so it's shaded and shadowed like it was painted onto the building or terrain — good for decals, posters, projected signage.

The projector reads the deferred G-buffer, so it works the same on 3D tiles and quantized-mesh terrain together. geo_osm_projection.ts is the full version of this tutorial: a night-lit Grand Canyon with a file picker (project any image), a visible frustum wireframe, and live tunables.

Next#