Skip to content

M-mat-C2: Smith Height-Correlated Geometric Term (G2)

Taught: 2026-05-04 Milestone: Phase 1 M-mat — Disney BRDF Result: PASS (after re-teach on the diagnostic angle for question #3 specifically)

Why this concept matters here

This is the subtle one in M-mat. G is where engineers — including ones who have written PBR shaders for years — quietly introduce a 50% energy-loss bug and never notice because the material just looks "a bit dim" and they tweak the light intensity to compensate. The math is not hard; the bookkeeping is where everyone slips. Getting this right means M-mat ships with energy-conserving specular instead of muted, hazy metals.

The walkthrough

Step 1 — What G is actually accounting for

D told you how many microfacets are oriented toward the half-vector h. But orientation alone doesn't mean a microfacet contributes to the reflection. A microfacet only contributes if both:

  • The eye can see it (it's not hiding behind a neighboring microfacet — masking)
  • The light can reach it (it's not in the shadow of a neighboring microfacet — shadowing)

Houdini analogy: an ocean surface in Vellum at high wind. From a low grazing angle, you see crests but the troughs behind them are occluded — that's masking. Sun coming in low from the other side, the same crests cast shadows into the troughs — that's shadowing. G is the fraction of microfacets surviving both tests.

So D tells you how many microfacets are pointed the right way, and G tells you what fraction of those are actually doing useful work. Without G, you'd over-count reflection at grazing angles, where most of the correctly-oriented microfacets are actually occluded by their neighbors.

Step 2 — Why "height-correlated" instead of separable

The naive Smith G (the one in older shaders, including a lot of Unity 4-era code) treats shadowing and masking as independent events:

G_separable = G1(NdotV) · G1(NdotL)

This is wrong because it assumes whether a microfacet is visible to the eye tells you nothing about whether it's lit by the light. But physically, a microfacet is visible to the eye precisely because it sits at a relatively high point on the surface — it pokes up above its neighbors. That same height also makes it more likely to be lit. Visibility and illumination are positively correlated through height.

Heitz (2014) derived the height-correlated form by treating the joint probability properly instead of factorizing it. The result matches measured BRDFs noticeably better, especially at grazing angles where the separable form predicts darker reflections than reality.

Step 3 — The formula and the packaging trick

The standard Cook-Torrance specular BRDF is:

f_spec = (D · F · G) / (4 · NdotL · NdotV)

The 4 · NdotL · NdotV denominator is a Jacobian — the change-of-variables factor that comes from integrating in the half-vector domain instead of the light domain. It's not a fudge factor; it's geometry.

Now the trick. The height-correlated G2 is defined as:

G2_HC = 0.5 / ( NdotL · sqrt(α² + (1−α²)·NdotV²)
              + NdotV · sqrt(α² + (1−α²)·NdotL²) )

Notice the 0.5 in the numerator and the NdotL/NdotV in the denominator. This formula has the 1/(4·NdotL·NdotV) Jacobian already absorbed into it. The 0.5 is half of 1/(2·something), and the NdotL, NdotV in the denominator pair with the original 4·NdotL·NdotV in a way that the algebra cancels cleanly.

So in your shader:

wgsl
let specular = D * F * G2_HC;   // CORRECT — no extra division
// NOT
let specular = D * F * G2_HC / (4.0 * NdotL * NdotV);  // WRONG — divides twice

The reason the bug is common: every textbook writes f_spec = D·F·G / (4·NdotL·NdotV) as the BRDF formula, so engineers reflexively write that in code. Then they use a height-correlated G implementation copied from a paper. The G implementation already absorbed the Jacobian — but the engineer doesn't realize it, and divides again.

Step 4 — Worked numerical example

Roughness = 0.4 (α = 0.16), NdotV = 0.8, NdotL = 0.6:

α² = 0.16, 1−α² = 0.84
NdotV² = 0.64, NdotL² = 0.36

sqrt(0.16 + 0.84·0.64) = sqrt(0.6976) ≈ 0.8352
sqrt(0.16 + 0.84·0.36) = sqrt(0.4624) ≈ 0.6800

denom = 0.6·0.8352 + 0.8·0.6800 = 0.5011 + 0.5440 = 1.0451
G2_HC = 0.5 / 1.0451 ≈ 0.4784

Two versions of the final specular factor:

CORRECT: G2_HC                            ≈ 0.4784
BUGGY:   G2_HC / (4·NdotL·NdotV)
        = 0.4784 / 1.92                   ≈ 0.2492

The buggy version is 48% dimmer here. Across a sphere at typical viewing angles the buggy form is systematically darker.

Step 5 — The bug has a SHAPE, not just a magnitude

Compute buggy / correct (after multiplying by NdotL throughput): ratio = 1/(4·NdotL·NdotV).

LocationNdotVNdotLRatioEffect
Center1.00.850.29Buggy is 71% dimmer
45° off-center0.70.50.71Buggy is 29% dimmer
Crossover0.50.51.00Buggy = correct
Near silhouette0.20.34.17Buggy is 4× brighter
Silhouette edge→0small→∞Buggy explodes / aliasing

The bug isn't uniformly dim. It's dim at the center and bright/blown-out at the silhouette. That's the unique fingerprint that distinguishes it from artist error:

  • Low base color → uniformly dim, smooth across the sphere
  • Weak light → uniformly dim, smooth across the sphere
  • Double-divide bug → wrong SHAPE: center too dim, edges too hot

The mental model in one sentence

The height-correlated G2 is not just the geometric attenuation; it's the geometric attenuation pre-divided by the half-vector-to-light-direction Jacobian, so multiplying by D · F · G2_HC is the entire specular BRDF — no further division required.

Diagnostic version: when debugging PBR, look at the angular distribution of brightness, not the absolute brightness. Artist error scales everything uniformly. Math bugs distort the shape.

Explain-back question

Three-part question on let specular = D * F * G2_HC / (4.0 * NdotL * NdotV);:

  1. Why is this wrong (the underlying double-counting)?
  2. What would you observe in the rendered output of a sphere lit by a single point light?
  3. The hard part: Looking at a single rendered frame of that sphere — no debug overlays, no comparison to a reference, just the pixels — what specific visual cue would tip you off that this bug is present, as opposed to the artist just having set the material's base color too low or the light intensity too weak?

First attempt — partial PASS, missed the diagnostic

  1. G2_HC already takes care of division by 1/2 and mul by NDotL and NdotV. We are doing this twice by doing it again outside.
  2. We would observe that it is significantly dimmer.
  3. The scene would be dim, so lightsources get significanly fall-out because of the double division

Got #1 and #2. Missed #3 — "dim" is what low albedo or weak light would also produce; the answer didn't differentiate. The diagnostic skill (recognizing math bugs by shape, not magnitude) is the actual goal of the question.

Re-teach for #3 — the bug has a shape

Walked through the buggy/correct ratio at center / mid / silhouette. Showed that the bug dims the center but amplifies the silhouette (eventually exploding to infinity at NdotV → 0). The visual fingerprint is "wrong shape," distinct from the "uniform dim" of artist error.

Re-test prompt

Where on the sphere would you look first to detect this bug, and what specifically would you expect to see at that location vs at the center? Tie your answer to how the ratio 1/(4·NdotL·NdotV) changes from center to silhouette.

User's answer (PASS)

At the center it will be dim and in the corners the silhouetts get's blown out while it should fall-off ideally due to fresnel coefficients!

Judgment

PASS on the diagnostic. Got:

  • Where to look: silhouette / corners (correct — that's where 1/(4·NdotL·NdotV) explodes)
  • What to expect: blown-out vs dim center (correct — the shape mismatch)
  • Distinguishes bug from uniform dimming (the senior-eye skill)

Minor terminology slip flagged for M-mat-C3: the user said "should fall-off ideally due to fresnel." Actually backwards — silhouette dimming on a correctly-shaded sphere comes from NdotL · D (light-geometry terms), not Fresnel. Fresnel does the opposite: it goes UP at grazing angles (the "Fresnel rim" on dielectric materials). Set up as the lead-in to M-mat-C3 (Schlick Fresnel + F0), where this gets formalized.

Conversational walkthroughs of real-time graphics + Rust/WASM concepts.