Toon Shading Effect And Simple Contour Detection

Toon Shading or Cel Shading is a render techniques used to simulate hand-drawn cartoons. The most important aspect of this technique is the steep difference between the colors used to render 2D or 3D models. If you want to have an effect like Don’t Starve or Borderlands games you probably want to add contour detection (or edge detection) to your models. Although these games might have more complicated techniques for rendering, toon shading and contour detection can offer a similar effect.

Don't starve

Don’t starve

First let’s see how can we find contour detection in fragment shader for our model. You know the backface culling algorithm or Lambert’s cosine law, right? The easiest way to explain this is to rely on Lambert’s cosine law but instead of light direction we will use the view direction:

Contour Detection

Contour Detection

In the  example below  I use Blinn-Phong lighting model on a sphere with the same color on all channels(RGB) and I apply contour detection:

Contour Detection

Contour Detection

This technique doesn’t give you the best results because it “eats” from the model, doesn’t render the same contours for two different view positions and doesn’t keep an uniform width. If you want better results you can:

  •  Render the same object a little bit larger, then draw the normal object over. Works only for convex models.
  • Use an edge detection filter like Sobel to build a texture with your scene and then combine with your actual texture.
  • Draw silhouette lines using the geometry shader.
  • Use a stencil buffer.
  • You can also skip all this computation by having textures that already have black color on edges and contour. The only drawback here is that they are static.
Eathing Spehre

I decreased the angle when the contour is detected just to see how it affects the overall model

Here is the vertex shader and fragment shader for this contour detection tehnique. Remember that all channles have the same color and we are using Blinn-Phong lighting model.

//Phong Reflection Model Vertex Shader
#version 330

layout(location = 0) in vec3 in_position;
layout(location = 1) in vec3 in_normal;

uniform mat4 model_matrix, view_matrix, projection_matrix;

//send them to fragment shader
out vec3 world_pos;
out vec3 world_normal;

void main()
{

 //convert in world coords
 world_pos = mat3(model_matrix) * in_position;//careful here
 world_normal = normalize(mat3(model_matrix) * in_normal);
 gl_Position = projection_matrix*view_matrix*model_matrix*vec4(in_position,1);
}
//Fragment shader contour detection
//Blinn-Phong with same color for RGB
#version 330
layout(location = 0) out vec4 out_color;

uniform vec3 light_position;
uniform vec3 eye_position;

uniform int material_shininess;
uniform float material_kd;
uniform float material_ks;

in vec3 world_pos;
in vec3 world_normal;

void main()
{
 vec3 L = normalize( light_position - world_pos);
 vec3 V = normalize( eye_position - world_pos);
 vec3 H = normalize(L + V );

 float diffuse = material_kd * max(0, dot(L,world_normal));
 float specular = 0;

 if( dot(L,world_normal) > 0.0)
 {
 specular = material_ks * pow( max(0, dot( H, world_normal)), material_shininess);
 }

 //Black color if dot product is smaller than 0.3
 //else keep the same colors
 float edgeDetection = (dot(V, world_normal) > 0.3) ? 1 : 0;

 float light = edgeDetection * (diffuse + specular);
 vec3 color = vec3(light,light,light);

 out_color = vec4(color,1);
}

Now for toon shading we must create a sharp transition effect between the colors. Toon shading can be computed in many ways. Here, for our Blinn-Phong reflection lighting model, toon shading affects only the diffuse and specular components, the ambient and emissive coefficients remain the same.

Ambinet Light Toon Shading

Ambinet Light Toon Shading

Diffuse light  can be quantized on a number of levels to create discontinuities between light intensities. To implement this discontinuities in our shader we can use an if else-if statement or we can use a 1D texture with already quantized color gradients, but we can be more elegant and create a math function. For example, we can floor or ceil the multiplication between the number of levels and the diffuse cosine term  and after that we scale it by a factor.

Diffuse Toon Shading

Diffuse Toon Shading

For the specular component we can use the same approach that we used to get the contour above. If the cosine rised to shininess coefficient is lesser than a threshold don’t draw it.

Toon shading with ambient specular and diffuse components

Toon shading with ambient, diffuse and specualar components

Toon shading result

Toon shading result

We keep the same vertex shader from above. Fragment shader is modified:

//Toon Shading Fragment Shader
#version 330
layout(location = 0) out vec4 out_color;

uniform vec3 light_position;
uniform vec3 eye_position;
uniform int material_shininess;
uniform float material_kd;
uniform float material_ks;

in vec3 world_pos;
in vec3 world_normal;

const vec3 ambinetColor = vec3(0.90,0.0,0.20);

//number of levels
//for diffuse color
const int levels = 5;
const float scaleFactor = 1.0 / levels;
const vec3 diffuseColor = vec3(0.30,0.80,0.10);

void main()
{ 
 vec3 L = normalize( light_position - world_pos);
 vec3 V = normalize( eye_position - world_pos);

 float diffuse = max(0, dot(L,world_normal));
 diffuseColor = diffuseColor * material_kd * floor(diffuse * levels) * scaleFactor;

 vec3 H = normalize(L + V);

 float specular = 0.0;

 if( dot(L,world_normal) > 0.0)
 {
   specular = material_ks * pow( max(0, dot( H, world_normal)), material_shininess);
 }

 //limit specular
 float specMask = (pow(dot(H, world_normal), material_shininess) > 0.4) ? 1 : 0;

 float edgeDetection = (dot(V, world_normal) > 0.2) ? 1 : 0;

 color = edgeDetection * (color + diffuseColor + specular * specMask);

 out_color = vec4(color,1);
}

toon_shading_source_code (VS 2013 OpengGL with C++ framework is  based on one of my school projects)

Other resources:

OpenGL 4 Shading Language Cookbook

Toon Shading Unity

Silhouette Extraction


blog comments powered by Disqus