Project overview
This project extends the deferred rendering pipeline from Projects 1 and 2 by replacing direct light sources with image-based lighting (IBL) and a full Cook-Torrance PBR BRDF. The BRDF combines a Lambertian diffuse term with a microfacet specular term built from the Trowbridge-Reitz GGX NDF, Smith G1 geometry masking, and Schlick Fresnel, with energy conservation enforced through a Fresnel-derived kD/kS split.
Environment illumination comes entirely from real HDR panoramas, through two techniques. Spherical harmonic (SH) irradiance covers the low-frequency diffuse contribution, precomputed into 9 coefficients at load time and evaluated at O(1) cost per fragment. GGX importance sampling covers the high-frequency specular contribution, evaluated at runtime by drawing N quasi-random samples from the Hammersley sequence and warping them into the GGX lobe. Mip level selection based on the GGX PDF makes smooth surfaces fetch sharp environment samples while rough surfaces fetch blurred ones. The lighting responds to material properties across the full metallic-roughness PBR parameter space.
Architecture overview
Updated rendering pipeline
The lighting pass is fully replaced by environment-driven IBL; no direct lights remain in the main lighting equation:
────────────────────────────────────────────────────────────
RENDERING PIPELINE
─────────────────────────────────────────────────────────────
1. GEOMETRY PASS
↓
Render scene geometry to G-Buffer (Same as Prj 1 & 2)
2. IBL LIGHTING PASS
↓
Evaluate SH irradiance for diffuse
Importance-sample environment map for specular
Apply Fresnel, energy conservation, tone mapping
3. POST-PROCESSING
↓
Draw sky dome from active HDR map
Render UI (ImGui)
─────────────────────────────────────────────────────────────
Components added
| Component | File | Purpose |
|---|---|---|
| SHIrradiance | include/SHIrradiance.h | Pre-compute 9 SH coefficients from HDR panorama |
| HammersleyRandomGenerator | include/HammersleyRandomGenerator.h | Generate quasi-random (e1, e2) sample pairs |
| IBL lighting shader | shaders/deferred_lighting.frag | Evaluate diffuse + specular IBL per pixel |
| Sky dome | include/SkyDome.h | Render the active equirectangular HDR as background |
Scene setup and materials
Scene composition
Instead of the single scene used in previous projects, we show one model at a time, so we can inspect models made of different materials against different environment maps.
Models in scene
| Model | Material |
|---|---|
| Lion Head | Aged Bronze |
| Bull Head | Polished Bronze |
| Horse Head | Polished Bronze |
| Marble Bust | Rough Marble |
| Horse Statue | Smooth Marble |
| Vintage Video Camera | Metal / Resin |
| Cassette Player | Metal / Plastic |
| Katana | Metal / Leather |
| War Hammer | Metal / Wood |
| Megaphone | Plastic |
Environment maps
Eight HDR panoramas are available at runtime, selectable via the ImGui panel:
| Index | File | Character |
|---|---|---|
| 0 | cedar_bridge_sunset_1_4k.exr | Warm sunset |
| 1 | moonless_golf_4k.exr | Dark night sky |
| 2 | cobblestone_street_night_4k.exr | Urban night with street lights |
| 3 | suburban_garden_4k.exr | Suburban afternoon sun |
| 4 | illovo_beach_balcony_4k.exr | Bright coastal |
| 5 | aft_lounge_4k.exr | Interior warm |
| 6 | signal_hill_sunrise_4k.exr | High-contrast sunrise |
| 7 | ferndale_studio_04_4k.exr | Studio with colored lights |
BRDF implementation
The lighting shader uses the Cook-Torrance BRDF throughout, combining a diffuse Lambertian term with a microfacet specular term.
Normal distribution function: Trowbridge-Reitz GGX
The NDF describes the statistical distribution of microfacet orientations. The GGX variant is used because it produces a longer specular tail compared to Blinn-Phong, which is more physically accurate for most real surfaces:
float DistributionGGX(vec3 N, vec3 H, float alpha) {
float a2 = alpha * alpha;
float NdotH2 = pow(max(dot(N, H), 0.0), 2.0);
float denom = PI * pow(NdotH2 * (a2 - 1.0) + 1.0, 2.0);
return a2 / denom;
}
The NDF takes alpha (roughness) as input, with a2 = alpha² used in the denominator following the standard GGX form.
Geometry function: Smith G1 for GGX
The geometry term accounts for microfacet self-shadowing and masking. Smith’s method applies the G1 function independently to the view and light directions and multiplies them:
float GeometryG1(float dotVN, float alpha) {
float tanTheta2 = (1.0 - dotVN * dotVN) / (dotVN * dotVN);
return 2.0 / (1.0 + sqrt(1.0 + alpha * alpha * tanTheta2));
}
float GeometrySmith(vec3 N, vec3 V, vec3 L, float alpha) {
return GeometryG1(dot(N,V), alpha) * GeometryG1(dot(N,L), alpha);
}
G1 is expressed in terms of tanTheta², computed as (1 - NdotV²) / NdotV², which is the exact GGX masking function derived from the microfacet height-correlated distribution.
Fresnel: Schlick approximation
The Fresnel term controls view-angle-dependent reflectance. Ks is the specular base reflectance: 0.04 for dielectrics, blended toward the albedo for metals:
vec3 FresnelSchlick(float LdotH, vec3 Ks) {
return Ks + (1.0 - Ks) * pow(1.0 - LdotH, 5.0);
}
vec3 F0 = mix(vec3(0.04), albedo, metallic);
Energy conservation: kD / kS split
The diffuse and specular terms are balanced using Fresnel-derived weights to ensure the total outgoing energy never exceeds the incoming energy:
vec3 kS = F; // specular fraction
vec3 kD = (1.0 - kS) * (1.0 - metallic); // diffuse fraction, zero for metals
return kD * diffuse + specColor;
Metals have zero diffuse by definition, and the (1.0 - metallic) factor drives kD to zero as metallic approaches 1.
IBL implementation
Diffuse: spherical harmonic irradiance
The diffuse component is precomputed into 9 second-order SH coefficients per environment map at application bootstrap. At render time, evaluating the irradiance at a surface normal is an O(1) dot product sum.
Coefficient computation (SHIrradiance.h)
The HDR panorama is read via OpenImageIO. Each pixel is mapped to a world-space direction using the inverse of uvOfW. The mapping must be the exact inverse of the UV convention used in the shader: otherwise the SH coefficients and the shader evaluation disagree on which direction a pixel represents:
// Exact inverse of uvOfW:
// uvOfW: u = 0.5 - atan(x,z)/(2PI), v = acos(y)/PI
// Inverting:
// phi = (0.5 - u) * 2PI
// theta = v * PI
double u = (i + 0.5) / double(W);
double phi = (0.5 - u) * 2.0 * M_PI;
float x = float(sinTheta * std::sin(phi)); // first arg of atan2
float y = float(cosTheta); // Y-up
float z = float(sinTheta * std::cos(phi)); // second arg of atan2
Each pixel is weighted by the solid-angle differential sin(θ) dθ dφ and accumulated into the raw SH projection:
// SH projection integral:
// L_lm = Σ L(x,y,z) · Y_lm(x,y,z) · sinθ · dθ · dφ
float Y[9];
evalBasis(x, y, z, Y);
float weight = float(sinTheta * dTheta * dPhi);
for (int k = 0; k < 9; ++k)
Llm[k] += L * Y[k] * weight;
The clamped-cosine Ahat convolution factors are baked in at the end, so the shader never needs to apply them at runtime:
// E_lm = Ahat_l * L_lm
// l=0: PI, l=1 (×3): (2/3)PI, l=2 (×5): (1/4)PI
const float Ahat[9] = {
float(M_PI),
float((2.0/3.0)*M_PI), float((2.0/3.0)*M_PI), float((2.0/3.0)*M_PI),
float((1.0/4.0)*M_PI), float((1.0/4.0)*M_PI), float((1.0/4.0)*M_PI),
float((1.0/4.0)*M_PI), float((1.0/4.0)*M_PI),
};
for (int k = 0; k < 9; ++k)
coeffs[k] = Llm[k] * Ahat[k];
Because Ahat is already folded into shCoeffs, the diffuse term in the shader reduces to a simple weighted sum with no extra multiplication:
vec3 irradN = evalSHIrradiance(normalize(N));
vec3 diffuse = (albedo / PI) * irradN;
SH basis functions (GLSL)
vec3 evalSHIrradiance(vec3 n) {
float x = n.x, y = n.y, z = n.z;
float Y[9];
Y[0] = 0.5 * sqrt(1.0 / PI);
Y[1] = 0.5 * sqrt(3.0 / PI) * y;
Y[2] = 0.5 * sqrt(3.0 / PI) * z;
Y[3] = 0.5 * sqrt(3.0 / PI) * x;
Y[4] = 0.5 * sqrt(15.0 / PI) * x * y;
Y[5] = 0.5 * sqrt(15.0 / PI) * y * z;
Y[6] = 0.25 * sqrt(5.0 / PI) * (3.0 * z*z - 1.0);
Y[7] = 0.5 * sqrt(15.0 / PI) * x * z;
Y[8] = 0.25 * sqrt(15.0 / PI) * (x*x - y*y);
vec3 irradiance = vec3(0.0);
for (int k = 0; k < 9; k++)
irradiance += shCoeffs[k] * Y[k];
return max(irradiance, vec3(0.0));
}
Specular: GGX importance sampling
The specular integral is evaluated at runtime by importance-sampling the GGX NDF. For each fragment, N samples are drawn from the Hammersley quasi-random sequence and warped into the GGX lobe around the reflection vector R.
Hammersley sequence (HammersleyRandomGenerator.h)
The generator produces N pairs (e1, e2) using the Van der Corput radical-inverse sequence for low-discrepancy sampling:
float x = float(i) / float(numSamples); // Stratified i/N
float y = radicalInverse_VdC(i); // Bit-reversed radical inverse
Pairs are packed into vec4 to satisfy std140 alignment and uploaded once to a UBO (HammersleyBlock). The sample count can be changed at runtime via updateUBO(newNumSamples), which reallocates the buffer.
Sampling direction construction
For each sample (e1, e2) and roughness a:
float theta = atan(a * sqrt(e2) / sqrt(1.0 - e2)); // GGX half-angle
vec3 D = vectorOf(e1, theta / PI); // Microfacet half-vector in local frame
// Rotate into reflection-aligned frame
vec3 L = tangent * D.x + R * D.y + bitangent * D.z; // World-space light direction
Environment map mip-level selection
To avoid noise and aliasing, each sample fetches a mip-filtered environment sample. The mip level is derived from the total environment resolution, the sample count N, and the GGX PDF value D. A larger D means the sample falls in a high-probability, narrow lobe direction (smooth surface) and fetches a sharper mip, directly reflecting the environment. A smaller D means the sample is spread across a wide lobe (rough surface) and fetches a blurrier mip, smearing the environment contribution:
ivec2 size = textureSize(envMap, 0);
int width = size.x;
int height = size.y;
level = 0.5 * (log2( float(width * height) / float(ibl.N))) - 0.5 * log2( max(D, 0.0001) / 4.0);
level = clamp(level, 0.0, maxLod);
return textureLod(envMap, uv, level).rgb;
Specular accumulation
Only samples where dot(N, L) > 0 contribute. The Cook-Torrance BRDF is evaluated per accepted sample:
float D = DistributionGGX(N, H, roughness);
float G = GeometrySmith(N, V, L, roughness);
vec3 F = FresnelSchlick(HdotV, F0);
specColor += Li * NdotL * (G * F) / (4.0 * NdotV * NdotL);
The accumulator is normalized by acceptedSamples (samples with dot(N,L) > 0) rather than N.
Tone mapping
Four tone mapping operators are implemented and switchable at runtime:
| Method | Description |
|---|---|
| None | Linear (clamp only, for reference) |
| Reinhard | x / (1 + x), simple, desaturates highlights |
| ACES | (x(ax+b)) / (x(cx+d)+e), industry filmic standard |
| Uncharted 2 | Hable curve with configurable shoulder, toe, and linear |
| PBR Neutral | Khronos specification, desaturation-preserving |
All operators are followed by gamma correction: color = pow(color, vec3(1.0/2.2)).
All the videos are recorded with ACES for consistency (and a personal bias towards it).
UI and debug controls
Interactive IBL controls
| Control | Range / Options | Effect |
|---|---|---|
| Environment map | 0-7 (8 HDR maps) | Switches active panorama + reloads SH coefficients |
| Sample count (N) | 1-1024 (default 40) | Specular quality vs. performance |
| Diffuse only | Toggle | Disables specular loop, shows SH diffuse only |
| Exposure | Scalar | Pre-tonemapping linear exposure multiplier |
| Tone map method | None / Reinhard / ACES / Uncharted 2 / PBR Neutral | Post-exposure curve |
| Material override | Toggle + metallic / roughness / color | Uniform material for comparison |
Debug visualization modes
Inherited from Projects 1 and 2, the G-Buffer components remain accessible:
| Mode | Channel | Visualization |
|---|---|---|
| Position | RGB (log-mapped) | World-space positions |
| Normal | RGB | Surface normals |
| Albedo | RGB | Base color |
| Metallic | Grayscale | Metal/dielectric factor |
| Roughness | Grayscale | Surface roughness |
Visual results
Diffuse-only IBL
We visualize the diffuse-only part of IBL against different environment maps.
For the first pair, a sunset scene with the sun low on the horizon casts the lower hemisphere in warm yellow while the upper hemisphere receives a cooler sky white, followed by a nighttime street scene where most of the sphere takes on a yellow cast from surrounding lights, with the section facing away from any source remaining dark and untinted.
For the next pair, a suburban garden under a sharp noon sun lights the top of the sphere bright white while the lower hemisphere picks up a green tint from the grass, followed by a coastal balcony with the sun at an angle where surfaces facing the bright walls and sun appear whiter and those oriented toward shade are visibly darker.
For the next pair, an indoor lounge with large glass windows on one side and warm ceiling lights on the other lights the window-facing hemisphere with the brighter afternoon sun while the interior-facing side picks up a warm yellow tint from the ceiling lights, followed by an early morning sunrise where the lower hemisphere picks up a yellow tint from ground bounce, the backside catches additional bounce off nearby rocks, and areas facing away from any surface fall into darker, cooler shadow.
The final map is a studio setup with multiple colored lights placed around the scene. As the sphere rotates, the irradiance transitions smoothly across green, yellow, and blue, each tint corresponding to the dominant light source in that direction.
Sample count comparison
To validate the importance sampling direction computation, we isolate the effect of N by fixing mip level to 0 and using a fully metallic surface (metallic = 1.0) at medium roughness (0.5).
Starting at N = 1, the sphere shows a clean single reflection of the environment map. Stepping to N = 2 produces two superimposed reflections, and N = 3 produces three. Each additional sample contributes a distinct reflected image, which confirms that the importance sampling directions are being computed correctly.
With N held constant, we vary roughness to observe its effect on the sampled directions. As roughness increases toward 1.0, the superimposed reflections diverge and spread apart, confirming that the GGX lobe is widening. As roughness decreases back to 0, all samples collapse to the same reflection direction and the images merge into one.
Finally with N pushed into the 40s, the sphere is covered with so many superimposed reflections that the individual images become indistinguishable. The same roughness behaviour holds: increasing roughness spreads the samples across a wider lobe, and reducing it back to 0 collapses them all to a single sharp reflection.
Material response
We traverse the material parameter space in a loop, varying metallic and roughness independently to isolate their effects. Since the GGX model treats these as separate parameters, we have dedicated sliders for each rather than a single combined alpha.
Starting with metallic = 0, roughness = 0 and increasing metallic to 1.0. At metallic = 0, the surface is a smooth dielectric: it shows a sharp specular highlight from Fresnel reflectance alongside a diffuse base. As metallic increases, the diffuse term is suppressed and F0 rises, transitioning the surface into a mirror metal that reflects the full environment.
With metallic fixed at 1.0, roughness increases from 0 to 1.0. At roughness = 0 the surface is a perfect mirror with a sharp sun reflection. As roughness increases, the GGX NDF broadens, spreading the specular lobe and smearing the sun spot into a wider, softer highlight until it becomes nearly indistinguishable at roughness = 1.0.
With roughness fixed at 1.0, metallic decreases from 1.0 to 0. As metallic drops, the specular contribution weakens (F0 falls to the dielectric baseline) and the diffuse term grows, gradually shifting the surface from a dark rough metal to a diffuse-dominant rough dielectric.
Finally, with metallic fixed at 0, roughness decreases from 1.0 back to 0. The surface transitions from a fully diffuse rough dielectric to a smooth marble. As roughness decreases, the specular lobe narrows and the broad soft highlight converges into a sharp, concentrated sun reflection.
Full IBL with different environment maps
We fix the surface as a brushed metal (metallic = 1.0, roughness = 0.5) and swap between environment maps to observe how the specular response changes with the lighting conditions.
For the first pair, a sunset HDRI with a single low-angle sun concentrates the specular lobe in the hemisphere tracking its reflection direction while the rest of the surface shows only the metallic sheen, followed by a nighttime HDRI with multiple scattered light sources producing several distinct highlights across the sphere, each carrying a faint color tint from its corresponding light.
For the next pair, a dense street scene with bright lights spread across a wide solid angle fills much of the sphere with overlapping highlights, with window lights on the darker building face also visible as the scene rotates, followed by a suburban noon HDRI where the sun directly overhead concentrates the specular lobe at the top while the sides show only a broad metallic sheen.
For the next pair, a coastal balcony HDRI with the sun at an angle produces a strong primary lobe tracking the sun direction and a fainter secondary from sunlight bouncing off nearby walls, so the specular integral picks up indirect bounce light captured in the map. Then an indoor lounge HDRI, where two distinct light sources produce two separate highlights: the window-side lobe sharp and intense from the high-radiance sun, the ceiling light lobe noticeably softer and dimmer, directly reflecting the radiance difference between the two sources.
For the next pair, a sunrise HDRI produces a concentrated warm-tinted lobe in the hemisphere facing the low-horizon sun (similar to the sunset case), followed by a studio HDRI with multiple colored lights where distinct green, blue, and yellow highlights appear and fade across the sphere as the scene rotates.
Full IBL with more complex geometry
Rather than uniform material sliders, these models load color, normal, metallic, and roughness from per-UV texture maps, producing spatially varying material responses across the surface.
Lion head statue
The lion head statue is an aged bronze (high metallic, high roughness) with the worn texture of aged metal spread non-uniformly across its surface.
In the sunset scene, the specular sheen appears as broad strokes across the crevices of the mane as we orbit the scene. Fixing the camera and rotating the statue, the sheen travels across the surface tracing the high-metallic regions of the texture map.
In the urban street scene with multiple light sources, the statue is more evenly and brightly lit. The specular reflections are broader and more distributed, with each light contributing its own streak across the mane’s crevices as the model rotates.
Switching to a brighter neutral outdoor scene, the aged bronze reads more clearly: the sharper sun brings out the contrast between the rough oxidized patches and the slightly smoother ridges. The indoor lounge scene produces a similar result but with a warmer, more subdued tint from the ceiling lights.
In the studio map, the result is less dramatic than the outdoor scenes. Since metals have near-zero diffuse, the color transitions between different light sources are largely suppressed. The most notable effect is on the flat back surface, which presents a large area facing the blue light and picks up a visible blue tint.
Horse head statue
The horse head is a similar bronze-brass metal but with lower roughness than the lion head, giving it a less aged and smoother appearance.
The lower roughness produces a sharper, more concentrated specular response. As the model rotates against the sunset, the sun’s reflection travels cleanly across the surface as a crisp highlight that follows the geometry.
Switching to a neutral midday scene cools the overall tint while keeping the sharp specular reflection. The indoor scene introduces a blend of cooler window light and warmer ceiling tones, with the specular sheen still visible as the model rotates.
Marble bust statue
Moving away from metals, the marble bust is a predominantly rough diffuse surface with only faint smoothness in certain areas, producing specular glints only at grazing angles.
In the urban street scene, the bust picks up the warm yellow diffuse tint from the surrounding lights. Switching to a nighttime map removes the warm tint and the surface reverts to its natural diffuse color, with faint specular glints at grazing angles and a subtle green tint from distant lights visible as the scene rotates.
In a neutral outdoor scene the statue appears whiter, losing its warm tones: surfaces facing the sun are brighter, those in shade are darker. Switching to the indoor lounge blends the cool window light and warm ceiling glow across the surface, producing a mixed tint that is neither fully warm nor fully cool.
The studio map is where the marble surface is most visually interesting. Without a strong metallic term suppressing the diffuse, it picks up all the colored lights cleanly and transitions through yellow, green, blue, and back to yellow with hints of purple as the scene rotates.
Marble horse statue
Similar to the marble bust but with slightly higher smoothness, the marble horse shows a more noticeable specular sheen at grazing angles.
The additional smoothness is most apparent in the nighttime scene, where the faint specular sheen at grazing angles is easier to distinguish against the darker background. The sheen is more prevalent here than on the marble bust under the same conditions.
In the indoor lounge scene, the statue picks up both the cool window light and the warm ceiling tint, with the ceiling light specular visible on the smoother surfaces as the model rotates. In the studio map, the horse shows a similar multi-colored transition as the marble bust, though more dispersed due to the different geometry and surface orientation.
Vintage video camera
The vintage camera is a mixed-material model: a rough resin body with embedded metallic components such as lens rings and side plates. Transparent lens rendering requires a more involved pipeline, so the lenses are rendered as smooth black surfaces instead.
In the sunset scene, the three smooth lens surfaces each show a soft spread specular highlight from the sun consistent with their low roughness. As the camera rotates, the embedded metallic components (lens rings, side plates) catch the sunlight and produce sharper, more intense reflections.
In the urban scene with multiple street lights, the lens surfaces pick up several overlapping highlights as the model rotates, one for each light source they face. The metal components similarly glisten multiple times per rotation, confirming the specular response from each individual source.
In a neutral midday scene with a single dominant sun, the interaction is closer to the sunset case: the lenses show a clean specular reflection tracking the sun, the metal components glisten as they rotate into alignment, and the overall appearance is cooler and less tinted.
Cassette player
The cassette player is predominantly plastic (a mix of smooth and rough surfaces) with minor metallic components such as the antenna and jack pins.
As the scene rotates, the smooth plastic panels show a soft specular sheen tracking the sun. Rotating the model itself moves the sheen across the surface, and the contrast between the smooth panels and the rough grill sections is clearly visible: the grill areas show no specular contribution while the smooth panels glint.
In a neutral scene the interaction is similar with a cooler tint. The difference between the plastic sheen and the metallic antenna is clearly visible: the antenna produces a sharper, more intense highlight consistent with its higher metallic and lower roughness values compared to the plastic body.
Antique katana
The katana is a predominantly metallic model with a leather-wrapped handle and a gold tip, offering a clear contrast between the smooth metal blade and the rough leather.
Placed horizontally against the sunset, the sun’s reflection travels cleanly along the length of the blade as the scene rotates: the specular highlight sweeps from one end to the other. The back face of the blade, away from the sun, remains dark with only the faint diffuse metallic sheen.
In a neutral midday scene, the reflection is less dramatic but still clearly present. The brighter overall lighting brings out the metallic character of the blade with the specular sheen visible as the model rotates, while the leather handle remains flat and diffuse throughout.
War hammer
The war hammer is predominantly metallic with a substantial wooden handle, offering a clear material contrast between the metal head and the rough wood.
In the sunrise scene, the metal head picks up the warm sun reflection and carries a specular sheen across its surface as the model rotates. The geometry of the hammer head produces a less sweeping highlight than the katana blade, but the sheen is still clearly present and tracks the reflection direction.
In a brighter neutral scene, the metallic character of the head is more prominent and the glistening is more visible as the model rotates. The wooden handle remains unchanged throughout, showing no specular response regardless of orientation.
Megaphone
The megaphone is an entirely smooth plastic object with a curved conical geometry that produces distinctive specular behavior.
Against the sunset, a specular streak runs across the face of the cone pointing toward the sun as the scene rotates. The opposite face shows no highlight. Rotating the model transfers the streak to the other face as it comes into sun alignment, and a faint secondary streak is also visible along the connecting wire to the amplifier.
In the studio map with multiple colored lights, the single streak gives way to several simultaneous streaks from different light sources, each with its own color. The interior of the cone picks up whichever light source it is oriented toward, producing a smooth color transition as the scene rotates.
File structure
src/
├── shaders/
│ ├── deferred_lighting.frag # IBL lighting: SH diffuse + importance-sampled specular
│ ├── deferred_lighting.vert # Full-screen quad vertex shader
│ ├── deferred_lighting_debug.frag # G-Buffer visualization (unchanged from Prj 1 & 2)
│ ├── gbuffer.vert / gbuffer.frag # Geometry pass (unchanged)
│ ├── skydome.vert / skydome.frag # HDR sky dome rendering
│ └── ...
│
├── include/
│ ├── SHIrradiance.h # SH coefficient precomputation from HDR
│ ├── HammersleyRandomGenerator.h # Quasi-random Hammersley UBO
│ ├── SkyDome.h # Sky dome mesh and binding
│ ├── Scene.h # Scene orchestration
│ └── ...
│
├── Scene.cpp # InitIBL(), LightingPass() with IBL uniforms
└── Main.cpp # Application entry point and render loop
Conclusion
This project replaced the direct lighting pipeline with an environment-driven IBL system. The SH irradiance precompute produces smooth, low-frequency diffuse response at O(1) shader cost, while GGX importance sampling handles the high-frequency specular contribution with quality controllable via sample count N. The acceptance-based normalization, dividing by acceptedSamples rather than N, prevents darkening on surfaces where GGX samples fall below the horizon. The mip level selection based on the GGX PDF value D ensures smooth surfaces fetch sharp environment mips for crisp reflections, while rough surfaces fetch blurred mips to spread the contribution across the wider lobe.
The material results confirmed expected PBR behavior across the full parameter space. Rough dielectrics like marble picked up environment colors cleanly, most evident in the studio map where the colored-light spread was captured without metallic suppression. High-roughness metals showed broad specular strokes tracking surface geometry, while low-roughness metals produced sharper mirror-like reflections. Mixed-material models clearly separated their regions under rotation, with metallic and dielectric surfaces responding distinctly to the same environment. The most visually rich interactions came from multi-source environments (the studio and urban street maps), where the specular integral accumulated independent contributions from each light, producing multi-lobe highlights and colored transitions not achievable with direct lighting.