OpenGL:Tutorials:GLSL Bump Mapping

From GPWiki
Jump to: navigation, search
40px-notepad.png To comply with our quality standards, this article may need to be rewritten.
Please help improve this article. The discussion page may contain suggestions.


Getting Started with GLSL – Tangent Space Bump Mapping.

An OpenGL Shader Tutorial

This tutorial is intended to demonstrate the application of a OpenGL Shader program to do bump mapping. Familiarity with OpenGL and its extension system is assumed. The code contained here is C++, OpenGL Version 1.4 with the use of ARB Extensions. For OpenGL Version 2.0 and higher, the ARB suffix on identifiers may be removed.

The GLSL Shader

In this section the bump-mapping shader will be presented. It is suggested you find an IDE for working with GLSL shaders.

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);
 }

The matrix is passed to the vertex shader as an attribute variable for simplicity's sake.

The rotmat 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. But it is usually more efficient to move the light direction into tangent space and do the calculations there, which is done by using the inverse TBN matrix on the light direction.

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 ATI's website that will 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. This code was mostly written by Søren Dreijer at [1]. This is a translation of his original code, with a few changes.

 void FindInvTBN(Vector3f 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 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 that shouldn't look pointy. There are a number of tricks to find these smoothed values, and generally they should be precomputed.

Rendering

For more information concerning passing data to GLSL shaders, see OpenGL:Tutorials:Passing Data to Shaders

Using smooth normals is very easy due to this setup. 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.

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, but the binormal can be the wrong direction. Save the "handedness" in a 4th scalar in the tangent vector.
  • 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