Toon shader redux

Over on the Codea Talk forum, we begun wondering whether it might be possible to do the toon shader in just a single pass. If you can halve the number of draw calls you make, you will significantly speed up your code. I went back to my original inspiration for the shader, this GLSL Programming e-Book, and found that after the description of the toon shader that ships with Unity (which I adapted in the previous post), they do indeed describe a single-pass method.

It’s a subtly different effect from the Unity ‘toon shader. Rather than “inflating” the model along its normals to produce a thick black outline, this approach compares the view direction (the normal of the vertex position relative to the camera position) to the normal to determine whether a fragment is perpendicular to the viewer, and if so sets the colour to the outline colour. It then uses the directional light source to further vary the thickness of the outline, creating an extreme chiaroscuro effect.


This could actually prove to be quite a flexible effect. In addition to creating the hard-edged, posterized effect that we’re aiming for in the toon shader, it could also, because of its use of the directional light source to create very hard shadows, be used to create a stylised sense of volume, particularly if it was combined with other volumetric devices such as specular highlights. The hard shadows have a pulp-y, noir-ish feel to them, so quite appropriate for a superhero character.


For this silhouette shader to work, the lighting calculations have to be moved from the vertex shader onto the fragment shader. The fragment shader then uses the dot product of the view direction and the fragment normal to determine whether the fragment is part of the outline or not, further varying the thickness according to the diffuse reflection term.

Because with this fragment shader we are now doing several of the calculations required for specular highlights, I’ve added specular code to the shader (but commented out by default).

The shader below is my adaptation of the code in the GLSL e-Book linked to above, adjusted to work in Codea. You can grab the updated source-code with the shader here.

Now you’re all set for some single-pass, noir-ish chiaroscuro!


I fixed a few things in the shader, so the specular lighting should now look a lot better. I’ve also added support for varying specular intensities for different materials in the model, by storing a specular intensity value in the alpha value of the vertex colour (this is set by the specular intensity slider in the materials pane in Blender). You should be able to see that now his suit is shiny, but his skin stays matte.

--# ToonShader3
FrameBlendNoTexToon = { --models with no texture image
    splineVert= --vertex shader with catmull rom spline interpolation of key frames
    uniform mat4 modelViewProjection;
    uniform mat4 modelMatrix;
    uniform float ambient; // --strength of ambient light 0-1
    uniform vec4 lightColor;
    const vec4 front = vec4(0.,0.,1.,0.);

    uniform int frames[4]; //contains indexes to 4 frames needed for CatmullRom
    uniform float frameBlend; // how much to blend by
    float frameBlend2 = frameBlend * frameBlend; //pre calculated squared and cubed for Catmull Rom
    float frameBlend3 = frameBlend * frameBlend2;

    attribute vec4 color;

    attribute vec3 position;
    attribute vec3 position1;
    attribute vec3 position2; //not possible for attributes to be arrays in Gl Es2.0 
    attribute vec3 position3;
    attribute vec3 position4;

    attribute vec3 normal;
    attribute vec3 normal1;
    attribute vec3 normal2;
    attribute vec3 normal3;
    attribute vec3 normal4;

    vec3 getPos(int no) //home-made hash, ho hum.  
        if (no==0) return position;
        if (no==1) return position1;
        if (no==2) return position2;
        if (no==3) return position3;
        if (no==4) return position4;

    vec3 getNorm(int no)
        if (no==0) return normal;
        if (no==1) return normal1;
        if (no==2) return normal2;
        if (no==3) return normal3;
        if (no==4) return normal4;

    varying lowp vec4 vAmbient;
    varying lowp vec4 vColor;
    varying lowp vec4 vNormal;
    varying lowp vec4 vPosition;

    vec3 CatmullRom(float u, float u2, float u3, vec3 x0, vec3 x1, vec3 x2, vec3 x3 ) //returns value between x1 and x2
    return ((2. * x1) + 
           (-x0 + x2) * u + 
           (2.*x0 - 5.*x1 + 4.*x2 - x3) * u2 + 
           (-x0 + 3.*x1 - 3.*x2 + x3) * u3) * 0.5;

    void main()

        vec3 framePos = CatmullRom(frameBlend, frameBlend2, frameBlend3, getPos(frames[0]), getPos(frames[1]), getPos(frames[2]), getPos(frames[3]) );
       vec3 frameNorm = CatmullRom(frameBlend, frameBlend2, frameBlend3, getNorm(frames[0]), getNorm(frames[1]), getNorm(frames[2]), getNorm(frames[3]) );

        vNormal = normalize(modelMatrix * vec4( frameNorm, 0.0 ));
        vPosition = modelMatrix * vec4(framePos, 1.);

        vAmbient = color * ambient;
        vAmbient.a = 1.; 
        vColor = color; 

        gl_Position = modelViewProjection * vec4(framePos, 1.);

    --frag shader with hard outline effect, option for posterization, specular highlights
    frag = [[ 
    precision highp float;
    uniform vec4 light; //--directional light direction (x,y,z,0)
    uniform vec4 eye; // -- position of camera (x,y,z,1)

    varying lowp vec4 vColor;
    varying lowp vec4 vAmbient;  
    varying lowp vec4 vNormal;
    varying lowp vec4 vPosition;

    const float posterize = 3.; //layers of posterization
    const float unposterize = 1./posterize;

    //specular highlights
    const float specularPower = 64.; // higher number = smaller highlight
    // const float shine = 8.; 

    //outline thickness
    const float litThickness = 0.2; //line thickness for well lit areas
    const float unlitThickness = 0.3; //line thickness for unlit areas

    void main()

        vec4 viewDirection = normalize(eye - vPosition);
        vec4 nNorm = normalize( vNormal );
        float intensity = max( 0.0, dot( nNorm, light ));

        if (dot(viewDirection, vNormal) < smoothstep(unlitThickness, litThickness, intensity)) 
            gl_FragColor = vAmbient; //vec4(0.,0.,0.,1.); //dark or black outline
            vec4 col = vec4(vColor.rgb, 1.);
            float shine = vColor.a; //shininess is encoded on color alpha
            //specular blinn-phong. Uncomment and add "+ specular" to end of gl_FragColor line below
            vec4 halfAngle = normalize( viewDirection + light );
            float spec = pow( max( 0.0, dot( nNorm, halfAngle)), specularPower );
            vec4 specular =  spec * shine * 8. * col; //

            //gl_FragColor=vAmbient + col * ceil(intensity * posterize) *unposterize + specular ; //posterize colours.
            gl_FragColor=vAmbient + col * intensity + specular; //non-posterized colours. 


Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s