Sgt. Conker We are "absolutely fine"

21Sep/100

Article: Shaders – Rim Lighting

by Daniel Greenheck

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

What is Rim Lighting?

Rim lighting is a shader technique you often see in role-playing games when there's a treasure chest or door which needs to be opened. Around the border of the object there is a soft glow which differentiates it from the rest of the objects in the scene. The concept behind the rim lighting shader is extremely simple: the more a surface faces away from the camera, the more rim lighting it should get. Looking at the sphere below illustrates the point:

Here, the rim light is set to be blue while the diffuse light is just a white light at 75% intensity. On the edges of the sphere, the normals of the triangles (remember a normal is the vector pointing perpendicular out of the surface) are more perpendicular to the line of sight, which is going straight into the page. The more perpendicular the normal is to the line of sight, the more rim light the surface receives.
Now what mathematical tool allows us to compare how perpendicular/parallel two vectors are? That's right! The dot product. If you take the dot product of two normalized vectors which are parallel and pointing in the same direction, you get 1. If the vectors are perpendicular, you get zero. Using this knowledge we will know manipulate some shader code to create a rim lighting shader!

The Rim Lighting 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;
float4 DiffuseColor;
float4 SpecularColor;
float4 RimColor;

float AmbientIntensity;
float DiffuseIntensity;
float SpecularIntensity;
float RimIntensity;         // Intensity of the rim light

float3 DiffuseLightDirection;
float3 CameraPosition;
float3 CameraDirection;
float Shinniness;

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 rim lighting component by subtract the dot product of the normal and camera direction
    // from 1, then taking that to the 2nd power. This will give pixels at more perpendicular angles to the camera
    // a glow effect. Multiply that by rim color and intensity to adjust the color and brightness.
    // - If you take the dot product to a higher power, the glow will be thinner and closer to the edges.
    // - If you take the dot product to a lower power, the glow will be more spread out away from the edges.
    float4 rim = pow( 1 - dot( norm, CameraDirection ), 1.5 ) * RimColor * RimIntensity;

    // Calculate the specular component
    float3 halfAngle = normalize( lightdir + input.CameraView );
    float4 specular = pow( saturate( dot( norm, halfAngle ) ), Shinniness ) * SpecularColor * SpecularIntensity;

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

    return rim + ambient + specular + diffuse;
}

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

The only difference between this and the specular lighting shader is a new variable called CameraDirection, some new variables to control the rim lighting effect and an extra line in the pixel shader. Let's start with the variable.

New Rim Light Variables

float4 RimColor;
float  RimIntensity;
float3 CameraDirection;

The first two variables are self-explanatory: they control the intensity and color of the rim light. CameraDirection is a very simple variable: all it is is a vector which points away from the camera in the direction you are looking. Below is a wonderful 2D drawing of what the camera direction vector is describing; 3D just adds another dimension but in essence is the same. The eye is at the center, the star is the object, and the red arrow is a normalized vector.

CameraDirection points TOWARDS the object

And that's really all you need to know in terms of new variables!

Pixel Shader

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

    // Calculate the rim lighting component by subtract the dot product of the normal and camera direction
    // from 1, then taking that to the 2nd power. This will give pixels at more perpendicular angles to the camera
    // a glow effect. Multiply that by rim color and intensity to adjust the color and brightness.
    // - If you take the dot product to a higher power, the glow will be thinner and closer to the edges.
    // - If you take the dot product to a lower power, the glow will be more spread out away from the edges.
    float4 rim = pow( 1 - dot( norm, CameraDirection ), 1.5 ) * RimColor * RimIntensity;

    // Calculate the specular component
    float3 halfAngle = normalize( lightdir + input.CameraView );
    float4 specular = pow( saturate( dot( norm, halfAngle ) ), Shinniness ) * SpecularColor * SpecularIntensity;

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

    return rim + ambient + specular + diffuse;
}

The pixel shader is the same except for the line which is accompanied by the massive comment. Let's analyze the statement starting with what's inside the pow() function. Remember back to what I mentioned above about the dot product. When the normal of a surface is perpendicular to the line of sight, it gets the most rim light. The dot product of a perpendicular normal with the line of sight would result in 0, so that is backwards from what we want. When the dot product is 0, we really want the rim light at full intensity. So by subtracting the dot product of the normal and the CameraDirection from one, we get just this. So we get some number between 0 and 1, 1 giving us full intensity rim light and 0 giving us no rim light at all.
Surrounding the dot product inverse is the pow() function. The power function allows us to adjust the falloff of our rim light. Since the amount of light is always between 0 and 1, taking it to a power greater than 1 results in a number less than the original number. For example, .5^2 = .25. This is how light eminating from a light source normally falls off, to the square of the distance. You can play around with the power (I have it set at a medium 1.5) to get the effect you want. The higher the power, the less rim lighting you will get on surfaces whose normals are more parallel to the camera direction than perpendicular. Here is an example of variation of power. The diffuse light is white and the rim light is blue.

As you can see, the smaller the power, the more spread out the rim light is. The larger the power, the more the light is confined to the edges. Finally, I just take that rim light component and multiply it with the RimColor and RimIntensity variables! And there you have it, glowing edges.

Conclusion

Rim lighting is a cool and easy technique to add a fun touch to your game or 3D model. The simple calculation shouldn't put much of a dent in your graphics performance either, although the pow() function is a little processing intensity. You could get around this by removing it entirely, but then you no longer have control over the spread of the light. It's up to you to decide based on your application and hardware limitations. Once again, any questions or comments, feel free to email me at dan@digitseven.com. And be sure to sign up for the newsletter, which I'll use to keep you posted on new updates to the site. Enjoy!

DOWNLOAD Rim Lighting PROJECT HERE

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.