This project is a TypeScript/WebGPU implementation of a physically-based pathtracer, containing a small demo scene of various materials.
This pathtracer begins by spawning a set of rays (defined by an origin point and direction vector) based on the viewer's location and orientation. Each ray is then checked against all the geometry in the scene to determine what it strikes first. The colors of the ray and direction in which the ray is reflected or refracted is determined based on the material of the surface it intersects.
Demo renders are provided below as well as their running times for performance comparison. The baseline is a
- After every sample rendering mode
- DoF off (0 radius, 1 distance)
- 10 maximum bounces
-
$4\times 4$ supersampling rate - 4 samples / grid cell
Tested using a Windows 11 machine with AMD Ryzen 7 8845HS w/ Radeon 780M Graphics (3.80 GHz), RTX 4070 notebook.
The UI provides 3 rendering options:
- After every sample (coherent). The output colors are rendered after each sample is complete. Techniques involving compaction/sorting/partitioning based on whether a ray is done processing and what material the ray bounced off of are used here. (This tends to perform the slowest.)
- After every sample. The output colors are rendered after each sample is complete. No compaction is performed.
- After all samples. The output colors are rendered only after all samples are complete. (This tends to perform the fastest, but may cause the GPU to timeout on the demo scene and the render to be stopped prematurely.)
The camera is controlled by an orbit control system and can be moved by dragging the left mouse button and dollied using the scrollwheel.
Instead of shooting rays out from a flat plane:
we simulate the rays based on the surface of a sphere instead. The center of the screen
This allows us to use very wide FoV values without the extreme stretching near the edges that the plane projection has. This also has the effect of rendering rounded edges even where mesh edges may be straight.
| Sphere projection | Plane projection |
|---|---|
![]() |
![]() |
![]() |
![]() |
| 0.445 s/frame | 0.551 s/frame |
Various types of materials are available in the scene:
- Diffuse, non-transmissive. Rays encountering the surface are reflected in all directions up to 90° from the surface normal, using cosine weighting.
- Glossy, non-transmissive. Rays encountering the surface are reflected across the surface normal.
- Diffuse, transmissive. Rays encountering the surface are either:
- Reflected as if the surface was non-transmissive, with a probability based on Shlick's approximation of fresnel.
- Refracted in all directions up to 90° from the negative of the surface normal, using cosine weighting.
- Glossy, transmisive. Rays encountering the surface are either:
- Reflected as if the surface was non-transmissive, with a probability based on Shlick's approximation of fresnel.
- Refracted using Snell's law based on the surface normal, with an IoR ratio of 1.5.
- Emissive/environment. Rays encountering the surface are terminated.
Each material type has a color associated with it. The final color of a ray is the product of all the colors of all the surfaces it encounters, or black if the ray is not terminated (i.e., does not encounter a light source) within the maximum number of bounces.
Antialiasing, or smoothing of edges, is performed by supersampling each pixel. Given a supersampling rate
| No supersampling, 64 samples / grid cell |
|
|
|---|---|---|
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
| 0.437 s/frame | 0.439 s/frame | 0.443 s/frame |
Depth of field is simulated by uniformly jittering the starting point of each ray within a disc aligned with the screen (correlated with aperture size), and then adjusting the ray direction based on the focus distance (correlated with focal length).
| No DoF | Default | Large aperture | Large focal length | Large focal length and aperture |
|---|---|---|---|---|
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
![]() |
| 0.438 s/frame | 0.442 s/frame | 0.443 s/frame | 0.460 s/frame | 0.547 s/frame |
The environment texture used is an equirectangular texture. Rays that do not intersect any geometry are converted into spherical coordinates to determine how to sample from the environment texture.
| Image environment | Cheap procedural environment |
|---|---|
![]() |
![]() |
![]() |
![]() |
| 0.445 s/frame | 0.446 s/frame |
To achieve more realistic lighting, RGB arithmetic is done in linear sRGB space and then approximately converted to gamma sRGB by raising each component to the power of
| Linear sRGB blending | Gamma sRGB blending |
|---|---|
![]() |
![]() |
![]() |
![]() |
| 0.445 s/frame | 0.449 s/frame |
The demo scene is stored as a GLB file, loaded using THREE.js's GLTFLoader. This processing is done on the CPU.
Supported features from the GLB include transformations and loading the material properties listed above (binary roughness, binary transmission, color, emission). The bounding box for each mesh is also computed for bounding box culling. Buffers for triangle data, materials, and bounding boxes are passed to the GPU.
Bounding box culling, the optimization where we only check a ray against a triangle if we know the ray intersects the mesh's bounding box, is togglable.
Despite the decrease in the number of processed triangles per ray, this optimization seems to increase the running time as currently implemented.
| Not culled | Culled |
|---|---|
![]() |
![]() |
![]() |
![]() |
| 0.445 s/frame | 2.054 s/frame |
If the render mode is After every sample (coherent), then after each bounce, the renderer will perform stream compaction/sorting/partitioning, by material before shading and then by terminated status after shading. Compute shaders for prefix sum, radix sort, and scatter are run to perform this partitioning.
Despite the cache efficiency of this optimization, it seems to increase the running time as currently implemented.
| Not coherent | Coherent |
|---|---|
![]() |
![]() |
![]() |
![]() |
| 0.445 s/frame | 1.020 s/frame |
This is a Deno/Node.js application that builds to a static web app. If you have Deno installed, you can use deno i and deno task dev to run this project locally and deno task build or deno task build:rel to build it.
Environment map: Minedump Flats by Dimitrios Savva and Jarod Guest
Libraries:
- THREE.js. gLTF loading
- SvelteKit. UI/reacitvity/routing
- SASS. CSS preprocessing
- Vite. Web app bundling and development environment




























