Ray Tracing in One Weekend — Notes
Why This Book First?
Before Vulkan, before BVH, before shaders — understand the ALGORITHM
Every line you write teaches something: normals, reflection, shadows, sampling
The CPU version is slow but 100% transparent — you can debug every pixel
Once you understand this, GPU path tracing is “just parallelism”
Recommended by the Godot NVPathtracer contributors in their chat
Book 1: Ray Tracing in One Weekend
Chapter Overview
Chapter 1 — Output an Image (PPM format)
Chapter 2 — The Ray and Camera
Chapter 3 — Ray-Sphere Intersection
Chapter 4 — Surface Normals and Multiple Objects
Chapter 5 — Anti-Aliasing
Chapter 6 — Diffuse Materials
Chapter 7 — Metal
Chapter 8 — Dielectrics (Glass)
Chapter 9 — Positionable Camera
Chapter 10 — Defocus Blur (Depth of Field)
Chapter 11 — Final Scene
Core Concepts — Book 1
The Ray
P(t) = origin + t * direction
origin — starting point (camera)
direction — unit vector (where the ray is going)
t — parameter: positive = forward, negative = behind camera
struct Ray {
vec3 origin, direction;
vec3 at ( float t ) const { return origin + t * direction; }
};
Camera and Primary Ray Generation
Pinhole camera: all rays pass through a single point
Define a “viewport” rectangle in 3D space
For each pixel (i, j), generate a ray from camera origin through the pixel
// Viewport at z = -1, aspect ratio preserved
float viewport_height = 2.0 f ;
float viewport_width = aspect_ratio * viewport_height;
vec3 lower_left = origin - vec3 (viewport_width / 2 , viewport_height / 2 , 1.0 f );
// Ray for pixel (u, v) in [0,1]²
Ray ray ( origin , lower_left + u * horizontal + v * vertical - origin );
Ray-Sphere Intersection
Sphere equation: |P - C|² = r²
Substitute ray P(t): |origin + t*dir - C|² = r²
Expand to get a quadratic in t: at² + bt + c = 0
Discriminant b² - 4ac:
> 0 — two intersections (ray passes through)
= 0 — one intersection (tangent)
< 0 — no intersection (miss)
Take the smaller t > t_min as the hit
bool hitSphere ( vec3 center , float radius , Ray r , float & t ) {
vec3 oc = r.origin - center;
float a = dot (r.direction, r.direction);
float b = 2.0 f * dot (oc, r.direction);
float c = dot (oc, oc) - radius * radius;
float discriminant = b * b - 4 * a * c;
if (discriminant < 0 ) return false ;
t = ( - b - sqrt (discriminant)) / ( 2.0 f * a);
return t > 0.001 f ;
}
Surface Normals
At hit point P, normal of a sphere centered at C: N = normalize(P - C)
Normal always points outward (away from center)
For front/back face detection: if dot(ray.dir, N) < 0 → front face
Visualize normals by mapping (N + 1) / 2 to RGB → great debug view
Anti-Aliasing
For each pixel, send multiple rays with random sub-pixel offsets
Average the results → smooth edges
vec3 color = vec3 ( 0.0 f );
for ( int s = 0 ; s < SAMPLES_PER_PIXEL; s ++ ) {
float u = ( float (i) + random_float ()) / (width - 1 );
float v = ( float (j) + random_float ()) / (height - 1 );
Ray ray = camera. get_ray (u, v);
color += trace (ray, world, MAX_DEPTH);
}
color /= SAMPLES_PER_PIXEL; // average
Diffuse (Lambertian) Material
When a ray hits a diffuse surface, scatter it in a random direction
The randomness models the microscopic roughness of matte surfaces
// Simple diffuse: random direction in hemisphere
vec3 scattered = hit.normal + random_unit_vector ();
// Lambertian (more accurate): cosine-weighted
vec3 scattered = hit.normal + random_unit_vector (); // same formula!
// This gives cosine-weighted distribution naturally
Each bounce multiplies by the albedo color
After many bounces → black (all light absorbed)
See PathTracer Learning BRDF — Lambertian BRDF = albedo / π
Perfect mirror reflection: reflect(v, n) = v - 2 * dot(v, n) * n
Add fuzz (roughness): perturb reflection by small random vector
vec3 reflected = reflect (ray.direction, hit.normal);
// Fuzz: reflected + fuzz * random_in_unit_sphere()
// fuzz = 0 → perfect mirror
// fuzz = 1 → very rough metal
Dielectric (Glass) Material
Light refracts when entering/exiting glass
Snell’s law: n₁ sin(θ₁) = n₂ sin(θ₂)
Schlick approximation for Fresnel — some light reflects even at normal incidence
Total internal reflection: when light tries to exit glass at too steep an angle
// Schlick approximation
float schlick ( float cos_theta , float ref_idx ) {
float r0 = ( 1 - ref_idx) / ( 1 + ref_idx);
r0 = r0 * r0;
return r0 + ( 1 - r0) * pow ( 1 - cos_theta, 5 );
}
Book 2: The Next Week
Adds: motion blur, textures, perlin noise, image textures, rectangles, lights, Cornell box, volumes, BVH
Key additions
BVH (Bounding Volume Hierarchy) — speed up ray-scene intersection from O(N) to O(log N)
Emissive Materials — make objects into light sources
When a ray hits an emissive surface: return emitted_color * intensity
Cornell Box — the classic test scene for global illumination
Two walls: red and green. Top light. Two boxes inside.
Participating Media — volumetric fog/smoke
Textures — map images onto geometry using UV coordinates
Book 3: The Rest of Your Life
Adds: PDF sampling, importance sampling, MIS — the real path tracing theory
Key additions
PDF sampling — replace random hemisphere sampling with smart sampling
Mixture PDFs — blend BRDF sampling and light sampling
MIS (Multiple Importance Sampling) — optimally combine multiple strategies
Importance Sampling BRDFs — sample toward specular highlights
The variance drops dramatically — the same scene converges much faster
Key Lessons from the Series
Lesson 1: A path tracer is just a recursive function — trace a ray, hit something, trace another ray
Lesson 2: The more you know about your sampling distribution, the less variance you get
Lesson 3: BVH is not optional — O(N) intersection will destroy you on real scenes
Lesson 4: Monte Carlo is the only practical way to solve the rendering equation
Lesson 5: Russian Roulette terminates paths correctly — fixed depth introduces bias
Lesson 6: Every material is just a scattering function — diffuse, metal, glass are all the same interface
After the Books — What’s Next?
Quick Reference — Important C++ Snippets
Random number in [0, 1)
float random_float () {
static std ::uniform_real_distribution <float> dist ( 0.0 f , 1.0 f );
static std ::mt19937 gen;
return dist (gen);
}
Random unit vector (for diffuse scatter)
vec3 random_unit_vector () {
while ( true ) {
vec3 p = vec3 ( random_float ( - 1 , 1 ), random_float ( - 1 , 1 ), random_float ( - 1 , 1 ));
if (p. length_squared () < 1.0 f ) return normalize (p);
}
}
Gamma correction
// Convert linear to sRGB for display
color = vec3 ( sqrt (color.r), sqrt (color.g), sqrt (color.b)); // gamma 2.0
Checklist
TODO Complete Book 1 (build your first ray tracer)
TODO Add anti-aliasing (multiple samples per pixel)
TODO Implement all 3 material types: diffuse, metal, dielectric
TODO Complete Book 2 (add BVH + lights + Cornell box)
TODO Complete Book 3 (add importance sampling + MIS)
TODO Time your tracer and measure samples/second improvement from BVH
TODO Render the final Cornell box scene