The last note owned a tangent-space height march so a quad reads as carved brick. The silhouette stayed the quad. This one does not pick a new sampler. An HDR environment is folded into two tables: a GGX-prefiltered specular cube and a DFG LUT. The fragment multiplies them. Split-sum makes environment lighting display-ready. Irradiance on/off is the GI-shaped control: distant environment, not local bounce.

Cream stoneware and measured brass on a catcher, loft window behind, full split-sum. The metal carries sharp mullions. The glaze picks up a dim rim and colored fill from the room, not a flat ambient. HUD: Khronos PBR Neutral exposure=1.05, distant environment irradiance, not local multi-bounce GI, photo-only - no metric. Do not hang an energy number on this photograph.

Same camera, same exposure, three shaders. Left SPEC IBL ONLY: ceramic flanks go dark; brass still shows the window. Middle IRRADIANCE ONLY: ceramic fills; brass is black because . Right FULL SPLIT-SUM. Caption: distant environment ambient, not local multi-bounce GI.
Hero, Mesa 25.0.7 llvmpipe, linear working space, Khronos PBR Neutral, exposure 1.05: white-env Karis prefilter energy 1.000 at . DFG at , : , . Flank crop (linear, full / spec-only luma ratio) 5.11. Env solid-angle mean luma 1.628. Assertions 25 pass / 0 fail.
What you are seeing
The environment is an authored loft HDR (procedural latlong, not a captured EXR): plaster, baseboard, timber-ish beams, a key factory window with jambs and a 4×5 mullion grid, offset backdrop sashes, a door/panel, warm floor. No extra analytic key light on the hero plates. The env does the lighting.
Presentation hook. Product still, , GL photograph, CPU tone map. Glazed ceramic (dielectric , glaze roughness in –) and measured brass . Soft planar contact on the catcher. Photo only.
GI-shaped control. Same objects, same camera eye (1.30, 0.54, 1.98) → (0.05, 0.25, 0.02), fov . Three-up . Metals stay black in the middle panel. That is the model, not a bug. Linear flank crop (ceramic, spec-only vs full) is the meter for “irradiance did something”; the JPEG is still a photograph.
Walk the three panels:
- Spec IBL only. The conductor can live on a prefiltered cube: window, frame, a strip of interior. The dielectric cannot. leaves the body dark. Studio void.
- Irradiance only. Distant-environment fills the ceramic. Brass is black (). No local object bounce, no bottle-in-sphere, no floor color bleeding onto the metal.
- Full split-sum. Specular cube DFG, plus irradiance. The plate that is supposed to look like a product shot.

Conductor ladder. Six rungs , measured brass, albedo-only so rung roughness stays the control. Smooth metal still shows mullions. The lobe widens; the highlight tints with ; the body is not an irradiance-albedo mix-up. lod = roughness * (mips-1). Photo only.

Dielectric ladder. Same rungs, , cream albedo. Grazing Fresnel remains as roughness climbs (that is the DFG). Same env / exposure / TM as the metal ladder. Photo only.
Two facts, never mixed:
- Beauty plates (heroes, ladders, failures, calibration strip) are GL-rendered split-sum, Khronos PBR Neutral, exposure 1.05, then sRGB OETF. HUD
photo-onlymeans do not invent a GL energy theorem from the JPEG. - Instruments (mip strip, spp residual, DFG LUT, roughness→mip map, and the CSV) are CPU tables: Karis prefilter, DFG integrate, white-env energy, mip variance, 64-spp vs 512-spp RMSE.
Why: the reflection integral, then Karis
Working space is linear radiance. Cubemaps are linear. Tone map is a named display step after shading, not baked into the tables.
Reflection equation (specular)
Microfacet specular (Cook–Torrance):
NDF is isotropic GGX / Trowbridge–Reitz. in the NDF only:
A hemisphere of HDR samples times that BRDF, every fragment, is the reference. Production does not do that.
Split-sum (Karis / UE4-style)
Factor the integral into prefiltered incident radiance (depends on and roughness, not on ) and a view-dependent BRDF integral against a white environment (depends on and roughness, not on ):
with . That multiply is the production trick. This note photographs it and names the two stored tables.
Term A — prefiltered radiance
Stored in cubemap mips. Karis weighting, , no second in the prefilter:
Importance-sample the GGX NDF in half-vector space, reflect to , weight by . Constant must return 1. That is the white-env energy row: 1.000 at four roughness keys.
This run: specular cube 128, 8 mips, CPU authored, uploaded per level. glGenerateMipmap is not called. Production spp is 64 (doubled on coarser mips). A separate 512 sky cube, mip0 only, GL_LINEAR, is the backdrop; it is not the GGX chain.
Roughness mip (linear, stated)
Eight mips . This is not a GGX solid-angle match. It is the default so the roughness ladder is readable. Shader: lod = rough * uMaxMip.
Term B — DFG / EnvBRDF LUT
2D: , roughness. Single-scatter. Schlick split into and the white-Fresnel remainder. Smith–GGX IBL geometry with
LUT RGB is (scale, bias). Integrate against a white environment; the env cube never enters this table.
Dielectric . Conductor linear RGB albedo (measured brass here; no dielectric coat). At , : , . CSV: , . LUT range [0, 1].
Distant irradiance (the locked GI control)
Lambert:
with so metals do not pick up a diffuse body. is a low-frequency irradiance cubemap from the same distant environment. This run: cube 64, 192 spp cosine, then a 3-pass blur on mip0 only so window mullions do not reprint as wood grain on Lambertian flanks. Specular mips are not blurred.
Honesty line: irradiance here is distant environment lighting preintegrated over the hemisphere. It is not local multi-bounce GI, not path tracing, not DDGI, not lightmaps, not SSGI.
On/off is the three-up above. No extra directional fill on the hero.
Display
TM is Khronos PBR Neutral (linear in, linear display-referred out). Exposure 1.05, identical on every comparison row. TM is not baked into the cubemaps. GL_FRAMEBUFFER_SRGB is off; encode is CPU.
Unique artifacts: the mip strip, the LUT, the map

This is the thing this note exists to draw. CPU GGX prefilter, face, mip , roughness mip (mips). Mip0 is a 4×5 factory window with dark frames. Then the mullions dissolve into the lobe. Caption: not glGenerateMipmap. luma variance is the monotone blur meter: 70.9 → 43.9 → 23.3 → 9.34 → 3.54 → 2.33 → 0.76 → 0.

Sample-count instrument, CPU both sides. Production spp vs 512 spp at , , , plus a relative-error heatmap. RMSE 0.777 / 1.045 / 1.465. That is 64-spp vs 512-spp on an HDR window with a sun disc, not production-mip vs analytic, and not a GL filtering theorem. Residual fireflies at 64 spp are expected.

Scale and bias , , CPU GGX, 64 spp. Axes labeled. , single-scatter. The smooth, face-on corner is the CSV row: , .

as a plot, plus mip thumbnails of the same window. Caption: not a solid-angle match; the ladder stays readable. GGX lives in the NDF, not in this map.
Quote the CSV. Do not quote the beauty photographs as energy.
Photoreal gallery, Mesa llvmpipe:
| metric | theory / gate | measured | note |
|---|---|---|---|
| white-env energy | 1 | 1.000 all four | Karis prefilter, constant |
| DFG | 0.999984 | sampled at (shader clamp) | |
| DFG same cell | 0 | ||
| flank luma spec-only | n/a | 0.134657 | linear crop, not the JPEG |
| flank luma full | n/a | 0.688492 | linear crop |
| flank ratio full / spec | 5.112927 | irradiance on vs spec-only | |
| env mean luma | HDR in | 1.628385 | solid-angle weighted latlong |
| prefilter RMSE | n/a | 0.777 / 1.045 / 1.465 | prod spp vs 512 spp, |
| metal highlight RMS | grows | 5.08 → 48.2 | linear render; then saturates |
| luma variance mips | decreasing | 70.9 → … → 0 | monotone blur meter |
Hero rounding used in the lede: energy 1.000; , ; flank ratio 5.11; env luma 1.628. The ratio is still . Do not invent a GL energy or RMSE theorem from the hero or the roughness ladders. Those frames are photo-only.
Highlight RMS grows on the first metal rungs, then saturates once the lobe covers the sphere (: 54.1, 46.9, 39.2, 39.2). Width after that is not a useful meter. Mip variance is.
Failures / controls
Wrong mip

Left: roughness , force mip0. Sparkly sandpaper; the window is still a window. Middle: same roughness, correct . Right: roughness , force max mip. Pewter blob; a smooth conductor has been given the roughest table. Photo only. The metal ladder is the honest map; this plate is the knife.
No DFG LUT ()

Same prefilter cube, same exposure. Left: skip the LUT, output the prefiltered radiance as if . Metals wash grey; the brass is gone; the dielectric loses the grazing rim and the body energy is wrong. Right: . Photo only.
LDR clip vs HDR

Same TM, same exposure. Left: env authored with , then prefiltered. Window highlight and interior both collapse; the metal loses punch. Right: float HDR env. TM is not in the cubemaps; clipping the source is the failure. Photo only.
Instrument strip (not a hero)

Smooth metal / rough metal / Lambertian with spec off. Caption: calibration balls, not the product shot. Photo only. If this were the cover, the note would be a void-sphere demo.
Two paths, do not mix the instruments
| path | frames | instrument |
|---|---|---|
| Science | mip strip, spp residual, DFG LUT, roughness→mip, CSV, white-env energy, mip variance, RMSE | CPU latlong, CPU Karis prefilter, CPU cosine irradiance, CPU DFG. Image is visualization of those buffers after the same TM. |
| Photograph | heroes, ladders, failures, calibration strip | GLSL 330 split-sum on this llvmpipe: textureLod of the CPU mips, DFG 2D, irradiance cube. HUD photo-only. Flank luma and metal RMS are linear-FBO crops, not JPEG theorems. |
| Display | every plate | expose → Khronos PBR Neutral → sRGB OETF. One operator, every row. |
The GI-control three-up is a photograph of the control and the source of the flank-ratio CSV row. Quote the CSV. Do not quote the 8-bit panel as 5.11.
Honesty gaps
- Distant-environment irradiance local GI. No bottle-to-brass bounce, no floor color in the metal, no path tracing, no DDGI, no lightmaps, no SSGI. Metals are black in the irradiance-only panel because .
- Single-scatter split-sum. No multi-scatter energy compensation. Grazing metals can pick up a white-ish LUT bias term.
- Roughness→mip is linear. , not a GGX solid-angle match.
- Prefilter RMSE is 64-spp vs 512-spp at matched roughness on the HDR window, not production-mip vs analytic. Sun disc → residual fireflies (RMSE 0.777–1.465). Documented; not a GL filtering theorem.
- Highlight RMS grows on the first metal rungs, then saturates. Mip variance is the monotone blur meter.
- Irradiance blur is a 3-pass presentation filter on so mullions do not reprint as wood grain on dielectrics. Specular prefilter mips are not blurred.
- Contact is a planar cosine term, not a shadow map or a ray trace.
- Env is procedural loft HDR, not a captured EXR. Solid-angle mean luma 1.628.
- Cubemap sampling is llvmpipe RGBA16F with CPU-authored mips. Seamless cubemap is requested; face-edge quality is still a Mesa caveat.
- No hardware IBL unit and no real-time convolution cost claim. Prefilter, irradiance, and DFG are CPU.
- Residual turned-form spec bands on the bottle are the loft windows wrapping a surface of revolution. Milder than a marble-chalk urn, not a second lobe.
- JPEG is visualization. Measurement is the CSV. Do not FFT or energy-integrate a beauty frame.
Mesa / llvmpipe — what this run can claim
| item | value |
|---|---|
GL_VERSION |
4.5 (Core Profile) Mesa 25.0.7-2+deb13u1 |
GL_RENDERER |
llvmpipe (LLVM 19.1.7, 256 bits) |
| FBO color | RGBA32F complete, photo |
| Specular / irradiance / sky cubes | RGBA16F, CPU mips uploaded per level |
glGenerateMipmap |
not called on the specular chain |
| DFG LUT | RGBA32F , CPU GGX |
GL_FRAMEBUFFER_SRGB |
disabled (TM + sRGB on CPU) |
| MSAA | disabled |
GL_TEXTURE_CUBE_MAP_SEAMLESS |
enabled |
| Exposure / TM | 1.05 / Khronos PBR Neutral |
Can claim: on this OSMesa / llvmpipe build, a CPU Karis GGX prefilter of an authored HDR loft conserves white-env energy (1.000 at four roughness keys), a CPU DFG LUT hits , at face-on smooth, and a GLSL 330 split-sum shader times those tables plus a distant-environment irradiance cube produces these photographs. Turning irradiance off darkens dielectric flanks (linear crop ratio 5.11). Wrong mip, missing LUT, and an LDR-clipped env are visible failures. Photographs of those paths are photographs of this software rasterizer.
Cannot claim: NVIDIA / AMD / Intel hardware IBL units, real-time convolution cost, occupancy, or bandwidth. That seamless cubemap filtering on llvmpipe equals a discrete GPU. That glGenerateMipmap would have been a GGX chain (it is not; we did not call it). That distant is local multi-bounce GI. That linear roughness→mip is a solid-angle match. That an FFT or energy integral of an 8-bit sRGB JPEG is a spectrum of the signal. Discrete-GPU metrics, or “this is how the hardware works.”
Science path does not depend on GALLIVM_PERF: CPU latlong, CPU prefilter, CPU DFG.
Assertions
This run: 25 pass / 0 fail.
| check | result |
|---|---|
| Required gallery plates + CSV exist and are non-empty | PASS |
| DFG LUT range in | PASS [0, 1] |
| DFG , | PASS , |
| White-env Karis prefilter energy at within 8–12% of 1 | PASS all 1.000 |
| Flank crop: full luma spec-only | PASS ratio 5.11 |
| Env mean luma in | PASS 1.628 |
| prefilter luma variance decreases with mip | PASS 70.9 → … → 0 |
| Metal highlight RMS grows on the first ladder rungs | PASS 5.08 → 48.2 |
Cube upload not fail |
PASS RGBA16F |
No assert tolerances were loosened for the photoreal plates.
Out of scope
Local path-traced GI, photon maps, irradiance caching of scene bounce. DDGI, lightmaps, SSGI/SSR as a substitute for the cube. Multi-bounce local solvers of any kind. Anisotropic GGX, Toksvig AA of the NDF, sheen, clearcoat, layered metals. Area lights / LTC. Deep tone-map bake-off (TM is one named operator). Shadow-map bias. Real-time convolution on a “hardware IBL unit.” Re-deriving the mipmaps chirp, the anisotropic ellipse, or the POM height march: cite continuity; this is a BRDF-integral approximation, not a new sampler and not new geometry.
Album sequence:
- Perspective-correct — what you interpolate
- Z-fighting — how depth is encoded
- Mipmaps — which frequencies survive an isotropic minify
- Anisotropic — which UV ellipse that pixel covers
- Parallax occlusion — fake depth from a height field; the silhouette is still the quad
- Split-sum IBL — environment lighting as a multiply: GGX-prefiltered cube DFG, plus distant
Fragment lock
float lod = rough * uMaxMip; // linear: r * (mips-1)
vec3 pre = textureLod(uPrefilter, R, lod).rgb;
vec2 dfg = texture(uDFG, vec2(Nv, rough)).rg;
spec = pre * (F0 * dfg.x + vec3(dfg.y)); // Karis split
// kD = (1-F)*(1-metal); diff = kD * albedo/PI * E(n)
Prefilter writes every mip. Irradiance is one low-res cube. DFG does not see . Pin the hero as the presentation. Pin the three-up as the GI-control photograph. Pin the mip strip plus the DFG LUT plus the roughness→mip map as the unique artifacts. The formula is the caption. Distant is why the ceramic flanks filled, and why the brass stayed black without a diffuse body.