GGX materials and spectral path transport
e6fe6bb · 2026-05-21 ↗
This part of the first commit begins with the hit record produced by the BVH. It resolves a material closure, evaluates emitted and direct light, samples a new BSDF event, and multiplies the four-component spectral throughput carried to the next bounce.
Material closure and BSDF object
The renderer first resolves the material ID stored in the hit. Missing materials use a defined fallback rather than leaving a null pointer in the bounce loop:
HitRecord hit{*nextHit};
const IMaterial* material{scene.GetMaterial(hit.MatId)};
if (material == nullptr) {
material = &DefaultMaterial::Instance();
}
BSDFClosure c{material->GetClosure(hit)};
if (!settings.EnableSubsurface) {
c.Subsurface = 0.0f;
}
This is a literal excerpt from
path_trace_pass.cpp.
The closure contains resolved parameters; the IBSDF created from it provides
evaluation, density, and sampling behavior. Keeping the hit and closure
separate allows texture evaluation and Hydra material translation to finish
before the transport loop requests a scattering operation.
The earlier volumetric path-tracer description uses a phase function for a medium interaction and GGX for a surface interaction. This chapter examines the surface branch in the HdRestir source instead of repeating the complete path algorithm.
GGX evaluation
The specular component uses a GGX microfacet BRDF:
\[f_{\mathrm{spec}}(\omega_i,\omega_o)= \frac{D(h)\,F(\omega_i,h)\,G(\omega_i,\omega_o)} {4\lvert n\cdot\omega_i\rvert\lvert n\cdot\omega_o\rvert}, \qquad h=\frac{\omega_i+\omega_o}{\lVert\omega_i+\omega_o\rVert}.\]The implementation evaluates the distribution and a separable masking term:
const float alpha {std::max(0.001f, _c.Roughness * _c.Roughness)};
const float alpha2{alpha * alpha};
const float denom {nDotH * nDotH * (alpha2 - 1.0f) + 1.0f};
const float Dval {alpha2 / (kPi * denom * denom)};
const float k_g {alpha / 2.0f};
const float G_l {nDotL / (nDotL * (1.0f - k_g) + k_g)};
const float G_v {nDotV / (nDotV * (1.0f - k_g) + k_g)};
const float Gval {G_l * G_v};
For \(c=n\cdot h\), the coded distribution is
\[D_{\mathrm{GGX}}(h)= \frac{\alpha^2} {\pi\left(c^2(\alpha^2-1)+1\right)^2}.\]The max(0.001f, ...) operations are denominator guards and roughness floors.
They modify the exact BRDF near grazing angles and at zero roughness. The code
therefore implements a regularized approximation rather than an ideal delta
mirror at roughness zero.
The Fresnel term uses Schlick’s fifth-power approximation:
const GfVec3f F0{
_c.SpecularColor * (1.0f - _c.Metallic) * 0.04f
+ _c.BaseColor * _c.Metallic};
const GfVec3f Fval{
F0 + (GfVec3f{1.0f, 1.0f, 1.0f} - F0)
* std::pow(1.0f - lDotH, 5.0f)};
const GfVec3f specBsdf{
(Fval * Dval * Gval) / (4.0f * nDotL * nDotV)};
F0 interpolates between a dielectric baseline and base-color-controlled
metal reflectance. This is the renderer’s material model; it is not a recovery
of measured complex index-of-refraction data. The derivation of the microfacet
denominator and the reflection/refraction Jacobians is available in
Walter et al..
Diffuse and specular mixture
The initial model adds a Lambertian diffuse term:
const GfVec3f finalDiffuse{
_c.BaseColor * (1.0f - _c.Subsurface)
+ _c.SubsurfaceColor * _c.Subsurface};
const GfVec3f diffBsdf{
finalDiffuse
* (1.0f - _c.Metallic)
* (1.0f - _c.Transmission)
/ kPi};
return diffBsdf + specBsdf;
Division by \(\pi\) normalizes a Lambertian lobe because \(\int_{\Omega^+}\cos\theta\,d\omega=\pi\). The metallic and transmission factors remove diffuse response for those limiting material modes. The sum is an implementation model; this block alone does not prove energy conservation for every combination of parameters because the diffuse term is not explicitly reduced by the angle-dependent Fresnel value.
One bounce of spectral transport
The first commit samples one wavelength packet per camera path. Emission, direct lighting, absorption, and bounce throughput are evaluated at those same four wavelengths:
totalRadiance += throughput * RGBToSpectrum(c.Emission, lambda);
const std::unique_ptr<IBSDF> bsdfOwner{
material->CreateBSDF(BSDFClosure{c})};
const ShadingPoint surface{
hit,
*bsdfOwner,
c,
shadingNormal,
currentRay.Dir,
lambda,
isInside};
totalRadiance += throughput
* SampleDirectLighting(surface, scene.GetLights(), scene, rng);
const BounceConfig config{
settings.MaxReflectionBounces,
settings.MaxRefractionBounces};
const BounceSample bs{
material->SampleBounce(surface, config, bounceState, rng)};
The line adding direct lighting estimates paths that connect the current
surface to a light. SampleBounce generates the continuation event. When that
event is valid, the loop applies:
throughput *= bs.ThroughputMul;
currentRay = bs.NextRay;
nextHit = scene.IntersectScene(currentRay.Origin, currentRay.Dir);
If ThroughputMul represents
\(f_s\lvert n\cdot\omega_i\rvert/p(\omega_i)\), multiplying it into the prior
throughput implements the recurrence derived in step 0.b. Every component
refers to the corresponding wavelength in the unchanged packet.
Limits visible in this commit
Direct-light estimation is a free function called by the path pass. Its light sampler and estimator cannot be replaced independently. The GGX evaluation also regularizes several dot products and adds diffuse and specular terms without a general energy-compensation construction. These are properties of the historical implementation, not conclusions implied by the general rendering equation.
The next commit separates path continuation, direct-light integration, and light selection. The current first-commit discussion stops before introducing those later interfaces.
Caption to keep: The first material model combines a Lambertian diffuse term with a regularized GGX specular term, then uplifts RGB parameters to the active wavelength packet.