You know the standard energy-preserving "modular" specular formula: Code (csharp): specTerm = D * F * G / (4 * dotNL * dotNV); Where F is just the standard fresnelSchlick(f0, dotHL) and D and G are "pluggable" normal-distribution and geometry-visibility terms. Main question: does anyone know whether this above formula is supposed to always result in a specific range such as 0..1 or other? Or is it permitted, even expected, to "go up arbitrarily"? Reason I'm asking, I have arrived at a sort-of-GGX implementation based (after various other detours) largely on Disney. But it's creating overly bright specular highlights. They're actually pretty cool but a lot flickering in small normal-cavities and a little too much blooming (with an otherwise reasonable/non-exaggerated bloom setup as far as thresholds etc are concerned) --- this is all fixed and nicely normalized as soon as I saturate(specTerm). But first of all that feels messy, no one else seems to do that or ever suggest that D*F*G/4*nl*nv should be saturated. Secondly, when saturating the spec-highlights are still correct and clean but actually I wouldn't mind a *little* brighter. So I could clamp, seems like clamp(specTerm, 0, 12) would result in a bloomy shinyness that'd work for me without going as crazy as unclamped does. But this bothers me, it feels... hacky! I know I know, everything in realtime graphics is a huge pile of hacks, but still! Here's my D: Code (csharp): half dotHN2 = dotHN * dotHN; // dot of half-vector and normal half alph = roughness * roughness; // my roughness is always linear but is this even correct for GGX? half alph2 = alph * alph; half denom = dotHN2 * (alph2 - 1.0) + 1.0; half ggx_D = alph2 / (PI * denom * denom); And here's my G: Code (csharp): #define SQRT_2_PI 0.79788456080286535587989211986876 half k = roughness * SQRT_2_PI; // others such as Hable use pow(alph/2, 2) but during many tests I settled on this variant for k, no idea where I first picked it up...... half k1 = 1.0 - k; half ggx_V = (dotNLc / (dotNLc * k1 + k)) * (dotNVc / (dotNVc * k1 + k)); I've read somewhere that using this Smith-Schlick geometric-visibility term I can get rid of the energy-conserving part (divide by 4*nl*nv) but in my extensive experiments that does not seem to be the case for me. Could be another indicator that I might have a bug somewhere.... or that just like me, most gfx writers out there also only have half-a-semi-clue Fresnel and all the rest I don't need to post, it's the proven battle-tested standard stuff.
Okay, we need to cover a few things. The answer to your main question is that the equation is expected to very frequently go well above the [0,1] range. Often it reaches several thousand in intensity as smoother materials concentrate reflected energy into a smaller point. This is expected behavior. The flickering you see is a limitation of using tangent-space normal maps where they do not correctly mipmap. As a result, they tend to get smoother for their higher mips, causing sparkling due to specular aliasing. The proper solution for this is specular antialiasing, a problem which is an active area of research as there are no easy solutions to it. Punctual Lighting Equation So since you are doing lighting from a Punctual Light (ie. infinitely small point) rather than integrating all incoming light over a hemisphere, you actually plug your BRDFs into this equation: Code (CSharp): illumination = NdotL * PI * (diffuseBrdf + specularBrdf); The net result is that the 1/PI in your Lambert diffuse and GGX specular BRDFs is cancelled. Visibility vs Geometric Functions It is increasingly common for Physical BRDFs to combine the foreshortening term (ie. 1 / (NdotL * NdotV)) with the Geometric function to create a Visibility function. This typically takes the form of the Geometric function's numerator cancelling the foreshortening term in the denominator. Note, you must still keep the 1/4 from the same term as that does not cancel. Simplification: V = (N.L / (N.L * (1 - k) + k)) * (N.V / (N.V * (1 - k) + k)) / (N.L * N.V) V = (N.L * N.V) / ((N.L * (1 - k) + k) * (N.V * (1 - k) + k)) / (N.L * N.V) V = (N.L * N.V) / ((N.L * N.V) * (N.L * (1 - k) + k) * (N.V * (1 - k) + k)) V = 1 / ((N.L * (1 - k) + k) * (N.V * (1 - k) + k)) Hope that helps.