OpenGL:Tutorials:GLSL Bump Mapping
From GDWiki
| 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.
Contents |
[edit] 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.
[edit] 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.
[edit] 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(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.
[edit] 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.
[edit] 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!

