Taos Engine ▦ Taos: API Documentation

Core (taos)

Everything re-exported from the top-level barrel src/index.ts — the symbols you get from a bare import { … } from 'taos/index.js'. It is the union of four subsystems:

// src/index.ts
export * from './math/index.js';      // Vec/Mat/Quaternion, noise, random
export * from './engine/index.js';    // Engine, GameObject, Component, Scene, components, controllers, features, presets
export * from './renderer/index.js';  // RenderContext, Material/PbrMaterial, every feature + render-graph pass
export * from './assets/index.js';    // Mesh, Texture, Shader, GltfLoader, AssetManager

This guide is the reference map of that surface. The narrative for the engine/scene/preset model lives in the Quick-Start Guide — read that first. This page fills in the parts the quick-start doesn't drill into: the math primitives, assets, input controllers, animation, the full feature catalog, and the renderer core classes.

import {
  Vec3, Mat4, Quaternion,           // math
  Engine, GameObject, Camera,       // engine
  Mesh, GltfLoader, Texture,        // assets
  RenderContext, PbrMaterial,       // renderer
  deferredPreset,                   // presets
} from 'taos/index.js';

Already covered in the Quick-Start Guide: Engine, GameObject (+ its Transform: local TRS, cached world matrix, world-space accessors), Component, Scene, Camera, the lights, MeshRenderer, Material/PbrMaterial basics, the render presets, the per-frame hooks, and the manual render graph. This page does not repeat them in depth.


Math (src/math/)#

Small, allocation-conscious primitives. Most operators return a new object; the set* / *InPlace / copyFrom variants mutate in place — prefer those in per-frame hot paths to avoid garbage.

Vectors — Vec2, Vec3, Vec4#

const v = new Vec3(0, 1, 0);
v.set(1, 2, 3);                 // in place, returns this
const w = v.add(other);         // new Vec3
const u = v.normalize();        // new (zero vector stays zero)
const d = v.dot(other);
const c = v.cross(other);       // Vec3, right-handed
v.scaleInPlace(2);              // in place

Vec3 ships direction constants/factories: Vec3.UP/DOWN/FORWARD/BACKWARD/RIGHT/LEFT (and Vec3.up(), …), plus fromArray(a, offset?). Vec2/Vec4 have the same shape (set, add, sub, scale, dot, length, lengthSq, normalize, clone, toArray).

Mat4#

Column-major; .data is the backing Float32Array(16). Static factories return new matrices; set* methods write in place.

Mat4.identity();
Mat4.translation(x, y, z);
Mat4.scale(x, y, z);
Mat4.rotationX(rad);  Mat4.rotationY(rad);  Mat4.rotationZ(rad);
Mat4.fromQuaternion(qx, qy, qz, qw);
Mat4.trs(t /*Vec3*/, qx, qy, qz, qw, s /*Vec3*/);   // Translate × Rotate × Scale
Mat4.perspective(fovY, aspect, near, far);          // WebGPU clip [0,1]
Mat4.perspectiveReversed(fovY, aspect, near, far);  // reversed-Z
Mat4.orthographic(l, r, b, t, near, far);
Mat4.lookAt(eye, target, up);

const m = a.multiply(b);       // new (a × b)
const p = m.transformPoint(v); // affine + perspective divide
const n = m.normalMatrix();    // inverse-transpose of upper 3×3
const i = m.invert();          // identity if singular
// in-place, alias-safe: m.setMultiply(a, b), m.copyFrom(src), m.setTrs(...)

Quaternion#

Stored (x, y, z, w), w scalar. set* mutate in place; ops return new.

const q = new Quaternion();
q.setAxisAngle(axis /*Vec3*/, rad);    // in place
q.setEuler(x, y, z);                   // intrinsic XYZ, radians
const r = q.multiply(other);           // Hamilton product, new
const s = q.slerp(other, t);           // new
const m = q.toMat4();
// statics: Quaternion.identity(), fromAxisAngle(axis, rad), fromEuler(x, y, z)

A GameObject's rotation is a Quaternion; setAxisAngle/setEuler are the no-allocation way to drive it each frame.

Frustum#

const f = new Frustum();
f.setFromViewProj(viewProjMat4);                  // Gribb–Hartmann extraction
f.intersectsSphere(cx, cy, cz, r, planeCount?);   // planeCount=5 skips the near plane

Noise & random#

perlinNoise3(x, y, z, xWrap?, yWrap?, zWrap?);                 // ~[-1, 1]
perlinNoise3Seed(x, y, z, xWrap, yWrap, zWrap, seed);
perlinFbmNoise3(x, y, z, lacunarity, gain, octaves);          // fractal sum
perlinRidgeNoise3(x, y, z, lacunarity, gain, offset, octaves);// sharp ridges
perlinTurbulenceNoise3(x, y, z, lacunarity, gain, octaves);   // billowy
perlinNoise3WrapNonpow2(x, y, z, xWrap, yWrap, zWrap, seed);  // arbitrary tiling period ≤256

const rng = new Random(seed);    // Xorwow PRNG
rng.randomFloat(min?, max?);     // inclusive; defaults [0,1]
rng.randomUint32();
Random.global;                   // shared instance
halton(index, base);             // low-discrepancy (base 2,3 for 2D jitter)

Assets (src/assets/)#

Mesh#

Procedural factories and fromData. The keepData: true option retains CPU vertex/index arrays (needed by, e.g., SDF baking).

Mesh.createCube(device, size = 1, opts?);
Mesh.createBox(device, w, h, d, opts?);
Mesh.createSphere(device, radius = 0.5, latSeg = 32, lonSeg = 32, opts?);
Mesh.createPlane(device, w = 10, depth = 10, segX = 1, segZ = 1, opts?);
Mesh.createCone(device, radius = 0.5, height = 1, segments = 16, opts?);
Mesh.createTorus(device, major = 0.5, minor = 0.2, majorSeg = 32, minorSeg = 16, opts?);
Mesh.fromData(device, vertices /*Float32Array*/, indices /*Uint32Array*/, opts?);
// opts (MeshDataRetention): { keepData?, colors?, storageBuffers? }

A mesh exposes vertexBuffer / indexBuffer / indexCount, a local bounding sphere (boundsCenterX/Y/Z, boundsRadius), optional colorBuffer, and destroy(). The interleaved layout is VERTEX_STRIDE (48 bytes: position·3, normal·3, uv·2, tangent·4) described by VERTEX_ATTRIBUTES; optional per-vertex color uses VERTEX_COLOR_STRIDE / VERTEX_COLOR_ATTRIBUTES.

SkinnedMesh is the rigged equivalent (SKINNED_VERTEX_STRIDE = 112 bytes, adding two joint-index + weight sets); usually built for you by GltfLoader.

Texture#

const tex   = await Texture.fromUrl(device, '/t/albedo.png', { srgb: true });
const solid = Texture.createSolid(device, 255, 255, 255);      // 1×1 rgba8unorm
const fromBmp = Texture.fromBitmap(device, imageBitmap, { srgb: true, mipLevelCount });
// also: Texture.fromCompressed(device, { format, width, height, levels }) for KTX2/Basis

tex.gpuTexture / tex.view / tex.type (TextureType = '2d' | '3d' | 'cube'). For HDR / IBL specifically, see the HDR + IBL loaders in src/assets/ (not on the top-level barrel — import directly).

GltfLoader#

Static, async, all on the device:

// Full model — skins, animations, node graph, lights, materials.
const model = await GltfLoader.load(device, '/m/fox.glb', { keepData?, smoothNormals? });
// Flat, non-skinned primitives (faster; skips skin/animation parsing).
const staticModel = await GltfLoader.loadStatic(device, '/m/rock.glb', { storageBuffers?, recenter? });
// Pre-fetched buffers:
await GltfLoader.loadFromArrayBuffer(device, buf, opts?);
await GltfLoader.loadStaticFromArrayBuffer(device, buf, opts?);

GltfModel carries meshes, skins (+ skin), clips (AnimationClip[]), nodes / nodeOrder / roots, meshInstances, lights, materials, bounds, and destroy(). GltfStaticModel is the lighter, skin-free form. Feed a GltfModel to an AnimatedModel (below) to play its clips.

AssetManager#

A simple named registry (addMesh/getMesh, addTexture/getTexture, addShader/getShader, destroy()). Shader wraps a compiled GPUShaderModule (new Shader(device, wgslCode, label)). createCloudNoiseTextures(device) returns the 3D base/detail noise volumes the cloud feature uses.


Input controllers (src/engine/)#

CameraController#

A dual free-fly + orbit mouse/keyboard camera (in the spirit of PlayCanvas's CameraControls). Forward is −Z; yaw rotates about +Y, pitch about local +X (clamped to ±89°). The mouse wheel dollies (fly) or zooms (orbit).

CameraController is a Component. Add it to the camera's GameObject and the engine drives it for you — it lazily attaches its mouse/keyboard listeners to the engine canvas and updates against its owner every frame, so no beforeFrame plumbing is needed:

const controller = CameraController.create({
  yaw: 0, pitch: -0.15,
  speed: 250,            // units/sec
  sensitivity: 0.002,    // radians/pixel
  pointerLock: false,
});
cameraGO.addComponent(controller);   // engine updates + auto-attaches it

If you drive the frame loop yourself (a raw render-graph sample, the editor, a test) — or need the controller to run in a specific order relative to other beforeFrame work (e.g. floating-origin reanchoring, see the geo tutorials) — use the standalone form instead: attach once, then update(cameraGO, dt) each frame.

controller.attach(canvas);
engine.beforeFrame((f) => controller.update(cameraGO, f.dt));

Vertical movement defaults to verticalKeyMode: 'hybrid' — Space or KeyE ascends, ShiftLeft or KeyQ descends. Set it to 'space-shift' or 'eq' to restrict to a single pair.

mode selects the scheme; both share the same yaw/pitch + position state, so you can flip it live with no snap:

  • 'fly' (default) — FPS free-fly: WASD + mouse-look, ControlLeft for a 3× boost, wheel to dolly along the view (wheelDolly/dollySpeed).
  • 'orbit' — turntable around a focus point at focusDistance: left-drag orbits, middle/right-drag (or Shift+left-drag) pans, wheel zooms (clamped to minDistance/maxDistance), WASD flies the focus. focusOn(point, distance?) recenters the pivot. Set enableDamping (with damping seconds) for eased orbit/pan/zoom and a smooth fly-to on focus.
const orbit = CameraController.create({
  mode: 'orbit', focusDistance: 12, minDistance: 2, maxDistance: 200,
  enableDamping: true,
});
orbit.focusOn(target.position, 8);   // recenter (eased when damping is on)
orbit.mode = 'fly';                  // flip live; no snap

Notable tunables: worldUp (set per-planet for curved-surface walking — works in orbit too), moveRelativeToView (true for 6-DOF space flight), and the programmatic inputForward/inputStrafe/inputUp/… fields for driving it without a keyboard.

Touch & gamepad#

Both are lazy — they cost nothing until the first touch / gamepad connect:

// Wires touch look + a joystick straight into a CameraController.
setupCameraTouchControls(canvas, controller, { lookScale: 1.5, addVerticalButtons: true });

// Lower-level: setupTouchControlsLazy(canvas, opts, onInit?) → { controls, cancel }
// Gamepad: setupGamepadControlsLazy(opts, onInit?) → { controller, cancel }
const pad = setupGamepadControlsLazy({
  leftStick:  { onChange: (fwd, strafe) => { /* move */ } },
  rightStick: { lookScale: 800, onLook: (dx, dy) => { /* look */ } },
  buttons: [{ button: 'A', onDown: jump }],
});

STANDARD_GAMEPAD_BUTTONS maps the standard-layout button names. Haptics: controller.vibrate({ duration, strongMagnitude, weakMagnitude }).


Animation (src/engine/)#

glTF animation is split across three components, each constructed from a GltfModel and driven by the engine's per-frame update(dt):

Component Animates Construct
AnimatedModel Skinned joints (skeleton) → jointMatrices for the skinned pass new AnimatedModel(model, { skinIndex: 0 })
AnimatedNodes Rigid node TRS + morph-target weights new AnimatedNodes(model)
AnimatedProperties KHR_animation_pointer material properties new AnimatedProperties(model)

All three share the play API:

const anim = new AnimatedModel(gltf);
characterGO.addComponent(anim);
anim.play(gltf.clips[0].name, /*loop*/ true, /*fade*/ 0.2);   // cross-fade in 0.2s
anim.speed = 1.0;
anim.pause(); anim.resume(); anim.stop();

From samples/gltf_viewer.ts — a model can need all three at once (a skin, animated rigid nodes, and animated materials):

const animators = gltf.skins.map((_, si) => new AnimatedModel(gltf, { skinIndex: si }));
const nodeAnimator = new AnimatedNodes(gltf);
const propertyAnimator = new AnimatedProperties(gltf);
if (gltf.clips.length > 0) {
  animators[0]?.play(gltf.clips[0].name);
  nodeAnimator.play(gltf.clips[0].name);
  propertyAnimator.play(gltf.clips[0].name);
}

Underneath: Skeleton holds the static joint hierarchy + inverse-bind matrices and turns per-joint TRS arrays into skinning matrices; AnimationClip / AnimationChannel / Interpolation ('LINEAR' | 'STEP' | 'CUBICSPLINE') are the decoded keyframe data. AnimatedModel also keeps previousJointMatrices for motion-blur velocity.

To render a skinned, animated model you also need the SkinnedGeometryFeature (deferred) — see the feature catalog below.


Renderer core (src/renderer/)#

RenderContext#

The WebGPU device + canvas owner. The Engine creates one for you (engine.ctx); you only construct it directly when dropping below the engine.

const ctx = await RenderContext.create(canvas, {
  enableErrorHandling: true,
  reversedZ: false,          // reversed-Z depth (flip near/far) — used by geo/planet scenes
  depthFormat: 'depth32float',
  maxPixelRatio: 1.2,        // clamp devicePixelRatio on mobile
});
ctx.update();   // top of frame: detect resize, advance timing; returns true if resized
ctx.device; ctx.queue; ctx.width; ctx.height; ctx.fps;

reversedZ flips depth interpretation; passes read ctx.reversedZ (and ctx.depthClearValue() / ctx.depthCompare()) to adapt. Turn it on for planet-scale depth precision — see the Geo guide.

Material / PbrMaterial#

Material is the abstract base (MaterialPassType enum: Forward, Geometry, SkinnedGeometry, …). PbrMaterial is the built-in. After mutating its properties, call update(queue) to push the uniforms — the engine does not do this for you (the call is a no-op when nothing's dirty).

const mat = new PbrMaterial({ albedo: [0.9, 0.2, 0.1, 1], roughness: 0.3, metallic: 0 });
mat.roughness = 0.5;
mat.update(device.queue);

PbrMaterialOptions is large — beyond the basics it covers the glTF PBR extension set: clearcoat/clearcoatRoughness (+ clearcoatNormalToGeometric for a smooth coat over a bumpy base), sheenColor/sheenRoughness (+ clothWrap for Filament-style cloth diffuse), iridescence, transmission/ior/thickness/attenuationColor, subsurface, anisotropyStrength, emissiveFactor, unlit, UV transform (uvOffset/uvScale/uvRotation), alphaCutoff, and texture slots (albedoMap, normalMap, merMap, emissiveMap, transmissionMap, thicknessMap, …). See pbr_material.ts.

Render graph + passes#

The renderer barrel also re-exports the entire render graph — RenderGraph, PhysicalResourceCache, the Pass base class, and every built-in pass (GeometryPass, DeferredLightingPass, ShadowPass, TonemapPass, OceanPass, …) — plus the debug overlay createRenderGraphViz. These are the building blocks the manual render graph section uses. RenderSpotLight is the pass-layer spot-light data form (renamed to avoid clashing with the SpotLight component).


Render features catalog#

A RenderFeature registers passes on the engine. Presets bundle these; you can also engine.addFeature(...) them individually (see the Quick-Start Guide). All are on the top-level barrel.

Category Features
Geometry GeometryFeature, SkinnedGeometryFeature
Lighting DeferredLightingFeature, PointSpotLightFeature, ForwardLitFeature, ForwardPlusFeature, ReflectionProbeFeature
Sky / atmosphere SkyTextureFeature, ConstantColorSkyFeature, AtmosphereFeature, AtmosphereLutsFeature, CloudFeature, GodrayFeature, StarsFeature, LensFlareFeature
Shadows ShadowFeature
Ambient occlusion / GI AOFeature ('gtao' | 'hbao+' | 'ssao'), SsgiFeature, SSRFeature
Anti-aliasing TAAFeature, SMAAFeature, VelocityFeature
Post-processing BloomFeature, DofFeature, MotionBlurFeature, AutoExposureFeature, TonemapFeature, CompositeFeature
Transparency ForwardOverlayFeature, TransmissionFeature
Effects ParticleFeature, OceanFeature

Each takes an options object named after it (BloomFeatureOptions, etc.); the ocean exports its lower-level pieces too (OceanPass, defaultOceanWaves, sampleGerstnerSurface, …).

Presets#

forwardPreset(opts?), forwardPlusPreset(opts), deferredPreset(opts?), and the shared SkyOption union (registerSkyFeature). Documented in depth in the Quick-Start Guide.


Audio re-exports#

For convenience the barrel also re-exports the audio subsystem (AudioEngine, AudioBus, SoundHandle, the effect factories, …) and the AudioSource / AudioListener components. They behave identically whether imported from taos/index.js or taos/audio/index.js — see the Audio guide.

The physics, geo, sdf, and terrain modules are not on this barrel. They're opt-in subpath imports (taos/physics/index.js, etc.) so apps that don't use them ship none of their code. See their dedicated guides.

See also#