A real-time spectral path tracer that runs entirely in the browser — WebGL2, React, TypeScript, no backend, no build server for rendering. Live at https://j4zz.eu/caustify/ (mirror: Hugging Face Spaces).
Most browser path tracers shade in RGB. This one carries actual wavelengths through the scene: light is sampled across the visible spectrum, glass bends each wavelength by a different amount (a Cauchy dispersion model, not a fixed per-channel IOR fudge), and the result is converted back to a display colour only at the very end, through a real CIE 1931 colour-matching step. That's what makes the dispersion in the prism and gemstone scenes look like a prism and not like a chromatic-aberration filter.
Dispersion runs on Cauchy's equation, driving wavelength-dependent refraction
through glass, tuned per material (glassIor, glassDispersion) rather than
faked with three offset IORs. Tinted glass absorbs light following Beer's
law, so thicker glass absorbs more, and absorbs wavelengths selectively
rather than uniformly. Light sources are Planckian blackbody radiation (pick
a colour temperature), a monochromatic laser, a fluorescent tube modelled on
mercury emission lines, or a flat equal-energy spectrum. The volumetric
scattering formula blends Rayleigh and Mie terms, though none of the four
built-in presets turns fog on by default — enable it and you're rendering
constant-density Mie scattering, since there's no slider yet for the
Rayleigh mix.
On the way out, XYZ gets mapped into sRGB, Adobe RGB, Display P3, or Wide
Gamut RGB, with a choice of Reinhard, ACES filmic, or Uncharted 2 tone
mapping — the spectral-to-RGB conversion is a real, swappable pipeline stage
here, not a hardcoded vec3. Click anywhere in the rendered image and the
exact colour that hit that pixel gets plotted on a live CIE 1931
chromaticity diagram.
Four scenes: a prism, a cut gemstone, a focusing lens, and a Cornell box with glass and gold.
WRITEUP.md goes through the actual dispersion math (hero
wavelength sampling, the Cauchy equation, where it's an approximation
rather than physics) in more detail than this README.
The denoiser is a spatial bilateral filter that leans on local variance
while the accumulation buffer is still thin, then fades itself out as
sample count builds — it's not a temporal or ML denoiser, so a scene that's
still noisy at frame one stays a little soft rather than clean. No
environment/HDRI lighting, no mesh import beyond the built-in scenes (there's
an OBJ parser in the code, parseObj, but nothing in the app calls it). It
runs a single browser tab's worth of a GPU path tracer, so don't expect
offline-renderer sample counts.
npm install
npm run devNeeds a browser with WebGL2. Build with npm run build; the routes assume
being served from a subpath (/caustify/), set in vite.config.ts — change
base if you deploy it elsewhere.
MIT. See LICENSE.