Creating a Bump Map Shader - Part 2/2

Parts > 1 | 2

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

The Bump Map Shader

Once again, we are going to add on to 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, like 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 your 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!

What's Next?

This has been our first tutorial where we did something that can't be done with BasicEffect. The rest of the tutorials in the HLSL tutorial set will continue to cover more advanced things that you can do with HLSL, including environment mapping (which produces sort of a glassy/reflective look) and eventually, toon shading.

Parts > 1 | 2


Troubleshooting.png Having problems with this tutorial? Try the troubleshooting page!