Introduction
Cartoon coloring may be the simplest non-real mode shader. It uses very few colors, usually several tones, so the effect varies with different colors. The result is what we try to achieve:
The tone on the teapot is selected by the cosine of the angle, which refers to the angle between the light and the surface normal. If the angle between the normal and light is small, we use a brighter color. As the angle increases, we gradually use a darker color. In other words, the angle cosine determines the intensity of the tone.
In this tutorial, we will first introduce how to calculate the intensity of a vertex by vertex, then move this computation to the segment shader, and how to access the direction of the light source in OpenGL.
Cartoon coloring-Version 1
This version uses the method of calculating the color intensity by vertex. Later, the segment shader uses the interpolation of the color intensity of the vertex to determine the color of the clip. Therefore, the vertex shader must declare a variable to save the strength value. The segment shader also needs to declare a variable with the same name to receive the strength value after interpolation.
In vertex shader, the light direction can be defined as a local variable or constant, but defining it as a consistent variable gives more flexibility, in this way, you can set it in the OpenGL program. Therefore, we define the direction of light in the shader as follows:
uniform vec3 lightDir;
Now let's assume that the direction of light is defined in the world space.
Vertex shader uses the attribute variable gl_normal to access the specified normal in OpenGL, which is defined by the glnormal function in OpenGL and therefore located in the model space.
If the model is not rotated or scaled in OpenGL, The gl_normal In The World Space passed to the vertex shader is exactly equal to the normal defined in the model space. The outer normal only contains the direction, so it is not affected by the moving transformation.
Because the normal and light directions are defined in the same space, the vertex shader can calculate the cosine directly. The two directions are lightdir and gl_normal. The formula for calculating the cosine is as follows:
Cos (lightdir, normal) = lightdir. Normal/(| lightdir | * | normal |)
"." In the formula indicates the inner product, also known as the dot product. If lightdir and gl_normal have been normalized:
| Normal | = 1
| Lightdir | = 1
The cosine formula can be simplified:
Cos (lightdir, normal) = lightdir. Normal
Because lightdir is provided by OpenGL, we can assume that it has been normalized before it is passed to the shader. Only when the light direction changes, you need to re-calculate the normalization. In addition, the normal passed by the OpenGL program should also be normalized.
We will define a variable named intensity to save the cosine value, which can be calculated directly using the dot function provided by glsl.
intensity = dot(lightDir, gl_Normal);
The final vertex is to transform the vertex coordinates. The complete code of vertex shader is as follows:
uniform vec3 lightDir;varying float intensity;void main(){ intensity = dot(lightDir,gl_Normal); gl_Position = ftransform();}
If you want to use the variables in OpenGL as the direction of light, you can use gl_lightsource [0]. position to replace the consistent variable lightdir. The Code is as follows:
varying float intensity;void main(){ vec3 lightDir = normalize(vec3(gl_LightSource[0].position)); intensity = dot(lightDir,gl_Normal); gl_Position = ftransform();}
The only thing we need to do now in the segment shader is to define the color of the segment based on intensity. As mentioned above, the variable intensity is defined as a variable in both shader, so it will be written in the vertex shader and read in the segment shader. The color in the part shader can be calculated as follows:
vec4 color;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);
We can see that when the cosine is greater than 0.95, the brightest color is used. If the cosine is less than 0.25, the darker color is used. After obtaining the color, you only need to write it into gl_fragcolor. The complete code of the segment shader is as follows:
varying float intensity;void main(){ vec4 color; 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 final effect of this section does not look very good. The main reason is that we perform intensity interpolation. The interpolation result is different from the intensity calculated using the segment normal. The next section will show you how to better implement the cartoon coloring effect.
Cartoon coloring-Version 2
In this section, we want to achieve the cartoon coloring effect by segment. To achieve this, we need to access the normal of each part. In the vertex shader, you need to write the normal of the vertex into a variable that is easy to change. In this way, you can obtain the interpolated normal in the segment shader.
Vertex shader is easier than the previous version, because the color intensity calculation is moved to the segment shader. The consistent variable lightdir should also be moved to the segment shader. below is the new vertex shader code:
varying vec3 normal;void main(){ normal = gl_Normal; gl_Position = ftransform();}
In the segment shader, We need to declare the consistent variable lightdir, and also need a variable to receive the normal after interpolation. The shader code is as follows:
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;}
Is the rendering result:
Surprisingly, the new rendering results are exactly the same as those in the previous section. Why?
Let's take a closer look at the differences between the two versions. In the first version, we calculated an intensity value in the vertex shader, and then used the interpolation result of this value in the segment shader. In the second version, we first interpolation the normal, and then calculate the dot product in the segment shader. Interpolation and dot product are both linear operations, so the order of the two operations does not affect the result.
The real problem is that when the segment shader performs dot product operations on the interpolated normal, although the normal direction is correct, it is not normalized.
We say that the line direction is correct, because we assume that the normal of the passed vertex shader is normalized, and the normal interpolation can get a correct vector in the direction. However, the length of this vector is incorrect in most cases, because a unit-length vector is obtained only when all normal directions are consistent during normalization normal interpolation. (For the question of normal interpolation, the following tutorial will explain it in detail)
In summary, in the segment shader, we receive a normal with the correct length in the direction. To correct this problem, we must normalize this normal. The following is the correct code:
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;}
It is the effect of the new version of cartoon coloring. It looks much more beautiful, although not perfect. As shown in the figure, the object is still somewhat distorted, but this is beyond the scope of this tutorial.
In the next section, we will set the light direction in the shader in the OpenGL program.
Cartoon coloring-Version 3
Before ending the cartoon coloring content, there is another thing to solve: use the light in OpenGL to replace the variable lightdir. We need to define a light source in the OpenGL program, and then use the direction data of this light source in our shader. Note: you do not need to enable this light source with glenable because shader is used.
We assume that the 1 light source (gl_light0) defined in OpenGL is a direction light. Glsl has declared a structure in C language to describe the property of the light source. These structs form an array that stores information about all light sources.
struct gl_LightSourceParameters{ vec4 ambient; vec4 diffuse; vec4 specular; vec4 position; ...};uniform gl_LightSourceParameters gl_LightSource[gl_MaxLights];
This means that we can access the direction of the light source in the shader (using the position field in the struct). Here we still assume that the OpenGL program has normalized the direction of the light source.
The OpenGL Standard specifies that when the position of a light source is determined, it is automatically converted to the eye space coordinate system, such as the camera coordinate system. If the top-left 3 × 3 sub-arrays of the model view matrix are orthogonal (this can be done if glulookat is used and scaling transformation is not used ), this ensures that the direction vector of light is normalized after it is automatically transformed to the viewpoint space.
We must also transform the normal to the viewpoint space, and then calculate the dot product between it and the light. It makes sense to compute the dot product of two vectors in the same space to obtain the cosine value.
To transform the normal to the viewpoint space, we must use the pre-defined mat3 consistent variable gl_normalmatrix. This matrix is the transpose matrix of the inverse matrix of the 3×3 sub-arrays on the top left of the model view matrix (for this question, the following tutorial will explain it specifically ). This transformation needs to be performed on each normal, and the vertex shader is now in the following form:
varying vec3 normal;void main(){ normal = gl_NormalMatrix * gl_Normal; gl_Position = ftransform();}
In the segment shader, We must access the light direction to calculate the intensity value:
varying vec3 normal;void main(){ float intensity; vec4 color; vec3 n = normalize(normal); intensity = dot(vec3(gl_LightSource[0].position),n); 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 shader desinger project in this section:
Http://lighthouse3d.com/wptest/wp-content/uploads/2011/03/toonf2.zip
Source code based on glew:
Http://lighthouse3d.com/wptest/wp-content/uploads/2011/03/toonglut_2.0.zip