# Sampling the light vs. sampling the BRDF: difference in intensity

(possible duplicate)

I'm trying to implement multiple importance sampling, as described in the PBRT book (no bouncing yet, I want to make MIS work first).

What my problem is, that while the BRDF sampling looks okay, the light sampling is much dimmer (yet according to the pictures in the book they should look the same in intensity).

I uploaded the (updated) compilable code (VS 2015). The relevant parts can be found in multipleimportance.frag (SampleLightsExplicit) and pbr_common.head (ConeSampleSphere).

(note that I don't apply the MIS weights yet)

for (int k = 0; k < numLights; ++k) {
SceneObject light = objects.data[k];

center = vec3(light.toworld[3].xyz);

// importance sample light (xyz = direction, w = PDF)
sample1 = ConeSampleSphere(p, center, radius, pixel, seed + float(k));

vis = Visibility(p, sample1.xyz, k);
ndotl = vis * max(0.0, dot(sample1.xyz, n));

// evaluate BRDF
fd = vec3(0.0); //obj.color.rgb * ONE_OVER_PI;
fs = CookTorrance_General(sample1.xyz, v, n, vec3(0.04), roughness);

sum += (light.color.rgb * (fd + fs) * ndotl) / sample1.w;
}


My question is why light sampling looks so dim? Did I misunderstand something?

UPDATE

Looking through the code I noticed some inconsistencies which probably originated from a typo in one of the older SIGGRAPH course notes (can't tell which one tho).

The PDF of the GGX distribution is:

PDF = D(h) * dot(n, h)


While some implementations (including UE4) incorrectly write it as

PDF = (D(h) * dot(n, h)) / (4 * dot(v, h))


which essentially means that they include the Cook-Torrance denominator into this variable (ndotl cancels out). Dividing with the correct PDF cancels out D in the Cook-Torrance model, which I forgot to account for, while the 1 / (4 * dot(v, h)) term goes into the Vis_Schlick function.

So regarding intensity the second picture is the "correct" one. The dimness is still a problem tho. Changing the Fresnel F0 to 0.11 (as it supposedly is in the book) and applying the MIS weights, the picture looks a lot more promising, except for the topmost plate which should almost explicitly reflect the light sources:

So this is almost okay now. I'm gonna look deeper into the MIS part to see why it makes those highlights disappear.

• Are you accounting for the probability of choosing a light? – Hubble Jan 29 '20 at 20:08
• I'm not 'choosing' a light (in the above code), I accumulate the contribution of all lights. Also, this is reproable if I use just one light only. – Asylum Jan 30 '20 at 13:04
• I saw your code, you are not dividing by the bsdf_pdf and missing the ndotl factor while computing your bsdf sample. Also post the code for both the calculations here. Not everyone's gonna download the whole project. – gallickgunner Feb 1 '20 at 6:44
• because the denominator of the Cook-Torrance BRDF cancels it out. I found some other errors in the code tho, I update the post with my findings. – Asylum Feb 1 '20 at 10:32

Shoot me... After the fixes I posted above, I started debugging Mitsuba to see whether it does anything differently on a large scale. Of course it didn't; my code is completely correct, the dimness is due to this macro def:

#define EPSILON   1e-5


This is used in divisions to avoid dividing with zero. Unfortunately, it accumulates to a large value during the calculations. Changing it to 1e-9 fixes everything:

For future generations I leave the source code up (just remember to change this value).

UPDATE:

I found another mistake in my calculations, but it isn't apparent in this scene. Microsurface-based BRDFs are usually sampled according to the microfacet normal distribution function (D(h, a), in this case GGX).

Don't forget however, that you also have a shadowing-masking function (G(l, v, a)) which has to be evaluated according to the chosen microfacet distribution. This means that I can't just mix-and-match f.e. GGX sampling and the Smith-Schlick evaluation; I have to use the Smith-GGX solution:

float G_Smith_GGX(float ndotl, float ndotv, float roughness)
{
float a = roughness * roughness;
float a2 = a * a;

float lambda_l = sqrt(a2 + (1.0 - a2) * ndotl * ndotl);
float lambda_v = sqrt(a2 + (1.0 - a2) * ndotv * ndotv);

return (2.0 * ndotl * ndotv) / (ndotv * lambda_l + ndotl * lambda_v);
}


Which explains the UE4 approach, as:

vec3 F = F_Schlick(F0, ldoth);
float G = G_Smith_GGX(ndotl, ndotv, roughness);

// PDF = (D(h) * dot(n, h)) / (4 * dot(v, h))
return (F * G * vdoth) / (ndotv * ndoth + EPSILON);