Blog of roxlu, co-founder of Apollo Media. Contact info[shift+2]apollomedia.nl.

OpenGL Rim Shader

Using a rim light gives your shading a nice volumentric effect which can greatly enhance the contrast with the background. A rim shader is very simple but has a great result. In this screenshot I applied a rim light effect which I blurred a bit to make it even more effective.

To create your rim shader, first a bit of background about the vectors that we need to perform the calculations. The contribution of the rim shading should be bigger around the peaks. Around the peaks, some normals are pointing towards the eye/cam position and some are pointing away. In the image below you can see that the angle between the normal and the eye vector (v) is big.

Getting the eye vector in your shader is easy. You convert your vertex position to view space by multiplying it with your view matrix, then you normalize and negate it.

Below you can see how small the angle between the eye and the normal of the other face is. It's clearly a lot smaller. This means that the rim contribution for this second drawing should be less then the first one.

In short you can say, the bigger the angle between the eye vector (v) and the normal (n), the bigger the contribution of the rim shading. To calculate this, we use the dot product which gives us the cosine between two vectors. As you might know, the cosine between two vectors that are perpendicular is 0. You can read up a bit on the dot product here. Because we want the contribution of the rim shading to be bigger when the angles are bigger, we will use 1.0 - dot(normal, eye_vector). The 1.0 - part is necessary to make sure that the value will be bigger when the angle is bigger. E.g. when the vectors are perpendicular the contribution will be 1.0 (as the dot product is zero).

In the GLSL example below we're implementing this rim shader, but we're skipping one important part to show you how using the calculated rim-contribution value looks like:

vec3 n = normalize(mat3(u_vm) * v_norm);      // convert normal to view space, u_vm (view matrix), is a rigid body transform.
vec3 p = vec3(u_vm * v_pos);                   // position in view space
vec3 v = normalize(-p);                       // vector towards eye
float vdn = 1.0 - max(dot(v, n), 0.0);        // the rim-shading contribution
 
fragcolor.a = 1.0;
fragcolor.rgb = vec3(vdn);

Using the rim shading contribution as color (the vdn) we get the following result.

As you can clearly see, there is much more contribution than just around the edges/peaks. What we want to do, is to remove some of the contribution that are below a certain value. We could use an if statement like if(vdn < 0.5) { // skip } but this will result in hard edges between the rim contribution. When we use smoothstep we can still limit the use of certain values but also make sure that the values have a nice smooth cutoff. Therefore we add a smoothstep like:

fragcolor.rgb = vec3(smoothstep(0.8, 1.0, vdn));

This results in the image below:

The complete shader looks like:

#version 330
 
uniform mat4 u_pm;
uniform mat4 u_vm;
 
layout( location = 0 ) out vec4 fragcolor;
 
in vec3 v_norm;
in vec4 v_pos; 
 
void main() {
 
  vec3 n = normalize(mat3(u_vm) * v_norm);      // convert normal to view space, u_vm (view matrix), is a rigid body transform.
  vec3 p = vec3(u_vm * v_pos);                   // position in view space
  vec3 v = normalize(-p);                       // eye vector
  float vdn = 1.0 - max(dot(v, n), 0.0);        // the rim contribution
 
  fragcolor.a = 1.0;
  fragcolor.rgb = vec3(smoothstep(0.6, 1.0, vdn));
}