Introduction
In a previous tutorial we created a very simple shader that performed ambient lighting. The shader was probably helpful, because it showed us the basics of creating a shader with HLSL. But beyond that, it probably wasn't too useful. Ambient lighting is important, but by itself, it is pretty useless. In this tutorial, we will cover the most significant component of lighting: diffuse lighting.
Remember that diffuse lighting is the light that reflects off of an object in all directions (it diffuses). It is what gives most objects their color. In this tutorial, we will modify our ambient light shader to include diffuse lighting as well.
The Diffuse Lighting Shader
We will work from where we left off with the ambient light shader. If you are going through these tutorials in order, feel free to just continue on with your shader from the ambient lighting tutorial, or make a copy. For this tutorial, I am making a copy, so that I can switch back to the ambient only shader if I want to later. I have renamed my file to "Diffuse.fx". If you change the name of your file here, be sure to change the line of code that indicates which file to open up (Content.Load<Effect>("…");).If you are just starting with this tutorial, it might be worth going back through the other HLSL tutorials, but if you don't want to, you can start with the HLSL code below:
float4x4 World;
float4x4 View;
float4x4 Projection;
float4 AmbientColor = float4(1, 1, 1, 1);
float AmbientIntensity = 0.1;
struct VertexShaderInput
{
float4 Position : POSITION0;
};
struct VertexShaderOutput
{
float4 Position : POSITION0;
};
VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
VertexShaderOutput output;
float4 worldPosition = mul(input.Position, World);
float4 viewPosition = mul(worldPosition, View);
output.Position = mul(viewPosition, Projection);
return output;
}
float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
return AmbientColor * AmbientIntensity;
}
technique Ambient
{
pass Pass1
{
VertexShader = compile vs_2_0 VertexShaderFunction();
PixelShader = compile ps_2_0 PixelShaderFunction();
}
}
We will make changes to each of the major components of our effect file in order. It might be worth running your program right now, just to make sure that it is properly displaying your shader, which should, at this point, only do ambient lighting. If you get lost during this tutorial, I have placed the completed code for the diffuse lighting shader near the bottom of this tutorial.
The basic idea of diffuse lighting is that the surface of the object is lit up with the light's color with a brightness that depends on how the surface is oriented to the light source. If the surface is facing directly towards the light, then a lot of light will be reflected and the surface will be bright. If the surface is not facing the light directly, less light will be reflected and the surface of the object will be dimmer. On the back of the object, the surface is facing away from the light, and so no light will be reflected.
To start with, we will perform per-vertex lighting, which is a simpler and faster approach but doesn't produce as nice of results as per-pixel lighting. This means we will do most of our calculations in the vertex shader, rather than the pixel shader. For each vertex, we will calculate the diffuse light color for that vertex and pass it on to the pixel shader, which will then just add that color value in with the color from the ambient light.
Adding Additional Variables
In order to actually perform the diffuse lighting calculations, we will need to add a few more variables, which determine the color and direction of the diffuse lighting. Here, we are just going to do a simple directional light. So at the top, by where we put in the code for our ambient light variables, add the following four lines of code:
float4x4 WorldInverseTranspose;
float3 DiffuseLightDirection = float3(1, 0, 0);
float4 DiffuseColor = float4(1, 1, 1, 1);
float DiffuseIntensity = 1.0;
The first variable is a matrix that stores the transpose of the inverse of the world matrix. With our ambient light shader, we had to transform the location of each of the vertices in our object. With this diffuse shader, we will need to also transform the normals of the vertex, so that we can do lighting calculations. This matrix will be what we use to transform the normal in the correct way so that the lighting calculations work. At the end of this tutorial, we will discuss how to calculate this in your XNA game, since we will need to set it there, at the same time as the world, view, and projection matrices.
The second variable indicates the direction that the light is coming from, which in this case is the positive x-axis. The diffuse light color is, by default, white, and the default intensity is 1.0 (which is as bright as it gets). Feel free to play around with these values later on.
Updating the Data Structures
Since we are going to be worrying about lighting, we will need to store a little more information in our data structures. In particular, we will need to store the vertex's normal until we get into the vertex shader, at which point, we will need to store the diffuse light color that we calculated for the vertex. (Remember that a normal is simply a vector that points directly away from the surface at any given point, which in our case is the vertex). So we want to change our VertexShaderInput structure from:
struct VertexShaderInput
{
float4 Position : POSITION0;
};
to:
struct VertexShaderInput
{
float4 Position : POSITION0;
float4 Normal : NORMAL0;
};
This adds one variable to the structure, which is the normal for the vertex. This will get filled in by XNA from the Model information. Similarly, we need to add a color value to the output of our vertex shader. To do this, change the VertexShaderOutput structure from:
struct VertexShaderOutput
{
float4 Position : POSITION0;
};
to:
struct VertexShaderOutput
{
float4 Position : POSITION0;
float4 Color : COLOR0;
};
Updating the Vertex Shader
This is the most complicated step of our diffuse lighting shader. In this step, we will perform the actual diffuse lighting calculations. Initially, our shader looks like this:
VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
VertexShaderOutput output;
float4 worldPosition = mul(input.Position, World);
float4 viewPosition = mul(worldPosition, View);
output.Position = mul(viewPosition, Projection);
return output;
}
This calculates the position of the vertex in the end but does not yet take into account the diffuse lighting. To do this, we will change our vertex shader to look like this:
VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
VertexShaderOutput output;
float4 worldPosition = mul(input.Position, World);
float4 viewPosition = mul(worldPosition, View);
output.Position = mul(viewPosition, Projection);
float4 normal = mul(input.Normal, WorldInverseTranspose);
float lightIntensity = dot(normal, DiffuseLightDirection);
output.Color = saturate(DiffuseColor * DiffuseIntensity * lightIntensity);
return output;
}
Notice that we've added three new lines that ultimately set the output color. The first line, float4 normal = mul(input.Normal, WorldInverseTranspose);, takes the normal and transforms it in such a way that the normal is now relative to where the object is in the world. We will need to do a little bit of math in our XNA game to calculate this matrix, which we will do once we have completed the shader code. It simply calculates where the normal is, once the world transformation has been performed, so the normal is now relative to the world, rather than the object like we want it to be.
The second line, float lightIntensity = dot(normal, DiffuseLightDirection);, essentially calculates the angle between the surface's normal vector and the light, which is used to measure the intensity of the light. The dot() function performs the dot product of two vectors, which can effectively be used as a measurement of the angle between the two vectors. If the surface is exactly facing the light source, this value will be 1. If the surface is sideways, compared to the light, this value will be 0. If the surface is facing away from the light, it will be a negative value. In the third line, we calculate the actual output color that was determined by the diffuse lighting. The color is the default diffuse color, specified by DiffuseColor, multiplied by the default diffuse intensity, specified by DiffuseIntensity, multiplied by the intensity of the light at that vertex, specified by lightIntensity. Notice that we call another intrinsic (built-in) HLSL function, saturate(). This function will take a color and ensure that it has values between 0 and 1 for each of the components.
Updating the Pixel Shader
It will be pretty easy to update the pixel shader since most of our work was done in the vertex shader. Our pixel shader, originally, looked like this:
float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
return AmbientColor * AmbientIntensity;
}
We want to change this to include the color for the diffuse light, which can be done by changing this function to the following:
float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
return saturate(input.Color + AmbientColor * AmbientIntensity);
}
Notice that all we do is add in the input color that was calculated by the vertex shader, and then call the saturate() function, to ensure that we don't try to add too much light. (You can't get brighter than pure white!)
Updating the Technique
There actually isn't anything that we need to do here. However, if you copied the original file as I did, or you are still working from your original file, you might want to change the name of the technique from "Ambient" to "Diffuse". In actuality, we could have two techniques here, one called "Ambient" that just does ambient lighting, and one called "Diffuse" that does diffuse lighting as well. We could have separate functions for the vertex and pixel shaders of these two techniques, and just combine the two into one file. In an XNA game, you can choose which technique you want to use pretty easily.
Some Changes to the XNA Code
Remember, we need to set the WorldInverseTranspose variable in our shader so that the diffuse lighting works out correctly. Go back to your XNA code in the Draw() method where we set all of the other shader parameters. (Where we had all of the lines that say Effect.Parameters["parameterName"].SetValue(…);.) Add the following two lines of code which calculate the world inverse transpose matrix, and give it to the shader.
Matrix worldInverseTransposeMatrix = Matrix.Transpose(Matrix.Invert(mesh.ParentBone.Transform * world)); effect.Parameters["WorldInverseTranspose"].SetValue(worldInverseTransposeMatrix);
The matrix is calculated by taking the world matrix that we are using, (in this case, it is mesh.ParentBone.Transform * world, because we are taking into account the transformation for the mesh itself, as the world transform for the entire object), inverting it using the Matrix.Invert() method, and then transposing it with the Matrix.Transpose() method. The second line just sets the value as we have done with all of our other parameters.
At this point, you should be ready to run your game with your new effect. Remember, you might need to go back into your game and tell it to use this new shader, instead of the ambient shader. The results should look something like the image below:
If you are having trouble with the XNA code, you might want to go back and verify that everything is working as it should in the previous tutorial. If you are having trouble with your HLSL code, my complete code for this shader is below:
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;
struct VertexShaderInput
{
float4 Position : POSITION0;
float4 Normal : NORMAL0;
};
struct VertexShaderOutput
{
float4 Position : POSITION0;
float4 Color : COLOR0;
};
VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
VertexShaderOutput output;
float4 worldPosition = mul(input.Position, World);
float4 viewPosition = mul(worldPosition, View);
output.Position = mul(viewPosition, Projection);
float4 normal = mul(input.Normal, WorldInverseTranspose);
float lightIntensity = dot(normal, DiffuseLightDirection);
output.Color = saturate(DiffuseColor * DiffuseIntensity * lightIntensity);
return output;
}
float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
return saturate(input.Color + AmbientColor * AmbientIntensity);
}
technique Ambient
{
pass Pass1
{
VertexShader = compile vs_2_0 VertexShaderFunction();
PixelShader = compile ps_2_0 PixelShaderFunction();
}
}
Additionally, my main game class looks code can be found below as well.
What's Next?
We have now created a more advanced shader that actually shades the object we are looking at. This will provide us with some good groundwork for doing all sorts of other more advanced shaders. The next thing that we will cover is specular lighting, and then continue on to a slightly more advanced topic: texturing.
Having problems with this tutorial? Try the troubleshooting page! |