OpenGL:Tutorials:GLSL Bump Mapping
From GPWiki
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.
[edit] IntroductionThe 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. [edit] GLSL Basics - VectorsFirst, 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. [edit] GLSL Basics – MatricesAnother 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. [edit] GLSL KeywordsThere are three specific added keywords in GLSL that can give a variable special properties. These keywords are uniform, varying, and attribute:
[edit] The GLSL ShaderIn 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. [edit] Finding the Inverse TBN MatrixThe 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. [edit] Importing the Shader FunctionsNow 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. [edit] Importing the Shader ItselfHere 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 } [edit] RenderingRendering 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:
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. [edit] ImprovementsFinally, how can this method improved? Here are some ideas for improvements:
Hope this helps add some lighting style! Happy programming! [edit] References |


