3
votes

I have a simple application that draws a sphere with a single directional light. I'm creating the sphere by starting with an octahedron and subdividing each triangle into 4 smaller triangles.

With just diffuse lighting, the sphere looks very smooth. However, when I add specular highlights, the edges of the triangles show up fairly strongly. Here are some examples:

Diffuse only:

Sphere with only diffuse lighting

Diffuse and Specular:

Sphere with diffuse and specular

I believe that the normals are being interpolated correctly. Looking at just the normals, I get this:

Sphere normals

In fact, if I switch to a flat shading, where the normals are per-polygon instead of per-vertex, I get this:

enter image description here

In my vertex shader, I'm multiplying the model's normals by the transpose inverse modelview matrix:

#version 330 core

layout (location = 0) in vec4 vPosition;
layout (location = 1) in vec3 vNormal;
layout (location = 2) in vec2 vTexCoord;

out vec3 fNormal;
out vec2 fTexCoord;

uniform mat4 transInvModelView;
uniform mat4 ModelViewMatrix;
uniform mat4 ProjectionMatrix;

void main()
{
    fNormal = vec3(transInvModelView * vec4(vNormal, 0.0));
    fTexCoord = vTexCoord;
    gl_Position = ProjectionMatrix * ModelViewMatrix * vPosition;
}

and in the fragment shader, I'm calculating the specular highlights as follows:

#version 330 core

in vec3 fNormal;
in vec2 fTexCoord;

out vec4 color;

uniform sampler2D tex;
uniform vec4 lightColor;        // RGB, assumes multiplied by light intensity
uniform vec3 lightDirection;    // normalized, assumes directional light, lambertian lighting
uniform float specularIntensity;
uniform float specularShininess;
uniform vec3 halfVector;        // Halfway between eye and light
uniform vec4 objectColor;

void main()
{
    vec4    texColor    = objectColor;

    float   specular    = max(dot(halfVector, fNormal), 0.0);
    float   diffuse     = max(dot(lightDirection, fNormal), 0.0);

    if (diffuse == 0.0)
    {
        specular = 0.0;
    }
    else
    {
        specular = pow(specular, specularShininess) * specularIntensity;
    }

    color = texColor * diffuse * lightColor + min(specular * lightColor, vec4(1.0));
}

I was a little confused about how to calculate the halfVector. I'm doing it on the CPU and passing it in as a uniform. It's calculated like this:

vec3    lightDirection(1.0, 1.0, 1.0);
lightDirection = normalize(lightDirection);
vec3    eyeDirection(0.0, 0.0, 1.0);
eyeDirection = normalize(eyeDirection);
vec3    halfVector  = lightDirection + eyeDirection;
halfVector = normalize(halfVector);
glUniform3fv(halfVectorLoc, 1, &halfVector [ 0 ]);

Is that the correct formulation for the halfVector? Or does it need to be done in the shaders as well?

1

1 Answers

11
votes

Interpolating normals into a face can (and almost always will) result in a shortening of the normal. That's why the highlight is darker in the center of a face and brighter at corners and edges. If you do this, just re-normalize the normal in the fragment shader:

fNormal = normalize(fNormal);

Btw, you cannot precompute the half vector as it is view dependent (that's the whole point of specular lighting). In your current scenario, the highlight will not change when you just move the camera (keeping the direction).

One way to do this in the shader is to pass an additional uniform for the eye position and then calculate the view direction as eyePosition - vertexPosition. Then continue as you did on the CPU.