Microfacet GGX not integrating as expected

I'm trying to complete a look-up table for an energy-conserving variation of microfacet GGX as implemented here: https://patapom.com/blog/BRDF/MSBRDFEnergyCompensation/

I have a (pretty standard) GGX form of the microfacet equation and my white furnace test behaves similar to what is shown elsewhere (i.e., it loses energy as roughness increases).

However, when I attempt to integrate the brdf and store the total energy in a texture, I get results that are inverse to my expectation - higher roughness yields more total energy in the integral. Some code below (this is done in a compute shader):

float evalSum = 0.0;
const float theta_i = 0.05;
const float phi_i = 0.05;
for (float phiWi = 0.0; phiWi < PI2; phiWi += phi_i)
{
for (float NoL = 0.0; NoL < 1.0; NoL += theta_i)
{
float nohA = 1.0 + NoL * NoV + (1.0 - NoV * NoV) * (1.0 - NoL * NoL) * cos(phiWi);
float NoH = (NoL + NoV) / sqrt(2.0 * nohA);

float D = Fs.eNDF(NoH, roughness);
float G = Fs.eGS(NoV, NoL, roughness);
float Fr = D * G;

evalSum += Fr * NoL;
}
}

outBuf[idx] = evalSum * phi_i * theta_i;


Here is my texture output:

I'm expecting something more like this:

As the Y axis goes down, roughness increases, and as the X axis goes to the right, NoV (N dot V) increases. Total energy increases with roughness and I also get a strange valley to the energy which I wasn't expecting.

Here's the two GGX terms:

float getNDF(const float NoH, float linearRoughness)
{
float a2 = linearRoughness * linearRoughness;
float b = NoH * NoH * (a2 - 1.0) + 1.0;

return a2 / (PI * (b * b));
}

float getGS(const float NoV, const float NoL, const float linearRoughness)
{
float a2 = linearRoughness * linearRoughness;
float G_V = NoV + sqrt((NoV - NoV * a2) * NoV + a2);
float G_L = NoL + sqrt((NoL - NoL * a2) * NoL + a2);
return rcp(G_V * G_L);
}


Pretty stumped here. I must be doing the integral wrong somehow because the in-engine renders look correct - the brdf code seems straightforward. Any ideas on why my LUT is not producing the expected result?

• I probably won't find the answer, I find your abbreviations a bit hard to read. However, I noitced you calculating float Fr = D * G; but never using it. Perhaps you forgot something? – Tare May 14 at 8:51
• Thanks Tare, that's just an artifact of me trying to debug the code. I'll edit the post. – polyrhythm May 15 at 1:11
• @polyrhythm I second that, just because dolt researchers like to use abhorrent abbreviations doesn't mean you have to. There's honestly no reason you can't name things properly here. Your names are misleading as well, NoL to me would have translated to "NormalOfLight" not "N dot L", regardless I would have just named it what it was (and no, N dot L is not an appropriate name, what is N? What is L?, what does the combination of the two represent? That's how you name these things. – opa May 15 at 15:40
• Thanks for your comment! I think my abbreviations are pretty industry-standard, though. A quick google search turns up a ton of high quality codebases using similar nomenclature, here is an example: github.com/google/filament/blob/master/shaders/src/brdf.fs I have never seen NoL to mean "normal of light"... – polyrhythm May 16 at 1:22