I worked by myself, and made a path tracer (runs on CPU, no GPU acceleration).
It entirely uses 32 bit floating point math (IEEE 754 binary32) and leverages
the glm library to get SIMD acceleration for free, as well as using openmp
to trivially parallelize the image rendering process. I loosely followed Peter
Shirley’s series on “ray tracing”, which actually
covers path tracing (and not Project 3 style ray tracing).
Note: I briefly outlined the distinction between a path tracer and a ray tracer in my check-in, and in my presentation video.
As far as my goals for the project went, I did get basic path tracing working with spheres and parallelogram surfaces pretty quickly, showcasing ambient occlusion, reflection, and refraction. To produce coherent lighting via random scattering of rays, supersampling was obviously used. (This also allowed for depth of field effects, although I didn’t produce any images that make it super obvious.) I initially used the sky background as an ambient light source, and then later added emissive materials which allow for using arbitrary surfaces as light sources. I also attempted to implement scattering volumes, which appear to work (besides having ungodly amounts of noise), although I suspect that the sampling complexity associated with getting close to convergence is many orders of magnitude greater than scattering surfaces (which comprise most of the geometry I used). This is because the volumes can scatter rays at a “continuum” of points inside them, while surfaces can only scatter rays at one or a few points on them.
After implementing the BVH, I also took a brief detour to work on my general
purpose fixed point math library. I already had a more or less functional
arithmetic type from a previous embedded project that was on a microcontroller
without hardware support for floating point (read: it was many orders of
magnitude slower at floating point arithmetic than integer arithmetic). For
that project, I had only implemented base 2 exponentiation exp2(). I severely
underestimated how much more of a pain it would be to get trigonometric
functions implemented, which I would need to implement lots of geometric
behaviors involved in a path tracer. At this point I hadn’t yet implemented the
parallelograms or light sources, so I decided to drop it for now and come back
to it if I had time. (I got smacked over the head with two very long assignments
from my math-heavy classes, so that didn’t happen.)
My more ambitious plans also included trying to implement more sophisticated
Monte Carlo integration techniques and potentially better sampling methods than
completely independent random sampling (for faster convergence). I ended up just
using rand() without seeding (for mostly deterministic outputs, except for
nondeterminism introduced by threading).
One last thing I wanted to implement and never got around to, even though it probably would have taken me less than an hour, was texture mapping. The most complicated part would probably have been assigning uv coordinates to points on spheres.
I hope this is pretty obvious, but this essentially takes the Project 3 ray tracer and completely ditches the Phone lighting model that cheaply approximates the behavior of physical light, which can at best capture the effects of direct (local) illumination. Instead it scatters many viewing rays in many directions to more closely imitate the behavior of real light. This captures indirect (global) illumination, which will yield diffuse reflections and ambient occlusion (and softer shadows in general).
Note: I’ve set the CSS for these images to display as “pixelated” instead of letting the browser do its default behavior of attempting to interpolate, so feel free to zoom and inspect the images more closely.
One of the earliest scenes showcasing diffuse materials and ambient sky lighting:

Metallic materials were added (notice that shadows are soft):

Dielectric material added (only does refraction without additional attenuation):

Concentric dielectric spheres with reciprocal indices of refraction make a “bubble”:

Zoomed in from above, it’s a bit clearer what the bubble does:

Test scene from Peter Shirley’s online book:

Finally added “quads” (parallelograms):

Replaced the material of the quad in the back with an emissive material to make it a light source, and turned off the ambient sky lighting:

A more interesting light test scene with a single “quad” light source, and two greenish spheres (including the ground):

More or less the same scene but with more light, and swapping out the white emissive quad for a purplish light source:

The Cornell box with nothing in it, admittedly without enough samples to be sufficiently denoised:

The “canonical” Cornell box with the two slightly angled boxes inside it:

This is the Cornell box with a bubble inside, which I managed to finish rendering in time with much more samples:

Some of this applies to the other Cornell box scenes, but this one is the least noisy so I mention it here:
This was a scene I attempted that has a blue scattering volume inside a dielectric sphere, which effectively gives us subsurface scattering, although it takes a lot more time to render:

This is the same scene, rendered with the same number of samples and max depth, but with a faint white scattering volume encompassing the whole thing to serve as “mist”, but the render itself is so noisy it’s not the most convincing:

This is the same as above, but with more samples and higher max depth. It took more than 8 hours to render and is still only slightly less noisy:

This still being so noisy is why I’m not 100% sure I implemented the scattering volume correctly.
I used the following libraries:
openmp for low-effort parallelismglm for vector math typeslibwebp and libavifThe source code is available on GitHub.