OpenGL:Tutorials:GLSL Bump Mapping

From GPWiki

Files:GUITutorial_warn.gif The Game Programming Wiki has moved! Files:GUITutorial_warn.gif

The wiki is now hosted by GameDev.NET at wiki.gamedev.net. All gpwiki.org content has been moved to the new server.

However, the GPWiki forums are still active! Come say hello.

Getting Started with GLSL – Tangent Space Bump Mapping.

Contents


Introduction

The game world relies on shaders more and more. Higher-level languages are much more attractive for the average programmer than the old low-level assembly code required in the past. This tutorial looks at one such language, OpenGL Shading Language (GLSL) and shows how to create a shader to handle a single bump-mapped directional light on arbitrary geometry.

GLSL Basics - Vectors

First, the GLSL basics. There are types that anyone should be well aware of – floats, ints, and bools – but in GLSL there are some other powerful types that are used frequently. One of these types is the vector (a physics vector, not an STL container vector). A vector can be declared as a vec2, vec3, or vec4 – determining the number of elements in the vector. The type of each element is a float. Vectors are hardware accelerated, so using them over sets of floats is greatly advised.

Vectors can be filled in various ways – Notice that floats do NOT have the f suffix!

 vec3 myvec = vec3(1.0,0.0,-1.0);  //A normally declared vector.
 vec4 newvec = vec4(myvec, 100.0); // A new vector declared from parts of another one.
 myvec.xy = newvec.yz;  //Another example of a declared vector from part of another one.
 

.xyzw or .rgba can be used to access specific parts of a vector in any order. Obviously, parts of a vector that do not exist cannot be accessed, such as a z component in a vec2. Vectors come with the ability to be normalized, by using normalize(vector). A lot of fun can be had with vectors, but that is out of the scope of this tutorial. Look at the reference links for some great information.

GLSL Basics – Matrices

Another type available is the matrix. A matrix can be declared as a 2x2 matrix with mat2, a 3x3 matrix with mat3, or a 4x4 with mat4. These behave as expected to, and allow the use of matrix math in shaders. This is a very important concept that will be used later in this tutorial. Matrices can be created in a variety of ways, but the way they are created below uses three vec3 types in a mat3.

GLSL Keywords

There are three specific added keywords in GLSL that can give a variable special properties. These keywords are uniform, varying, and attribute:

  • A uniform variable is one that is passed to a shader from a program, but can’t be reset in the middle of rendering. A uniform variable is used for the light direction, as it only is updated when the light moves (not often for a directional light.)
  • A varying variable is one that is passed from a vertex shader to a fragment shader. The variable must be declared in both. A varying variable allows the edited light direction to be passed to the fragment shader.
  • An attribute variable is one that is passed like a color, normal, or vertex position. Attribute variables are sent to the vertex shader with every polygon sent, and if a new one isn’t specified the old one is used until a new one is sent. The values are interpolated correctly between vertices if OpenGL’s smooth shading is enabled.

The GLSL Shader

In this section the bump-mapping shader will be presented. I suggested you find an IDE for working with GLSL shaders, such as the one here.

The following is the working commented Bump-Mapping shader.

The Vertex Shader:

varying vec4 passcolor; //The vertex color passed
  varying vec3 LightDir; //The transformed light direction, to pass to the fragment shader
  attribute vec3 tangent; //The inverse tangent to the geometry
  attribute vec3 binormal; //The inverse binormal to the geometry
  uniform vec3 lightdir; //The direction the light is shining
  void main() 
  {
    //Put the color in a varying variable
    passcolor = gl_Color;
    //Put the vertex in the position passed
    gl_Position = ftransform(); 
    //Construct a 3x3 matrix from the geometry’s inverse tangent, binormal, and normal
    mat3 rotmat = mat3(tangent,binormal,gl_Normal);
    //Rotate the light into tangent space
    LightDir = rotmat * normalize(lightdir);
    //Normalize the light
    normalize(LightDir); 
    //Use the first set of texture coordinates in the fragment shader 
    gl_TexCoord[0] = gl_MultiTexCoord0;
  }

The Fragment Shader:

uniform sampler2D BumpTex; //The bump-map 
  uniform sampler2D DecalTex; //The texture
  varying vec4 passcolor; //Receiving the vertex color from the vertex shader
  varying vec3 LightDir; //Receiving the transformed light direction 
  void main() 
  {
    //Get the color of the bump-map
    vec3 BumpNorm = vec3(texture2D(BumpTex, gl_TexCoord[0].xy));
    //Get the color of the texture
    vec3 DecalCol = vec3(texture2D(DecalTex, gl_TexCoord[0].xy));
    //Expand the bump-map into a normalized signed vector
    BumpNorm = (BumpNorm -0.5) * 2.0;
    //Find the dot product between the light direction and the normal
    float NdotL = max(dot(BumpNorm, LightDir), 0.0);
    //Calculate the final color gl_FragColor
    vec3 diffuse = NdotL * passcolor.xyz * DecalCol;
    //Set the color of the fragment...  If you want specular lighting or other types add it here
    gl_FragColor = vec4(diffuse, passcolor.w);
  }

You might wonder why the matrix is passed to the vertex shader as attribute variable - For simplicity’s sake, the code uses the GPWiki vector class for everything in the set-up code.

Also - the rotmat matrix. This matrix represents an inverse TBN matrix. A TBN matrix is formed from the tangent, binormal, and normal of a triangle. The matrix looks like this:

 [t.x b.x n.x]
 [t.y b.y n.y]
 [t.z b.z n.z]

The matrix above moves a normal on a bump-map texture from tangent space into world space, so that the light interacts correctly with that normal and it can be used to find the lighting. Sadly, it isn’t efficient to use the TBN matrix to bring the light direction into tangent space with texture. An inverse TBN matrix is created to do all the calculations on a per-vertex basis instead of a per-fragment basis, which will save a lot of rendering time.

The bump texture is one that has a normal at every pixel so that a program can simulate slightly different lighting across a 2D surface. There are utilities on www.ATI.com that will help you calculate a bump texture from a simple height-map.

Finding the Inverse TBN Matrix

The following is the working commented code to find an inverse TBN matrix. NOTE – this code was mostly written by Søren Dreijer at http://www.blacksmith-studios.dk/projects/downloads/bumpmapping_using_cg.php. This is a translation of his original code, with a few changes.

void FindInvTBN(Vertor3f Vertices[3], Vector2f TexCoords[3], Vector3f & InvNormal,
                  Vector3f & InvBinormal, Vector3f & InvTangent) 
  {
                /* Calculate the vectors from the current vertex
                   to the two other vertices in the triangle */
  
                Vector3f v2v1 = Vertices[0] - Vertices[2];
                Vector3f v3v1 = Vertices[1] - Vertices[2];
  
                //Calculate the “direction” of the triangle based on texture coordinates.
  
                // Calculate c2c1_T and c2c1_B
                float c2c1_T = TexCoords[0].x() - TexCoords[2].x();
                float c2c1_B = TexCoords[0].y() - TexCoords[2].y();
  
                // Calculate c3c1_T and c3c1_B
                float c3c1_T = TexCoords[1].x() - TexCoords[2].x();
                float c3c1_B = TexCoords[1].y() - TexCoords[2].y();
  
                //Look at the references for more explanation for this one.
                float fDenominator = c2c1_T * c3c1_B - c3c1_T * c2c1_B;  
                /*ROUNDOFF here is a macro that sets a value to 0.0f if the value is a very small
                  value, such as > -0.001f and < 0.001. */
 
                /* EDIT by c programmer: you should NEVER perform an equality test against a floating point value, even if
                   your macro has set fDenominator to 0.0f.  The comparison can still fail.  The code needs fixed.
                   Instead you should check if fDenominator is within an epsilon value of 0.0f. */
 
                if (ROUNDOFF(fDenominator) == 0.0f) 
                {
                       /* We won't risk a divide by zero, so set the tangent matrix to the
                          identity matrix */
                        InvTangent = Vector3f(1.0f, 0.0f, 0.0f);
                        InvBinormal = Vector3f(0.0f, 1.0f, 0.0f);
                        InvNormal = Vector3f(0.0f, 0.0f, 1.0f);
                }
                else
                {            
                        // Calculate the reciprocal value once and for all (to achieve speed)
                        float fScale1 = 1.0f / fDenominator;
  
                        /* Time to calculate the tangent, binormal, and normal.
                           Look at Søren’s article for more information. */
                        Vector3f T, B, N;
                        T = Vector3f((c3c1_B * v2v1.x() - c2c1_B * v3v1.x()) * fscale1,
                                     (c3c1_B * v2v1.y() - c2c1_B * v3v1.y()) * fScale1,
                                     (c3c1_B * v2v1.z() - c2c1_B * v3v1.z()) * fScale1);
  
                        B = Vector3f((-c3c1_T * v2v1.x() + c2c1_T * v3v1.x()) * fScale1,
                                     (-c3c1_T * v2v1.y() + c2c1_T * v3v1.y()) * fScale1,
                                     (-c3c1_T * v2v1.z() + c2c1_T * v3v1.z()) * fScale1);
  
                        N = T%B; //Cross product!
  /*This is where programmers should break up the function to smooth the tangent, binormal and
    normal values. */
  
  //Look at “Derivation of the Tangent Space Matrix” for more information.
  
                        float fScale2 = 1.0f / ((T.x() * B.y() * N.z() - T.z() * B.y() * N.x()) + 
                                                (B.x() * N.y() * T.z() - B.z() * N.y() * T.x()) + 
                                                (N.x() * T.y() * B.z() - N.z() * T.y() * B.x()));
                        InvTangent.set((B%N).x() * fScale2,
                                       ((-1.0f * N)%T).x() * fScale2,
                                       (T%B).x() * fScale2);
                        InvTangent.normalize();
  
                        InvBinormal.set(((-1.0f *B)%N).y() * fScale2,
                                        (N%T).y() * fScale2,
                                        ((-1.0f * T)%B).y() * fScale2);
                        InvBinormal.normalize();
  
                        InvNormal.set((B%N).z() * fScale2,
                                      ((-1.0f * N)%T).z() * fScale2,
                                      (T%B).z() * fScale2);
                        InvNormal.normalize();			
  }

This code works, but users will be easily able to see the difference between each triangle. This is because the lighting is uniform across the each entire triangle. A good way to fix this is to take the computations for the tangent, binormal, and normal shown above, and then loop through every vertex. If a vertex shares its position with another vertex, take the tangent, binormal, and normal for each vertex shared and take their average, so later they can be passed on a per-vertex basis. Afterwards, go back and calculate the matrix for every vertex. If smooth shading is enabled, the vertex shader will automatically interpolate the values between the triangle vertices. The lighting will then be a lot smoother, giving the object a more rounded, curved look. This should be used for terrain, buildings, and just about anything shoudn't look pointy. There are a number of tricks to finding these smoothed values, and generally they should be precomputed.

Importing the Shader Functions

Now the shaders can be imported into a program. This is a bit tricky, as an updated glext.h file is required. Some of the glext.h files don’t have all the shader functions, such as lacking attributes but including uniforms. If a computer supports one function it probably supports the others, so glext.h not including one is probably an oversight.

The computer will need to have the functions below in order to have shaders run. Not all of these are needed for every program, but they all are nice to have.

PFNGLCREATEPROGRAMOBJECTARBPROC  glCreateProgramObjectARB ;
 PFNGLDELETEOBJECTARBPROC         glDeleteObjectARB       ;
 PFNGLUSEPROGRAMOBJECTARBPROC     glUseProgramObjectARB   ;
 PFNGLCREATESHADEROBJECTARBPROC   glCreateShaderObjectARB ;
 PFNGLSHADERSOURCEARBPROC         glShaderSourceARB        ;
 PFNGLCOMPILESHADERARBPROC        glCompileShaderARB       ;
 PFNGLGETOBJECTPARAMETERIVARBPROC glGetObjectParameterivARB;
 PFNGLATTACHOBJECTARBPROC         glAttachObjectARB        ;
 PFNGLGETINFOLOGARBPROC           glGetInfoLogARB          ;
 PFNGLLINKPROGRAMARBPROC          glLinkProgramARB         ;
 PFNGLGETUNIFORMLOCATIONARBPROC   glGetUniformLocationARB  ;
 PFNGLUNIFORM4FARBPROC            glUniform4fARB           ;
 PFNGLUNIFORM3FARBPROC            glUniform3fARB           ;         
 PFNGLUNIFORM1FARBPROC            glUniform1fARB           ;
 PFNGLUNIFORM1IARBPROC            glUniform1iARB           ;
 
 
 PFNGLGETATTRIBLOCATIONARBPROC  glGetAttribLocationARB;
 PFNGLVERTEXATTRIB3FARBPROC glVertexAttrib3fARB;
 
 PFNGLVERTEXATTRIBPOINTERARBPROC glVertexAttribPointerARB;
 PFNGLENABLEVERTEXATTRIBARRAYARBPROC glEnableVertexAttribArrayARB ;
 PFNGLDISABLEVERTEXATTRIBARRAYARBPROC glDisableVertexAttribArrayARB;

Then define them using code like the following –

//Get the extensions.
  char *ext = (char*)glGetString( GL_EXTENSIONS );  
  
  if( strstr( ext, "GL_ARB_shading_language_100" ) == NULL )
  {
       //This extension string indicates that the OpenGL Shading Language,
       // version 1.00, is supported.
       /*This will pop up if shaders aren’t supported.	This assumes windows.h is included, so you
         might have to rewrite this message to use your own code.*/
       MessageBox(NULL,"Your Computer doesn't support this high a graphics level!
       Try lower graphics.","ERROR",MB_OK|MB_ICONEXCLAMATION);
       return;    
   }
  
  else { //Create all the functions
  glCreateProgramObjectARB = (PFNGLCREATEPROGRAMOBJECTARBPROC)wglGetProcAddress("glCreateProgramObjectARB");
  glDeleteObjectARB = (PFNGLDELETEOBJECTARBPROC)wglGetProcAddress("glDeleteObjectARB");
  glUseProgramObjectARB = (PFNGLUSEPROGRAMOBJECTARBPROC)wglGetProcAddress("glUseProgramObjectARB");
  glCreateShaderObjectARB = (PFNGLCREATESHADEROBJECTARBPROC)wglGetProcAddress("glCreateShaderObjectARB");
  glShaderSourceARB = (PFNGLSHADERSOURCEARBPROC)wglGetProcAddress("glShaderSourceARB"); 
  glCompileShaderARB = (PFNGLCOMPILESHADERARBPROC)wglGetProcAddress("glCompileShaderARB"); 
  glGetObjectParameterivARB = (PFNGLGETOBJECTPARAMETERIVARBPROC)wglGetProcAddress("glGetObjectParameterivARB"); 
  glAttachObjectARB = (PFNGLATTACHOBJECTARBPROC)wglGetProcAddress("glAttachObjectARB"); 
  glGetInfoLogARB = (PFNGLGETINFOLOGARBPROC)wglGetProcAddress("glGetInfoLogARB");
  glLinkProgramARB = (PFNGLLINKPROGRAMARBPROC)wglGetProcAddress("glLinkProgramARB");
  
  glGetUniformLocationARB = (PFNGLGETUNIFORMLOCATIONARBPROC)wglGetProcAddress("glGetUniformLocationARB");
  glUniform4fARB = (PFNGLUNIFORM4FARBPROC)wglGetProcAddress("glUniform4fARB");
  glUniform3fARB = (PFNGLUNIFORM3FARBPROC)wglGetProcAddress("glUniform3fARB");
  glUniform1fARB = (PFNGLUNIFORM1FARBPROC)wglGetProcAddress("glUniform1fARB");
  glUniform1iARB = (PFNGLUNIFORM1IARBPROC)wglGetProcAddress("glUniform1iARB");
  glGetAttribLocationARB = (PFNGLGETATTRIBLOCATIONARBPROC)wglGetProcAddress("glGetAttribLocationARB");
  glVertexAttrib3fARB = (PFNGLVERTEXATTRIB3FARBPROC)wglGetProcAddress("glVertexAttrib3fARB");
  
  glVertexAttribPointerARB=(PFNGLVERTEXATTRIBPOINTERARBPROC)wglGetProcAddress("glVertexAttribPointerARB");
  glEnableVertexAttribArrayARB=(PFNGLENABLEVERTEXATTRIBARRAYARBPROC)wglGetProcAddress("glEnableVertexAttribArrayARB");
  glDisableVertexAttribArrayARB=( PFNGLDISABLEVERTEXATTRIBARRAYARBPROC)wglGetProcAddress("glDisableVertexAttribArrayARB");
  
   if( strstr( ext, "GL_ARB_shader_objects" ) == NULL )
   {
  
       //Another check.
  
       MessageBox(NULL,"GL_ARB_shader_objects extension was not found",
           "ERROR",MB_OK|MB_ICONEXCLAMATION);
       return;            
   }
   else
   {
  
  //If some unseen reason pops up…
  
       if( !glCreateProgramObjectARB || !glDeleteObjectARB || !glUseProgramObjectARB ||
           !glCreateShaderObjectARB || !glCreateShaderObjectARB || !glCompileShaderARB || 
           !glGetObjectParameterivARB || !glAttachObjectARB || !glGetInfoLogARB || 
           !glLinkProgramARB || !glGetUniformLocationARB || !glUniform4fARB || !glUniform1iARB || 
           !glVertexAttribPointerARB || !glEnableVertexAttribArrayARB ||
           !glDisableVertexAttribArrayARB || !glGetAttribLocationARB)
  
       {
           MessageBox(NULL,"One or more GL_ARB_shader_objects functions were not found",
               "ERROR",MB_OK|MB_ICONEXCLAMATION);
       return;                
       }
   }

This code doesn’t support all the functions of GLSL but supports all the aspects needed for the bump-mapping shader. Creating functions for other amounts of values, such as glUniform2fARB, should be fairly obvious. For more information and simplier methods look at some of the references for more information and declaration of these values using GLEW.

Importing the Shader Itself

Here is the commented code for importing the shader. (Personally, I use the text file importer from lighthouse 3D. Try here for their files and introduction.) A few variables need to be declared to import the shader, which presumably is in two separate files (one for the vertex shader, one for the fragment shader).

GLhandleARB prgmBMap,frgBMap,vrtBMap;  //Aspects of the shader, and the shader itself
  GLuint BMapTangent;  //Allow passage of a tangent
  GLuint BMapBinormal;  //Allow passage of a binormal
  GLuint BMapDecalTex; //Allow passage of a texture
  GLuint BMapBumpMapTex;    //Allow passage of a bump-map
  GLuint BMapLightDir;   //Allow passage of a light direction 

The handles represent the program as a whole and the two parts of the program. All the GLuints allow for communication of data with the shader. The variables can be renamed, but once three or more shaders are used it’s good to find a notation that helps make the easily recognizable.

The following working, commented code shows how to import the shader.

char *vs,*fs; //The textfiles
  prgmBMap = glCreateProgramObjectARB(); //Create the shader
  
  // Create the two parts of the shader
                vrtBMap = glCreateShaderObjectARB(GL_VERTEX_SHADER_ARB);
                frgBMap = glCreateShaderObjectARB(GL_FRAGMENT_SHADER_ARB);	
                //Read in the shader
   	        vs = textFileRead("Shaders/DirectionalBumpLight.vert");  //assuming this is the path and name!
                fs = textFileRead("Shaders/ DirectionalBumpLight.frag");
                //Create temporary variables and compile the shaders 
                const char * vvBMap = vs;
                const char * ffBMap = fs;
  
                glShaderSourceARB(vrtBMap, 1, &vvBMap,NULL);
                glShaderSourceARB(frgBMap, 1, &ffBMap,NULL);
  
                free(vs);free(fs);
  
                glCompileShaderARB(vrtBMap);
                glCompileShaderARB(frgBMap);
  
                //put the parts of the shader into the shader.
                glAttachObjectARB(prgmBMap,vrtBMap);
                glAttachObjectARB(prgmBMap,frgBMap);
  
                //Make the shader ready to go!
                glLinkProgramARB(prgmBMap);

The shader can now be used! It hasn’t been given any data yet, so that needs to be done next. The following commented working code sets up shader – program interaction.

//find the location of the variables in the shader
        BMapTangent=glGetAttribLocationARB(prgmBMap,"tangent");
        BMapBinormal=glGetAttribLocationARB(prgmBMap,"binormal");        
        
        BMapDecalTex = glGetUniformLocationARB(prgmBMap,"DecalTex"); 
        BMapBumpMapTex = glGetUniformLocationARB(prgmBMap,"BumpTex"); 
        BMapLightDir = glGetUniformLocationARB(prgmBMap,"lightdir");

The shader can now be turned on and be passed data. That be can done by using a function like the following:

void ShaderBumpMap(Vector3f LightDir)
  {
    glUseProgramObjectARB(prgmBMap); //Turn on the shader
    //This will be the texture defined in the first slot.  Use   multitexturing!
    glUniform1iARB(BMapDecalTex,0);  
    glUniform1iARB(BMapBumpMapTex,1);  // The texture in the second slot
    //Send the light direction
    glUniform3fARB(BMapLightDir,LightDir.x(),LightDir.y(),LightDir.z());
  }

Turning off the shader:

void ShadersOff()
  {
    glUseProgramObjectARB(0);  //Use a null value
  }

Rendering

Rendering can be a pain, and there are multiple ways of doing it. The way generally considered best is using some sort of vertex array call. Data can be passed this way using glVertexAttribPointerARB. This function requires several things:

  • The GLuint that has the variable location in the shader to modify
  • How many variables it takes per vertex (vec3 takes 3, for instance)
  • The type of data being sent (most likely GL_FLOAT)
  • A stride (distance between each variable in the array, likely 0)
  • Whether to normalize data (1 for yes, 0 for no. This is already done.)
  • A pointer to an array of floats, like a regular vertex array

It’s good to know that all of the calls below are per vertex. Using smooth normals is very easy due to this setup. Remember to turn on the shader! (Note - the normals, binormals, and tangents are actually the inverse TBN matrix here! They should have been run through the matrix creation code by this point.) The commented, working code below draws our scene.

glEnableClientState( GL_VERTEX_ARRAY );  //Pass a vertex position
        glEnableClientState( GL_NORMAL_ARRAY ); //Pass a normal
        glEnableClientState( GL_COLOR_ARRAY );  //Pass a color
        glEnableClientState( GL_TEXTURE_COORD_ARRAY );  //Pass a set of texture coordinates
        glEnableVertexAttribArrayARB(BMapTangent);     //Hopefully self explanatory!
        glEnableVertexAttribArrayARB(BMapBinormal);
        /*Pass the actual values.   The Triangle Positions etc. should be arrays of floats, 
          or simply structs or classes.  If this is confusing, look up a vertex array tutorial!*/
        glVertexPointer( 3, GL_FLOAT, 0, TrianglePositions );
        glNormalPointer( GL_FLOAT, 0, TriangleNormals );	
        glColorPointer( 3, GL_FLOAT, 0, TriangleColors);
        glTexCoordPointer( 2, GL_FLOAT, 0, TriangleTexCoords );	
        glVertexAttribPointerARB(BMapTangent,3,GL_FLOAT,0,0,TriangleTangents); 
        glVertexAttribPointerARB(BmapBinormal,3,GL_FLOAT,0,0,TriangleBinormals);	
  
        glDrawArrays(GL_TRIANGLES, 0, NumOfTriangles);  //Draw everything
  
        glDisableVertexAttribArrayARB(BMapBinormal);  //Always remember to clean up!
        glDisableVertexAttribArrayARB(BMapTangent);
        glDisableClientState( GL_VERTEX_ARRAY );	
        glDisableClientState( GL_NORMAL_ARRAY );	
        glDisableClientState( GL_COLOR_ARRAY );	
  
        glDisableClientState( GL_TEXTURE_COORD_ARRAY );

If simple glBegin/glEnd drawing is desired, use the general attribute call:

 glVertexAttrib3fARB(BMapTangent,TriangleTangent.x(),TriangleTangent.y(),TriangleTangent.z())

It is also possible to import other functions by changing the 3 at the end and then call those functions. Remember to import them with the rest of the functions! vec2s can be sent as attributes or other similar happy stuff by doing this.

Improvements

Finally, how can this method improved? Here are some ideas for improvements:

  • Calculate the binormal in the vertex shader. It's equal to the cross product of the normal and tangent.
  • Add specular lighting. A half angle vector will be required, but it can add to realism.
  • A point light would be a good addition to this code. This code could be rewritten to handle a point light by passing the light position then finding the direction by subtracting the vertex location from the light position and normalizing the result. The effects of the code will be a lot more noticable, and you can then change it into a spot light with some math....
  • The code can also be rewritten to handle multiple lights in a single pass. Should be farily easy to do.
  • Color can be added to a light by passing an additional unsigned normalized color value then multiplying the final diffuse color result by it. This can make for some very nifty effects. An ambient color might also be something to add.
  • Look into parallax (fake displacement) bump-mapping, a process which actively changes texture coordinates using a height map to heighten the pseudo-3D effect. Cool stuff...

Hope this helps add some lighting style! Happy programming!

References