It's probably something obvious but there seems to be a strange issue with lighting in my pipeline. I'm trying to keep everything in world space at the moment for simplicity but cannot figure out what's going on here. Take a look at my code:

Geometry pass:


#version 410 core

layout (location = 0) in vec4 aPos;
layout (location = 1) in vec4 aNormal;
layout (location = 2) in vec2 aTexCoords;
layout (location = 3) in vec3 aTangent;
layout (location = 4) in vec3 aBitangent;

out vec3 FragPos;
out vec2 TexCoords;
out vec3 Normal;
out vec3 Tangent;
out vec3 Binormal;

void main() {
    vec4 worldPos = model * aPos;
    FragPos = worldPos.xyz;
    TexCoords = aTexCoords;

    mat3 normalMatrix = transpose(inverse(mat3(model)));
    Normal = normalize(normalMatrix * aNormal.xyz);
    Tangent = normalize(normalMatrix * aTangent);
    Binormal = normalize(normalMatrix * aBitangent);

    gl_Position = projection * view * worldPos;


#version 410 core

layout (location = 0) out vec3 gPosition;
layout (location = 1) out vec3 gNormal;
layout (location = 2) out vec4 gColor;
layout (location = 3) out vec4 gSpecular;
layout (location = 4) out vec4 gEmissive;

in vec2 TexCoords;
in vec3 FragPos;
in vec3 Normal;
in vec3 Tangent;
in vec3 Binormal;

// Textures passed from assimp
uniform sampler2D albedo;
uniform sampler2D material;
uniform sampler2D normalmap;
uniform sampler2D ambient;
uniform sampler2D emissive;

void main() {
    // world space model fragment position
    gPosition = FragPos;
    // world space normals translated from tangent space
    mat3 tangentToWorld = mat3(Tangent, Binormal, Normal);
    gNormal = tangentToWorld * (texture(normalmap, TexCoords).rgb * 2.0 - 1.0);
    // diffuse per-fragment color
    gColor = texture(albedo, TexCoords);
    // Beyond here is material properties
    gSpecular = vec4(
        texture(material, TexCoords).rgb,
        texture(ambient, TexCoords).a
    gEmissive = texture(emissive, TexCoords);

Lighting pass:


#version 410 core

layout (location = 0) in vec3 aPos;
layout (location = 1) in vec2 aTexCoords;

out vec2 TexCoords;

void main() {
    // rendering onto a framebuffer quad
    TexCoords = aTexCoords;
    gl_Position = vec4(aPos, 1.0);


#version 410 core

in vec2 TexCoords;
out vec4 color;

uniform sampler2D gPosition;
uniform sampler2D gNormal;
uniform sampler2D gColor;
uniform sampler2D gSpecular;

// light position
uniform vec3 position;

// phong (lambertian) diffuse term
float phong_diffuse() {
    return (1.0 / PI);

// compute fresnel specular factor for given base specular and product
vec3 fresnel_factor(in vec3 f0, in float product) {
    return f0 + (1.01 - f0)*pow(1.01 - product, 5.0);

float D_GGX(in float roughness, in float NdH) {
    float m = roughness * roughness;
    float m2 = m * m;
    float d = (NdH * m2 - NdH) * NdH + 1.0;
    return m2 / (PI * d * d);

float G_schlick(in float roughness, in float NdV, in float NdL) {
    float k = roughness * roughness * 0.5;
    float V = NdV * (1.0 - k) + k;
    float L = NdL * (1.0 - k) + k;
    return 0.25 / (V * L);

// cook-torrance specular calculation
vec3 cooktorrance_specular(in float NdL, in float NdV, in float NdH, in vec3 specular, in float roughness) {
    float D = D_GGX(roughness, NdH);

    float G = G_schlick(roughness, NdV, NdL);

    float rim = mix(1.0 - roughness * texture(gSpecular, TexCoords).w * 0.9, 1.0, NdV);

    return (1.0 / rim) * specular * G * D;

void main() {
    // World space model vertex
    vec3 v_pos = texture(gPosition, TexCoords).rgb;

    // light attenuation
    float A = 20.0 / dot(light_position - v_pos, light_position - v_pos);

    // L, V, H, N vectors
    vec3 L = normalize(light_position - v_pos);
    vec3 V = normalize(-v_pos);
    vec3 H = normalize(L + V);
    vec3 N = normalize(texture(gNormal, TexCoords).rgb);

    // albedo/specular base
    vec3 base = texture(gColor, TexCoords).xyz;

    // roughness
    float roughness = texture(gSpecular, TexCoords).y;

    // metallic
    float metallic = texture(gSpecular, TexCoords).x;

    // mix between metal and non-metal material, for non-metal
    // constant base specular factor of 0.04 grey is used
    vec3 specular = mix(vec3(0.04), base, metallic);

    // compute material reflectance

    float NdL = max(0.0  , dot(N, L));
    float NdV = max(0.001, dot(N, V));
    float NdH = max(0.001, dot(N, H));
    float HdV = max(0.001, dot(H, V));
    float LdV = max(0.001, dot(L, V));

    // specular reflectance with COOK-TORRANCE
    vec3 specfresnel = fresnel_factor(specular, HdV);
    vec3 specref = cooktorrance_specular(NdL, NdV, NdH, specfresnel, roughness);

    specref *= vec3(NdL);
    vec3 diffref = (vec3(1.0) - specfresnel) * phong_diffuse() * NdL;

    // compute lighting
    vec3 reflected_light = vec3(0);
    vec3 diffuse_light = vec3(0);

    // point light
    vec3 light_color = vec3(1.0) * A;
    reflected_light += specref * light_color;
    diffuse_light += diffref * light_color;

    // final result
    vec3 result =
        diffuse_light * mix(base, vec3(0.0), metallic) +

    color = vec4(result, 1);

This is before IBL is applied but I don't think that will make a difference here. Anyways, here's the weird result I'm getting (keep in mind the camera is the same as the point light position):


For reference I'm using this model https://github.com/KhronosGroup/glTF-Sample-Models/tree/master/2.0/DamagedHelmet . Thanks to anyone who can help

  • 3
    $\begingroup$ Do you know if this is coming from the specular or diffuse contribution. I would start by making your shader output one contribution at a time so you can narrow down the source of the problem. $\endgroup$
    – PaulHK
    Jul 8, 2019 at 7:57
  • $\begingroup$ I'm not sure I understand how the textures are being read, it looks like you read normals from a texture in the geometry shader, and then use another texture (whose origin I don't understand) in the pixel shader. Can I see your varying and attribute declarations too, as well as the code which sets up the textures so I can see their format ? $\endgroup$
    – PaulHK
    Jul 8, 2019 at 8:03
  • $\begingroup$ @PaulHK No problem, I've updated the above code. Since this is deferred rendering, everything is flowing from top to bottom. The geometry pass outputs to multiple render targets in an off screen framebuffer then switching to the on screen framebuffer to combine all the gBuffers then render onto a single quad. Btw the geometry pass fragment shader is reading a normalmap (aka bump map) in tangent space then transformed to world space to be sent through a gBuffer to the lighting pass. $\endgroup$ Jul 8, 2019 at 16:47
  • $\begingroup$ Also, it seems to be coming from the diffuse component. $\endgroup$ Jul 8, 2019 at 16:56

1 Answer 1


Turns out it had to do with the V vector working differently in deferred rendering than it does in forward rendering, something that I overlooked and seems obvious now. Specifically, I passed the view direction calculated in the camera class to the lighting pass fragment shader then negated it like so vec3 V = normalize(-(cameraPosition - v_pos));. Things seems to be working well now!

Fixed lighting helmet


Your Answer

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

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