Help end child hunger

Toon Shader – Version II

 
Prev: Toon - Version I Next: Toon - Version III
 

GLSL has access to part of the OpenGL state. In this tutorial we’ll see how to access the color as set in an OpenGL application with glColor.

GLSL has an attribute variable where it keeps track of the current color.

In this section we will do the toon shader effect per fragment. In order to do that, we need to have access to the fragments normal per fragment. Hence the vertex shader only needs to write the normal into a varying variable, so that the fragment shader has access to the interpolated normal.

The vertex shader gets simplified, since the color intensity computation will now be done in the fragment shader. The uniform variable lightDir also has moved to the fragment shader, since it is no longer used in the vertex shader. See the code bellow for the new vertex shader:

varying vec3 normal;

void main()
{
	normal = gl_Normal;
	gl_Position = ftransform();

}

In the fragment shader we now need to declare the uniform variable lightDir since the intensity is based on this variable. A varying variable is also defined to receive the interpolated normal. The code for the fragment shader then becomes:

uniform vec3 lightDir;
varying vec3 normal;

void main()
{
	float intensity;
	vec4 color;
	intensity = dot(lightDir,normal);

	if (intensity > 0.95)
		color = vec4(1.0,0.5,0.5,1.0);
	else if (intensity > 0.5)
		color = vec4(0.6,0.3,0.3,1.0);
	else if (intensity > 0.25)
		color = vec4(0.4,0.2,0.2,1.0);
	else
		color = vec4(0.2,0.1,0.1,1.0);
	gl_FragColor = color;

}

And the result is:

No, its not a bug! Its the same result as in the previous section. So what happened?

Let’s look closely at the differences between the two versions. In the first version we computed an intensity in the vertex shader and used the interpolated value in the fragment shader. In the second version we interpolated the normal, in the vertex shader, for the fragment shader where we computed the dot product. Interpolation and dot product are both linear operations, so it doesn’t matter if we compute the dot product first and then interpolate, or if we interpolate first and then compute the dot product.

What is wrong in here is the usage of the interpolated normal for the dot product in the fragment shader! And it is wrong because the normal, although it has the right direction, it most likely has a not unit length.

We know that the direction is right because we assumed that the normals that arrived at the vertex shader were normalized, and interpolating normalized vectors, provides a vector with the correct direction. However the length is wrong in the general case because interpolating normalized normals only yields a unit length vector if the normals being interpolated have the same direction, which is highly unlikely in smooth surfaces. See Normalization Issues for more details.

The main reason to move the intensity computation from the vertex shader to the fragment shader was to compute it using the proper normal for the fragment. We have a normal vector that has the correct direction but is not unit length. In order to fix this all we have to do is to normalize the incoming normal vector at the fragment shader. The following code is the correct and complete toon shader:

uniform vec3 lightDir;
varying vec3 normal;

void main()
{
	float intensity;
	vec4 color;
	intensity = dot(lightDir,normalize(normal));

	if (intensity > 0.95)
		color = vec4(1.0,0.5,0.5,1.0);
	else if (intensity > 0.5)
		color = vec4(0.6,0.3,0.3,1.0);
	else if (intensity > 0.25)
		color = vec4(0.4,0.2,0.2,1.0);
	else
		color = vec4(0.2,0.1,0.1,1.0);
	gl_FragColor = color;

}

The result for this version of the toon shader is depicted below. It looks nicer, yet it is not perfect. It suffers from aliasing, but that is outside the scope of this tutorial 😉

 

In the next section we will use an OpenGL light to set the light’s direction of the shader.

 

 

Prev: Toon - Version I Next: Toon - Version III
 

  3 Responses to “Toon Shader – Version II”

  1. Hi. Wonderful tutorial. Any chance you could post one including a black outlines?

  2. Hi,
    I think “normal = gl_Normal;” should be changed to normal = gl_NormalMatrix * gl_Normal;

    • Hi,

      That is true if your light is specified in eye space. In this case both the light direction and normals are specified in global model space (note that in the previous section it was stated that no scales or rotations were done, only translations). In the next section the normal matrix is introduced.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.

%d bloggers like this: