Specular light
Adapted to XNA 4.0 by A. Nadif. To see the original article for XNA 3.1, follow this link.
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 shader programming, vector math and matrix math. Also, the project is for XNA 4.0 and Visual Studio 2010..
Before we start:
In this tutorial, you will need some basic knowledge of shader programming, vector math and matrix math. Also, the project is for XNA 4.0 and Visual Studio 2010..
Specular light
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:
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
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 also 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.
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:
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:
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 pixel shader. We start with normalizing the Normal, LightDir and ViewDir to make calculations a bit simpler.
The pixel shader will return a float4, that represents the finished color, I, of the current pixel, based on the formula for specular lighting 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 reflection vector for L by N, and using this vector to compute the specular light.
So, we start with computing the reflection vector of L by N:
R = 2 * (N.L) * N – L
As we can see, we have already computed the Dotproduct N.L when computing the diffuse light. Lets use this and write the following code:
The pixel shader will return a float4, that represents the finished color, I, of the current pixel, based on the formula for specular lighting 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 reflection vector for L by N, and using this vector to compute the specular light.
So, we start with computing the reflection vector of L by N:
R = 2 * (N.L) * N – L
As we can see, 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);
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 dot-product of the reflection vector 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.
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 pixel color:
return vAmbient + vDiffuseColor * Diff + vSpecularColor * Specular;
This formula should no longer be a surprise 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 pixel shader for this tutorial could look like this:
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 pixel shader 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 of course, 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_2_0 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_2_0 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.
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're 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".
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 intensiveness 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.
Download Sample
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 intensiveness 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.
Download Sample