about a thousand lines.
one photon at a time.
a monte carlo pathtracer in plain c. cornell box, bidirectional scattering, russian roulette, importance sampling. compiles in 0.4 seconds. no dependencies except libm. renders the header image of this page on 8 threads in 43–52 seconds depending on the scene.
$ cc -O3 -lm -fopenmp raytrace.c -o rt $ ./rt scene/cornell.txt --spp 512 --out out.ppm resolution 1280 x 720 triangles 12418 bvh depth 22 threads 8 ┌────────────────────────────────┐ │ 68%│ └────────────────────────────────┘ 49.2 Mray/s · eta 00:14
shoot a ray. sample the integral. repeat.
// 34 lines of the inner estimator. the rest of the file is bvh + io. Vec3 radiance(Ray r, Scene* s, int depth, uint32_t* rng) { Hit h; if (!bvh_intersect(s->bvh, r, &h)) return s->sky; Vec3 L = h.mat->emit; // direct emission if (depth >= MAX_DEPTH) return L; // russian roulette after min_depth float q = fmaxf(h.mat->albedo.x, fmaxf(h.mat->albedo.y, h.mat->albedo.z)); if (depth > 3 && randf(rng) > q) return L; // next event estimation, sample a light directly Vec3 nee = sample_direct(s, h, rng); // bsdf bounce, importance-sample cook-torrance Ray next; float pdf; Vec3 brdf = sample_bsdf(h, r.d, &next, &pdf, rng); if (pdf <= 0.0f) return L + nee; Vec3 Li = radiance(next, s, depth + 1, rng); return L + nee + vmul(brdf, Li) * (1.0f / (pdf * (depth > 3 ? q : 1.0f))); }
small file. real renderer.
bvh w/ sah split
bounding volume hierarchy built bottom-up with the surface area heuristic. median split is fine for toys. sah gets you roughly 3x on dense meshes.
cook-torrance + disney
ggx normal distribution, smith geometry term, schlick fresnel. a trimmed-down disney principled shader on top. metals look like metals.
multiple importance sampling
next event estimation on emitters plus bsdf importance sampling, combined with the balance heuristic. caustics still hate you. everything else settles fast.
russian roulette
after a min depth of 3, each bounce gets killed with probability proportional to albedo. unbiased, and it shaves ~40% off render time on diffuse scenes.
counted with cloc: 1038
no single-letter golf, no #include hacks, no hidden files. headers, scene loader, bvh, brdf, integrator, main. close enough to call it 1k.
blender exists. why write your own? because every line teaches you why graphics is hard. you open a paper on microfacet brdfs, it hand-waves a distribution function, and then you try to sample it correctly and suddenly you understand why everyone writes their own half-vector routine from scratch.
light transport is an integral you can't solve analytically. the rendering equation is recursive, infinite dimensional, and full of discontinuities. so we do what physicists did in the 40s (we give up on being exact). the first time your cornell box converges and the red wall bleeds onto the white ceiling, you understand global illumination in a way no tutorial can hand you.
not public yet.
source drops on github soon, i'm still cleaning up the repo. any c99 compiler when it ships. email bennett@frkhd.com if you want a preview of the code.