Last update: 2 September 2015

Tutorial 7:  Lighting with bumps

This tutorial assumes that you know the basic of OpenGL lighting and what ambient, diffuse and specular lighting means and the maths behind). The tutorial will not cover the theory behind phong lighting and how ambient, diffuse and specular factors are calculated. There is a lot of articles and docs on the net, Check the following link for more details.

This tutorial shows how to implement a point lighting shader with bumps. Bump mapping adds  details to the scene by perturbating the surface normals for objects and using the perturbated normal in the pixel shader for light calculation. this result in a bumpy surfaces. The screen shots bellow shows the same scene rendered with and without bumps ( in the sample code you can switch between the two scenes using the switch button).

Code walkthrough

On the CPU side, light and camera positions are sent to the shader in the model space coordinate system.

 First we get the inverse of the model matrix of the object as follow:
    //get the model matrix
    mat4f &modelBumpMeshMat =  mesh->GetTransfromationMatrix();
    mat4f modelInv = modelBumpMeshMat.inverse();

Next we multiply the light position by the inverse of the model matrix to get the light position in the model space:

    //light position in the model space
    vec4f lightPosition = modelInv * lightPos;
    //send uniforms to shader
    m_pActiveShader->SetUniform3fv("lightPosModel", 1, &lightPosition.x);

For the Camera position. The camera is always located at the origin ( 0, 0, 0). The view matrix transfrom each object from its model space to the world space. so if we multiply the camera position located at (0, 0, 0, 1) by the inverse of the view matrix we get the camera position in the world space, next we multiply it by the inverse of the model space to get the camera posiition in the model space:

    vec4f cameraPos = inv( modelMatrix ) * (inv(ViewMatrix) * vec4(0, 0, 0, 1);)

For a compact expression:
vec4f cameraPos= modelInv * m_pCamera->GetPosition();

the m_pCamera->GetPosition() calculates the inverse of the view matrix and multiply it by vec4f(0, 0, 0, 1.f) homogeneous vector. Please note that multiplyling a 4x4 matrix 'M' with a 4*1 homogeneous vector 'v' extracts the translation part from the matrix ( M[12] M[13] M[14]).
Now that we have the camera position in the model space, we sent it to the shader,

m_pActiveShader->SetUniform3fv("camPosModel", 1, &cameraPos.x);

In addition, We have to send the Light parameters and material properties defined by the ambient, diffuse and specular vectors.

For the lighting parameters:

    m_pActiveShader->SetUniform4fv("lightColorAmbient", 1, &lightAmbient.x);
    m_pActiveShader->SetUniform4fv("lightColorDiffuse", 1, &lightDiffuse.x);
    m_pActiveShader->SetUniform4fv("lightColorSpecular", 1, &lightSpecular.x);

For material properties, in the Render function of the CMeshEntity object, we send the material properties as follow:

            //send ambient
            if(shader->matColorAmbient != -1)
                glUniform4fv(shader->matColorAmbient, 1, material->ambient);
            //send diffuse
            if(shader->matColorDiffuse != -1)
                glUniform4fv(shader->matColorDiffuse, 1, material->diffuse);
            //send specular
            if(shader->matColorSpecular != -1)
                glUniform4fv(shader->matColorSpecular, 1, material->specular);
            //send shininess
            if(shader->matShininess != -1)
                glUniform1f(shader->matShininess, material->shininess);

Now on the vertex shader side, we start by constructing the tangent matrix, this matrix is used to transform all vectors to the same coordinate system ( the tangent space). Light direction and half vector are transformed to the tangent space,

     //calculate bitangent ( this can be don on cpu)
    vec3 bitangent = cross(normal, tangent);
    //create tangent space matrix
    mat3 tangentSpace = mat3(tangent, bitangent, normal);
    //get light direction for the current vertex
    v_lightVector =  lightPosModel -  ;
    //transform light direction to tangent space
    v_lightVector = v_lightVector * tangentSpace;
    v_lightVector = normalize(v_lightVector);

    //get eye direction for the current vertex
    v_halfVector = camPosModel - ;
    //transfrom to tangent space
    v_halfVector = v_halfVector * tangentSpace;
    v_halfVector = normalize(v_halfVector);
    //calculate the half vector
    v_halfVector = (v_halfVector + v_lightVector) /2.0;
    v_halfVector = normalize(v_halfVector) ;

In the pixel shader, we fetch a disturbed normal per pixel from the normal map. Then we calculate the final pixel color which is the sum of the ambient, diffuse and specular contributions of the light.

    //fetch per vertex diffuse color
    vec4 color  = texture2D(texture0, texCoord) ;
    //fetch per pixel normal
    vec3 bump = texture2D(textureBump, texCoord).rgb * 2.0 - 1.0;
    //invert normal for back faces ( if face culling is enabled this can be removed)
    if (!gl_FrontFacing)
        bump = - bump;
    //calculate light contribution
    //1- lamber or diffuse factor
    float lamber = max(0.0, dot(normalize(v_lightVector), bump) );
    //2- specular factor
    float specular = 0.0;
    if (dot(bump, v_lightVector) < 0.0)
        specular = 0.0;
        specular = max(0.0, pow(dot(normalize(v_halfVector), bump), matShininess)) ;

    //get the final ambient diffuse and specular color
    vec4 finalAmbientContrib  = lightColorAmbient  * color /***/;
    vec4 finalDiffuseContrib  = lightColorDiffuse  * color *  lamber * matColorDiffuse;
    vec4 finalSpecularContrib = lightColorSpecular  * specular * matColorSpecular;
    //the final color is the sum of ambient dffuse and specular
    gl_FragColor = finalAmbientContrib + (finalDiffuseContrib + finalSpecularContrib)  ;

Lit scene without bumps

Lit scene with bumps