16 min read

Bump Mapping

note

A complete example of this code is available: BumpMapShader.zip

Introduction

In the previous HLSL tutorials, we have done lighting and texturing. All of this can be done with the BasicEffect class with ease. But now we are going to move on to a more sophisticated shader that will do bump mapping. Bump mapping is a method where you use a texture to make it appear like a model has a lot more detail than it really does. Usually, this detail is in the form of bumps, but it isn’t really limited to this. You can also do quite a bit of detailing with bump maps. In fact, there are programs out there that will take a low detail 3D model and a high detail 3D model and create a bump map for the low detail model that will make it look almost the same as the high detail model.

I have split this tutorial into two parts. In this first part, we will look at how bump mapping works, as well as the general process of creating them. I will also show you a couple of different ways that you can create bump maps. This way, we will be able to create all of the content that we need for our XNA game and our bump mapping shader. In part two, we will go over the actual shader and the changes we will need to make in our XNA game.

Below is an image of the final outcome of our shader. I’ll be the first to admit that the bump map I used is not the best for this object, nor is this object the coolest object to demonstrate bump mapping on, but it will do.

Screenshot 1

All Kinds of Maps

The first thing we need to talk about is how we are going to actually do bump mapping. Probably the most common method of bump mapping is called normal mapping. In normal mapping, we will create a texture (or map) that stores the normal for the point. In the normal map, the normal is represented as a color.

Usually, the process starts with the texture map. For the helicopter that I’ve been using, the texture is below:

Helicopter Texture Map

This is just a simple texture, and many models will have much more sophisticated textures. I just have a few blocks of color for various parts of the helicopter. The main light blue color is the body of the helicopter, the darker blue is the glass window, the yellow is on the rotors’ edges, the black is the rest of the rotors, and the white is a sort of stripe on the tail of the helicopter.

We will need to create a normal map that lines up with this texture map. However, determining the normals at every point in the texture and coloring the normal map is not a simple process. It is a little easier to create yet another kind of map called a heightmap. Below is a height map that I created for the helicopter. You can see that it lines up with the various parts of the helicopter texture.

Helicopter Height Map

Height maps are usually grayscale images where lighter colors represent high “elevations” and dark colors represent low elevations. These are usually pretty easy to make in an image editing program, and we will talk a little bit more about how to make them in the next section. You can see that some of the regions I have made perfectly flat because they are all the same color. The window (lower right corner) has some roughness to it because the glass is never perfectly smooth. The body of the helicopter is fairly bumpy.

Once the heightmap has been created, we can generate a normal map from it. There is a simple algorithm that is used to create a normal map from a height map, but we don’t need to worry about it, because we will just use some free software to calculate it. Below is the normal map for the helicopter.

Helicopter Normal Map

Normal maps always end up looking kind of bluish and reddish. Each pixel indicates what the normal is like at that point in the texture. In this method of normal mapping, these values indicate how the normal at this point is different from what the normal should be like without the bumps. In other words, we will need to still factor in the regular normal in with the value of the normal maps. It is less frequent, but occasionally, people will make a normal map that stores the exact normal of the texture, so you don’t need to factor in the regular normal. These normal maps end up looking more like a rainbow because you’ll see lots of greens and yellows as well. If you do a Google image search for “normal map” you will most likely find examples of both, but you’ll see that the reddish-bluish ones are more common.

Generating Height and Normal Maps

There are many tools out there that will create normal maps for you. I will walk you through the process that I usually use because it involves only free tools. Usually, I will create my heightmap with the same program that I use to create my regular texture map. You can use any program you want, but it needs to be able to export images in the .tga file format. Both Photoshop and Paint.NET (which is free) can do this. If not, you will need another way to convert your image file to a .tga file, because the program that we will use to create the normal map requires .tga files for input. If you are using Paint.NET, when you save a .tga file, be sure to turn off the compression, because the tool we will use does not know how to deal with a compressed .tga file.

Once you have the heightmap, you are ready to create the normal map. I usually use a free tool called xNormal, which can be downloaded from the xNormal downloads page. Once you’ve got xNormal installed (it may take a few minutes, and it will probably install a variety of other libraries that come up as separate installations) it should be pretty easy to make your normal map. xNormal actually has a ton of features, and we’re only going to use one of them for what we’re doing. Open up xNormal and look on the right side. There you’ll find a button that says Tools. Click on that to open up the Tools window. Click on the one that says Height map to normal map and a new window will pop up to allow you to create your normal map. On the Height map side, right-click and choose Browse height map. Find the height map file you want from the file system. Then go over to the Normal map side, right-click, and choose Generate, and the normal map will be generated for you. Right-click in the Normal map side again, and choose Save normal map and save your generated normal map out to file.

Remember that you’ll need to add your normal map file to your XNA project like usual.

The Bump Map Shader

Once again, we are going to build on our previous shaders. I am going to extend my textured shader, though you really ought to be able to add on to the diffuse or specular shaders instead if you want. So, as we’ve done before, I have made a copy of my textured shader and renamed it to “NormalMap.fx”. Alternatively, you can start with the shader code below, which is, essentially, my shader code from the end of the texturing tutorial.

float4x4 World;
float4x4 View;
float4x4 Projection;

float4 AmbientColor = float4(1, 1, 1, 1);
float AmbientIntensity = 0.1;

float4x4 WorldInverseTranspose;

float3 DiffuseLightDirection = float3(1, 0, 0);
float4 DiffuseColor = float4(1, 1, 1, 1);
float DiffuseIntensity = 1.0;

float Shininess = 200;
float4 SpecularColor = float4(1, 1, 1, 1);
float SpecularIntensity = 1;
float3 ViewVector = float3(1, 0, 0);

struct VertexShaderInput
{
    float4 Position : POSITION0;    
    float4 Normal : NORMAL0;   
};

struct VertexShaderOutput
{
    float4 Position : POSITION0;
    float4 Color : COLOR0;
	float3 Normal : TEXCOORD0;
};

VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
    VertexShaderOutput output;

    float4 worldPosition = mul(input.Position, World);
    float4 viewPosition = mul(worldPosition, View);
    output.Position = mul(viewPosition, Projection);

    float4 normal = normalize(mul(input.Normal, WorldInverseTranspose));
    float lightIntensity = dot(normal, DiffuseLightDirection);
    output.Color = saturate(DiffuseColor * DiffuseIntensity * lightIntensity);

    output.Normal = normal;

    return output;
}

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
    float3 light = normalize(DiffuseLightDirection);
    float3 normal = normalize(input.Normal);
    float3 r = normalize(2 * dot(light, normal) * normal - light);
    float3 v = normalize(mul(normalize(ViewVector), World));

    float dotProduct = dot(r, v);
    float4 specular = SpecularIntensity * SpecularColor * max(pow(dotProduct, Shininess), 0) * length(input.Color);

    return saturate(input.Color + AmbientColor * AmbientIntensity + specular);
}

technique Specular
{
    pass Pass1
    {
        VertexShader = compile vs_2_0 VertexShaderFunction();
        PixelShader = compile ps_2_0 PixelShaderFunction();
    }
}

Adding More Variables

Like usual, our first task is to add in the necessary variables so that we can perform normal mapping. So add the following as variables at the start of your effect file:

float BumpConstant = 1;
texture NormalMap;
sampler2D bumpSampler = sampler_state {
	Texture = (NormalMap);
	MinFilter = Linear;
	MagFilter = Linear;
	AddressU = Wrap;
	AddressV = Wrap;
};

The variable BumpConstant indicates how big the bumps should be. A value near 0 will mean the surface isn’t affected by the bump map very much. A higher value means larger bumps. Values bigger than about 3 start looking odd. A value of 1 is usually about what you are looking for. Also, this value can be negative, which would have the reverse effect, where all of the original holes are actually bumps, and all of the original bumps are actually holes.

The second variable, NormalMap is a texture that will store our normal map.

The third variable, bumpSampler should look quite a bit like the sampler we made in the texturing tutorial, so I’m not really going to go into it in much detail.

Modifying the Data Structures

This time we will need to add quite a bit to our data structures to do bump mapping, so I’m going to tell you to just replace the old data structures with the new stuff below.

So replace the vertex shader input, which currently says:

struct VertexShaderInput
{
    float4 Position : POSITION0;
    float4 Normal : NORMAL0;
    float2 TextureCoordinate : TEXCOORD0;
};

with:

struct VertexShaderInput
{
    float4 Position : POSITION0;
    float3 Normal : NORMAL0;
    float3 Tangent : TANGENT0;
    float3 Binormal : BINORMAL0;
    float2 TextureCoordinate : TEXCOORD0;
};

Notice that we’ve added in a Tangent field and a Binormal field. These go along with the normal and tell us how the surface is oriented. Remember that a normal vector points directly away from the surface. A tangent vector points directly along the surface. The binormal vector will also point along the surface, but it will be perpendicular to the tangent vector. (For those of you who remember your math, the binormal vector is the cross product of the normal and tangent vectors.) We won’t have to worry about calculating these, though, because the Model class and XNA will take care of them for us. But we will use them later on.

Next, we will want to modify our vertex shader output. So change replace the current vertex shader output, which says:

struct VertexShaderOutput
{
    float4 Position : POSITION0;
    float4 Color : COLOR0;
    float3 Normal : TEXCOORD0;
    float2 TextureCoordinate : TEXCOORD1;
};

with this:

struct VertexShaderOutput
{
    float4 Position : POSITION0;
    float2 TextureCoordinate : TEXCOORD0;
    float3 Normal : TEXCOORD1;
    float3 Tangent : TEXCOORD2;
    float3 Binormal : TEXCOORD3;
};

Notice that we have gotten rid of the Color field, which was used for the diffuse lighting color. This is because we will need to do this calculation on a pixel by pixel basis in the pixel shader, rather than in the vertex shader. We have also changed the TextureCoordinate semantic to TEXCOORD0 which we didn’t need to do, but I just decided it would be better. We have also added in a Tangent and a Binormal field.

Changing the Vertex Shader

We will once again, need to make some pretty big changes to the vertex shader, and so we’ll just replace the old stuff with the new stuff. So replace the vertex shader, which currently says:

VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
    VertexShaderOutput output;

    float4 worldPosition = mul(input.Position, World);
    float4 viewPosition = mul(worldPosition, View);
    output.Position = mul(viewPosition, Projection);

    float4 normal = normalize(mul(input.Normal, WorldInverseTranspose));
    float lightIntensity = dot(normal, DiffuseLightDirection);
    output.Color = saturate(DiffuseColor * DiffuseIntensity * lightIntensity);

    output.Normal = normal;

    output.TextureCoordinate = input.TextureCoordinate;
    return output;
}

with:

VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
    VertexShaderOutput output;

    float4 worldPosition = mul(input.Position, World);
    float4 viewPosition = mul(worldPosition, View);
    output.Position = mul(viewPosition, Projection);

    output.Normal = normalize(mul(input.Normal, WorldInverseTranspose));
    output.Tangent = normalize(mul(input.Tangent, WorldInverseTranspose));
    output.Binormal = normalize(mul(input.Binormal, WorldInverseTranspose));

    output.TextureCoordinate = input.TextureCoordinate;
    return output;
}

Notice that our vertex shader is actually quite a bit simpler. This is because we are no longer doing the diffuse shading calculation here. (We’ll do that calculation on a per-pixel basis, inside of the pixel shader.) We just put the vertex in the correct location and transform all of the vectors that we need. The texture coordinate will remain the same. That’s all our vertex shader will need to do.

Changing the Pixel Shader

Our pixel shader will become quite a bit more complicated because we will need to do our diffuse lighting calculations here, as well as our bump/normal map calculations. Once again, we’ll just replace the old stuff with completely new stuff, so remove the current pixel shader, which says:


float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
    float3 light = normalize(DiffuseLightDirection);
    float3 normal = normalize(input.Normal);
    float3 r = normalize(2 * dot(light, normal) * normal - light);
    float3 v = normalize(mul(normalize(ViewVector), World));
    float dotProduct = dot(r, v);

    float4 specular = SpecularIntensity * SpecularColor * max(pow(dotProduct, Shininess), 0) * length(input.Color);

    float4 textureColor = tex2D(textureSampler, input.TextureCoordinate);
    textureColor.a = 1;

    return saturate(textureColor * (input.Color) + AmbientColor * AmbientIntensity + specular);
}

and replace it with:

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
    // Calculate the normal, including the information in the bump map
    float3 bump = BumpConstant * (tex2D(bumpSampler, input.TextureCoordinate) - (0.5, 0.5, 0.5));
    float3 bumpNormal = input.Normal + (bump.x * input.Tangent + bump.y * input.Binormal);
    bumpNormal = normalize(bumpNormal);

    // Calculate the diffuse light component with the bump map normal
    float diffuseIntensity = dot(normalize(DiffuseLightDirection), bumpNormal);
    if(diffuseIntensity < 0)
        diffuseIntensity = 0;

    // Calculate the specular light component with the bump map normal
    float3 light = normalize(DiffuseLightDirection);
    float3 r = normalize(2 * dot(light, bumpNormal) * bumpNormal - light);
    float3 v = normalize(mul(normalize(ViewVector), World));
    float dotProduct = dot(r, v);

    float4 specular = SpecularIntensity * SpecularColor * max(pow(dotProduct, Shininess), 0) * diffuseIntensity;

    // Calculate the texture color
    float4 textureColor = tex2D(textureSampler, input.TextureCoordinate);
    textureColor.a = 1;

    // Combine all of these values into one (including the ambient light)
    return saturate(textureColor * (diffuseIntensity) + AmbientColor * AmbientIntensity + specular);
}

Remember, all we are really doing is using the normal map stuff to calculate a different normal at every pixel that we draw. The first three lines do the work of calculating the normal for this pixel. In the first line, we pull out the normal information from the normal map. We subtract 0.5 from each component so that our values are centered around 0. Before, the values were in the range from 0 to 1, and now they will be from -0.5 to +0.5. We then multiply it by our BumpConstant amount, which will stretch it out appropriately.

In the second line, we calculate the actual normal at this pixel. Notice that we start with the normal that this pixel should have, based solely on the geometry. We then add in an amount based on the values in the normal map, factored in with the tangent and binormal vectors.

In line 3, we just make sure the normal vector has a length of 1 by normalizing it.

The next three lines calculate the diffuse light at this point, which should look pretty similar to what we did before. Notice, that we are using the normal that we calculated from the bump/normal map for this.

After that, the pixel shader is pretty much like it always has been. We calculate the specular lighting, just like before, but with the new bump-mapped normal, and then calculate the combined color, including ambient lighting and return it.

Other Changes

You may also want to rename the technique from “Textured” to something else like “BumpMapped”.

This gives us a final effect file that should look something like this:

float4x4 World;
float4x4 View;
float4x4 Projection;

float4 AmbientColor = float4(1, 1, 1, 1);
float AmbientIntensity = 0.1;

float4x4 WorldInverseTranspose;

float3 DiffuseLightDirection = float3(1, 0, 0);
float4 DiffuseColor = float4(1, 1, 1, 1);
float DiffuseIntensity = 1.0;

float Shininess = 200;
float4 SpecularColor = float4(1, 1, 1, 1);
float SpecularIntensity = 1;
float3 ViewVector = float3(1, 0, 0);

texture ModelTexture;
sampler2D textureSampler = sampler_state {
    Texture = (ModelTexture);
    MinFilter = Linear;
    MagFilter = Linear;
    AddressU = Clamp;
    AddressV = Clamp;
};

float BumpConstant = 1;
texture NormalMap;
sampler2D bumpSampler = sampler_state {
	Texture = (NormalMap);
	MinFilter = Linear;
	MagFilter = Linear;
	AddressU = Wrap;
	AddressV = Wrap;
};

struct VertexShaderInput
{
    float4 Position : POSITION0;
    float3 Normal : NORMAL0;
    float3 Tangent : TANGENT0;
    float3 Binormal : BINORMAL0;
    float2 TextureCoordinate : TEXCOORD0;
};

struct VertexShaderOutput
{
    float4 Position : POSITION0;
    float2 TextureCoordinate : TEXCOORD0;
    float3 Normal : TEXCOORD1;
    float3 Tangent : TEXCOORD2;
    float3 Binormal : TEXCOORD3;
};

VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
    VertexShaderOutput output;

    float4 worldPosition = mul(input.Position, World);
    float4 viewPosition = mul(worldPosition, View);
    output.Position = mul(viewPosition, Projection);

    output.Normal = normalize(mul(input.Normal, WorldInverseTranspose));
    output.Tangent = normalize(mul(input.Tangent, WorldInverseTranspose));
    output.Binormal = normalize(mul(input.Binormal, WorldInverseTranspose));

    output.TextureCoordinate = input.TextureCoordinate;
    return output;
}

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
    // Calculate the normal, including the information in the bump map
    float3 bump = BumpConstant * (tex2D(bumpSampler, input.TextureCoordinate) - (0.5, 0.5, 0.5));
    float3 bumpNormal = input.Normal + (bump.x * input.Tangent + bump.y * input.Binormal);
    bumpNormal = normalize(bumpNormal);

    // Calculate the diffuse light component with the bump map normal
    float diffuseIntensity = dot(normalize(DiffuseLightDirection), bumpNormal);
    if(diffuseIntensity < 0)
        diffuseIntensity = 0;

    // Calculate the specular light component with the bump map normal
    float3 light = normalize(DiffuseLightDirection);
    float3 r = normalize(2 * dot(light, bumpNormal) * bumpNormal - light);
    float3 v = normalize(mul(normalize(ViewVector), World));
    float dotProduct = dot(r, v);

    float4 specular = SpecularIntensity * SpecularColor * max(pow(dotProduct, Shininess), 0) * diffuseIntensity;

    // Calculate the texture color
    float4 textureColor = tex2D(textureSampler, input.TextureCoordinate);
    textureColor.a = 1;

    // Combine all of these values into one (including the ambient light)
    return saturate(textureColor * (diffuseIntensity) + AmbientColor * AmbientIntensity + specular);
}

technique BumpMapped
{
    pass Pass1
    {
        VertexShader = compile vs_2_0 VertexShaderFunction();
        PixelShader = compile ps_2_0 PixelShaderFunction();
    }
}

Tangent and Binormal Vectors in Your Model

To do bump mapping, we need to have tangent and binormal vectors for each vertex of your 3D model. Some 3D modeling programs will put this into your file (sometimes it is an option when you export a model), but many others don’t. If your 3D model file has tangent and binormal information in it already, then you’re ready to go on to the next section and finish making changes to your 3D model.

In the common event that your model does not have those, we need to tell XNA to generate them. (By the way, if you generate them, and your model already had them, then XNA will overwrite the existing ones.)

If you need to do this step, and you skip it, you will see an error that says “The current vertex declaration does not include all the elements required by the current vertex shader. Tangent0 is missing.” when you try to draw your model with the shader we’ve written.

Telling XNA to automatically generate this information is relatively simple:

  1. Select the model that you are going to draw in the Project Explorer
  2. Right-click and choose Properties. (Or if the Properties window is already open, you can just go to it.)
  3. In the Properties window, go to Content Processor and open up that group by clicking on the little arrow on the left side.
  4. Find the property that says Generate Tangent Frames and change it to say true.

Doing this makes it so your model will have the tangent and binormal vectors it needs to draw inside of your bump map shader.

Changes to your XNA Game

We will now go back to our XNA game and look at what we need to do there. First, if you have changed the name of your shader, go back to the LoadContent method and make sure you are trying to load in the correct effect file.

We will also need to load in the normal map, and so we will need a place to store it. I have added the following line as an instance variable to my main game class:

private Texture2D normalMap;

Then in the LoadContent method, I have added the following, to load in the normal map:

normalMap = Content.Load<Texture2D>("Textures/HelicopterNormalMap");

By the way, I have added the height and normal maps to the Helicopter model in the 3D Model Library , which you can add to your project. Notice, though, that in the code above, the normal map is located in the Textures directory.

The only thing left to do is set the correct texture for the normal map when we go to draw. So down in the DrawModelWithEffect() method, in the same place where we set all of the other properties, add the following line of code to set the right normal map:

effect.Parameters["NormalMap"].SetValue(normalMap);

You should now be able to run your game and see your bump mapping in action!