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?


1 Answer 1


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\$ Jan 18, 2017 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, 2017 at 22:33

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .