8. Bump Mapping in GLSL

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);
}
  • June 24, 2010
  • 14