I am trying to implement the Cook-Torrance BRDF within OpenGL, but I keep running into issues. The most obvious one are black patches and a bright spot at (0, 0). I checked my shader multiple times, but I can't find what's wrong with it: the functions I use are in line with everything I've seen online, and I can't see anything obviously wrong, neither there nor in the way I've implemented my deferred rendering pipeline.

The code I am using is:

#version 440

uniform sampler2DRect samplerWorldPosition;
uniform sampler2DRect samplerWorldNormal;
uniform sampler2DRect samplerWorldMatInfo;
uniform sampler2DRect samplerWorldMatColour;

uniform vec3 ambient_light_intensity;
uniform vec3 light_direction;
uniform vec3 light_intensity;

out vec3 reflectedLight;

// Approximates the fresnel effect
float fresnel_schlick(float VHdot, float refractive_index)
    float F0 = pow(1 - refractive_index / 1 + refractive_index, 2);
    return F0 + (1 - F0) * pow(1 - VHdot, 5);

// Uses the GGX distribution function
float GGX_distribution_func(float NHdot, float roughness)
    float alpha = roughness * roughness;
    float alpha_sqr = alpha * alpha;

    float NHdot2 = NHdot * NHdot;
    float exp = NHdot2 * alpha_sqr + (1 - NHdot2);

    return (alpha_sqr * max(0, NHdot)) / (3.14159 * exp * exp);

// Attenuation based on geometry
float geometric_attenuation(float NHdot, float NVdot, float VHdot, float NLdot)
    float G1 = (2 * NHdot * NVdot) / VHdot;
    float G2 = (2 * NHdot * NLdot) / VHdot;
    return min(1, min(G1, G2));

void main(void)
    vec3 texel_position = texelFetch(samplerWorldPosition,
    vec3 texel_normal = normalize(texelFetch(samplerWorldNormal,
    vec3 texel_diffuse = texelFetch(samplerWorldMatColour,
    vec3 mat_info = texelFetch(samplerWorldMatInfo,

    float diff_kd = mat_info.x;
    float roughness = mat_info.y;
    float IOR = mat_info.z;

    float NLangle = max(dot(texel_normal, light_direction), 0.0);

    float specular_light = 0;
    float geometric_term = 0;
    float GGX_facet_term = 0;
    float fresnel_term = 0;
    if (NLangle > 0.0)
        vec3 view_direction = normalize(-texel_position);

        vec3 H = normalize(light_direction + view_direction);
        float NHangle = max(dot(texel_normal, H), 0.0);
        float NVangle = max(dot(texel_normal, view_direction), 0.0);
        float VHangle = max(dot(view_direction, H), 0.0);

        geometric_term = geometric_attenuation(NHangle, NVangle, VHangle, NLangle);
        GGX_facet_term = GGX_distribution_func(NHangle, 0.5);
        fresnel_term = fresnel_schlick(VHangle, 0.5);

        specular_light = (geometric_term * GGX_facet_term * fresnel_term) / (3.14 * NVangle * NLangle);

    reflectedLight = NLangle * (light_intensity * specular_light +
        (texel_diffuse)) + ambient_light_intensity;

This code uses (or at least is meant to use) the standard Cook-Torrance model, with GGX microfacet distribution, the Schlick fresnel approximation and the Cook-Torrance geometric attenuation.

This is an example of the errors I am getting: enter image description here The errors are quite evident. What is wrong with this shader?


When you see abrupt patches of black like these often the cause of this are NaNs (Not A Number) from divisions by zero.

And then anything multiplied by an NaN will give an NaN or (almost) any other operations and functions for that matter.

Things like

float NVangle = max(dot(texel_normal, view_direction), 0.0);

specular_light = (geometric_term * GGX_facet_term * fresnel_term) / (3.14 * NVangle * NLangle);

Are likely culprit.

I'd look at the dot(texel_normal, view_direction) to see if it gives a zero. Even if neither NVangle nor NLangle are zero, both multiplied by each other can give a number so small that it gets rounded to zero.

You need to leave a good safety margin before doing a division in a shader as there is practically no guarantee it will be done with full 32bit float precision.


specular_light = (geometric_term * GGX_facet_term * fresnel_term) / max(3.14 * NVangle * NLangle, 0.00390625);

And put a max() like this around every divider until you've found the culprit.

  • 1
    \$\begingroup\$ The magic number 0.00390625 is (1/256) a safe minimum even when the GPU driver may optimize the shader to use 8bit values. \$\endgroup\$ – Stephane Hockenhull Jan 18 '17 at 1:23
  • \$\begingroup\$ The issue was that the texel_position was the wrong value to use. Now it works better. \$\endgroup\$ – MKII Jan 18 '17 at 22:33

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.