pure c · ~1000 lines · no deps except libm · mit

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
— 512 samples per pixel · cold cache to final ppm —
— the inner loop —

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.