Writing a one-line cel shader


Cel shading, also know as toon shading, has been around for a while. The idea behind it is to have only a few colors on the surface of the object, with some "banding". Let's see some examples:

Jet Set Radio (2000)

The Legend of Zelda: The Wind Waker (2003)

Ni no Kuni (2013)

And this is how we made it for Duckson in Duck in Town:


You may see we omitted the borders that Ni no Kuni and Jet Set Radio had, because we were looking for a more simplistic, less comic-like. So, let's implement this in Godot! We'll be doing it using shader code. You can use the visual shader editor, but I just feel more comfortable writing code. We'll be working with an sphere and a basic directional light.



The math behind

The most basic approach is to take into acount the difference between the surface normal and the light vector. This allows us to light points that are directly affected by light, and darken points that are less impacted. This is done using the dot product between the normal and the light vector.  We'll call this value ndotl. The idea is to threshold this value, so it creates hard transitions between colors.

Let's try it in the light shader.



By thresholding it, we get the banding that we wanted! Note that we apply the shade by simply multiplying the ALBEDO by 0.3. You can tune these factors to get the effect that looks better to you.

This may look cool, but there are some inconvenients:

  • Using branching is not good in shaders (for performance reasons)
  • May not work well with multiple lights
  • This doesn't react to shadows


Shadows don't work using this method

The branching can be easily avoided by using some math (more on that later). However, the shadow issue needs a different approach.

Taking into acount attenuation, not light direction


In the light shader, Godot exposes a nice value, called "ATTENUATION", that tells us how much lit a pixel is (taking into account shadows!). If we calculate the banding using this value instead of ndotl, shadowing will affect our object!



Nice! This way we get the effect and also the object is affected by shadows and more than one light. You may notice some irregular edges in super regular shapes like the sphere, but it isn't noticeable in complex shapes (for example, the Duckson model). If you need to use it for regular shapes, you could experiment by adding some smoothing :)


Optimizing the shader

The title mentions a one-liner, where's the one-liner?!

Well, we need to optimize the code a bit for that. First of all, let's delete the branching. Since in both paths of the branching we are setting a value to the same variable, we can avoid branching using a bit of clever math. We'll be using the mix and step functions. Mix does exactly what you'd expect from its name: mix (interpolate) between two colors. It receives two colors and a float between 0 and 1, which will indicate how much of each color we want in our mix. Since we want only one color or another, we only need to pass 0 or 1. Here's where step comes in. Step receives two values: a threshold and a value. It will return 0 if the value is less than the threshold, else it will return 1. Can you see how it will work? Let's see our one-liner:

void light() {
    DIFFUSE_LIGHT = mix(ALBEDO * 0.3, ALBEDO, step(0.9, length(ATTENUATION)));
}

It does exactly the same that we did with branching, but avoiding it, so we get greater performance! Keep in mind that the factors can be extracted as uniforms, so your artists can tune the values without having to touch code :p


So, this is all! Not that difficult, right? If you wanted to go further, you could always do some fancy things like adding border (maybe using sobel filter or some kind of postprocessing) or playing with specular toon shading. Also, if you want more than two bands you could try it to design it without using branching (the one-liner only works with two bands). As a clue, usually it is done using LUT (look-up textures). 

If you have any questions, don't hesitate to use the comments below. Thank you!

Get Duck in Town - A Rising Knight

Buy Now$2.99 USD or more

Leave a comment

Log in with itch.io to leave a comment.