Taos Engine · Modules

Modules

Path Tracer

A progressive wavefront GPU path tracer in WebGPU compute (no hardware ray tracing). It traces its own two-level BVH: a per-mesh BLAS over triangles plus a TLAS over instances, where an instance is either a triangle mesh or an analytic shape. Everything lives under src/renderer/pathtracer/ with the WGSL kernels in src/shaders/pathtracer/.

Building a scene#

TypeScript
import { assemblePtScene, makeMat, loadGltfMeshes } from '../src/renderer/pathtracer/scene.js';
import { sphere, box, torus, rect, csg, CsgOp, trs, rotXYZ } from '../src/renderer/pathtracer/shapes.js';

const floor = makeMat({ baseColor: [0.8, 0.8, 0.8], checker: [0.3, 0.3, 0.3], checkScale: 0.5 });
const gold = makeMat({ baseColor: [1, 0.78, 0.35], metalness: 1, roughness: 0.2 });
const lamp = makeMat({ baseColor: [0, 0, 0], specularWeight: 0, emission: [40, 38, 35] });

const shapes = [
  rect([0, 0, 0], 10, 10, floor),                 // XZ rectangle, +Y normal, double-sided
  sphere([0, 1, 0], 1, gold),
  torus([3, 0.4, 0], 1, 0.35, gold, rotXYZ(Math.PI / 2, 0, 0)),
  sphere([2, 5, 2], 0.4, lamp),                   // emissive sphere → sampled as a light
];
// CSG: operands are indices into the same array; the parent gets its own entry.
shapes.push(box([-3, 1, 0], [0.8, 0.8, 0.8], gold));
shapes.push(sphere([-3, 1, 0], 1.05, gold));
shapes.push(csg(CsgOp.Intersection, shapes.length - 2, shapes.length - 1, gold));

const gltf = await loadGltfMeshes(device, duckUrl, { transform: trs([0, 0, 3], 0.01) });
const scene = assemblePtScene(device, gltf.meshes, gltf.insts, shapes);
gltf.destroy(); // after assembling — the atlas bake reads the glTF textures

assemblePtScene(device, meshes, insts, shapes) returns a PtScene: GPU buffers, counts, sceneCenter / sceneRadius, and setInstanceWorld(i, world)

Shapes (shapes.ts)#

Every shape is a unit solid placed by an object→world matrix (trs, rotXYZ, rotateYTo help build them): sphere (ellipsoid with a radius triple), box, cylinder (capped or open tube), cone (frustum via topRadius), paraboloid, hyperboloid (one sheet, waist ratio), hyperbolicParaboloid (saddle sheet), capsule, torus (exact, sphere-traced SDF), disk, rect, quadric (4×4 coefficient matrix, quadricCoeffs(A..J) builds it), convex (up to four half-spaces, optionally mirrored), csg (union / difference / intersection of two non-CSG shapes), terrain (ray-marched fbm / ridged heightfield inside its box) and ocean (animated wave heightfield driven by settings.time).

Materials (makeMat)#

OpenPBR-style parameters: baseColor, metalness, roughness, specularWeight, ior, transmission (+ attenuationColor / attenuationDistance Beer absorption), coatWeight / coatRoughness / coatColor, fuzz* sheen, subsurfaceWeight / subsurfaceColor / sssRadius (random-walk subsurface, mean free path in world units), emission, a world-space checker second color with checkScale, and slopeBlend + slopeColor (terrain: steep → flat blend).

Lights#

Any emissive surface emits. Emissive triangles, spheres, disks and rects are also added to the next-event-estimation light list, chosen in proportion to their power and combined with BSDF sampling through MIS. Point lights are tiny emissive spheres; a spot is an emissive disk inside an open cylinder housing.

Environment#

PathTracer.create(ctx, scene, envTexture, rgbe) builds the importance-sampling distribution from an HDR's RGBE bytes. For an analytic sky use sky.ts:

TypeScript
const baked = bakeSky({ sunElevation: 35, sunAzimuth: 120, turbidity: 3 });
const tex = createFloatEnvTexture(device, baked.width, baked.height, baked.data);
const pt = PathTracer.createWithDistribution(ctx, scene, tex, envDistributionFromFloat(baked.data, baked.width, baked.height));
pt.settings.sunDir = baked.sunDir;           // used by the cloud layer
pt.settings.sunRadiance = baked.sunRadiance;

The sky is normalized so sun + sky deliver an irradiance of π (a white ground reads as 1.0); sunStrength sets the sun / sky irradiance ratio (default 5).

Settings (pt.settings)#

Integrator: maxBounces, sortEnable, fireflyClamp. Image: exposure, envIntensity, envRotation, denoise (à-trous SVGF) + temporal. Camera: aperture / focusDistance, ortho + orthoScale. Media: fogDensity (extinction per unit, 0 = off), fogColor (single-scattering albedo), fogAniso (Henyey–Greenstein g). Sky: clouds, cloudCover, cloudBase, cloudThickness, cloudScale, sunDir, sunRadiance, time.

Call trace() per sample and present() per frame; reset() after anything that changes the integrand (camera, scene, most settings).

Samples#

samples/lib/pt_sample_base.ts drives all the demo pages (camera, control panel, environment loading, animation loop, ?scene=<key> URL parameter): pt_geometry_showcase, pt_classic_scenes, pt_csg, pt_materials, pt_volumetric, pt_terrain, pt_ocean_sky, pt_shapes_bvh, pt_bvh_lights, plus the original glTF / MaterialX path_tracer page.

Limits#

No bidirectional path tracing (hidden-light scenes converge slowly), no GPU BVH refit for animated triangle meshes (shape instances animate through commit()), one homogeneous global fog, and the texture atlas is 512² per textured material.