Introduction
Bump mapping is essential in todays computer games, and computer graphics in general. Would you like to know the best thing about it? It is extremely simple to implement. Bump mapping works by taking an image which stores surface normals on a per-pixel basis. This means that we can apply standard lighting techniques that require a normal value on a per-pixel basis instead of a per-vertex or per-surface basis. This gives our applications a greatly added sense of realism.
How does bump mapping work in OpenGL with GLSL though? GLSL texturing works on a per-texel basis, where a texel can be thought of as a scaled pixel, either larger or smaller, and we get access to a texel at any fragment on an objects surface when we are texturing in GLSL. We already covered texturing in GLSL in a previous tutorial, so I won’t go over it again.
When you look at a normal map, it is generally a blue and purple color. To get your head around normal mapping, all you need to understand is that each pixel in this image file, can be mapped to a normal value. Once you can understand that, normal mapping is extremely straight forward.
Don’t get this tutorial on bump mapping confused with tangent-space bump mapping though. There aren’t many tutorials around that explain the different, but regular bump mapping only cares about the normal maps relation to lights. Tangent space bump mapping takes it a step further and takes into account the objects surfaces in relation-to the light as well as the normal maps relation.
Coding
Enough theory, how can we get it into OpenGL and GLSL. The first thing we will want to do is load in our normal map, and apply it to our object in OpenGL. This requires multi-texturing in OpenGL, which I don’t believe I have covered in GLSL, so I will include that in this tutorial. This code is going to be based on the GLSL texturing tutorial, but you may notice some additions to make it cross-platform between OSX and Windows correctly.
Main.cpp
We are only going to need one new variable for the main.cpp file which will be used to store our normal map texture. This is stored and used just like a normal texture. I am going to call this normal_texture, and define it with our texture variable at the top of our file.
GLuint normal_texture; // Our normal map texture
Then go down to your init method, and remove the current line that creates a texture, and replace it with the following two lines, which will create both of our new
texture = LoadTexture("colour_map.raw", 256, 256); normal_texture = LoadTexture("normal_map.raw", 256, 256);
Inside of our init method you may also notice the following code, this is for cross-platform compatibility and simply checks if we are on OSX or not. If not, we initialize glew.
#if ( (defined(__MACH__)) && (defined(__APPLE__)) ) #else glewInit(); #endif
The rest of our code for the main source code file, goes into our cube method, which I have now modified to draw a square instead.
You should already have code to set the first texture, and we are going to adjust this slightly by adding the following line after we set our active texture:
glEnable(GL_TEXTURE_2D);
Our code to set up our first texture should now look like:
glActiveTexture(GL_TEXTURE0); glEnable(GL_TEXTURE_2D); int texture_location = glGetUniformLocation(shader.id(), "color_texture"); glUniform1i(texture_location, 0); glBindTexture(GL_TEXTURE_2D, texture);
Now we need to set up our second texture. This is done by activating the next texture, GL_TEXTURE1 and modifying the other calls to point to our normal map texture. When we do this, we need to make sure that our glUniform1i for our normal map location in the shader, has the value 1. When multi-texturing in GLSL, each texture used will have an incremented value in the shader. Our final code will look like:
glActiveTexture(GL_TEXTURE1); glEnable(GL_TEXTURE_2D); int normal_location = glGetUniformLocation(shader.id(), "normal_texture"); glUniform1i(normal_location, 1); glBindTexture(GL_TEXTURE_2D, normal_texture);
I won’t explain the code for drawing the square, as I have done this in previous tutorials, so I will jump to after our square. We will need to do some cleaning up, as we don’t want our texture states to mess up when we render other objects later on. All I am doing to clean up, is setting the active texture, binding it to nothing, and then disabling texturing on this texture. Note that I am doing this in reverse to how I have set the textures. You can do this in any order, but I like this as I find it easier to follow (count up on initialization, and then count down on de-initialization).
glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, 0); glDisable(GL_TEXTURE_2D); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, 0); glDisable(GL_TEXTURE_2D);
Shader.vert
Our vertex shader requires absolutely no extra calls, so just use the one provided in the previous texturing tutorial.
Shader.frag
Shader.frag is going to contain all of our bump mapping calculations. To get this going, we need to add a new uniform variable, which will load in our normal mapping texture:
uniform sampler2D normal_texture;
Now that we have access to our normal map, lets load it in. For a normal, we only require a 3 value vector, or vec3 in GLSL, and we are going to normalize this to make sure the final normal vector is of unit length. In other words, it has a length of 1.0. But in this line take note of the (* 2.0 – 1.0). When we read in a texture value, we get a number between 0.0 and 1.0 as color cannot have a negative value. This then multiplies the value by 2.0, giving us values between 0.0 and 2.0, and then subtracts 1.0 so we get our final values in the range of -1.0 to 1.0. We have to do this manipulation as a normal vector is 3D and can be pointing in any direction. It’s a nifty little trick which allows us to convert a normal map texture file to a normal map.
// Extract the normal from the normal map vec3 normal = normalize(texture2D(normal_texture, gl_TexCoord[0].st).rgb * 2.0 - 1.0);
Since we now have the normal value for the current pixel, I am going to set up a quick light, located at the top right of the scene, and also a little towards us. Usually you would base this on actual lights in your scene, and to do so, take a look at my GLSL lighting tutorial.
// Determine where the light is positioned (this can be set however you like) vec3 light_pos = normalize(vec3(1.0, 1.0, 1.5));
Finally we can get to some familiar code. We are going to calculate the diffuse value for the current pixel, based on our normal extracted from the normal map. Then we are going to multiply the diffuse value against our color texture, and assign the output to our gl_FragColor variable.
// Calculate the lighting diffuse value float diffuse = max(dot(normal, light_pos), 0.0); vec3 color = diffuse * texture2D(color_texture, gl_TexCoord[0].st).rgb; // Set the output color of our current pixel gl_FragColor = vec4(color, 1.0);
And now you should have a perfectly working bump mapping shader. If you have any questions, feel free to email me at swiftless@gmail.com or leave a comment below.
Downloads
Colour Map Texture
Normal Map Texture
Tutorial Code
Main.cpp
#if ( (defined(__MACH__)) && (defined(__APPLE__)) ) #include <stdlib.h> #include <OpenGL/gl.h> #include <GLUT/glut.h> #include <OpenGL/glext.h> #else #include <stdlib.h> #include <GL/glew.h> #include <GL/gl.h> #include <GL/glut.h> #include <GL/glext.h> #endif #include "shader.h" Shader shader; GLuint texture; GLuint normal_texture; // Our normal map texture GLuint LoadTexture( const char * filename, int width, int height ) { GLuint texture; unsigned char * data; FILE * file; //The following code will read in our RAW file file = fopen( filename, "rb" ); if ( file == NULL ) return 0; data = (unsigned char *)malloc( width * height * 3 ); fread( data, width * height * 3, 1, file ); fclose( file ); glGenTextures( 1, &texture ); //generate the texture with the loaded data glBindTexture( GL_TEXTURE_2D, texture ); //bind the texture to it’s array glTexEnvf( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE ); //set texture environment parameters //And if you go and use extensions, you can use Anisotropic filtering textures which are of an //even better quality, but this will do for now. glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); //Here we are setting the parameter to repeat the texture instead of clamping the texture //to the edge of our shape. glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_REPEAT ); glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_REPEAT ); //Generate the texture glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, width, height, 0, GL_RGB, GL_UNSIGNED_BYTE, data); free( data ); //free the texture return texture; //return whether it was successfull } void FreeTexture( GLuint texture ) { glDeleteTextures( 1, &texture ); } void init(void) { glEnable(GL_DEPTH_TEST); glDepthFunc(GL_LESS); #if ( (defined(__MACH__)) && (defined(__APPLE__)) ) #else glewInit(); #endif shader.init("shader.vert", "shader.frag"); texture = LoadTexture("colour_map.raw", 256, 256); normal_texture = LoadTexture("normal_map.raw", 256, 256); } void cube (void) { glActiveTexture(GL_TEXTURE0); glEnable(GL_TEXTURE_2D); int texture_location = glGetUniformLocation(shader.id(), "color_texture"); glUniform1i(texture_location, 0); glBindTexture(GL_TEXTURE_2D, texture); glActiveTexture(GL_TEXTURE1); glEnable(GL_TEXTURE_2D); int normal_location = glGetUniformLocation(shader.id(), "normal_texture"); glUniform1i(normal_location, 1); glBindTexture(GL_TEXTURE_2D, normal_texture); glBegin(GL_QUADS); glTexCoord2f(0.0f, 0.0f); glVertex3f(-1.0f, -1.0f, 0.0f); glTexCoord2f(1.0f, 0.0f); glVertex3f(1.0f, -1.0f, 0.0f); glTexCoord2f(1.0f, 1.0f); glVertex3f(1.0f, 1.0f, 0.0f); glTexCoord2f(0.0f, 1.0f); glVertex3f(-1.0f, 1.0f, 0.0f); glEnd(); glActiveTexture(GL_TEXTURE1); glBindTexture(GL_TEXTURE_2D, 0); glDisable(GL_TEXTURE_2D); glActiveTexture(GL_TEXTURE0); glBindTexture(GL_TEXTURE_2D, 0); glDisable(GL_TEXTURE_2D); } void display (void) { glClearColor (0.0,0.0,0.0,1.0); glClear (GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT); glLoadIdentity(); gluLookAt (0.0, 0.0, 2.0, 0.0, 0.0, 0.0, 0.0, 1.0, 0.0); shader.bind(); cube(); shader.unbind(); glutSwapBuffers(); } void reshape (int w, int h) { glViewport (0, 0, (GLsizei)w, (GLsizei)h); glMatrixMode (GL_PROJECTION); glLoadIdentity (); gluPerspective (60, (GLfloat)w / (GLfloat)h, 1.0, 100.0); glMatrixMode (GL_MODELVIEW); } int main (int argc, char **argv) { glutInit(&argc, argv); glutInitDisplayMode(GLUT_DOUBLE | GLUT_RGBA | GLUT_DEPTH); //set up the double buffering glutInitWindowSize(500, 500); glutInitWindowPosition(100, 100); glutCreateWindow("A basic OpenGL Window"); init(); glutDisplayFunc(display); glutIdleFunc(display); glutReshapeFunc(reshape); glutMainLoop(); return 0; }
Shader.vert
void main() { gl_TexCoord[0] = gl_MultiTexCoord0; // Set the position of the current vertex gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex; }
Shader.frag
uniform sampler2D color_texture; uniform sampler2D normal_texture; void main() { // Extract the normal from the normal map vec3 normal = normalize(texture2D(normal_texture, gl_TexCoord[0].st).rgb * 2.0 - 1.0); // Determine where the light is positioned (this can be set however you like) vec3 light_pos = normalize(vec3(1.0, 1.0, 1.5)); // Calculate the lighting diffuse value float diffuse = max(dot(normal, light_pos), 0.0); vec3 color = diffuse * texture2D(color_texture, gl_TexCoord[0].st).rgb; // Set the output color of our current pixel gl_FragColor = vec4(color, 1.0); }
Hi Swiftless,
Thank you for the brilliant tutorial.
I downloaded shader.h, shader.cpp and the material code in this tutorial. However, this project can’t work because there is an access violation problem. To be specific, it seems that this problem occurs in “glDeleteProgram(shader_id);” in the destructor ~Shader(). Could you help me with it? Thanks a lot!
Catherine
Sorry, I just fixed the compile error. But after I pressed ctrl+F5, I can only see a snow in the display window…What’s wrong?
Sorry again! Snow means…it’s done! Thank you sooooo much!
Hey, nice job on the GLSL tutorials, I’ve finally come to understand and run a toon shader based on what I learned here.
I’ve been testing this with a glut teapot, and I can’t see the effects of the bump mapping shade. Should I be noticing anything different with the teapot? Or will I have to render a complex model?
Where is “Shader.h”?
You’ve probably figured this out by now, but I was able to copy shader.h and shader.cpp from any of the previous lessons in this tutorial. The files don’t change at all between lessons 2 through 7, so just copy the files from any of those and they’ll work fine.
I was checking your OpenGL 4 tutorials, which are great btw, and I stumbled on this one. I implemented a bump mapping in the past so I was interested to see your technique.
I seems to me that the way you implemented it is too simplistic and can only work if the geometry is facing up. If the orientation of the object changes, the values inside the normal map won’t consider it and the lighting will not be accurate.
You need to take the geometry and the orientation of the object into account. You either have to bring the normals from the normal map into the object space or bring the light into the space of every face of the geometry you render.
Hi MarioL,
I thought I had mentioned in the tutorial that you can go on to tangent based bump mapping techniques which take into account geometry tranformations.
You are exactly on the right track though! Good work.
Cheers,
Swiftless
Sorry Swiftless, my bad. I should have read every paragraph carefully.
Keep on the good work.
MarioL
Thanks for the tutorial.
My result of the program showing in the lower link is strange.
http://img148.imageshack.us/img148/3256/22014693.jpg
It seems that I made a blend of the color map and the normal map…
Hey YellowMind,
This often happens when there is an error in your shader and it fails to compile.
Cheers,
Swiftless
Nice tutorial. You do not have to enable/disable texturing when using GLSL. Your glEnable(GL_TEXTURE_2D) and glDisable(GL_TEXTURE_2D) calls are redundant.
Hey Michal,
That is totally correct, it is a habit I picked up from using the fixed-function pipeline for years, but isn’t needed with GLSL.
Cheers,
Swiftless