SHADERS - Diffuse Lighting
What should I be familiar with before I go through this tutorial?
- The content discussed in the introduction and ambient shader tutorials.
- A simple knowledge of vectors, matrices and dot products (click on the links for Wiki articles) will REALLY help you understand some of the light calculations going on. although I'll do my best to explain those topics in this tutorial.
What is Diffuse Lighting?
Diffuse lighting can best be described as directional light. There are three types of diffuse lighting: directional, point light and spotlight. I have a picture of each below in respective order. Directional light comes down at the same angle no matter where you are standing; the light has no position, just direction. Point light is like a light bulb, it all emanates from a single point equally in all directions. A spot light is like an oriented floodlight or a flashlight. Diffuse light also creates shadows, but that's a much more advanced topic we won't dive in to until later.
The Diffuse Shader
Once again, here's the entire diffuse shader for you to look at. You can probably notice it has all the same elements of the ambient shader, along with a few extra parts.
// Matrices
float4x4 World;
float4x4 View;
float4x4 Projection;
// Ambient Variables
float4 AmbientColor;
float AmbientIntensity;
// Diffuse Variables
float4 DiffuseColor;
float DiffuseIntensity;
float3 DiffuseLightDirection;
struct VertexShaderInput
{
float4 Position : POSITION0;
float3 Normal : NORMAL0;
};
struct VertexShaderOutput
{
float4 Position : POSITION0;
float3 Normal : TEXCOORD1;
};
VertexShaderOutput VertexShader( VertexShaderInput input )
{
VertexShaderOutput output;
// Transform our position by the matricies
float4 worldPosition = mul( input.Position, World );
float4 viewPosition = mul( worldPosition, View );
output.Position = mul( viewPosition, Projection );
// Transform the normal from model space to world space
output.Normal = normalize( mul( input.Normal, World ) );
return output;
}
float4 PixelShader( VertexShaderOutput input ) : COLOR0
{
// Determine the diffuse component by finding the angle between the light and the normal.
// The smaller the angle between the normal and the light direction, the closer the dot
// product will be to 1, and the brighter the pixel will be.
float4 diffuse = dot( DiffuseLightDirection, input.Normal ) * DiffuseIntensity * DiffuseColor;
// Calculate our ambient component
float4 ambient = AmbientIntensity * AmbientColor;
// Return the total light component as a combination of the diffuse and ambient.
// Saturate it to keep the color between 0 and 1.
return saturate( diffuse + ambient );
}
technique Diffuse
{
pass Pass0
{
VertexShader = compile vs_1_1 VertexShader();
// We upgraded to pixel shader 2.0 so we can used more advanced commands
PixelShader = compile ps_2_0 PixelShader();
}
}
So what's the same?
- Variables: All three matrices (World, View, Projection), AmbientColor and AmbientIntensity are back.
- Structures: Position makes an appearance once again.
- Vertex Shader: The vertices are still transformed using the World, View and Projection matrices.
- Pixel Shader: The ambient lighting component is still calculated.
Now, let's take a look at what's new.
- We have three new variables for our diffuse light: DiffuseColor, DiffuseIntensity, DiffuseLightDirection. Direction is the only new concept here.
- A float3 called Normal has been added to both the input and output structures for the vertex shader.
- In our vertex shader, we do a transformation on the normal from VertexShaderInput and pass that on to the pixel shader.
- In our pixel shader, we calculate the diffuse component of the light.
- We changed the pixel shader version from 1_1 to 2_0 to allow us to do more advanced features in the pixel shader (i.e. normalize a vector).
Let's take a look at each of those additions in detail. First we need to take a little side step to talk about normals...
Just a Normal Kind of Light...
When we did ambient light, it didn't matter where the light was coming from or how the light hit a certain face. We just assumed each face was hit by the same amount of light, giving the effect of everything being the same color. With diffuse lighting, however, we now have a light coming in at a specific direction, so each face isn't going to get hit by the same amount of light. Imagine placing a small object by your desk lamp (and if you don't have one of those, imagine that too). You'd notice right away that the side facing the light gets more light than the side facing away, resulting in a brighter color. This is because the
normal of the face aimed towards the light is pointed more directly at the light source. A normal is a pretty simple concept. Here's a 3D model with the normals shown as red rays coming out of the faces.

Each of those rays is
orthogonal to the face it comes out of (fancy term for perpendicular.) Our model uses normals like these and compares it with the light direction to see how much light the face should get. If a normal is pointing either perpendicular or completely away from the light source, there's not a chance even a ray of light will directly hit
the face. We'll get back to normals later. All you need to know is normals are used for calculating how much light a face should get. Let's get back into our shader code now.
Variable Declaration
// Matrices
float4x4 World;
float4x4 View;
float4x4 Projection;
// Ambient Variables
float4 AmbientColor;
float AmbientIntensity;
// Diffuse Variables
float4 DiffuseColor;
float DiffuseIntensity;
float3 DiffuseLightDirection;
The matrices and ambient variables we covered in the last tutorial haven't changed at all, so I won't go over those. However, we've added three new variables for our diffuse lighting. The first two-
DiffuseColor and
DiffuseIntensity- are analogs to
AmbientColor and
AmbientIntensity. They control the color and brightness our diffuse light, respectively. Our next variable, however, is brand spankin' new:
DiffuseLightDirection. From the name you can probably construe it defines the direction our light will shine down at. And that's exactly what it is. There's one peculiarity you should know about defining light direction though:
the light shines down in the opposite direction you define it. So if you want your light to shine straight down on your object, naturally you would think to assign float3(0, -1, 0) to your
DiffuseLightDirection. But nope, that will cause the light to shine straight up.
The Vertex Shader Input and Output Structures
struct VertexShaderInput // Input for the vertex shader
{
float4 Position : POSITION0;
float3 Normal : NORMAL0;
};
struct VertexShaderOutput // Output for the vertex shader
{
float4 Position : POSITION0;
float3 Normal : TEXCOORD0;
};
Since we now know that we need normals from our model for our shader, I went ahead and added a
float3 (equivalent of a Vector3 in XNA) to our
VertexShaderInput structure to represent the normal for the vertex. One problem you probably noticed is that the normals I described above were for
surfaces not
vertices. Well this isn't a problem at all! To find the vertex normal, you simply average the normals of all the faces that that use that vertex. I described the actual code below when I get into the XNA part, so don't worry. The normal parameter was also added to the
VertexShaderOutput, but we're going to be doing some work on it in the vertex shader so it isn't the same as the normal we pass in through
VertexShaderInput. We'll look at the vertex shader next to see how we're actually modifying the normal.
Vertex Shader
VertexShaderOutput VertexShader( VertexShaderInput input )
{
// Initialize our vertex shader output
VertexShaderOutput output;
// Transform our position by the matricies
float4 worldPosition = mul( input.Position, World );
float4 viewPosition = mul( worldPosition, View );
output.Position = mul( viewPosition, Projection );
// Transform the normal from model space to world space
output.Normal = normalize( mul( input.Normal, World ) );
return output;
}
You'll notice there's a lot of familiar code here already. The transformations on the vertex position are exactly the same as they were in the ambient shader. The only new line we added takes the input normal, transforms it by the World matrix and normalizes the result. Let's break that down... Inside the parentheses, we take the vertex normal we passed in and multiply it with the World matrix. We do this for the same reason we multiply our position with the World matrix: our normal is defined in model space (relative to the model origin) and we need to get it into world space, since that's where our light direction is defined. Our model could be rotated and translated and all sorts of stuff- which is all taken care of in the World matrix- so we need to apply those transformations to the normal too. After we get our normal in world space, we normalize it. Don't confuse "normal" with "normalize" either. A "normal" defines which way a face or vertex is pointing. "Normalizing" a vector makes it unit length, or have a length of 1. Unit vectors are used to specify direction without magnitude. Two normal vectors pointing in the same direction with different magnitudes would somehow mean that one face is pointing in a direction with more force than the other, which really makes no sense at all. After calculating the output normal, we now have everything we need to calculate diffuse light in our pixel shader.
Pixel Shader
float4 PixelShader( VertexShaderOutput input ) : COLOR0
{
// Normalize our light direction
float3 normLightDirection = normalize( DiffuseLightDirection );
// Determine the diffuse component by finding the angle between the light and the normal.
// The smaller the angle between the normal and the light direction, the closer the dot
// product will be to 1, and the brighter the pixel will be.
float4 diffuse = dot( normLightDirection, input.Normal ) * DiffuseIntensity * DiffuseColor;
// Calculate our ambient component like we did with the ambient shader.
float4 ambient = AmbientIntensity * AmbientColor;
// Return the total light component as a combination of the diffuse and ambient.
// Saturate it to keep the color within 1.
return saturate( diffuse + ambient );
}
We've finally made it to the last step of the shader! Once again, we've copied over a lot of stuff from the ambient lighting pixel shader. In fact, we've copied all of it. We've also added two things: normalizing our light direction at the pixel-shader-level forThe only thing we've added is the calculation of our diffuse component. Since we want per-pixel diffuse lighting, we've put the calculation for the diffuse component in the pixel shader. To calculate how much light the pixel gets from our diffuse light, we take the dot product of the diffuse light direction with the transformed vertex normal and multiply it with
DiffuseIntensity and
DiffuseColor. Wow, that's a mouthful! First, you might be wondering what a dot product is. Read this little section first. If you already know what a dot product is, feel free to skip it.
A BRIEF DIGRESSION ON THE DOT PRODUCT
The dot product is something we haven't come across yet. What it does is compare the angle between two vectors and returns a float. If the vectors are both unit length, you'll get a result between -1 and 1. If they aren't, you'll get a wildly varying value that you really can't do anything with. A dot product of 20 could mean a billion things. Now you know why we normalized our light direction and vertex normal.
If Vector1 and Vector2...
- are pointing in the exact same direction then dot( V1, V2 ) = 1
- are perpendicular to each other then dot( V1, V2 ) = 0
- are pointing in exact opposite directions then dot( V1, V2 ) = -1
Those are just the exact cases. Everything in between will vary depending on where it's pointing. The closer they are to pointing in the same direction, the closer to 1. The opposite holds with pointing in the opposite direction. If you're confused, check out the Links section on the navigation bar for more information on dot products.
Now that we have dot products out of the way, we can finally apply it to our diffuse calculation. When our normal is pointing in the opposite direction of the
DiffuseLightDirection (remember that our light direction is defined opposite of what you think it should be), our dot product will be 1. Multiply that with
DiffuseColor and
DiffuseIntensity and we'll end up with our maximum possible value for the diffuse component. If the normal and light direction are perpendicular, the dot product will be zero and we'll get no light. This makes sense since a side perpendicular to a light source isn't going to get hit by light rays.
Phew! Now that we finally have our diffuse and ambient components, all we do is add them together. Before we return the sum however, we want to put apply the saturate() function. This caps any component of our pixel color between 0 and 1. If the dot product was negative, we'd have negative color values which would throw an error.
Time to get XNA up to speed with our diffuse shader...
Adding Normals to Our Models
The biggest change we need on the XNA side is adding our normal data to ou vertices. For the ambient shader I used the
VertexPositionColor structure for storing my vertices. Unfortunately, this doesn't have anywhere to store normal information. So I've upgraded the vertices on my box to
VertexPositionNormalTexture, leaving the texture coordinates blank for now. Remember that shaders don't care if there's extra information there; they just take what they need and move on. I'll be covering a texture shader within the next few tutorials so let's go ahead and use that structure. Now, for calculating the normals. This can vary greatly depending on what you have: a list of verticies, a list of indexed vertices, a model. I won't cover the first case and normally any model you find will already have the normals in it, so I'm going to give a brief overview of indexed vertices defined in the
TriangleList format. I apologize if you defined your verticies in a different fashion, but don't get too upset. A quick check over at the
XNA Forums or a Google search should help you out with that. Here's a step-by-step guide on how to calculate normals for indexed vertices which use the
TriangleList method.
- Create you vertices and indices as you normally would.
- Loop through your indices three at a time. Since your indicies are defined using a TriangleList, you know each three consecutive indices represent a triangle in your model.
- Get the three vertices from I1, I2 and I3 and name them V1, V2 and V3.
- Create two Vector3's: VectorA = V2.Position - V1.Position and VectorB = V3.Position - V1.Position.
- Take the cross product of VectorB with VectorA (order matters) and normalize that result. This will give you your face normal.
- Now go back and add that face normal to V1.Normal, V2.Normal and V3.Normal.
- Repeat for every three indices and voila, each vertex now has it's vertex normal.
By adding the face normal to each vertex that made up the face, we've effectively averaged together all the face normals for each vertex, which is the definition of a vertex normal.
Once you get that implemented, our models should now have all the necessary information to draw them with diffuse light.
Setting Our Shader Variables
// Set the parameters of the shader
diffuseEffect.Parameters[ "World" ].SetValue( Matrix.CreateRotationY( (float)gameTime.TotalGameTime.TotalSeconds ) );
diffuseEffect.Parameters[ "View" ].SetValue( viewMatrix );
diffuseEffect.Parameters[ "Projection" ].SetValue( projectionMatrix );
diffuseEffect.Parameters[ "AmbientColor" ].SetValue( new Vector4( 1, 1, 1, 1 ) );
diffuseEffect.Parameters[ "AmbientIntensity" ].SetValue( .3f );
diffuseEffect.Parameters[ "DiffuseColor" ].SetValue( new Vector4( 0, 1, 0, 1 ) );
diffuseEffect.Parameters[ "DiffuseIntensity" ].SetValue( .8f );
diffuseEffect.Parameters[ "DiffuseLightDirection" ].SetValue( new Vector3( .7f, 1f, -.8f ) );
Nothing has really changed here. We've just tacked on three more SetValue()'s for our three diffuse variables. Set your
DiffuseColor, DiffuseIntensity and
DiffuseLightDirection (we normalize it here to avoid the calculation in the shader. And remember, it's opposite of the direction the light shines at!) and you're almost good to go!
Drawing Vertices With Our Diffuse Effect
Once again, not much has changed here. Make sure you change your VertexDeclaration and DrawUserPrimitives call to
VertexPositionNormalTexture so the graphics card knows we've added normal data. If you skip that, you'll probably end up with nothing but black blob. Everything should be ready to go now... so go ahead, hit F5 and watch that cube spin in it's shaded, rainbowed glory! You should get something similar to this video:
Our Good Ol' Reliable Cube, Now Colorful and Shaded
Conclusion
So there we go, another shader down. By now I hope you're starting to get a feel for how these shaders work and what goes into them. You should start seeing a pattern as you go along: vertex shaders transforming data, pixel shaders using the transformed data to calculate colors. Every vertex shader and pixel shader you come across is going to work like that for the most part. I'll be covering specular light in the next tutorial, which will give you a nice shiny effect on your objects. I might have to ditch the cube for a sphere... I don't think he'll take kindly to that news... Anyway, best of luck to you in your programming! If you happen to catch any errors or need any clarifications, be sure to send me an email at
dan@digitseven.com, I'd be more than happy to help. Make sure to
sign up for the newsletter to stay updated with my latest tutorials and code uploads!
Download the XNA Project: Diffuse Lighting (Right-click and Save As)
Last Update: 8/19/10