XNA 4.0 Shader Programming #3–Specular light

image

Hi, and welcome to Tutorial 3 of my XNA 4.0 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. :)

Technique: 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? Diffuse light does not include any of the tiny reflections that make a smooth surface shine.

image

fig 3.1 – specular light in EVE Online (GREAT game btw.)

To simulate this shininess, we can use a lighting model named Specular highlights.
Specular highlights calculate another vector that simulates a reflection of a light source, which hits the camera, or “the eye”.

What’s “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, z), Vector3.Zero, Vector3.Up );

The position of “The eye” is the first parameter in CreateLookAt:

new Vector3(x, y, z)

So let’s take this vector, and store it in a variable:

Vector4 vecEye = new Vector4(x, y, z,0);

Note: x,y,z represents a point in 3d space.

Let’s look more closely about how to use the shader after we have created it.

The formula for Specular light is

I=Ai*Ac+Di*Dc*N.L+Si*Sc*(R.V)n (3.1)

Where

R=2*(N.L)*N-L

 

image

fig 3.2 – Vector diagram for specular light

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.

Implementing the shader

We start with the VertexShader. There is only one modification, and that is to add a View vector to the VertexShaderOutput structure, so we will need to calculate this in the VertexShaderFunction.

struct VertexShaderOutput
{
    float4 Position : POSITION0;
	float3 Normal : TEXCOORD0;
	float3 View : TEXCOORD1;
};

To calculate the View vector V (check figure 3.2), we need the position of the “eye”, in other words – the position of the camera, we will need to add a new global variable to the shader.

float3 EyePosition;

Now, in the vertex shader, we need to set the View-vector in the output structure:

output.View = normalize(float4(EyePosition,1.0) - worldPosition);

The whole VertexShaderFunction can be seen below:

// The output from the vertex shader, used for later processing
struct VertexShaderOutput
{
    float4 Position : POSITION0;
	float3 Normal : TEXCOORD0;
	float3 View : TEXCOORD1;
};

// 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;
	output.View = normalize(float4(EyePosition,1.0) - worldPosition);

    return output;
}

Now we got what we need to calculate the specular light in the pixel shader! Smile

The pixel shader will return a float4, which represents the finished color, I, of the current pixel, based on the formula for specular lighting described earlier.

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 Dot product N.L when computing the diffuse light.

float4 diffuse = saturate(dot(-LightDirection,normal));

Let’s use this and write the following code:

float4 reflect = normalize(2*diffuse*normal-float4(LightDirection,1.0));

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 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 ab.

float4 specular = pow(saturate(dot(reflect,input.View)),15);

Now we are finally ready to put all this together and compute the final pixel color:

return AmbientColor*AmbientIntensity+DiffuseIntensity*DiffuseColor*diffuse+SpecularColor*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 we created in the previous techniques.

The whole PixelShaderFunction code can be seen below.

// The Pixel Shader
float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
	float4 normal = float4(input.Normal, 1.0);
	float4 diffuse = saturate(dot(-LightDirection,normal));
	float4 reflect = normalize(2*diffuse*normal-float4(LightDirection,1.0));
	float4 specular = pow(saturate(dot(reflect,input.View)),15);

    return AmbientColor*AmbientIntensity+DiffuseIntensity*DiffuseColor*diffuse+SpecularColor*specular;
}

And the whole shader effect code:

// XNA 4.0 Shader Programming #2 - Diffuse light

// Matrix
float4x4 World;
float4x4 View;
float4x4 Projection;

// Light related
float4 AmbientColor;
float AmbientIntensity;

float3 LightDirection;
float4 DiffuseColor;
float DiffuseIntensity;

float4 SpecularColor;
float3 EyePosition;


// 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;
	float3 View : TEXCOORD1;
};

// 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;
	output.View = normalize(float4(EyePosition,1.0) - worldPosition);

    return output;
}

// The Pixel Shader
float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
	float4 normal = float4(input.Normal, 1.0);
	float4 diffuse = saturate(dot(-LightDirection,normal));
	float4 reflect = normalize(2*diffuse*normal-float4(LightDirection,1.0));
	float4 specular = pow(saturate(dot(reflect,input.View)),15);

    return AmbientColor*AmbientIntensity+DiffuseIntensity*DiffuseColor*diffuse+SpecularColor*specular;
}

// Our Techinique
technique Technique1
{
    pass Pass1
    {
        VertexShader = compile vs_2_0 VertexShaderFunction();
        PixelShader = compile ps_2_0 PixelShaderFunction();
    }
}

 

In the example, I made the zombie spin instead of the camera so you can see how the specular light is working.

To implement the shader, all we need to do is to add the two new parameters and set them, check the shader source to see how it all fit together Smile

downloadDownload Source (XNA 4.0)

This entry was posted in Tutorial, XNA Shader Tutorial. Bookmark the permalink.

5 Responses to XNA 4.0 Shader Programming #3–Specular light

  1. Nice! Cant wait for more 😀

  2. Tr0ma says:

    I noticed that the formula you gave in the beginning of this tut for the specular part is
    Si*Sc*(R.V)n (3.1)
    I was expecting Si to be the specular intensity. However you don’t seem to use it when writing the return value for the PixelShaderFunction:
    return [..] + SpecularColor*specular;

    By the way, thank you so much for those tuts, it’s a massive knowledge booster!

  3. Pingback: Windows Client Developer Roundup 086 for 1/11/2012 - Pete Brown's 10rem.net

  4. Norris says:

    Thanks for your great tutorials ! I try to get the same results as in my 3D modeling package (Maxon C4D) with a simple teapot. Strangely, I need to modify a little your code to have specular highlights at the right place. Let me explain.
    Your “LightDirection” vector is in fact my “LightPosition”. He has these value (0,1,1). So the light is above and “behind” the camera. In the pixel shader, I need to remove the minus sign before LightDirection for the “diffuse” component, and add it before the one in the “reflect” component.
    So, the solution form e is:
    (PixelShader)
    float4 diffuse = saturate(dot(LightDirection,normal)); //removed minus sign
    float4 reflect = normalize(2*diffuse*normal-float4(-LightDirection,1.0)); //added minus sign

    I’m not really sure if that’s an error from you (you seem very better than me to understand the true lighting model), but what is sure, is that all the rest of the code is the same as yours.
    Also, could you confirm this lighting model is “Blinn” and not “Phong” ?

  5. Peter says:

    Excellent tutorial! Thank you very much.
    I also got a little confused about the light direction.
    According to float4 diffuse = saturate(dot(-LightDirection,normal)); The light direction should point at the object, so the direction reverses the direction of L in the fig 3-2
    But in float4 reflect = normalize(2*diffuse*normal-float4(LightDirection,1.0)); The light direction should point out from the object, which is same as the direction of L in the fig 3-2.
    I change the code to float4 reflect = normalize(2*diffuse*normal+float4(LightDirection,1.0)); but I’m not sure which way is correct. Would you like to confirm it? Thank you.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

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