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(+ itsTransform: local TRS, cached world matrix, world-space accessors),Component,Scene,Camera, the lights,MeshRenderer,Material/PbrMaterialbasics, 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 atfocusDistance: left-drag orbits, middle/right-drag (or Shift+left-drag) pans, wheel zooms (clamped tominDistance/maxDistance), WASD flies the focus.focusOn(point, distance?)recenters the pivot. SetenableDamping(withdampingseconds) 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, andterrainmodules 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#
- Quick-Start Guide — the narrative for the engine, presets, and render graph
- Audio · SDF · Terrain · Geo · Physics
- Chapter 4 — Geometry and Chapter 6 — Materials — the theory