I'm following 'Moving Frostbite to PBR course notes' to implement IBL in my rendering engine in OpenGL but I'm having some trouble pre-integrating the specular component of the equation.

As you'll see from my next images, the problem is visible in the mipmaps of the cubemap result of the pre-filtered importance sampling.

This is the positive X face mip chain: enter image description here

This is the negative X face mip chain: enter image description here

This is the environment map I use (it is dynamically created physically based sky texture in an HDR format): enter image description here

This is the positive X face mip chain of the environment map: enter image description here

As you can see, on the right side of the mips of the positive X face seems like the sampling direction goes similar to the sampling direction of the left side of the mip.

Also, there are those 'dot-like' shapes visible on the second mip of the positive X face that I think are due to a low amount of samples maybe?

This is the code I use to pre-integrate the specular IBL:

float radicalInverse_VdC(uint bits)
    bits = (bits << 16u) | (bits >> 16u);
    bits = ((bits & 0x55555555u) << 1u) | ((bits & 0xAAAAAAAAu) >> 1u);
    bits = ((bits & 0x33333333u) << 2u) | ((bits & 0xCCCCCCCCu) >> 2u);
    bits = ((bits & 0x0F0F0F0Fu) << 4u) | ((bits & 0xF0F0F0F0u) >> 4u);
    bits = ((bits & 0x00FF00FFu) << 8u) | ((bits & 0xFF00FF00u) >> 8u);
    return float(bits) * 2.3283064365386963e-10; // / 0x100000000

// Attributed to:
// http://holger.dammertz.org/stuff/notes_HammersleyOnHemisphere.html
// Holger Dammertz.
vec2 Hammersley(uint i, uint N) 
    return vec2(float(i)/float(N), radicalInverse_VdC(i));

// Based on GGX example in:
// http://blog.selfshadow.com/publications/s2013-shading-course/karis/s2013_pbs_epic_notes_v2.pdf
vec3 importanceSampleGGX(vec2 u, float roughness, vec3 N, vec3 upVector, vec3 tangentX, vec3 tangentY)
    float a = roughness * roughness;

    float phiH = u.x * PI * 2.0f;
    float cosThetaH = sqrt((1.0f - u.y) / (1.0f + (a * a - 1.0f) * u.y));
    float sinThetaH = sqrt(1.0f - min(1.0f, cosThetaH * cosThetaH));

    vec3 H = vec3(sinThetaH * cos(phiH), sinThetaH * sin(phiH), cosThetaH);
    H = normalize(tangentX * H.x + tangentY * H.y + N * H.z);
    return H;

// D(h) for GGX.
// http://graphicrants.blogspot.com/2013/08/specular-brdf-reference.html
float D_GGX(float roughness, float NdotH)
    float a = roughness * roughness;
    float a2 = a * a;
    float NdotH2 = NdotH * NdotH;
    float f = 1.0f + (NdotH2 * (a2 - 1.0f));
    return a2 / (f * f);

float D_GGX_Divide_Pi(float roughness, float NdotH)
    return D_GGX(roughness, NdotH) / PI;

vec3 ImportanceSample (vec3 N)
    vec3 V = N;

    float size2 = ConvolutionSrcSize * ConvolutionSrcSize; 
    vec3 upVector = abs(N.z) < 0.999 ? vec3(0.0f, 0.0f, 1.0f) : vec3(1.0f, 0.0f, 0.0f);
    vec3 tangentX = normalize(cross(upVector, N));
    vec3 tangentY = cross(N, tangentX);

    vec3 accBrdf = vec3(0.0f);
    float accBrdfWeight = 0.0f;
    float roughness = ConvolutionRoughness;

    uint samplesCount = uint(ConvolutionSampleCount);
    for(uint i = uint(0); i < samplesCount; i++)
        vec2 eta = Hammersley(i, samplesCount);

        vec3 H = importanceSampleGGX(eta, roughness, N, upVector, tangentX, tangentY);
        vec3 L = 2.0f * dot(V, H) * H - V;
        float NdotL = dot(N, L);

        if(NdotL > 0.0f)
            float NdotH = saturate(dot(N, H));
            float LdotH = saturate(dot(L, H));
            float pdf = D_GGX_Divide_Pi(roughness, NdotH) * NdotH / (4.0f * LdotH);

            float omegaS = 1.0f / (samplesCount * pdf);
            float omegaP = 4.0f * PI / (6.0f * size2);

            float mipLevel = roughness == 0.0f ? 0.0f : clamp(0.5f * log2(omegaS / omegaP), 0.0f, ConvolutionMipCount);

            vec4 Li = textureLod(ConvolutionSrc, L, mipLevel);

            accBrdf += Li.rgb * NdotL;
            accBrdfWeight += NdotL;

    if(accBrdfWeight > 0.0f)
        return accBrdf * (1.0f / accBrdfWeight);
        return accBrdf;

void main()
    // VertexIn.textureCoord is the normal of a sphere I use as mesh to draw to the IBL cubemap
    FragColor = vec4(ImportanceSample(VertexIn.textureCoord), 1.0f);
  • \$\begingroup\$ Did you find an answer to your problem ? \$\endgroup\$ – CpCd0y Sep 14 '16 at 14:46
  • \$\begingroup\$ Not quite yet. I found out that part of the problem was the number of mips of the source image. Having too many mip levels increase the error since the smallest mips don't have enough information so by limiting the source texture to have a 8x8 (or even better a 16x16) as the smallest mip decreased the error, but the problem is still there. I contacted the author of IBLBacker (on GitHub) since I was following his code and he has some ideas. We'll what we get. He thought about prefitering the source by blurring it a little bit, but I still have to try \$\endgroup\$ – zeb Sep 15 '16 at 19:27

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.