Project overview
This project extends the explicit path tracer from Project 2 to support physically based materials via a microfacet BRDF model. Project 2 established the core path tracing infrastructure (implicit and explicit sampling, Russian Roulette termination, and fixed-weight blending) but evaluated surfaces using only a Lambertian (purely diffuse) BRDF. Project 3 replaces that with a full Cook-Torrance microfacet model, which renders glossy and reflective surfaces physically accurately.
The framework introduces a toggle (ENABLE_BRDF) that switches between the legacy diffuse-only mode and the new full BRDF evaluation, keeping all other path tracing infrastructure identical. The linear blending of weights is also upgraded from the fixed 0.5f split of Project 2 to the Balance Heuristic (discussed as part of next project), which further reduces variance when the BRDF lobe and explicit light sampling strategies differ in quality.
Test scenes
Three scenes of increasing complexity validate the implementation, from a single reflective sphere on a table to a full scene with high-polygon meshes and multiple inter-reflecting spheres.
BRDF model overview
The implemented BRDF is a microfacet specular model combined with a Lambertian diffuse term, following the Cook-Torrance formulation. The full BRDF evaluated in EvalScattering is:
f(L, V) = Kd/π * (N·L) + D(m) * G(L, V) * F(L, m) / (4 * (N·V))
Where:
D: Phong Normal Distribution Function (NDF), which models the statistical distribution of microfacet normals.G: Smith geometry / shadowing-masking term, which accounts for mutual occlusion of microfacets.F: Fresnel reflectance (Schlick approximation), which models the fraction of light reflected vs. refracted at a surface.m: half-vector,normalize(L + V), the microfacet normal that reflectsLtowardV.α(alpha): roughness parameter, which controls the width of the specular lobe.
Phong normal distribution function (NDF)
The NDF D(m) describes what fraction of microfacets are oriented with their normal aligned to the half-vector m. A Phong-based NDF is used:
float Scene::DistributionPhong(glm::vec3 normal, glm::vec3 m, float alpha)
{
float mDotN = dot(m, normal);
return xPlus(mDotN) * ((alpha + 2) / (2.0f * PI)) * pow(mDotN, alpha);
}
As alpha increases, the distribution becomes tighter and more mirror-like. At low alpha values, the distribution is broad and the material appears diffuse-dominant; at high values, it produces sharp, concentrated specular highlights.
Geometry / shadowing-masking term
The geometry term G(L, V) accounts for self-shadowing and masking between microfacets. The implementation uses a Smith factorization, computing an independent G1 term for both the incident and outgoing directions and multiplying them together:
float Scene::GeometrySmith(glm::vec3 normal, glm::vec3 inDir, glm::vec3 outDir, float alpha)
{
glm::vec3 m = normalize(inDir + outDir);
float G1_in = GeometryPhong(normal, inDir, m, alpha);
float G1_out = GeometryPhong(normal, outDir, m, alpha);
return G1_in * G1_out;
}
Each G1 factor is evaluated using the Phong-specific approximation:
float Scene::GeometryPhong(glm::vec3 normal, glm::vec3 v, glm::vec3 m, float alpha)
{
float a = sqrt(alpha * 0.5f + 1.0f) / tanThetaV;
// Rational approximation for a < 1.6
return (3.535f * a + 2.181f * a * a) / (1.0f + 2.276f * a + 2.577f * a * a);
}
Rays that are nearly grazing (small N·V) produce small G values, correctly attenuating the specular contribution to avoid energy non-conservation at extreme viewing angles.
Fresnel reflectance: Schlick approximation
The Fresnel term models the angle-dependent ratio of reflected light. At normal incidence, a surface reflects a fraction determined by Ks (the specular color). As the viewing angle approaches grazing, reflectance tends toward 1 (total reflection). The Schlick approximation is used:
glm::vec3 Scene::FresnelSchlick(glm::vec3 inDir, glm::vec3 m, Material* mat)
{
vec3 Ks = mat->Ks;
float d = dot(inDir, m);
return Ks + (vec3(1.0f) - Ks) * pow((1.0f - abs(d)), 5.0f);
}
Ks acts as the base reflectance at normal incidence. For metals, Ks is a tinted color reflecting the metal’s wavelength-dependent reflectance. For dielectrics, it is a low-value grey. The pow(..., 5) term gives the physically plausible rapid rise in reflectance at grazing angles.
Full BRDF evaluation: EvalScattering
The full BRDF is assembled from the D, G, and F terms in EvalScattering:
vec3 Scene::EvalScattering(glm::vec3 normal, glm::vec3 inDir, glm::vec3 outDir, Material* mat)
{
float D = DistributionPhong(N, m, alpha);
float G = GeometrySmith(N, L, V, alpha);
vec3 F = FresnelSchlick(L, m, mat);
float denom = 4.0f * glm::max(outDotN, 0.0001f);
vec3 diffuse = mat->Kd / PI;
vec3 specular = (D * G * F) / denom;
return inDotN * (diffuse) + specular;
}
The denominator clamp (glm::max(outDotN, 0.0001f)) prevents numerical blow-up at extreme grazing configurations. The final result combines the diffuse and specular lobes, multiplied by the cosine factor inDotN = N·L to account for the projected surface area facing the light.
BRDF sampling: SampleBRDF
With a mixed diffuse-specular BRDF, the ray direction at each bounce must be sampled proportionally from both lobes. The sampling probability is determined by the relative magnitudes of Kd and Ks:
glm::vec3 Scene::SampleBRDF(const glm::vec3& normal, const glm::vec3& outDir, Material* mat)
{
float s = glm::length(mat->Kd) + glm::length(mat->Ks);
float pd = glm::length(mat->Kd) / s;
float e = myrandom(RNGen);
float e1 = myrandom(RNGen);
float e2 = myrandom(RNGen) * 2.0f * PI;
if (e < pd) {
return SampleLobe(normal, sqrt(e1), e2); // Cosine-weighted diffuse
} else {
float cosTheta = pow(e1, 1.0f / (alpha + 2.0f));
glm::vec3 m = SampleLobe(normal, cosTheta, e2);
return 2.0f * dot(outDir, m) * m - outDir; // Phong specular reflection
}
}
With probability pd, a cosine-weighted diffuse direction is chosen. Otherwise, a specular microfacet normal m is sampled from the Phong NDF, and the outgoing direction is computed as the specular reflection of outDir about m. Sampling in proportion to Kd and Ks keeps either lobe from being over- or under-sampled regardless of material composition.
BRDF PDF: pdfBrdf
To correctly weight the Monte Carlo estimate, the PDF must account for both the diffuse and specular sampling strategies:
float Scene::pdfBrdf(const glm::vec3& normal, const glm::vec3& inDir, const glm::vec3& outDir, Material* mat)
{
float pd = glm::length(mat->Kd) / s;
float pr = glm::length(mat->Ks) / s;
// Diffuse PDF: cos(theta) / PI
float Pd = lDotN / PI;
// Specular PDF: (D * mDotN) / (4 * vDotM)
float dm = DistributionPhong(N, m, alpha);
float Pr = (dm * dot(m, N)) / (4.0f * vDotM);
return pd * Pd + pr * Pr;
}
The mixed PDF is a weighted sum of both lobe PDFs using the same pd / pr probabilities as the sampler, which keeps the Monte Carlo weight f / p unbiased across all path configurations.
Equal-weight sampling
When both explicit shadow rays and implicit BRDF-sampled rays are active simultaneously, both strategies can contribute light from the same source. To combine them without double-counting, the project baseline assigns each strategy an equal fixed weight of 0.5f:
// Explicit shadow ray contribution
C += 0.5f * W * (f / p) * hit.shape->mat->Kd;
// Implicit BRDF ray that happens to hit the light
C += W * nextHit.shape->mat->Kd * 0.5f;
This is simple, unbiased, and sufficient for purely diffuse scenes where both strategies contribute roughly equally at every surface point. On some specular and glossy surfaces, though, the two strategies are far from equal in quality: the BRDF sampler concentrates rays tightly around the specular lobe, while the light sampler draws directions uniformly toward the light regardless of the surface’s reflective peak. Assigning each a flat 0.5f weight lets the weaker strategy inject disproportionate variance into the estimate, which shows up as bright noise speckles and fireflies concentrated near reflective surfaces.
Multiple importance sampling: the Balance Heuristic
To resolve fireflies introduced by equal weighting on specular surfaces, the fixed 0.5f weights are replaced with the Balance Heuristic. The root cause of fireflies is a BRDF-sampled ray that accidentally strikes a bright light source from a direction with very low BRDF probability: the Monte Carlo weight f/p becomes large because p is small, spiking that pixel far above its neighbors.
For the explicit shadow ray connection:
float p_brdf = pdfBrdf(P.normal, inDir, outDir, P.shape->mat);
float weight = p / (p + p_brdf); // p = light PDF
C += weight * W * (f / p) * hit.shape->mat->Kd;
For the implicit BRDF-sampled ray that hits the light:
float p_brdf = pdfBrdf(N, inDir, outDir, P.shape->mat) * RUSSIAN_ROULETTE_PROB;
float p_light = PdfLight(nextHit) / GeometryFactor(P, nextHit);
float weight = p_brdf / (p_brdf + p_light);
C += W * nextHit.shape->mat->Kd * weight;
The Balance Heuristic suppresses this by incorporating the light sampling PDF into the denominator alongside the BRDF PDF, acknowledging that the explicit strategy could have reached that same direction with much higher probability:
weight_brdf = p_brdf / (p_brdf + p_light)
This larger denominator pulls the weight down for exactly the rays that cause fireflies: those sampled by the BRDF at low probability but easily reachable by the light sampler. On a diffuse surface where p_brdf and p_light are comparable in magnitude, the heuristic recovers weights close to 0.5f, matching the baseline behavior. Fireflies on specular surfaces are suppressed with no additional bias introduced into the estimator.
Comparison: equal-weight vs. Balance Heuristic
| Passes | Equal-Weight Sampling | Balance Heuristic (MIS) |
|---|---|---|
| 256 |
Click to zoom
|
Click to zoom
|
| 512 |
Click to zoom
|
Click to zoom
|
| 2048 |
Click to zoom
|
Click to zoom
|
| 4096 |
Click to zoom
|
Click to zoom
|
Test scenes
Three test scenes of increasing complexity validate the BRDF implementation. Each scene is rendered at pass counts growing in powers of 2 from 8 to 4096, which shows how quickly the specular model converges under the Balance Heuristic MIS.
Scene 1: reflective table with a single sphere
The baseline test scene has a rectangular table with a reflective top surface and metallic edges, with a single highly reflective sphere at its center. Colored diffuse walls (green at the back, blue on the side, neutral on the floor) surround the scene and contribute indirect bounce lighting. A spherical area light positioned directly above the sphere provides the primary illumination, and the table legs are thin white cylinders.
The metallic table edges pick up green and blue tints from the surrounding walls, which is colored indirect transport. The sphere, being nearly mirror-like, prominently reflects the overhead light source as a sharp highlight and captures the colored environment across its surface. A faint reflection of the sphere itself is also visible on the table top, which confirms specular inter-reflection between the two surfaces.
Test images
| 256 Passes | 512 Passes |
|---|---|
Click to zoom
|
Click to zoom
|
| 2048 Passes | 4096 Passes |
Click to zoom
|
Click to zoom
|
Scene 2: table with sphere, rabbit, and dwarf
The second scene extends Scene 1 by placing two high-polygon mesh objects on the table surface: a Stanford Rabbit on the right and an armored Dwarf character on the left. Both meshes use a diffuse-dominant white material with minimal specular response, which contrasts with the highly reflective table and sphere. The colored-wall environment and overhead light from Scene 1 are unchanged.
The rabbit and dwarf accumulate soft colored indirect illumination from the surrounding walls, with green and blue tints visible across their matte surfaces. Their geometry casts shadows onto the reflective table top, producing visible occlusion patterns in the table’s specular reflection. The sphere’s curved surface also reflects both figures, though with significant angular distortion due to its curvature: the rabbit’s ear in particular is identifiable in the reflection. Faint specular reflections of both meshes are also visible on the table surface, consistent with the inter-reflection behavior established in Scene 1.
Test images
| 256 Passes | 512 Passes |
|---|---|
Click to zoom
|
Click to zoom
|
| 2048 Passes | 4096 Passes |
Click to zoom
|
Click to zoom
|
Scene 3: table with meshes and multiple reflective spheres
The third scene is the most complex, retaining the rabbit and dwarf from Scene 2 while replacing the single central sphere with two reflective spheres positioned across the table surface. Each sphere reflects the other, the mesh objects, and the colored environment simultaneously, forming a network of multi-bounce specular paths that places the highest demand on the path tracer.
Specular-to-specular transport between the two spheres requires multiple bounces to resolve correctly. The rabbit and dwarf, now flanked by spheres on both sides, accumulate colored specular bounce light from the adjacent sphere surfaces. The rabbit’s silhouette and the dwarf’s form are both identifiable as distorted reflections on the second sphere. A close inspection of the first sphere reveals the reflection of the second sphere nested within it, and within that reflection a further recursive specular highlight, which confirms that the renderer correctly propagates specular-to-specular paths across multiple bounces.
Test images
| 256 Passes | 512 Passes |
|---|---|
Click to zoom
|
Click to zoom
|
| 2048 Passes | 4096 Passes |
Click to zoom
|
Click to zoom
|
Summary
- Microfacet BRDF: the renderer now evaluates a full Cook-Torrance specular model (Phong NDF + Smith G + Schlick F) alongside Lambertian diffuse, which covers a wide range of real-world surface appearances.
- Mixed sampling:
SampleBRDFprobabilistically samples from either the diffuse or specular lobe proportional to|Kd|and|Ks|, andpdfBrdfreturns the corresponding mixed PDF, keeping the Monte Carlo estimator unbiased. - MIS, equal weights to Balance Heuristic: combining explicit and implicit light contributions with a fixed
0.5fweight sometimes produces fireflies near specular surfaces. The Balance Heuristic replaces it and weights each strategy in proportion to its PDF at the sampled direction, which suppresses high-frequency noise on reflective geometry while preserving unbiasedness. - Framework extensibility: the clean separation of
EvalScattering,pdfBrdf, andSampleBRDFinto per-material, direction-aware overloads makes it straightforward to incorporate additional material types (e.g., glass / transmission, anisotropic microfacets) in future projects.