I'm making a program that illuminates a sphere using an environment map. For each fragment in the sphere, I compute a many random directions in the hemisphere along the surface normal (no importance sampling), and then sample the environment map. Then I evaluate the GGX BRDF for each sample.

The problem I have is that it looks too dark, and I'm not sure why.

enter image description here

This is for a metallic sphere with low roughness. For such a low roughness, it could make sense that the GGX lobe is so narrow that we are missing it. But things don't improve with higher roughness.

When I increase the roughness it goes completely dark:

enter image description here

If I multiply the resulting color by 10 I can tell there is something:

enter image description here

This is my main shader function:

vec3 bruteForce()
    vec3 N = normalize(v_normal);
    vec3 V = normalize(u_camPos - v_pos);
    float NoV = max(0.0001, dot(N, V));

    vec3 F0 = mix(vec3(0.04), u_albedo, u_metallic);
    vec3 F = fresnelSchlick(NoV, F0);
    vec3 k_spec = F;
    vec3 k_diff = (1 - k_spec) * (1 - u_metallic);

    vec3 specular = vec3(0.0, 0.0, 0.0);
    for(uint iSample = 0u; iSample < u_numSamples; iSample++)
        uvec3 seedUInt = pcg_uvec3_uvec3(uvec3(gl_FragCoord.x, gl_FragCoord.y, iSample));
        vec2 seed2 = vec2(makeFloat01(seedUInt.x), makeFloat01(seedUInt.y));
        vec3 L = uniformSample(seed2, N);
        vec3 H = normalize(V + L);
        vec3 env = texture(u_envTex, L).rgb;

        float NoL = max(0.0001, dot(N, L));
        float NoH = max(0.0001, dot(N, H));
        float NoH2 = NoH * NoH;
        float rough4 = u_rough2*u_rough2;
        float q = NoH2 * (rough4 - 1.0) + 1.0;
        float D = rough4 / (PI * q*q);
        float G = ggx_G_smith(NoV, NoL, u_rough2);
        vec3 fr = F * G * D / (4 * NoV * NoL);
        specular += fr * env * NoL;
    specular /= float(u_numSamples);

    return specular;

Here are the other helper functions:

vec3 fresnelSchlick(float NoV, vec3 F0)
    return F0 + (1.0 - F0) * pow(1.0 - NoV, 5.0);

float ggx_G(float NoV, float rough2)
  float rough4 = rough2 * rough2;
  return 2.0 * (NoV) /
    (NoV + sqrt(rough4 + (1.0 - rough4) * NoV*NoV));

// geometry term
float ggx_G_smith(float NoV, float NoL, float rough2)
  return ggx_G(NoV, rough2) * ggx_G(NoL, rough2);

// this is a hash function for generating pseudo-random numbers. Taken from here: http://jcgt.org/published/0009/03/02/
uvec3 pcg_uvec3_uvec3(uvec3 v)
    v = v * 1664525u + 1013904223u;
    v.x += v.y*v.z;
    v.y += v.z*v.x;
    v.z += v.x*v.y;
    v = v ^ (v>>16u);
    v.x += v.y*v.z;
    v.y += v.z*v.x;
    v.z += v.x*v.y;
    return v;

// Construct a float with half-open range [0:1] using low 23 bits.
// All zeroes yields 0.0, all ones yields the next smallest representable value below 1.0.
// https://stackoverflow.com/a/17479300/1754322
float makeFloat01( uint m ) {
    const uint ieeeMantissa = 0x007FFFFFu; // binary32 mantissa bitmask
    const uint ieeeOne      = 0x3F800000u; // 1.0 in IEEE binary32

    m &= ieeeMantissa;                     // Keep only mantissa bits (fractional part)
    m |= ieeeOne;                          // Add fractional part to 1.0

    float  f = uintBitsToFloat( m );       // Range [1:2]
    return f - 1.0;                        // Range [0:1]

Can you help me find the problem please?

  • 1
    $\begingroup$ Do you need to divide by the probablity of taking the sample? In the uniform case it would be divide by 1/2pi? $\endgroup$
    – Peter
    Nov 23, 2020 at 16:41
  • $\begingroup$ @Peter Thanks for your comment. Why do I have to divide by 1/pi? I know that with importance sampling you need to divide by the PDF (probability distribution function), but I thought with uniform sampling you don't have to? $\endgroup$ Nov 23, 2020 at 18:57
  • 1
    $\begingroup$ So far your code you have taken random samples and divided by the number of samples. If you think about what this is doing, you are getting an approximation of the average light from each ray hitting the point. However you don't want the ligh of just one ray you want the total light from all directions. Luckily we know that in discrete maths TOTAL = AVERAGE * COUNT, it is also true here but we are collecting over the area of the hemisphere, 2*PI. So to get the total light in you need to multiply your approxmate average by 2*PI. Same as dividing by 1 / (2*PI). $\endgroup$
    – Peter
    Nov 24, 2020 at 15:13
  • 1
    $\begingroup$ Another way to think of it: you are right that in importance sampling you need to divide by the PDF, and uniform sampling is just a special case of importance sampling where every sample is equally important. You would still need to divide by the PDF. $\endgroup$
    – Peter
    Nov 24, 2020 at 15:17
  • 1
    $\begingroup$ Did you look at?: While accumulating specular, the last statement computes the average from the samples, but it "looks" like some of the samples are effectively zero from the line max(0.0001, dot(N,L)), so the average has to high of a weight...and suggests that many of the random samples are not useful. (since the dot product is near or below zero). $\endgroup$
    – pmw1234
    Nov 25, 2020 at 13:28


Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Browse other questions tagged or ask your own question.