XNA Shader Programming – Tutorial 3, Specular light

XNA Shader Programming
Tutorial 3 – Specular light
Hi, and welcome to Tutorial 3 of my XNA Shader Programming tutorial. Today we are going to implement an other lighting algorithm called Specular Lighting. This algorithm builds on my Ambient and Diffuse lighting tutorials, so if you haven’t been trough them, now is the time. 🙂
 
Before we start:
In this tutorial, you will need some basic knowledge of shaderprogramming, vector math and matrix math. Also, the project is for XNA 2.0 and Visual Studio 2005.
 
Specular lighting
So far, we got a nice lighting model for making a good looking lighting on objects. But, what if we got a blank, polished or shiny object we want to render? Say a metal surface, plastic, glass, bottle and so on.
To simulate this, we need to implement a new vector to our lighting algorithm: The eye vector.
Whats "the eye" vector, you might think? Well, it’s a pretty easy answer to this. It’s the vector that points from our camera position to the camera target.
We already got this vector in our application code:
viewMatrix   = Matrix.CreateLookAt( new Vector3(x, y, zHeight), Vector3.Zero, Vector3.Up );
 
The position of "The eye" is located here:
Vector3(x, y, zHeight)
 
So let’s take this vector, and store it in a variable:
Vector4 vecEye = new Vector4(x, y, zHeight,0);
 
Let’s look more closely about how to use the shader after we have created it.
 
The formula for Specular Lighting is
I=Ai*Ac+Di*Dc*N.L+Si*Sc*(R.V)^n

Where
R=2*(N.L)*N-L
 
As we can see, we got the new Eye vector V, and aslo we got a reflection vector R.
 
To compute the Specular light, we need to take the dot product of R and V and use this in the power of n where n is controlling how "shiny" the object is.
 
Implementing the shader
It’s time to implement the shader.
As you can see, this object looks polished/shiny, only by using the shader we are going to implement!
Pretty cool, ey?
Lets start by declaring a few variables we will need for this shader:
float4x4 matWorldViewProj;   
float4x4 matWorld;   
float4 vecLightDir;
float4 vecEye;
float4 vDiffuseColor;
float4 vSpecularColor;
float4 vAmbient;
 
And then the output structure for our Vertex Shader. The shader will return the transformed position , Light vector, Normal vector and view vector( the Eye vector ) for a given vertex.
 
struct OUT
{
    float4 Pos  : POSITION;
    float3 L : TEXCOORD0;
    float3 N : TEXCOORD1;
    float3 V : TEXCOORD2;
};
 
Not much new in the vertex shader since last time, except for the V vector. V is calculated by subtracting the transformed position from the Eye vector.
Since V is a part of the OUT structure, and we have defined OUT Out, we can calculate V with the following code:
float4 PosWorld = mul(Pos,matWorld);
Out.L = vecEye – PosWorld
 
where vecEye is a vector passed into the shader trough a shader-parameter( The camera position ).
 
OUT VS(float4 Pos : POSITION, float3 N : NORMAL)
{
    OUT Out = (OUT)0;     
   
    Out.Pos = mul(Pos, matWorldViewProj);   
    Out.N = mul(N, matWorld);               
   
    float4 PosWorld = mul(Pos, matWorld);   
   
    Out.L = vecLightDir;
    Out.V = vecEye – PosWorld;
   
   return Out;
 
And then its time to implement the pixelshader. We start with normalizing the Normal, LightDir and ViewDir to make calculations a bit simpler.
The pixelshader will reatun a float4, that represents the finished color, I, of the current pixel, based on the formula for specular lighding described earlier.
Then, we will calculate direction of the diffuse light as we did in Tutorial 2.
 
The new thing in the Pixel Shader for Specular Lighting is to calculate and use a reflectionvector for L by N, and using this vector to compute the specular light.
So, we start with computing the reflectionvector of L by N:
R = 2 * (N.L) * N – L
As we can se, we have already computed the Dotproduct N.L when computing the diffuse light. Lets use this and write the following code:
float3 Reflect = normalize(2 * Diff * Normal – LightDir);
 
Note: We could also use the reflect function that is built in to HLSL instead, taking an incident vector and a normal vector as parameters, returning a reflection vector:
float3 ref =  reflect( L, N );
 
Now, all there is left is to compute the specular light. We know that this is computed by taking the power of the dotproduct of the reflection vecotor and the view vector, by n: (R.V)^n
You can think of n as a factor for how shiny the object will be. The more n is, the less shiny it is, so play with n to get the result you like.
 
As you might have noticed, we are using a new HLSL function pow(a,b). What this does is quite simple, it returns a^b.
float Specular = pow(saturate(dot(Reflect, ViewDir)), 15);
 
 
Phew, we are finally ready to put all this together and compute the final pixelcolor:
return vAmbient + vDiffuseColor * Diff + vSpecularColor * Specular;
 
This formula should no longer be a suprise for anyone, right?
We start by calculating the Ambient and Diffuse light, and add these together. Then we take the specular light color and multiply it with the Specular component we just calculated, and add it with the Ambient and Diffuse color.
 
The pixelshader for this tutorial could look like this:
 
float4 PS(float3 L: TEXCOORD0, float3 N : TEXCOORD1,
            float3 V : TEXCOORD2) : COLOR
{  
    float3 Normal = normalize(N);
    float3 LightDir = normalize(L);
    float3 ViewDir = normalize(V);   
   
    float Diff = saturate(dot(Normal, LightDir));
   
    // R = 2 * (N.L) * N – L
    float3 Reflect = normalize(2 * Diff * Normal – LightDir); 
    float Specular = pow(saturate(dot(Reflect, ViewDir)), 15); // R.V^n
    // I = A + Dcolor * Dintensity * N.L + Scolor * Sintensity * (R.V)n
    return vAmbient + vDiffuseColor * Diff + vSpecularColor * Specular;
}
 
And offcourse, we have to specify a technique for this shader, and compile the Vertex and Pixel shader:
technique SpecularLight
{
    pass P0
    {
        // compile shaders
        VertexShader = compile vs_1_1 VS();
        PixelShader  = compile ps_2_0 PS();
    }
}
 
The whole code for the shader( .fx ) file is:
float4x4 matWorldViewProj;   
float4x4 matWorld;   
float4 vecLightDir;
float4 vecEye;
float4 vDiffuseColor;
float4 vSpecularColor;
float4 vAmbient;
struct OUT
{
    float4 Pos  : POSITION;
    float3 L : TEXCOORD0;
    float3 N : TEXCOORD1;
    float3 V : TEXCOORD2;
};
OUT VS(float4 Pos : POSITION, float3 N : NORMAL)
{
    OUT Out = (OUT)0;     
   
    Out.Pos = mul(Pos, matWorldViewProj);   
    Out.N = mul(N, matWorld);               
   
    float4 PosWorld = mul(Pos, matWorld);   
   
    Out.L = vecLightDir;
    Out.V = vecEye – PosWorld;
   
   return Out;
}
float4 PS(float3 L: TEXCOORD0, float3 N : TEXCOORD1,
            float3 V : TEXCOORD2) : COLOR
{  
    float3 Normal = normalize(N);
    float3 LightDir = normalize(L);
    float3 ViewDir = normalize(V);   
   
    float Diff = saturate(dot(Normal, LightDir));
   
    // R = 2 * (N.L) * N – L
    float3 Reflect = normalize(2 * Diff * Normal – LightDir); 
    float Specular = pow(saturate(dot(Reflect, ViewDir)), 15); // R.V^n
    // I = A + Dcolor * Dintensity * N.L + Scolor * Sintensity * (R.V)n
    return vAmbient + vDiffuseColor * Diff + vSpecularColor * Specular;
}
technique SpecularLight
{
    pass P0
    {
        // compile shaders
        VertexShader = compile vs_1_1 VS();
        PixelShader  = compile ps_2_0 PS();
    }
}
 
Using the shader
There is almost nothing new when it comes to using the shader in an application since my last tutorial, except for setting the vecEye parameter to the shader.
We just take the position of the camera and pass it to our shader. If you are using a camera-class, there might be a function for getting the camera position. It’s really up to you how you decide to get it.
In my example, i use the same variables for setting the camera position, and creating a vector that is passed to the shader.
 
Vector4 vecEye = new Vector4(x, y, zHeight,0);
and pass it to the shader:
effect.Parameters["vecEye"].SetValue(vecEye);
 
Setting parameters in a shader from the application and how to implement the shader should not be a new topic for you if you’r at this stage, so I won’t go into further detail about this. Please refer to Tutorial 2 and Tutorial 1 about this, or send me an e-mail.
 
We also have to remember to set the technique to "SpecularLight".
 
Excersises
1. Make a new global variable in the shader that specifies the "shininess" of the object. You should be able to set this variable from the application that is using the shader.
2. In this tutorial, you don’t have so much control over the light settings( like setting Ai and Ac, Di and Dc ). Make this shader to support setting Ai, Ac, Di, Dc, Si and Sc where Si and Sc is the color and intensitivity for the specular light
 
Thanks for reading this tutorial, hope I covered it enough for you to understand what this is all about!
If you have any comments, feedback or questions, please ask me on petriw(at)gmail.com.
Next time I’m going to cover Normal mapping, and how to use textures in shaders.
 
NOTE:
You might have noticed that I have not used effect.commitChanges(); in this code. If you are rendering many objects using this shader, you should add this code in the pass.Begin() part so the changed will get affected in the current pass, and not in the next pass. This should be done if you set any shader paramteres inside the pass.
 
This entry was posted in XNA Shader Tutorial. Bookmark the permalink.

9 Responses to XNA Shader Programming – Tutorial 3, Specular light

  1. Daniel says:

    Thanks for the tutorials. Really helpful. I was lost before these. But, I found your diagram of the reflection vector of "L" misleading b/c the light is actually pointing away from the center to 1,1, 0 in your code, not into the origin as your diagram shows. Also isn\’t the correct reflection function 2*(L.N)*N – L, not 2*(N.L)*N – L. B/c L and N are both normalized I guess it doesn\’t matter here though.

  2. Byron says:

    Nice tutorial. You might point out that you can get the reflection vector with the "reflect" intrinsic in the later shader models.

  3. Petri says:

    Byron: Thats a good idea, I will add this, and I will also add a custom version of the refract function in tutorial 16. :)Dnaiel: As these are normalized it does not matter. The drawing is incorrect as I have the vector V poining in the opposite direction. In the diagram, all vectors should be poining away from the surface. Thanks for noticing 🙂

  4. Unknown says:

    In your previous tutorial you used the inverse world matrix in the vertex shader. Why do you not use it here? Is up to this part not same procdure as in the diffuse light tutorial?

  5. Unknown says:

    For the light calculation the normal vector needs to brought into the world space. You do this by transforming it with the world matrix. But is the normal vector not only affected by the rotation? E.g.: If there is a normal vector -1, 0, 0 and the vertex to which it belongs is moved by two coordinates on the x-axis. If I now would transform the normal vector with the corresponding world matrix including this translation, the normal vector is pointing in the opposite direction, wouldn´t it? Perhaps I´m wrong. What do you say?

  6. Petri says:

    I have updated tutorial two so it transforms the Normal with matWorld instead of the matWorldInverse. This was a mistake from my side 🙂

  7. That was by far one of the better posts I have discovered since the last two months. What a wonderful article!

  8. Pingback: Föreläsning 28/1 samt startprojekt inför laborationerna. - Shaderprogrammering

  9. Pingback: Föreläsningar - Shaderprogrammering

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.