Blender Cel Shading in Eevee

One of my interests in 3D is generating cel-shaded images and animations. Blender supports cel (aka toon) shading and other non-photorealistic (NPR) shading methods through clever uses of node networks. If you’re using the Cycles render engine, you can even use the built-in Toon Shader.

But the Blender comes with a real-time render engine, Eevee, and it would be great to combine its fast render times with a cel-shading solution. Unfortunately, the Toon Shader BDSF only works with Cycles (as of v2.82), but there are some workarounds for using Eevee.

For reference, I rendered this image with Cycles with no light bounces. Both objects use the same single Toon BSDF shader and the ground plane uses a Principled BSDF. This one frame took over a minute (1:06) to render.

Toon-shaded render using Cycles.

Custom Node Group Using Diffuse Shader Output

My first approach uses a Diffuse BSDF shader to get existing lighting information on the surface. Eevee has a convenient Shader to RGB node that I can use to drive the rest of the node network. Then, I extract the “Value” from the HSV color and use that to determine which color band to render.

I organized the entire node network in a convenient Node Group you can drop into your scene, if you want to try it out or see how I implemented it. It supports three color bands: the base color, a shadow, and a highlight. To use it in your scene, use File -> Append to add the node group. Then in the Node Editor, you can select the CelShader3 from the Add -> Groups menu.

Here is the same frame rendered using Eevee and my node network. This render took 0.47 seconds, which is about 140 times faster than the Cycles render!

Frame rendered with custom Node Group in Eevee.

There are some shortcomings with this approach. I could only get clean shadows if I used a Point Lamp with the size set to 0. Any other light or size caused glitching with self and cast shadows.

Shadow problems from large light sources.
Increasing the light size caused self and cast shadows to become too soft, which does not fit the desired look.

The node network is also pretty simplistic. It works great for simple color bands, but if you need anything more complicated (for instance, texture maps) you’re out of luck.

Node Network Using a Light Source and Vector Math

For a different approach, I used vector math to calculate the angle relative to a light source, and then shaded each point using that math:

Node network for vector math solution with one light source.
  • The Value nodes at the left are driven by the X, Y, and Z location values of the light source (using a separate Driver for each Value).
  • They are combined into a single vector by the Combine XYZ node, which is the position of the light in world coordinates.
  • By subtracting the Geometry position value, you get a new vector that represents the direction from the surface to the light.
  • The Normalize node ensures that the vector has a length of exactly 1, which is important for the ColorRamp later on.
  • Taking the Dot Product of the normalized vector pointing to the light and the normal returns a value between -1 and 1, where 1 means the normal is pointing exactly at the light, 0 means the normal is perpendicular, and -1 is when the normal is facing directly away from the light.
  • The next two Math nodes convert the -1…1 range to a 0…1 range,
  • Which is finally fed into a ColorRamp that drives the final color of the material.

Points that are almost directly facing the light are shaded the brightest, points that are facing away from the light are in shadow, and the points in between are shaded in the base color. The ColorRamp lets us control the thresholds between those colors.

Frame rendered using a single light source and some vector math.
Render time: 0.45 seconds.

There are shortcomings to this approach:

  • Only 1 light can affect any given object.
  • You must manually tie that light to the material using Drivers.
  • The material does not support self-shadows or shadows cast from other objects onto itself.

If you’re looking for a simple shader that accurately handles one light and does not need self or cast shadows on itself, this is a great option.

Freestyle

While we are on the topic of cel-shading, Blender has a very powerful line-drawing feature called Freestyle. It allows you to add lines to the render based on elements of the geometry, such as the silhouette, calculated creases, material boundaries, etc. Or, for much more powerful control over the final lines, you can also use Python scripts to generate them, although that is a very advanced topic.

Using the Parameter Editor (no scripting), I was able to quickly add some lines to the render. The actual lines are added after the render is complete, which increased the render time to 0.87 seconds.

Example render using the custom Node Group and Freestyle lines.

It also looks nice when rendered as an animation!

Animation rendered with my Node Group and Freestyle lines.

Final Notes

Blender has a lot of powerful ways to implement NPR styles in Eevee, and I feel that I have only scratched the surface. I like my first approach best so far, but I plan on spending some time improving it to get better control over the output and support more features, like texture maps.

After fumbling around on my own to figure out these approaches, I searched online for additional resources. There are some great articles and videos about cel shading and NPR approaches, and here is a sample of some of the better ones I found: