Simple, educational C# Pathtracer
Following on from my simple C# raytracer, here’s the same codebase adapted as a pathtracer.

Read on for full source/binary download.
With pathtracing you get a different set of wonderful light transport effects for free (”free” being in respect of programmer effort rather than CPU cycles) – soft shadows and gorgeous diffuse lighting. Check out the room corner shadowing in the image, and the color bleed on the floor near the lightsource. I adjusted the curves a little in Photoshop to brighten the image up; a gamma correction pass in the code would deal with that.
The principle is simple. We’re still doing a first intersection check as we do for classical raytracing. Instead of then evaluating each light’s contribution to the pixel, we instead create a reflection ray in a random direction by picking a point on an imaginary hemisphere placed over the intersection point (with the hemisphere’s north pole being the surface normal at point of intersection).
Then we recurse; trace from the intersection point through the point we picked on the hemisphere and check for collisions. When one occurs, we do the same thing — bounce off in a random direction.
Note that we’re not spawning any secondary rays, as classical raytracing does to calculate reflections; one ray passed into our recursive Trace() function results in one ray only bouncing through the scene.
Some primitives in the scene are marked as emitters; there are no other light sources in the scene. If the ray bouncing around the scene hits an emitter, Trace() returns the colour of the emitter and that ray bounces no further.
Otherwise, Trace() returns the colour of the current surface multiplied by the calculated reflection colour. So a ray that bounces three times then hits an emitter will result in the following being returned by Trace():
Final bounce (deepest call level): emitter colour
3rd bounce: emitter colour * 3rd surface colour
2nd bounce: (emitter col * 3rd surface colour) * 2nd surface colour
1st bounce (shallowest call level): ((emitter col * 3rd surface colour) * 2nd surface colour) * 1st surface colour
If a ray bounces a given maximum number of times without hitting an emitter, Trace() will bail out and as can be seen from the call tree above, the pixel we’re calculating for will end up black.
Given enough time we could calculate the complete hemispheric viewpoint as “seen” by an intersected point! That would take longer than we have, so we’re making an approximation to that integral. If we just fired off one initial ray per pixel, the results would be extremely noisy (try it and see). Just following one possible photon path per pixel obviously isn’t enough; so, we repeat the whole operation n times per pixel, and average the results. That way the pixel has n possible photon paths contributing to it, creating a more realistic approximation than just one.
There are many possible enhancements. Path tracing can simulate most forms of light transport. Really when we hit a point, we should choose something to do with a weighted probability based on surface properties. For example a photon could take a diffuse or specular path — or indeed a refractive path instead of reflective. For a glass surface for example, a photon could well choose a diffuse reflective path but alternatively it could choose to follow a refractive path into the glass. This doesn’t take much implementing and gives us caustics — lots of fun to be had!
Download source and binary (13Kb RAR file)







Thank you Iain for sharing your work!
I was just wondering about the statistics on the image above with red, green, and an emitter balls.
So, how many path per pixel did you trace to get that image, and how long did it take?
Comment by Hae Jeong — Nov 17 2009 @ 2:32 pm
Hi Hae, thanks for your comment. That image was 512 paths per pixel, with a maximum bounce depth of 4. It took a few hours, possibly 6 to 8, but I was rendering on my little pre-Atom EeePC!
The code’s pretty unoptimised to make it hopefully highly readable.
Comment by IainC — Nov 17 2009 @ 5:39 pm
Oh, I should also say that was for an 800 x 600 render
Comment by Iain — Nov 18 2009 @ 3:33 pm