SinclaM/ray-tracer-challenge
A simple ray tracer to experiment with Zig
3c4f73a45bccc48cda4319a629a45d2469d8e24e.tar.gz
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:
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.