Simple GLSL Bump-Mapping / Normal-Mapping

I had an hour to kill earlier on so thought I’d take a shot at some bump-mapping/normal-mapping (whatever you want to call it) – and it’s actually not too hard. Well, it’s probably kinda hard to do properly, but it’s pretty easy to get something that works without going into TBN (Tangent/Binormal/Normal) space, like this…

[nggallery id=5]

For this simple way of doing things there are two important steps:

  • Generate normal-map versions of your texture(s)
  • Perturb your per-pixel geometry normals by a value derived from sampling your normal map.

You can use lots of different tools to generate normal maps, I wanted something quick and free, so I used the GIMP, specifically the GIMP Normal-Map Plugin. Just load the texture image, then select Filters | Map | Normalmap… from the menu and and save that bad boy for later use. I upped the scale of the normal-map generation to 10.000 from it’s original 1.000 just to magnify the effect, you can do the same, or you can magnify it in the fragment shader, but it’s probably going to run quicker if you encode the normal-map with the magnitude of normal perturbation you’re going to use in your final work.

Hint: If you’re on Ubuntu, just install the package gimp-plugin-registry – it comes with a stack of neat & useful plugins, including Normal Map. I tried to install it separately and it conflicted with the plugin-registry package, which would be because it’s already a part of it and I had all the tools I needed already!

Original and Normal-Map Textures
Original Texture on the left, Normal-Map version on the right

Notice how the normal-mapped version of the texture on the right is mainly blue? This is because the data being stored in it isn’t really “colour” anymore – instead of thinking of each value as a RGB triplet, think of it as an XYZ vector. If we look at the data that way, what we’re really seeing when we see “blue” is nothing on the X and Y axis’, and positive on the Z axis. Sneaky, huh?

Now that you’ve got your original texture and the normal-mapped version of it, texture your geometry as usual but add an additional Sampler2D uniform in your fragment shader for your normal-map texture. Bind to another texture unit before loading and glTexImage2D-ing it so you have the texture on one texture image unit and your normal map on another (so you can sample from each independently).

Once you’ve got that all sorted, use shaders along the lines of these:

Vertex Shader

Fragment Shader

Source code after the jump. Cheers!

Main C++ Source

I’ve used a couple of functions from my r3dTools package in the above code, so I’ll add those in here and you can use ’em if you want:

8 thoughts on “Simple GLSL Bump-Mapping / Normal-Mapping”

  1. Hi your bump mapping shader does not work on my old ati card. I however have found one somebody wrote and it does work with one exception i get flashing textures. This greatly reduces the effect and does not look professional. Can you please take a look and make some minor modifications to the shader programs please.

    [fragment shader]
    uniform sampler2D TexUnit;
    uniform sampler2D BaseTexUnit;
    uniform vec4 SurfaceColor;
    uniform float AmbientFactor;
    uniform float DiffuseFactor;
    uniform float SpecularFactor;
    uniform float Shininess;

    varying vec3 LightDir;
    varying vec3 ViewDir;

    void main()
    {
    vec3 normal = vec3(texture2D(TexUnit, gl_TexCoord[0].st));
    vec3 base = vec3(texture2D(BaseTexUnit, gl_TexCoord[0].st)); //shomari

    normal = vec3(2.0)*normal – vec3(1.0);
    normal = normalize(normal);
    vec3 light = normalize(LightDir);
    vec3 view = normalize(ViewDir);
    float diffuseCoeff = max(dot(light,normal),0.0);
    float specularCoeff = 0.0;
    if (diffuseCoeff > 0.0) {
    vec3 reflectedLight = reflect(-light,normal);
    specularCoeff = pow(max(dot(view, reflectedLight),0.0), Shininess) ;
    }

    vec3 color =
    vec3(DiffuseFactor*diffuseCoeff + AmbientFactor) * base +
    vec3(specularCoeff*SpecularFactor) ;

    color = min(color,vec3(1.0));
    gl_FragColor = vec4(color,1.0);
    }

    [vertex shader]
    uniform vec4 LightPos;

    attribute vec3 Tangent; // align with s-coord of texture map

    varying vec3 ViewDir;
    varying vec3 LightDir;

    const bool shaderTool = true; // using Apple’s Shader Builder Tool?

    void main() {
    vec3 eyeNormal = normalize(gl_NormalMatrix * gl_Normal);
    vec3 eyeTangent = gl_NormalMatrix * (shaderTool ? vec3(-1,0,0) : Tangent);
    vec3 eyeBinormal = normalize(cross(eyeNormal, eyeTangent));
    eyeTangent = cross(eyeBinormal, eyeNormal);
    vec4 eyeVertex = gl_ModelViewMatrix*gl_Vertex;
    vec3 eyeLightDir = normalize(vec3(LightPos – eyeVertex));
    LightDir.x = dot(eyeLightDir,eyeTangent);
    LightDir.y = dot(eyeLightDir,eyeBinormal);
    LightDir.z = dot(eyeLightDir,eyeNormal);
    vec3 eyeViewDir = normalize(-eyeVertex.xyz);
    ViewDir.x = dot(eyeViewDir,eyeTangent);
    ViewDir.y = dot(eyeViewDir,eyeBinormal);
    ViewDir.z = dot(eyeViewDir,eyeNormal);
    gl_TexCoord[0] = gl_MultiTexCoord0;
    gl_Position = ftransform();
    }

    1. My shaders and source code probably don’t work on your old ATI card because of the GLSL version – it’s likely that your old ATI card doesn’t support GLSL #330, but it will probably support #120 or #130 or #140 or whatever.

      Give me the link to where you got the shaders you provided and I’ll take a look and see what I can do.

  2. I have attempted to implement this using g-truc’s GLM, and I’ve noticed that this shader draws the geometry only in white. I have adjusted color, etc.

    My implementation had to vary slightly by sending the M and V components separately and multiplying them, otherwise its precisely the same.

    Question regarding VBO binding for GLSL’s in vecs in the vertex shader, is there any trouble with using glVertexAtrib related functions and treating this as an attrib? (I am assuming that’s ok) Another question is, do you have to use the older-style glAttribArray types of GL_TEX_COORD_ARRAY etc or can you use bind to them as above?

    Secondly, the model does not seem to respond to camera movements.
    Uniform(&Umvp,”mvpMatrix”);
    Uniform(&Um,”mMatrix”);
    Uniform(&Uv,”vMatrix”);
    glUniformMatrix4fv(Umvp, 1, GL_FALSE, &camera.MVP[0][0]);
    glUniformMatrix4fv(Um, 1, GL_FALSE, &camera.ModelMatrix[0][0]);
    glUniformMatrix4fv(Uv, 1, GL_FALSE, &camera.ViewMatrix[0][0]);

    The camera is constructed with GLM’s camera used in opengl-tutorial #13. This tutorial seems simpler than that tutorial in its approach on purpose, but I’m not sure how to make it go with the same math setup.

    1. Hi there,

      I’m afraid I don’t know the answer to your attribute questions – I’ve only ever used the helper code from the OpenGL SuperBible to bind attributes and such when I was playing with shaders

      However, I’ve wanted to know the deal is with GLM for a while, so I’ve basically taken the day to hack around and get it to play ball. As such, you can find a working version of the bump mapping source which uses GLM for all matrix and vector math here. It still uses the SuperBible shader loader and vertex generation for the torus’.

      It’s was hacky to being with, and then I bolted on the camera… and now it’s really hacky ;-) Use W/S/A/D and the mouse to move around, the light source is in eye coordinates so attached to the camera, you’ll have to convert the light eye-coords into world coords to fix it in place.

      Hope this helps, at least a bit.

      1. It says the link is not found, however here is a nice camera with GLM based on GLM tutorials. I’d like to see your demo though!

        class GLMCamera {
        public:
        glm::vec3 position;
        glm::vec3 direction;
        glm::vec3 right;
        glm::vec3 up;
        float horizontal,vertical,initialfov,fov,speed,mouseSpeed;
        glm::mat4 ViewMatrix;
        glm::mat4 ProjectionMatrix;
        glm::mat4 ModelMatrix;
        glm::mat4 ModelViewMatrix;
        glm::mat3 ModelView3x3Matrix;
        glm::mat3 NormalMatrix;
        glm::mat4 MVP;
        int cumulativeMouseWheel;
        float Near,Far,Aspect;
        GLMCamera() {
        position.x=0;
        position.y=0;
        position.z=5;
        up.x=0;
        up.y=1;
        up.z=0;
        horizontal=3.14f;
        vertical=0.0f;
        initialfov=45.0f;
        speed=3.0f;
        mouseSpeed=0.005f;
        cumulativeMouseWheel=0;
        fov=0.0f;
        Near=0.001f;
        Far=100.0f;
        Aspect=4.0f/3.0f; // set later to display.aspect or window aspect
        ModelMatrix = glm::mat4(1.0);
        }
        void Upload() {
        glMatrixMode(GL_PROJECTION);
        glLoadMatrixf((const GLfloat*)&ProjectionMatrix[0]);
        glMatrixMode(GL_MODELVIEW);
        glLoadMatrixf((const GLfloat*)&ModelViewMatrix[0]);
        }
        void MouseChangeOrientation() {
        float deltaT=FRAMETIME;
        // Compute new orientation
        horizontal = 3.14f + mouseSpeed * float(display.w2-input.mxi);
        vertical = mouseSpeed * float(display.h2-input.myi);

        // Direction : Spherical coordinates to Cartesian coordinates conversion
        direction.x= cos(vertical) * sin(horizontal);
        direction.y= sin(vertical);
        direction.z= cos(vertical) * cos(horizontal);
        right.x= sin(horizontal – HALF_PIf);
        right.y= 0;
        right.z= cos(horizontal – HALF_PIf);
        up = glm::cross( right, direction );

        // Move forward
        if ( input.KeyDown(DX_W) || input.KeyDown(DX_UP) ) position += direction * deltaT * speed;
        // Move backward
        if ( input.KeyDown(DX_S) || input.KeyDown(DX_DOWN) ) position -= direction * deltaT * speed;
        // Strafe right
        if ( input.KeyDown(DX_D) || input.KeyDown(DX_RIGHT) ) position += right * deltaT * speed;
        // Strafe left
        if ( input.KeyDown(DX_A) || input.KeyDown(DX_LEFT) ) position -= right * deltaT * speed;

        // “zoom”
        if ( input.wheelDown ) cumulativeMouseWheel++;
        else if ( input.wheelUp ) cumulativeMouseWheel–;

        fov = initialfov – 5.0f * (float) cumulativeMouseWheel;
        UpdateMatrices();
        }
        void UpdateMatrices() {
        // Projection matrix : 45° Field of View, 4:3 ratio, display range : 0.1 unit 100 units
        ProjectionMatrix = glm::perspective(fov, Aspect, Near,Far);
        // Camera matrix
        ViewMatrix = glm::lookAt(
        position, // Camera is here
        position+direction, // and looks here : at the same position, plus “direction”
        up // Head is up (set to 0,-1,0 to look upside-down)
        );
        ModelViewMatrix = ViewMatrix * ModelMatrix;
        ModelView3x3Matrix = glm::mat3(ModelViewMatrix);
        NormalMatrix = glm::transpose(glm::inverse(ModelView3x3Matrix));
        MVP = ProjectionMatrix * ViewMatrix * ModelMatrix;
        }
        void fromLap( LookAtPerspective *lap ) {
        up.x=(float)lap->up.x;
        up.y=(float)lap->up.y;
        up.z=(float)lap->up.z;
        position.x=(float)lap->eye.x;
        position.y=(float)lap->eye.y;
        position.z=(float)lap->eye.z;
        direction.x=(float)lap->center.x;
        direction.y=(float)lap->center.y;
        direction.z=(float)lap->center.z;
        fov=(float)lap->halfFOV;
        UpdateMatrices();
        }
        void Debug() {
        Blending(none);
        glColor3d(1.0,0.8,0.3);
        MultilineText(
        FORMAT(buf,1024,
        “GLMCamera:\nPOS: %f,%f,%f DIR: %f,%f,%f RIGHT: %f,%f,%f UP: %f,%f,%f\nHORIZ: %f VERT: %f initial-FOV: %f FOV: %f SPEED: %f mouseSPEED: %f\n”
        “V: %1.3f %1.3f %1.3f %1.3f P: %1.3f %1.3f %1.3f %1.3f M: %1.3f %1.3f %1.3f %1.3f MV: %1.3f %1.3f %1.3f %1.3f MVP: %1.3f %1.3f %1.3f %1.3f\n”
        ” %1.3f %1.3f %1.3f %1.3f %1.3f %1.3f %1.3f %1.3f %1.3f %1.3f %1.3f %1.3f %1.3f %1.3f %1.3f %1.3f %1.3f %1.3f %1.3f %1.3f\n”
        ” %1.3f %1.3f %1.3f %1.3f %1.3f %1.3f %1.3f %1.3f %1.3f %1.3f %1.3f %1.3f %1.3f %1.3f %1.3f %1.3f %1.3f %1.3f %1.3f %1.3f\n”
        ” %1.3f %1.3f %1.3f %1.3f %1.3f %1.3f %1.3f %1.3f %1.3f %1.3f %1.3f %1.3f %1.3f %1.3f %1.3f %1.3f %1.3f %1.3f %1.3f %1.3f\n”,
        position.x, position.y, position.z,
        direction.x, direction.y, direction.z,
        right.x, right.y, right.z,
        up.x, up.y, up.z,
        horizontal,vertical,initialfov,fov,speed,mouseSpeed,
        ViewMatrix.value[0][0], ViewMatrix.value[0][1], ViewMatrix.value[0][2], ViewMatrix.value[0][3],
        ProjectionMatrix.value[0][0], ProjectionMatrix.value[0][1], ProjectionMatrix.value[0][2], ProjectionMatrix.value[0][3],
        ModelMatrix.value[0][0], ModelMatrix.value[0][1], ModelMatrix.value[0][2], ModelMatrix.value[0][3],
        ModelViewMatrix.value[0][0], ModelViewMatrix.value[0][1], ModelViewMatrix.value[0][2], ModelViewMatrix.value[0][3],
        MVP[0][0], MVP[0][1], MVP[0][2], MVP[0][3],
        ViewMatrix.value[1][0], ViewMatrix.value[1][1], ViewMatrix.value[1][2], ViewMatrix.value[1][3],
        ProjectionMatrix.value[1][0], ProjectionMatrix.value[1][1], ProjectionMatrix.value[1][2], ProjectionMatrix.value[1][3],
        ModelMatrix.value[1][0], ModelMatrix.value[1][1], ModelMatrix.value[1][2], ModelMatrix.value[1][3],
        ModelViewMatrix.value[1][0], ModelViewMatrix.value[1][1], ModelViewMatrix.value[1][2], ModelViewMatrix.value[1][3],
        MVP[1][0], MVP[1][1], MVP[1][2], MVP[1][3],
        ViewMatrix.value[2][0], ViewMatrix.value[2][1], ViewMatrix.value[2][2], ViewMatrix.value[2][3],
        ProjectionMatrix.value[2][0], ProjectionMatrix.value[2][1], ProjectionMatrix.value[2][2], ProjectionMatrix.value[2][3],
        ModelMatrix.value[2][0], ModelMatrix.value[2][1], ModelMatrix.value[2][2], ModelMatrix.value[2][3],
        ModelViewMatrix.value[2][0], ModelViewMatrix.value[2][1], ModelViewMatrix.value[2][2], ModelViewMatrix.value[2][3],
        MVP[2][0], MVP[2][1], MVP[2][2], MVP[2][3],
        ViewMatrix.value[3][0], ViewMatrix.value[3][1], ViewMatrix.value[3][2], ViewMatrix.value[3][3],
        ProjectionMatrix.value[3][0], ProjectionMatrix.value[3][1], ProjectionMatrix.value[3][2], ProjectionMatrix.value[3][3],
        ModelMatrix.value[3][0], ModelMatrix.value[3][1], ModelMatrix.value[3][2], ModelMatrix.value[3][3],
        ModelViewMatrix.value[3][0], ModelViewMatrix.value[3][1], ModelViewMatrix.value[3][2], ModelViewMatrix.value[3][3],
        MVP[3][0], MVP[3][1], MVP[3][2], MVP[3][3]
        ), 10, 100, 7, 8, 2, false
        );
        // glm::mat3 ModelView3x3Matrix;
        }
        };

        extern GLMCamera glmCamera;

        1. It was only through email that the link was not found, anyway. Thanks for getting back to me, I’ll try this.

          Question about the geometry:

          I get weird output and the light seems to penetrate the back faces, is there an easy way to avoid that when moving the camera around? The weird output is just that the sides are flipped or otherwise wrong. Do texcoords effect this, or normals, or both? I’ll try using this to see if it fixes the issues.

          1. With the “light penetrating the back faces” – I think that this is just due to the light being specified in eye coordinates, so when you move, the light also moves – it’s fixed in relation to the camera.

            As for the “sides are flipped or otherwise wrong” – I’m not entirely sure what you mean, sorry – maybe send me a screenshot at mail at r3dux dot org and I’ll take a look.

            Cheers!

            1. When implementing this in my engine, it actually turned out I had cross-mapped my normals to my texcoords when using glVertexAttribPtr, so I was seeing texcoords as only solid colors. I debugged this by setting glFragColor=final.r,UV.x,UVy .. I use that camera I provided, but now it can be simplified. However, it was not yours that I used, I used the #13 from OpenGL Tutorial . org and set it up for #version 120, though 330 would be fine too

Leave a Reply

Your email address will not be published.