This project is a simple Zig implementation of the ray tracer described in The Ray Tracer Challenge.
You can find an interactive demo of this ray tracer online at sinclam.github.io/ray-tracer-challenge.
- Chapter 1 - Tuples, Points, and Vectors
- Chapter 2 - Drawing on a Canvas
- Chapter 3 - Matrices
- Chapter 4 - Matrix Transformations
- Chapter 5 - Ray-Sphere Intersections
- Chapter 6 - Light and Shading
- Chapter 7 - Making a Scene
- Chapter 8 - Shadows
- Chapter 9 - Planes
- Chapter 10 - Patterns
- Chapter 11 - Reflection and Refraction
- Chapter 12 - Cubes
- Chapter 13 - Cylinders
- Chapter 14 - Groups
- Chapter 15 - Triangles
- Chapter 16 - Constructive Solid Geometry (CSG)
- Chapter 17 - Next Steps
- A1 - Rendering the Cover Image
- Bonus Chapter - Rendering soft shadows
- Bonus Chapter - Bounding boxes and hierarchies
- Bonus Chapter - Texture mapping
Teapot model from https://groups.csail.mit.edu/graphics/classes/6.837/F03/models/teapot.obj.
Dragon model from http://raytracerchallenge.com/bonus/assets/dragon.zip.
Nefertiti bust model from https://github.com/alecjacobson/common-3d-test-models/blob/master/data/nefertiti.obj.
Earth texture from https://planetpixelemporium.com/earth.html.
Lancellotti Chapel texture from http://www.humus.name/index.php?page=Textures.
To build for native:
zig build -Doptimize=ReleaseFast
To target the web (populating www/
with the all the site's files):
zig build --sysroot [emsdk]/upstream/emscripten -Dtarget=wasm32-emscripten -Dcpu=generic+bulk_memory+atomics+simd128 -Doptimize=ReleaseFast
&& sed -i'' -e 's/_emscripten_return_address,/() => {},/g' www/ray-tracer-challenge.js
Although the ray tracer is not (yet) heavily optimized (e.g. it does not yet leverage Zig's SIMD builtins), it is still very fast—faster in fact on a single thread than almost every other Ray Tracer Challenge implementation on multiple threads I've compared with. And there is still significant room for optimization.
The optimizations I do make are largely informed by profilers. When built for native, the binary can be profiled with
valgrind --tool=callgrind
and the results inspected with qcachegrind
, which works well enough. Unfortunately,
Valgrind's troubled state on macOS,
combined with Zig's incomplete Valgrind support, means profiling is not
always simple. For example, I've seen Valgrind erroneously run into SIGILL
and the like. Using std.heap.raw_c_allocator
on native seems to fix most of these issues.
The ray tracer currently runs about 2x slower on WebAssembly than on native, which is reasonable. I use Firefox's "performance" tab in the developer tools for profiling on the web.
I also use hyperfine for benchmarking.
Below are some benchmarks for scenes that can be found on the website. These benchmarks are not rigorously controlled and averaged, but rather a general overview of speeds for various scenes. They may also change depending significantly between Zig compiler versions. For example, I noticed a perfromance regression of up to 25% going from 0.11.0 to the WIP 0.12.0 (perhaps related to this similar issue). The best way to get a feel for the performance is to try things out yourself!
All benchmarks were done on a 2019 MacBook Pro (2.6Ghz, 6-Core Intel i7; 16GB RAM; macOS 12.6.7). WASM specific benchmarks were done on Firefox 117 using 6 web workers (the maximum number of web workers Firefox will run in parallel, even on my 12 logical CPU system 🤷♂️). Native runs used 12 threads.
The 'WASM Preheated' category refers to renders done with the scene pre-built (scene description already parsed, objects and bounding boxes already made, textures already loaded, etc.), which is supported on the site through the arrow key camera movememnt. This preheating is irrelevant for simple scenes, but gives massive speedups for scenes that load textures or construct BVHs.
Also note that renders on the website are periodically polled for completion. Renders may actually complete up to 100ms before the reported time, which affects the benchmarks for very short renders.
Scene | Resolution | Native | WASM | WASM Preheated |
---|---|---|---|---|
Cover Scene | 1280x1280 | 1.413 s | 2.408 s | 2.299 s |
Cubes | 600x300 | 0.225 s | 0.418 s | 0.407 s |
Cylinders | 800x400 | 0.111 s | 0.221 s | 0.109 s |
Reflection and Refraction | 400x200 | 0.113 s | 0.213 s | 0.205 s |
Fresnel | 600x600 | 0.283 s | 0.429 s | 0.411 s |
Groups | 600x200 | 0.091 s | 0.217 s | 0.202 s |
Teapot | 250x150 | 0.175 s | 0.413 s | 0.210 s |
Dragons | 500x200 | 6.957 s | 12.663 s | 2.492 s |
Nefertiti | 300x500 | 4.827 s | 6.358 s | 3.036 s |
Constructive Solid Geometry | 1280x720 | 0.267s | 1.920 s | 1.792 s |
Earth | 800x400 | 0.095 s | 0.212 s | 0.103 s |
Skybox | 800x400 | 1.466 s | 1.531 s | 0.102 s |
Raytracer REPL Default | 1280x720 | 0.210 s | 0.220 s | 0.209 s |
There are many great implementations of the Ray Tracer Challenge. At many points throughout the project, I referred to others to verify my implementation, draw inspiration, or compare performance. I recommend you check out the following ray tracers:
- graytracer: fully-implemented, easy to setup, performant.
- The Raytracer Challenge REPL: online demo with amazing site design.
- RayTracerCPU: very fast, helpful online demo.
The website for this project uses the SharedArrayBuffer
type, which requires certain HTTP headers to be set, something that GitHub Pages does not support. To get around this, I use
coi-serviceworker, which has the disadvantage of not working in
Private/Incognito sessions.
Some devices (mobile phones in particular) may not have enough memory to render some of the scenes on the website, especially the "Skybox" scene.