Lesson 37 - Cel-Shading



This lesson is fairly simple. Cel-shading or toon shading is a type of non-photorealistic rendering designed to make 3-D computer graphics appear to be flat by using less shading color instead of a shade gradient or tints and shades. Cel-shading is often used to mimic the style of a comic book or cartoon and/or give it a characteristic paper-like texture. In image processing it is also known as posterization. However, cel-shading can be done in many ways. In this case, we apply a linear, rendering the object in N levels, but the filtering can be done in myriad ways.

The Basics

There are three models used in to demonstrate the technique:

Each of these is rendered in a brown, monochromatic light. The number of levels can be chosen by the user, 1..9. Choosing 0 disables the cel-shading, resulting in continuous tones.


First we set up the scene:

var gfxScene = new GFX.Scene( {
    cameraPos : [10, 10, 6],
    defaultLights: false,

var dirLight = gfxScene.addLight( 'directional', { color:0xffffff, intensity:1.0,  position:[-10,10,20]});

We don't use the default lights because we want to pass the light to the shader (thoough we could have gotten the light by gfxScene.directionalLight[0]).

The intialization consists of loading the models and setting up the uniforms to pass to the shader. The teapot is basically an external library that consists of the commands to generate the teapot's geometry. Then the shader material is created:

function createShaderMaterial( light ) {

    var uniforms =  {

        uDirLightPos: { type: "v3", value: light.position },
        uDirLightColor: { type: "c", value: light.color },
        uMaterialColor: { type: "c", value: new THREE.Color( 1.0, 0.8, 0.6 ) },
        uLevels: { type: "f", value: nLevels }

    var vs = document.getElementById("vertexShader").textContent;
    var fs = document.getElementById("fragmentShader").textContent;

    return new THREE.ShaderMaterial({ uniforms: uniforms, vertexShader: vs, fragmentShader: fs });

The (Cel) Shader

Once again, the real work is done in the fragment shader.

<script id="vertexShader" type="x-shader/x-vertex">
    varying vec3 vNormal;

    void main() {
        gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
        vNormal = normalize( normalMatrix * normal );

<script id="fragmentShader" type="x-shader/x-fragment">
    uniform vec3    uMaterialColor;
    uniform vec3    uDirLightPos;
    uniform vec3    uDirLightColor;
    uniform float   uLevels;
    varying vec3    vNormal;

    void main() {
        vec4 lDirection = viewMatrix * vec4( uDirLightPos, 0.0 );
        vec3 lVector = normalize( lDirection.xyz );

        float diffuse = dot( vNormal, lVector );

        if (uLevels > 0.0) {
            float sign = diffuse < 0.0 ? -1.0 : 1.0;
            diffuse = (floor((abs(diffuse) + 0.001) * uLevels ) / uLevels) * sign + (1.0 / (uLevels * 2.0));

        gl_FragColor = vec4( uMaterialColor * uDirLightColor * diffuse, 1.0 );

The vertex shader doesn't do anything since we aren't altering the vertice's position at all, so we just pass the normal along to the fragment shader. In the fragment shader we use the light's position and direction to calculate the amount of light that would be reflected towards the camera - the diffuse value. Finally, we have a tricky little equation that converts the diffuse value, which is in the range of -Π and +Π to a value which is clamped to the nearest value of diffuse rounded to the uLevel approximation.

Note that this is just a simple linear posterization, there are literally an infinite number of ways that the cel-value could be approximated.

And that's it! Click on this link to see the actual rendered demo in all it's posterized glory!

As always, the original sources are on github here.

About Us | Contact Us | ©2017 Geo-F/X