HLSL Tutorial 2: Ambient, Diffuse And Specular Lighting


One of the most fundamental concepts in 3D computer games graphics is lighting. Without lighting, models don’t even tend to look 3D, as you can see in the screen shot below:


The above shading technique applied to a low poly sphere adequately draws the model shape but due to the fact there is no lighting whatsoever it looks more like a red 2D sprite.

So let’s cast some light on this... ok no more stupid puns :P

The three main lighting terms are:

Ambient Light: The general illumination of the area the model is in.
Diffuse Light: The direct illumination of an area of the surface of the model that’s facing the light source.
Specular Light: The illumination highlight present if the angle of the model surface to the eye is just right.

Following on from tutorial 1, where we had a basic new effect file created inside an XNA environment, we first need to add some more variables in addition to the ones we have. I will list everything below to save you flicking backwards and forwards.

1)  Gobal Variables
struct PointLight
{
       float3 position;
       float4 ambientColour;
       float4 diffuseColour;
       float4 specularColour;
       float ambientIntensity;
       float diffuseIntensity;
       float specularIntensity;
};

float4x4 World;
float4x4 View;
float4x4 Projection;
PointLight Light;
float3 EyePosition;

As you can see we’re declaring a custom struct to represent a point light. Running through the variables quickly we have the light’s position in our game world, the three lighting type colours (should you wish to mix and match for different looks) and separate intensities for each colour so you can change lighting levels from within the game, for example to create day and night cycles etc.
In addition to the world view and projection matrices we saw last time we now have a Light object using the above struct (note the struct must be declared above because of how the file is compiled) and a world position for our eye position, this vector would be your camera’s position in game.

2) Shader Function Input & Output Structs
struct VertexShaderInput
{
    float4 Position : POSITION0;
    float3 Normal    : NORMAL0;
};

struct VertexShaderOutput
{
    float4 Position : POSITION0;
    float3 Normal    : TEXCOORD0;
    float4 LightDir  : TEXCOORD1;
    float3 ViewDir   : TEXCOORD2;
};

Again we have our vertex shader function input and output structs, featuring some extra parameters.

In the input struct we have added a float3 Normal vector, a normal vector is simply this vertex’s facing direction vector, for example if this were a flat plane on the X & Z axis, all the normals would be pointing up on the Y axis.

Similarly we are passing the normal vector back out in the vertex shader output struct, along with a light direction vector and a view direction vector.

3) Vertex & Pixel Shader functions
VertexShaderOutput VertexShaderFunction(VertexShaderInput input)
{
    VertexShaderOutput output;

    //Multiply position by world, view and projection matrices
    float4 worldPosition = mul(input.Position, World);
    float4 viewPosition = mul(worldPosition, View);
    output.Position = mul(viewPosition, Projection);

    //Calculate world space normal from model space normal
    output.Normal = mul(normalize(input.Normal), World);

    //Calculate un-normalized light direction
    output.LightDir = -(float4(Light.position, 1.0f) - worldPosition);

    //Calculate un-normalized view direction
    output.ViewDir = float4(EyePosition, 1.0f) - worldPosition;

    return output;
}

As with tutorial 1’s vertex shader function we calculate the projection space position of the vertex by multiplying the incoming position from the input struct with the world, view and projection matrices.

We are also calculating un-normalized vectors for the outgoing normal, light direction and view direction vectors. The reason they not normalized here is because sometimes precision can be lost when normalized vectors pass from the vertex to the pixel shader. This is not something I can comment on further sadly, it is merely something I have picked up as you are now. We multiply the incoming normal vector by the world matrix to convert it from model space to game world space. 
We also calculate outgoing light and view directions using the incoming light and eye positions and the earlier calculated world position.

float4 PixelShaderFunction(VertexShaderOutput input) : COLOR0
{
       //Calculate local normalized normal vector
       float4 Normal = float4(normalize(input.Normal), 1.0f);

       //Calculate local normalized view direction
       float3 ViewDir = normalize(input.ViewDir);

       //Calculate local normalized light direction
       float3 LightDir = normalize(input.LightDir);

       //Calculate the amount of diffuse light hitting this pixel
       float Diff = saturate(dot(Normal, -LightDir));

       //Calculate reflection vector of the light hitting this pixel
       float4 Reflect = normalize(2 * Diff * Normal - float4(LightDir, 1.0f));

       //Calculate the amount of specular at this pixel
       float4 Specular = pow(saturate(dot(Reflect, ViewDir)), 256);

       //If no light is hitting the pixel cut the specular power
       if (Diff <= 0.0f)
              Specular = 0.0f;

       //Calculate ambient light values
       float4 AmbientColour = Light.ambientColour * Light.ambientIntensity;

       //Calculate diffuse light values
       float4 DiffuseColour = Light.diffuseColour * Light.diffuseIntensity * Diff;

       //Calculate specular highlight colour
       float4 SpecularColour = (Light.specularColour * Light.specularIntensity) *       Specular;

       return AmbientColour + DiffuseColour + SpecularColour;
}

Now this is where the magic happens, firstly we need to normalize those normal, view and light directional vectors.  Next we need to know how much directional light is hitting this pixel, we call this the light’s diffuse over the model surface. We then calculate the reflecting vector of the light, as light hits a surface one of the things that happens is that some of it is reflected back out off the surface, we are calculating the angle at which this takes place for the specular light.

Next we calculate the three light colours, using the parameters from the Light struct created earlier. Note how the diffuse and specular colours are being multiplied by the Diff and Specular powers, this gives them the correct levels based on the surface’s exposure to the light and the angle to the eye respectively.

Last but not least we add all three colours together to produce the final colour value of the pixel. And the result?


Here I used a dark blue ambient light, a green diffuse light and a white specular light. The specular intensity has been ramped up to make it obvious.

If you want to know more about the model space versus world space conversions discussed in this tutorial and why they are important, checkout my quick tutorial explaining the difference between the two.

Please feel free to comment, I’m still new to all this online blog/tutorials business so if you spot anything please let me know!

Cheers,

P D Flower.

No comments:

Post a Comment