| rust | ||
| cornell_hq.png | ||
| raytracer.py | ||
| README.md | ||
| requirements.txt | ||
Cornell Box Raytracer
A pure Python raytracer that renders the Cornell Box scene with:
- Reflections - mirror/chrome spheres reflect surrounding geometry
- Caustics & Refraction - glass spheres bend light (dielectric material with Schlick approximation)
- Multiple Bounces - recursive path tracing up to configurable depth
- Next Event Estimation (NEE) - explicit light sampling for fast convergence
- Anti-aliasing - supersampled pixels with jittered rays
- Multiprocessing - parallel rendering across CPU cores
Requirements
- Python 3.8+
- numpy
- Pillow
- rich (progress bars, colored tables)
- matplotlib (render metrics graphs)
pip install numpy Pillow rich matplotlib
Features
- Rich Progress Bar - animated progress with ETA, speed, and row count
- Scene Configuration Table - formatted parameter display
- Region Brightness Analysis - color-coded brightness per region (left/right wall, floor, ceiling, center)
- Render Metrics Graph - saved as
render_metrics.png, shows brightness convergence and render preview - Live Preview -
preview.pngupdated during rendering - Reflections - mirror/chrome spheres reflect surrounding geometry
- Caustics & Refraction - glass spheres bend light (dielectric material with Schlick approximation)
- Multiple Bounces - recursive path tracing up to configurable depth
- Next Event Estimation (NEE) - explicit light sampling for fast convergence
- Anti-aliasing - supersampled pixels with jittered rays
- Multiprocessing - parallel rendering across CPU cores
Usage
python raytracer.py [width] [height] [samples_per_pixel] [max_bounces] [output]
Examples
# Quick preview (4 min)
python raytracer.py 200 150 20 10 preview.png
# Good quality (~10 min)
python raytracer.py 400 300 50 15 cornell.png
# High quality (~40 min)
python raytracer.py 800 600 100 15 cornell_hq.png
# Custom camera and scene parameters can be set by editing the __main__ block
Parameters
| Parameter | Default | Description |
|---|---|---|
| width | 400 | Image width in pixels |
| height | 300 | Image height in pixels |
| samples_per_pixel | 50 | Anti-aliasing samples per pixel |
| max_bounces | 15 | Maximum ray bounce depth |
| output | cornell_box.png | Output PNG filename |
Workers
Multiprocessing uses one worker per CPU core by default. Pass workers=N to the render() call to change it.
Rust version
A much faster port lives in rust/ (renders 800x600 at 100 spp in seconds). It takes the same arguments, plus an optional --scene:
cd rust
cargo build --release
./target/release/pathtracer 800 600 100 15 cornell_hq.png
./target/release/pathtracer --scene caustics 800 600 1024 15 caustics.png
./target/release/pathtracer --list-scenes
It uses all cores via rayon, shows a progress bar and (for the cornell scene) the region brightness table, but does not write preview.png or render_metrics.png. Without an output name it writes <scene>.png.
Scenes
| Scene | What it shows |
|---|---|
cornell (default) |
The scene from raytracer.py |
caustics |
Clear, amber and blue glass spheres and a mirror under one light, placed so each sphere focuses a sharp caustic into its shadow. Caustics are the hardest light paths for a path tracer, so use ~1000+ spp |
spheres |
Large scene: ~470 random diffuse/mirror/glass spheres under a sky (after Ray Tracing in One Weekend). The BVH makes it ~11x faster than testing every object |
night |
The same field at night, lit by 61 glowing spheres. Each shading point picks one light, weighted by power / distance², so many lights cost no more shadow rays than one |
classic |
The classic Cornell box: one-sided ceiling panel light, two boxes, colour bleeding from the walls |
Scenes are defined in rust/src/scenes.rs with a small builder (plane, sphere, cuboid, box_light, sphere_light, rect_light). Every emitter must be added with a *_light method so light sampling knows about it.
Beyond the Python version
-
BVH - bounded objects live in a bounding volume hierarchy (median split); planes are tested separately.
-
Many-light sampling - box, sphere and one-sided rectangle lights, one picked per shading point in proportion to its estimated contribution.
-
Russian roulette - after 3 bounces, dim paths are terminated randomly (survivors are re-weighted, so the image stays unbiased). About 2.5x more image quality per second of rendering in this scene.
-
Cheap cosine-weighted diffuse sampling - two uniform random numbers and an orthonormal basis instead of Gaussian samples.
-
Adaptive sampling (optional 6th argument, off by default) - after 32 samples per pixel, keeps sampling only pixels whose 3x3 neighbourhood still has a display-space standard error above the threshold, up to
samples_per_pixel:./target/release/pathtracer 800 600 4096 15 cornell.png 0.03In this scene noise is spread fairly evenly (caustic fireflies reach almost every pixel), so adaptive sampling roughly breaks even with uniform sampling and can darken the image slightly at loose thresholds. It pays off in scenes with large easy regions.
Scene Description
The scene is the classic Cornell Box:
- 5 walls (floor, ceiling, left=red, right=green, back=white) with the front face open
- A small bright light source on the left wall
- A glass sphere (refractive, IOR=1.5) - demonstrates refraction and caustics
- A mirror sphere (perfect reflection) - demonstrates reflections
- A red diffuse sphere - demonstrates diffuse scattering
Camera
- Position: (278, 278, -800)
- Look-at: (278, 278, 278) (box center)
- FOV: 40° vertical (earlier versions effectively used half of this, zooming the image in 2x)
- Focus distance: 1078
Materials
| Material | Implementation |
|---|---|
| Diffuse (Lambertian) | Cosine-distributed random scattering + NEE |
| Mirror | Perfect specular reflection |
| Dielectric | Snell refraction + Schlick fresnel + random bounce |
| Light | Emissive surface, sampled via NEE |
Progress Indicators
The renderer uses Rich for terminal UI and matplotlib for metrics graphs:
- Progress Bar - animated with spinner, bar, percentage, ETA, and row count
- Settings Table - formatted parameter display with colored values
- Region Brightness - color-coded ASCII bar chart showing brightness per region
- Metrics Graph - saved as
render_metrics.png, shows brightness convergence curve and live render preview - Preview Image - saved as
preview.pngand updated during rendering
Performance
Measured on 4 workers, single machine:
| Resolution | spp | Time |
|---|---|---|
| 200x150 | 20 | ~40s |
| 400x300 | 50 | ~10 min |
| 800x600 | 50 | ~40 min |
How It Works
- Primary rays are cast from the camera through each pixel (with supersampling jitter)
- Ray-object intersection tests against planes (walls), spheres, and boxes (light)
- Surface scattering:
- Diffuse: random hemisphere direction + explicit light sampling (NEE)
- Mirror: perfect reflection vector
- Dielectric: refraction with Snell's law, Schlick approximation for total internal reflection
- Recursive bouncing: each scattered ray continues tracing up to
max_bounces - Radiance estimation:
L = Le + ∫ f_r * L_i * cos θ dωapproximated via Monte Carlo integration
Next Event Estimation
For diffuse surfaces, the direct light contribution is computed by explicitly sampling points on the light source, testing visibility via shadow rays, and weighting by the BRDF and light PDF. This dramatically reduces variance compared to pure path tracing.
Output
Renders save as PNG with gamma correction (sqrt/gamma 2.0 applied).