🔆 Creating a physically-based path tracer
As so many people out there that want to get into writing a raytracer, I started with raytracing in one weekend. I can 100% recommend this as a starting point, even if you are not familiar with ray tracing at all.
While there exists a second and a third book, I started diverging after the first book and ended up using the later books only as a reference, instead of following them step-by-step.
Naive Path Tracing
After roughly following the raytracing in one weekend book, I ended up with a naive path tracer that just randomly sends out rays and results in a radiance contribution if the ray ends up hitting a light source.
As you can see in the image above, the result is noisy. When the ray bounces throughout the scene, it will only result in a non-zero contribution only if it happens to hit the light source. A lot of rays will simply miss the light source and not produce a contribution, which causes longer convergence times. This is illustrated in the images below where a scene is shown with a light source that varies in size (power is kept constant).
Because the lighting contribution depends on the ray randomly hitting the light, a small light source will rarely be hit and result in a darker scene. A large light source will be hit by most rays and the scene will be bright.
Explicit Light Sampling
To avoid the noisy result we got from naive path tracing, lights may be explicitly sampled. For each surface interaction, the direct lighting is explicitly evaluated by sending a shadow ray towards the light source.
By explicitly sampling the lights, there no longer is a dependency on the probability of the light path randomly hitting a light source to get a non-zero contribution. Instead, each bounce gets a non-zero contribution because the light sources are explicitly sampled.
When looking at the convergence between implicit light sampling (naive path tracing) and explicit light sampling, the render converges much quicker when explicitly sampling the lights. Below, a graph is shown that quantifies the convergence by measuring the MSE (mean-squared-error).
The term Brute-force is used for naive path tracing and the term Hybrid is used to denote the method where direct illumination is evaluated by explicitly sampling the light sources.
BRDF Importance Sampling
Instead of randomly generating a ray direction within the hemisphere whenever a ray bounces on a surface, an importance-sampling scheme could be used. Using a cosine-weighted sampling scheme, rays do not get randomly generated but are instead generated according to a probability proportional to a cosine-lobe (for diffuse materials). This is in an attempt to generate fewer rays that would result in a low contribution.
In the images above, a scene with a diffuse sphere and a white environment light are shown. By sampling proportional to the BRDF (diffuse so cosine-weighted in this case), the noise is reduced. Different sampling schemes may be used for different BRDFs. This is discussed further in my note about sampling the hemisphere. An example is shown below where a Cook-Torrance BRDF is used and both a cosine-weighted and BRDF-weighted sampling scheme are used. By properly generating samples proportionally to the Cook-Torrance BRDF, the noise is greatly reduced.
Especially for sharp BRDFs, generating random samples in the hemisphere will result in a lot of samples that result in a near-zero contribution of the BRDF factor. This is illustrated in the BRDF plot below.
Support was added for several material type such as mirrors, metallic surfaces and dielectrics (glass). The combination of a glass object with an environment light results in pretty renders and caustics.
My current toy path tracer supports much of the basics that any path tracer needs
- explicit light sampling
- dielectrics (refraction and caustics)
- BRDF importance-sampling
- bounding volume hierarchy (BVH)
- environment lights
- .obj loading with texture support
Much work is still to be done and I have future plans for this so stay tuned!