Hi, and welcome to Tutorial 2 of my XNA 4.0 Shader Programming tutorial. Today we are going to work on Tutorial 1 in order to make the lighting equation a bit more interesting, by implementing Diffuse lighting.
Diffuse light isn’t very different from ambient light implementation wise, but it got one very important property, a direction to the light. As we saw, using only ambient light can make a 3D scene look 2D, but adding a diffuse will increase the realism of the scene and add a nice 3D look to it. Figure 1 shows the same zombie we rendered in the first example, but with diffuse white light, and a dark gray ambient light
Figure 1 – Diffuse light
As mentioned above, the ambient light got the following equation:
I = Aintensity * Acolor (1.1)
Diffuse light builds on this equation, adding a directional light to the equation:
I = Aintensity x Acolor + Dintensity x Dcolor x N.L (1.2)
From this equation, you can see that we still use the Ambient light, with an addition of two more variables for describing the color and intensity of the Diffuse light, and two vectors N and L for describing the light direction L and the surface normal N.
We can think of diffuse lighting as a value that indicates how much a surface reflects light. The light that is reflected will be stronger and more visible when the angle between the Normal N and the light direction L gets smaller.
If L is parallel with N, the light will be most reflected, and if L is parallel with the surface, the light will be reflected with the minimal amount.
To compute the angle between L and N, we can use the Dot-product, or the scalar product. This rule is used to find the angle between two given vectors and can be defined as the following:
N.L = |N| x |L| x cos(a)
where |N| is the length of vector N, |L| is the length of vector L and cos(a) is the angle between the two vectors.
Let’s try to convert this in to HSLS! In order to do this, we start by defining three new global variables:
float3 DiffuseDirection; float4 DiffuseColor; float DiffuseIntensity;
These will contain the direction of the light ( L ), the color of the diffuse light and the intensity of the light.
The Vertex Shader input is the same as before, containing only the position.
struct VertexShaderInput { float4 Position : POSITION0; };
The VertexShaderOutput will contain one additional member, the Normal. The normal will be calculated in the Vertex Shader function, based on the objects Normal at the given vertex.
struct VertexShaderOutput { float4 Position : POSITION0; float3 Normal : TEXCOORD0; };
But where do we get this Normal from? We need to pass in the Normal as input to the Vertex Shader! So.. Why didn’t we add it to the VertexShaderInput structure? Because the NORMAL semantic isn’t supported when defining structures in XNA. So how do we get the normals? We pass in the Normal as a parameter to the VertexShaderFunction in addition to the VertexShaderInput parameter. Sounds complicated? Some code might clear it up for you
VertexShaderOutput VertexShaderFunction(VertexShaderInput input,float3 Normal : NORMAL)
Here you can see that we pass in the VertexShaderInput structure, as well as the Normal. When passing in the parameter directly, the NORMAL semantic will work
The Vertex Shader function is very similar to the one in the ambient light tutorial, but we also transform the Normal into worldspace, as this is the space we would like to calculate the normal in, just like the position:
VertexShaderOutput VertexShaderFunction(VertexShaderInput input,float3 Normal : NORMAL) { VertexShaderOutput output; float4 worldPosition = mul(input.Position, World); float4 viewPosition = mul(worldPosition, View); output.Position = mul(viewPosition, Projection); float3 normal = normalize(mul(Normal, World)); output.Normal = normal; return output; }
So what’s really happening here? We take the input Normal and multiply it with the World matrix, and then we normalize it and store it in a variable normal. Next, we must remember to set the Normal variable in output, to make sure it’s passed to the Pixel Shader by setting output.Normal = normal.
And that’s really it for the Vertex Shader. But it’s still in the Pixel Shader most of the “magic” happens.
In order to pass the Normal to the pixel shader, we added a float3 Normal : TEXCOORD0 variable to the VertexShaderOutput structure. We needed to store the Normal in a register on the GPU, but since NORMAL doesn’t exist, we just used another one that is not in use yet; the TEXCOORD0. In other words, TEXCOORDn (n = any number up to the supported amount of registers on your GPU, anything between 0 and 7 is usually safe to use) can be used for any values, and as we don’t yet use any texture coordinates, we can easily just use these registers as a storage for our Normal-vector.
Moving on. The pixel shaders job is to calculate the final light equation. It will need to implement 1.2:
I = Aintensity x Acolor + Dintensity x Dcolor x N.L
We already got Aintensity and Acolor. Also, the Dintensity and Dcolor are just variables passed in to the shader, just as the ambient light.
But what about the last part? N.L? This means that we take the dot-product between N and L (remember, the angel of the incoming light, “compared” with the Normal).
Let’s try! First, we store our Normal vector, and convert it to a float4, as this is the variable-type we use in the other calculations. When we use the build in HLSL function dot(x,y) to calculate the dot-product between L and N. Also, we negate L as the value L is containing the direction the light comes FROM, and not where the light points TO.
Now, all that is left is to put this into eq. 1.2 like the code shows below:
float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0 { float4 norm = float4(input.Normal, 1.0); float4 diffuse = saturate(dot(-DiffuseDirection,norm)); return AmbientColor*AmbientIntensity+DiffuseIntensity*DiffuseColor*diffuse; }
Not very hard right?
Now if you apply this shader to a scene, the output will be something like this:
There is nothing new when it comes to how this shader is used in XNA. Take a look at the source so see how it all fits together and play with the values.
The complete shader listing can be seen below:
// XNA 4.0 Shader Programming #2 - Diffuse light // Matrix float4x4 World; float4x4 View; float4x4 Projection; // Light related float4 AmbientColor; float AmbientIntensity; float3 DiffuseDirection; float4 DiffuseColor; float DiffuseIntensity; // The input for the VertexShader struct VertexShaderInput { float4 Position : POSITION0; }; // The output from the vertex shader, used for later processing struct VertexShaderOutput { float4 Position : POSITION0; float3 Normal : TEXCOORD0; }; // The VertexShader. VertexShaderOutput VertexShaderFunction(VertexShaderInput input,float3 Normal : NORMAL) { VertexShaderOutput output; float4 worldPosition = mul(input.Position, World); float4 viewPosition = mul(worldPosition, View); output.Position = mul(viewPosition, Projection); float3 normal = normalize(mul(Normal, World)); output.Normal = normal; return output; } // The Pixel Shader float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0 { float4 norm = float4(input.Normal, 1.0); float4 diffuse = saturate(dot(-DiffuseDirection,norm)); return AmbientColor*AmbientIntensity+DiffuseIntensity*DiffuseColor*diffuse; } // Our Techinique technique Technique1 { pass Pass1 { VertexShader = compile vs_2_0 VertexShaderFunction(); PixelShader = compile ps_2_0 PixelShaderFunction(); } }
Great tutorial!
Can’t wait to the more advanced stuff 😀
Pingback: Windows Client Developer Roundup 086 for 1/11/2012 - Pete Brown's 10rem.net
Thanks for this tutorial. It works fine, but if I move my object, it becomes lighter and lighter to white (or black if don’t inverse light direction vector). For the WorldMatrix, I give to the shader the initial world matrix (identity) multiplied by my object matrix. I thought it is a directional light, so it has no position, right ?
All the tutorials I try have the same problem. Do you have a solution to this ?
Here is my very late answer! I think it is because the normal’s are being translated.
They shouldn’t be. Try first removing the translation like this:
// Casting the world matrix to a 3×3 strips out the translation.
// This prevents translating the normals.
float3x3 normalRotationMatrix = (float3x3)World;
It’s explained well in recipe 6-5 of XNA 3.0 game programming recipes.