openglglslshaderphong

OpenGL Phong lighting: specular highlight is wrong


There seems to be an odd problem with my Phong lighting shader in OpenGL. The specular highlight appears on the wrong side of the object.

The problem at hand:

No highlight closest to the light

Highlight on the opposite side

As you can see, the specular highlight appears on the opposite side of the cube (and also appears in the corners of the perpendicular edges of the cube, from the light's perspective. It should only appear on the side closest to the light.

The cube's vertices:

float cubeVertices[] = {
    // positions          // normals           // texture coords
    -1.0f, -1.0f, -1.0f,  0.0f,  0.0f, -1.0f,  0.0f, 0.0f,
    -1.0f,  1.0f, -1.0f,  0.0f,  0.0f, -1.0f,  1.0f, 0.0f,
     1.0f,  1.0f, -1.0f,  0.0f,  0.0f, -1.0f,  1.0f, 1.0f,
     1.0f,  1.0f, -1.0f,  0.0f,  0.0f, -1.0f,  1.0f, 1.0f,
     1.0f, -1.0f, -1.0f,  0.0f,  0.0f, -1.0f,  0.0f, 1.0f,
    -1.0f, -1.0f, -1.0f,  0.0f,  0.0f, -1.0f,  0.0f, 0.0f,
    -1.0f, -1.0f,  1.0f,  0.0f,  0.0f, 1.0f,   0.0f, 0.0f,
     1.0f, -1.0f,  1.0f,  0.0f,  0.0f, 1.0f,   1.0f, 0.0f,
     1.0f,  1.0f,  1.0f,  0.0f,  0.0f, 1.0f,   1.0f, 1.0f,
     1.0f,  1.0f,  1.0f,  0.0f,  0.0f, 1.0f,   1.0f, 1.0f,
    -1.0f,  1.0f,  1.0f,  0.0f,  0.0f, 1.0f,   0.0f, 1.0f,
    -1.0f, -1.0f,  1.0f,  0.0f,  0.0f, 1.0f,   0.0f, 0.0f,
    -1.0f,  1.0f,  1.0f, -1.0f,  0.0f,  0.0f,  1.0f, 0.0f,
    -1.0f,  1.0f, -1.0f, -1.0f,  0.0f,  0.0f,  1.0f, 1.0f,
    -1.0f, -1.0f, -1.0f, -1.0f,  0.0f,  0.0f,  0.0f, 1.0f,
    -1.0f, -1.0f, -1.0f, -1.0f,  0.0f,  0.0f,  0.0f, 1.0f,
    -1.0f, -1.0f,  1.0f, -1.0f,  0.0f,  0.0f,  0.0f, 0.0f,
    -1.0f,  1.0f,  1.0f, -1.0f,  0.0f,  0.0f,  1.0f, 0.0f,
     1.0f,  1.0f,  1.0f,  1.0f,  0.0f,  0.0f,  1.0f, 0.0f,
     1.0f, -1.0f,  1.0f,  1.0f,  0.0f,  0.0f,  1.0f, 1.0f,
     1.0f, -1.0f, -1.0f,  1.0f,  0.0f,  0.0f,  0.0f, 1.0f,
     1.0f, -1.0f, -1.0f,  1.0f,  0.0f,  0.0f,  0.0f, 1.0f,
     1.0f,  1.0f, -1.0f,  1.0f,  0.0f,  0.0f,  0.0f, 0.0f,
     1.0f,  1.0f,  1.0f,  1.0f,  0.0f,  0.0f,  1.0f, 0.0f,
    -1.0f, -1.0f, -1.0f,  0.0f, -1.0f,  0.0f,  0.0f, 1.0f,
     1.0f, -1.0f, -1.0f,  0.0f, -1.0f,  0.0f,  1.0f, 1.0f,
     1.0f, -1.0f,  1.0f,  0.0f, -1.0f,  0.0f,  1.0f, 0.0f,
     1.0f, -1.0f,  1.0f,  0.0f, -1.0f,  0.0f,  1.0f, 0.0f,
    -1.0f, -1.0f,  1.0f,  0.0f, -1.0f,  0.0f,  0.0f, 0.0f,
    -1.0f, -1.0f, -1.0f,  0.0f, -1.0f,  0.0f,  0.0f, 1.0f,
    -1.0f,  1.0f, -1.0f,  0.0f,  1.0f,  0.0f,  0.0f, 1.0f,
    -1.0f,  1.0f,  1.0f,  0.0f,  1.0f,  0.0f,  1.0f, 1.0f,
     1.0f,  1.0f,  1.0f,  0.0f,  1.0f,  0.0f,  1.0f, 0.0f,
     1.0f,  1.0f,  1.0f,  0.0f,  1.0f,  0.0f,  1.0f, 0.0f,
     1.0f,  1.0f, -1.0f,  0.0f,  1.0f,  0.0f,  0.0f, 0.0f,
    -1.0f,  1.0f, -1.0f,  0.0f,  1.0f,  0.0f,  0.0f, 1.0f
};

The vertex shader:

#version 330 core

layout (location = 0) in vec3 vPos;
layout (location = 1) in vec3 vNormal;
layout (location = 2) in vec2 vTexCoords;

out vec3 FragPos;
out vec3 Normal;
out vec2 TexCoords;

uniform mat4 model;
uniform mat4 view;
uniform mat4 projection;

void main()
{
    FragPos = vec3(model * vec4(vPos, 1.0));
    Normal = mat3(transpose(inverse(model))) * vNormal;  
    TexCoords = vTexCoords;

    gl_Position = projection * view * vec4(FragPos, 1.0);
}

The fragment shader:

#version 330 core

out vec4 FragColor; 

in vec3 FragPos;  
in vec3 Normal;  
in vec2 TexCoords;

uniform vec3 viewPos;
uniform sampler2D diffuseMap;

struct Light {
    vec3 position;
    vec3 ambient;
    vec3 diffuse;
    vec3 specular;
};

uniform Light lights[8];
uniform int numLights;

vec3 calculateAmbient(vec3 fragPos, vec3 materialAmbient)
{
    vec3 ambient = vec3(0.0);

    for (int i = 0; i < numLights && i < 8; i++)
    {
        ambient += lights[i].ambient * materialAmbient;
    }

    return ambient * 0.5;
}

vec3 calculateDiffuse(vec3 fragPos, vec3 normal, vec3 materialDiffuse)
{
    vec3 diffuse = vec3(0.0);

    for (int i = 0; i < numLights && i < 8; i++)
    {
        vec3 lightDir = normalize(lights[i].position - fragPos);
        float diff = max(dot(normal, lightDir), 0.0);
        diffuse += lights[i].diffuse * (diff * materialDiffuse);
    }

    return diffuse;
}

vec3 calculateSpecular(vec3 fragPos, vec3 normal, vec3 viewDir, vec3 materialSpecular, float shininess)
{
    vec3 specular = vec3(0.0);

    for (int i = 0; i < numLights && i < 8; i++)
    {
        vec3 lightDir = normalize(lights[i].position - fragPos);
        vec3 reflectDir = reflect(-lightDir, normal);
        float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0f * shininess);
        specular += lights[i].specular * (spec * materialSpecular);
    }

    return specular;
}

void main()
{
    vec4 tex = texture(diffuseMap, TexCoords);

    vec3 norm = normalize(Normal);
    vec3 viewDir = normalize(viewPos - FragPos);

    vec3 ambient = calculateAmbient(FragPos, material.ambient);
    vec3 diffuse = calculateDiffuse(FragPos, norm, material.diffuse);
    vec3 specular = calculateSpecular(FragPos, norm, viewDir, material.specular, material.shininess);

    vec3 result = (ambient + diffuse + specular) * tex.rgb;
    FragColor = vec4(result, 1.0);
} 

Solution

  • The specular highlight appears also on the faces which are back side facing to the light source, because relfect actually computes:

    refelct(I, N) = I - 2.0 * dot(N, I) * N
    

    On back faces the normal vector points away from the light source, but refelct(I, N) == refelct(I, -N), because:

    I - 2.0 * dot(N, I) * N == I - 2.0 * dot(-N, I) * -N
    

    In the Phong reflection model, the specular hightlight is only add, if the diffuse light is > 0. e.g:

    for (int i = 0; i < numLights && i < 8; i++)
    {
        vec3 lightDir = normalize(lights[i].position - fragPos);
        float NdotL = dot(normal, lightDir);
        if (NdotL > 0.0)
        {
            vec3 reflectDir = reflect(-lightDir, normal);
            float spec = pow(max(dot(viewDir, reflectDir), 0.0), 32.0f * shininess);
            specular += lights[i].specular * (spec * materialSpecular);
        }
    }