XNA Meeting point

Multi language XNA tutorials meeting point

Accueil
English
Français
Español
Search
Archive (EN)
Storage
Video & Transparency
Physics : tutorial 1
Shaders : tutorial 1
Shaders : tutorial 2
Shaders : tutorial 3
Shaders : tutorial 4
Shaders : tutorial 5
Shaders : tutorial 6
Shaders : tutorial 7
Shaders : tutorial 8
Shaders : tutorial 9
Shaders : tutorial 10
Shaders : tutorial 11
Shaders : tutorial 12
Shaders : tutorial 13
Shaders : tutorial 14
Shaders : tutorial 15
Shaders : tutorial 16
Shaders : tutorial 17
Shaders : tutorial 18
Shaders : tutorial 19
Shaders : tutorial 20
Shaders : tutorial 21
Shaders : tutorial 22
Shaders : tutorial 23
Shaders : tutorial 24
C# : Tutorial 1
Planets : Part I
Planets : Part II
Planets : Part III
Planets : Part IV
Planets : Part V
Planets : Part VI
Planets : Part VII
Planets : Part VIII
Planets : Part IX
Planets : Part X
Star backdrop
HDR
Publishing: part 1
Publishing: part 2
Publishing: part 3
Publishing: part 4
Publishing: part 5
Archive (FR)
Archivo (ES)
Specular light
by Petri Wilhelmsen 

 

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".
 
Exercises
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.