A Whitted-style ray tracer written from scratch in C++17, accelerated by a custom SAH-based Bounding Volume Hierarchy. It reads XML scene descriptions, renders to PPM, and supports reflection, refraction, Phong shading, textured materials, anti-aliasing, shadows, OBJ mesh loading, and GIF animation.
- Whitted-style recursive ray tracing with configurable bounce depth
- Reflection and refraction (Snell's law) with energy splitting by reflectance/transmittance coefficients
- Anti-aliasing via 5x5 supersampling (25 rays per pixel, averaged)
- Phong shading with ambient, diffuse, and specular terms
- Multiple light types: ambient, point, parallel (directional), and spot
- Shadow rays with early-exit intersection test
- Solid and textured materials (PNG textures via LodePNG)
- OBJ mesh loading with per-triangle intersection
- XML scene format (parsed with RapidXML)
- GIF animation and motion-blur modes (via gif-h)
- BVH acceleration with SAH build (12-bin binning)
- OpenMP parallelism across image rows
- Bit-identical brute-force fallback via the
RT_NO_BVHcompile flag
The BVH is the differentiator. Brute-force intersection scales as O(N) per ray — every triangle is tested for every ray. The BVH reduces this to O(log N) by pruning subtrees whose AABB the ray misses, so render time stays nearly flat as the scene grows.
| Triangles | Brute-force (ms) | BVH (ms) | BVH build (ms) | Speedup |
|---|---|---|---|---|
| 1,012 | 684.2 | 21.9 | 0.5 | 31.2x |
| 5,000 | 3001.3 | 25.9 | 1.9 | 115.9x |
| 25,088 | 13332.5 | 31.8 | 15.3 | 419.3x |
| 99,904 | 83705.0 | 32.1 | 39.8 | 2607.6x |
| 500,000 | 387425.6 | 37.0 | 260.4 | 10471.0x |
BVH render time stays flat at 22-37ms (O(log N)) while brute-force scales linearly (O(N)) from 684ms to 387 seconds. The speedup grows with triangle count, reaching ~10,500x at 500K triangles. The BVH build cost is a one-time expense (≤261ms for 500K triangles) that is amortized across every ray in the image — primary, shadow, reflection, and refraction rays all benefit.
Methodology: procedural UV-sphere meshes, 256x256 resolution, single-threaded to isolate the algorithmic improvement, no anti-aliasing, 3-rep median, on an Intel i7-10750H (6C/12T).
CMake is required (3.16+), along with a C++17 compiler and OpenMP.
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j
./build/raytracer example6
The output is written to output/<output_file>.ppm, where output_file is set in the scene XML. The program also prints per-stage timing (parse / render / write).
To render without anti-aliasing (faster, useful for benchmarking):
./build/raytracer example6 --no-aa
To run in interactive mode, launch the binary with no arguments and type scene names (with or without the .xml suffix). Type exit to quit:
./build/raytracer
To build the brute-force variant (no BVH) for benchmarking or correctness comparison:
cmake -S . -B build -DCMAKE_BUILD_TYPE=Release -DRT_NO_BVH=ON
cmake --build build -j
Eight example scenes ship in scenes/:
| Scene | Contents |
|---|---|
example1 |
Basic geometry and Phong shading |
example2 |
Multiple lights and shadows |
example3 |
Used by the gif animation mode |
example4 |
Reflection |
example5 |
Textured materials |
example6 |
Refraction + textured room (hero image) |
example7 |
Used by the motionBlur mode |
example8 |
Composite scene |
OBJ meshes (open_room.obj, box.obj, cone.obj, cylinder.obj, plane_small.obj) and PNG textures (rainbow.png, mramor6x6.png, MarbleBeige.png, Brick.png) are bundled under scenes/. Two special commands are available in interactive mode (or as the scene argument):
gif— renders a 15-frame animation of growing spheres tooutput/animation.gifmotionBlur— accumulates 6 frames of a translating sphere into a single motion-blurred image
Data flow:
XML -> Scene -> trace() -> PPM
Modules (one per header):
Ray— ray origin/direction with reflection and refraction helpersCamera— pinhole camera, FOV, resolution, per-pixel ray generation with optional subpixel jitterScene— owns objects, lights, and the camera; background colorMaterial— solid and textured materials, Phong coefficients, reflectance/transmittance/refractionMesh+BVH— triangle container integrated with the BVH;RT_NO_BVHswitches to linear scanTriangle— Möller-Trumbore intersection with barycentric UVsSphere— analytic sphere intersectionLight— ambient, point, parallel, and spot light typesppm— PPM image buffer with supersampling accumulation and write-out
The BVH (BVH.h) is the core performance feature:
- SAH build. Each internal node evaluates the Surface Area Heuristic over 12 bins on all three axes and picks the split minimizing the cost
area(L)*N(L) + area(R)*N(R)normalized by the parent area. When no SAH split improves on a flat leaf (or the centroids are degenerate), it falls back to a median split on the longest axis. Leaves are formed at 12 triangles or fewer. The build is deterministic: ties are broken by(centroid, original triangle index). - Stack-based traversal.
closestHituses an explicit 128-entry stack (no recursion), visiting children front-to-back ordered by the AABB slab-entry distance. This guarantees the near child is processed first, so the tightestbest.tbound prunes the far subtree aggressively. - AABB slab test.
AABB::hitTimplements the slab method with a safe inverse-direction infinity for rays parallel to a slab and returns the ray-entry distance, reused for both culling and front-to-back ordering. - Shadow-ray early-exit.
anyHittraverses the same BVH but returnstrueon the first hit withinmaxDist, enabling cheap shadow tests that usually terminate after one leaf. - Bit-identical fallback. The
RT_NO_BVHcompile flag (seeCMakeLists.txt) disables the BVH entirely, falling back to a linear scan over all triangles. The BVH traversal replicates the brute-force tie-break exactly, so output is pixel-identical between the two builds.
BVH output is verified bit-identical to brute-force on all 8 example scenes and the gif/motionBlur modes.
All third-party libraries are vendored in the repository:
- RapidXML (
rapidxml-1.13/) — XML parsing - LodePNG (
lodepng/) — PNG decoding for textures - gif-h (
gif-h-master/) — GIF encoding
OpenMP is provided by the compiler and linked via CMake's FindOpenMP. Build requirements: CMake 3.16+ and a C++17 compiler (GCC, Clang, or MSVC).
The benchmark generates procedural UV-sphere meshes on-the-fly, times both BVH and brute-force builds at increasing triangle counts, and writes bench/results.{md,csv} plus bench/scaling_plot.svg:
python3 bench/bench.py --build
Optional flags: --reps N (repetitions per scene, default 3). The generated meshes and scene files are gitignored. Results in this README were measured single-threaded with 3 reps on an Intel i7-10750H (6C/12T).
MIT — see LICENSE.

