Sgt. Conker We are "absolutely fine"

21Sep/100

Article: Shaders – Specular Lighting

by Daniel Greenheck

What should I be familiar with before I go through this tutorial?
- The content discussed in the introduction, ambient and diffuse tutorials.

One Thing Before I Begin...

To keep things succinct, I will no longer explain or comment things explained in previous tutorials. Throughout these series of tutorials, I'm going to expect you've gone through the previous tutorials. No sense in beating a dead horse. So if you look at the variables section and only see the Specular variables, rest assured the others are still in the actual program. I just think it will be easier for you as a reader if I only present you with new information. That aside, let's begin!

What is Specular Lighting?

Specular lighting in one word: shiny. The world specular in itself means a mirror-like surface, so you can imagine that anything that has specular lighting is very reflective, causing it to have shiny highlights. If you place a polished chrome ball under a direct light source, you'll probably see a big shiny spot blinding you; this is the specular highlight. Specular reflections are caused by the reflection of the actual light source itself. If the surface isn't flat however, it will warp the light and you'll usually see it as a small dot of bright light or curved lines of light. When you're out on a sunny day, look around at smooth, metallic objects and notice their specular highlights. Try and determine how the light is coming in and being reflected off the surface to your eye. The more you understand about real-world lighting, the better you'll understand what we're trying to replicate in our shaders.


The perfect example of specular lighting. Notice the highlight is really the reflection of the sun.

The Specular Shader

As usual, I once again present you with the entire specular shader. If you skipped my intro to this article, which I can't blame you for, I'll remind you that I left out the commenting on anything non-specular to save space and make the presentation a little nicer. If you don't know what the other parts of the program do, go back and read the Ambient and Diffuse shaders.

float4x4 World;
float4x4 View;
float4x4 Projection;

float4 AmbientColor;
float AmbientIntensity;

float4 DiffuseColor;
float DiffuseIntensity;
float3 DiffuseLightDirection;

// Specular Variables
float4 SpecularColor;
float SpecularIntensity;
float Shinniness;                // Sharpness the specular highlights. Higher number = sharper highlight
float3 CameraPosition;

struct VertexShaderInput
{
    float4 Position : POSITION0;
    float3 Normal : NORMAL0;
};

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

VertexShaderOutput VertexShader( VertexShaderInput input )
{
    VertexShaderOutput output;

    float4 worldPosition = mul( input.Position, World );
    float4 viewPosition = mul( worldPosition, View );
    output.Position = mul( viewPosition, Projection );

    output.Normal = mul( input.Normal, World );

    // Get the vector from the camera to the vertex for the specular component by
    // subtracting the world position from the camera
    output.CameraView = normalize( CameraPosition - worldPosition );

    return output;
}

float4 PixelShader( VertexShaderOutput input ) : COLOR0
{
    // Normalize our variables
    float3 lightdir = normalize( DiffuseLightDirection );
    float3 norm = normalize( input.Normal );

    // Calculate the half angle: the half the angle between our light direction and camera view
    float3 halfAngle = normalize( lightdir + input.CameraView );

    // Take the dot product between the normal of the pixel and the half angle. The closer the two
    // vectors are to pointing in the same direction, the higher the dot product. Take that to the [Shinniness]
    // power to make the highlight strong in the middle and fade out as you get towards the edges.
    float specular = pow( saturate( dot( norm, halfAngle ) ), Shinniness ) * SpecularColor * SpecularIntensity;

    float4 diffuse = dot( lightdir, input.Normal ) * DiffuseIntensity * DiffuseColor;
    float4 ambient = AmbientIntensity * AmbientColor;

    return saturate( diffuse + ambient + specular );
}

technique Specular
{
    pass Pass0
    {
        VertexShader = compile vs_1_1 VertexShader();
        PixelShader = compile ps_2_0 PixelShader();
    }
}

There's four things of interest we've added to the program:

  1. Four new variables: SpecularColor, SpecularIntensity, Shinniness and CameraPosition.
  2. In the output structure, we added a new float3 for CameraView.
  3. In the vertex shader, we calculate our CameraView by subtracting the WorldPosition of the vertex from our CameraPosition.
  4. The pixel shaders does some calculations to calculate the specular component. They look pretty confusing right now, but don't worry, we'll break them down.

New Specular Variables

float4 SpecularColor;
float SpecularIntensity;
float Shinniness;                // Sharpness the specular highlights. Higher number = sharper highlight
float3 CameraPosition;

The first two variables are the same as the Ambient and Diffuse variables of similar name: they adjust the color and intensity of the specular light, respectively. The next variable is brand new however: Shinniness. This is a floating point number that adjusts the size and sharpness of the specular light. If you have a higher value, you're going to get a very intense, small highlight. If you set it to a lower value, the specular highlight will be broader, but overall not as sharp. Play with different values to get the effect you want. The range of values you'll want to stay within is 10 and 2000, though you can really pick whatever you want.

The next variable is the current position of the camera in the scene, a.k.a. the "eye" you are looking through. Since the specular highlights you see depend on your angle with the object and the light source, we'll need to update this variable every frame. For example: If you walk around a freshly-waxed car, you'll notice the specular highlights on the body and windows don't stay in the same spot. Depending on your position, the sun will be reflected to your eye off different parts of the car and at different angles.

Adding the CameraView to the Output Structure

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

We've added one new parameter to our VertexShaderOutput: CameraView. All we are passing here is a unit vector which represents the direction from the camera to the current vertex.

Vertex Shader

VertexShaderOutput VertexShader( VertexShaderInput input )
{
    VertexShaderOutput output;

    float4 worldPosition = mul( input.Position, World );
    float4 viewPosition = mul( worldPosition, View );
    output.Position = mul( viewPosition, Projection );

    output.Normal = mul( input.Normal, World );

    // Get the vector from the camera to the vertex for the specular component by
    // subtracting the world position from the camera
    output.CameraView = normalize( CameraPosition - worldPosition );

    return output;
}

Compared to the diffuse shader, everything is the almost exactly the same. The only new thing we added is calculating the CameraView, which I just mentioned above. We just take the CameraPosition and subtract the position of the vertex in world space. Vector algebra determines that this gives the vector from the camera to the vertex. Then we normalize that to get a unit vector. Simple! We'll be using CameraView next...

Pixel Shader

float4 PixelShader( VertexShaderOutput input ) : COLOR0
{
    // Normalize our variables
    float3 lightDir = normalize( DiffuseLightDirection );
    float3 norm = normalize( input.Normal );

    // Calculate the half angle: the half the angle between our light direction and camera view
    float3 halfAngle = normalize( lightDir + input.CameraView );
    // Take the dot product between the normal of the pixel and the half angle. The closer the two
    // vectors are to pointing in the same direction, the higher the dot product. Take that to the [Shinniness]
    // power to make the highlight strong in the middle and fade out as you get towards the edges.
    float specular = pow( saturate( dot( norm, halfAngle ) ), Shinniness ) * SpecularColor * SpecularIntensity;

    float4 diffuse = dot( lightdir, input.Normal ) * DiffuseIntensity * DiffuseColor;
    float4 ambient = AmbientIntensity * AmbientColor;

    return saturate( diffuse + ambient + specular );
}

The picture on the left is my cartoon depiction of what is going on. We have an eyeball representing the camera position, the sun representing where the light is coming in from, and a little red box to act as our scene object. I've labeled each vector, all which you can find in the pixel shader code. Take this time to locate all of them now so you can follow along as we pick apart the code. The picture on the right is what the eyeball in the cartoon would actually see.
First, we stored the normalized DiffuseLightDirection in lightDir. Anything but a unit vector will give us weird results. We follow that by normalizing the normal of the current pixel (Remember we're normalize in the pixel shader because we want the interpolated normal of the pixel. If normalized in the vertex shader, it would still interpolate is it went to the pixel shader and therefore wouldn't be a normalized vector anymore.) and storing that in norm. Next, we calculate the halfAngle by adding together lightDir and CameraView (vector from the camera to the box). If you look at the picture, you can see halfAngle directly bisects the angle from lightDir to CameraView. Okay. Now all our variables are defined. Now we can finally calculate our specular component.
If you remember, in the diffuse component we took the dot product of the normal and the light direction to find out how much light actually hit the surface. The specular light uses the same concept, except it now also depends on the position of the camera (or "eye"). Instead of taking the dot product between the normal and the light direction, we're going to take the dot product of normal and the half angle. Why? Physics break!

A Brief Digression on Optics

If you really want to understand how shaders work, a very thorough understanding of optical properties is an absolute must. Knowing the real-world phenomena we are trying to simulate and how they physically work gives you a much better idea of what we're trying to accomplish. I'll give you a little optics background so you can understand what's going on in this tutorial. However, I would highly suggest looking at more sources to further your understanding.
When light strikes a surface at a certain vector, called the angle of incidence, it bounces off the surface, reflecting across the normal of the point on the surface it hits. If the normals of a surface are all pointing in the same direction (think mirror), the reflected light will all be reflected at the same angle, so the light rays will still be parallel to eachother. This keeps the image intact when it gets reflected back to your eye. If the normals of the surface are all pointing in different directions (think rough boulder), the reflected light is scattered in all different directions, some light rays even bouncing back towards the light source. The rays are no longer parallel to eachother, so different parts of the image are shooting off in all different directions. This is why you don't see a reflected image in a boulder like you do in a mirror. The shinnier the surface, the less scattering and the closer you get to a mirror. I've provided a picture below that demonstrates what I've just explained.

The Shinniness variable helps us define how smooth or rough a surface is. The rougher the surface, the smaller the value. You'll see why in a minute. In the cartoon diagram with the red box, the light travels from the sun, reflects across the normal of the pixel and makes it to our eye. However, our eye isn't exactly at the receiving end of the reflected ray; it's slightly above. Yet in the 3D rendering we can see it still gets light. Why is this? Because in that picture, the box isn't perfectly smooth, so we can assume that there will be some scattering to the eye. It's really all just guess and check if you think about it. Comforting isn't it?

Now that we know some laws of optics, we can start where we left off. We take the dot product of norm and halfAngle to see how much direct light the eye will get. The larger the angle between the two, the further away our eye is from the "predicted" path of the reflected rays; therefore we will get less specular/reflective light. We take the result of that and take it to the Shinniness power. If you think about an exponential graph, the higher the exponent, the quicker a number below 1 goes to zero. This is a way to simulate how rough surfaces can scatter light more than smooth surfaces. A smaller power, a rougher surface, more scattering. A higher power, a smoother surface, less scattering. We multiply that result by SpecularColor and SpecularIntensity like our other light types and voila! We now have specular light. We just add the specular component to our final sum at the end of the pixel shader and our model should now be specularly lit.

If you want to know how to set the variables from XNA, be sure to check out the Diffuse or Ambient light tutorial.

Conclusion

Specular lighting is a step towards the more complicated aspects of programming shaders. By adding it into your 3D application, you can see it definitely adds a lot of realism. Once you start getting into advanced lighting techniques, you really need to know your stuff: vectors, optics, matrix multiplication, etc. etc. I'll put up some resources on optics on the Links page if you want to learn a bit more. There's already some links for matrices. As these tutorials get more complex, I'll probably be making a lot more mistakes so be sure to send me an email at dan@digitseven.com. And of course, if you have any questions about anything I've covered, please email me and I'll do my best to help you out. Also, make sure you sign up for the newsletter on the home page to stay updated with my latest posts Enjoy!

Download the source: SpecularLighting.zip

About Absolutely Fine Tutorial Contest

Look out for the complete list of entries in our tutorial contest! Coming soon (it's being built step by step)!
Comments (0) Trackbacks (0)

No comments yet.


Leave a comment


*

No trackbacks yet.