# 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.

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:

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:

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.

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.

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.

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.

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: