Lab 06: Toon Shading

Goals

By the end of this lab, you will:

  • practice writing shaders in GLSL,
  • control rendering parameters in the WebGL context,
  • implement non-photorealistic rendering (NPR) methods.

The main goal for this lab is to practice with GLSL. We will do so by implementing the Phong reflection model again, but this time, it will be in a shader (specifically, the fragment shader to assign the final pixel color). With a few modifications to this lighting model, we can also add silhouettes, toon-shading and a cool-to-warm shading effect. These are known as non-photorealistic rendering (NPR) techniques.

In terms of new concepts and code to write, this lab will be shorter than previous labs. However, it's our first lab with GLSL and WebGL so it can be hard to debug because we cannot "print/log" in a shader. Please read and try to interpret the cause of the error messages in the Console when your shader does not compile. When an error is reported on line X, try to pinpoint which line this is by starting at the first line of the fragmentShaderSource string and moving to an offset of X lines.

(almost) All of the code you will write in this lab will be in the fragmentShaderSource variable at the top of renderer.js. Feel free to look at the other stuff, which will be covered in the next few weeks. I said "almost" because there is one small change we will make on the JavaScript side with the WebGL context used by the Renderer.

Before proceeding, let's look at how the template is set up. The rightmost dropdown in the HTML file selects the shading algorithm to use. Try changing this to "Phong". You should see the model in blue now. When the selected option in this dropdown changes, it changes a variable in our WebGL program called u_shader. Notice that when u_shader == 0 (the first option in the dropdown, i.e. "None"), then the fragment shader defined in fragmentShaderSource sets the final color (gl_FragColor) to red and returns. Otherwise, it sets gl_FragColor to blue. We'll talk about the details of the u_shader variable next week, but please note the relationship between u_shader and the color we want to set from the following table:

u_shader color to use
$\hspace{30pt}$1 Phong (Part 1 + Part 3)
$\hspace{30pt}$2 Toon (Part 3 + Part 4)
$\hspace{30pt}$3 Cool-Warm (Part 3 + Part 5)

Part 1: implement the Phong reflection model.

The fragment shader defined in fragmentShaderSource currently sets a constant color to the fragment (pixel). Please implement the Phong reflection model in the fragment shader. Use a light that is positioned exactly at the eye (camera) location (please ask yourself: what are the coordinates of eye in the camera coordinate system?). Note that the coordinates of the surface point being processed in the fragment shader (relative to the camera coordinate system) is available in the variable point. The surface normal is available in the variable normal which has already been normalized. Remember to use a unit vector for the direction to the light $\vec{l}$. Please pick values for $c_a$, $c_l$ and $k_m$. The final color should be assigned to gl_FragColor (use a transparency of 1 to make the result opaque).

This part will look a bit strange at first because we need to fix a few things in Part 2 (left-most image below after rotating the model a bit).

Part 2: clear buffers and enable depth testing in the WebGL context.

In the Renderer draw method, note that the gl context is used to set some attributes. Every time we draw, we need to clear the existing buffer of color and depth values. Add the following line after the gl.clearColor call:

gl.clear(gl.COLOR_BUFFER_BIT | gl.DEPTH_BUFFER_BIT);

This will clear the buffers used to store the color and depth of each pixel. When you rotate the model, you should no longer see a cascading effect. However, it still doesn't look quite right (middle image above). This is because we need to enable depth testing. Please see the following documentation and add the appropriate line to enable depth testing.

https://developer.mozilla.org/en-US/docs/Web/API/WebGLRenderingContext/enable

Your rendering should then look like the right-most image above. When this works, please take a screenshot of a model of your choice and add the image to your repl, naming the file phong.png (or .jpg).

Part 3: add silhouettes.

A silhouette is a black border where the model surface normal starts to point away from the observer. This can be added by checking the dot product between the surface normal $\vec{n}$ (normal) and the vector $\vec{v}$ from the surface point $\vec{p}$ (point) to the eye ($\vec{e}$): $\vec{v} = \vec{e} - \vec{p}$. If $\vec{v} \cdot \vec{n} \lt \alpha$, set gl_FragColor to black and return from the fragment shader. Use $\alpha = 0.2$. The left-most image below shows an example of a silhouette, but please note I made the model more transparent to make the silhouette stand out.

Part 4: implement toon shading (E status).

Toon shading can be implemented by "banding" the result of the diffusion term in the Phong reflection model into discrete values. There are various ways to do this. Here we will band the color by resetting the diffusion term according to the following table:

$\cos\theta = \vec{n}\cdot\vec{l}$ value used
0 - 0.25 0
0.25 - 0.5 0.25
0.5 - 0.75 0.5
0.75 - 1.0 0.75

The color returned for toon shading would then be the ambient + diffuse contributions (no need for specular reflection, but you can discretize that as well if you want). You should still use a silhouette for this effect. When this works, please take a screenshot of a model of your choice and add the image to your repl, naming the file toon.png (or .jpg).

Note: there is a one-line way to implement this, but if statements are okay.

Part 5: implement cool-to-warm shading (E status).

Another artistic shading technique is to use a "cool-to-warm" shading model. The idea here is to, again, use the diffusion term to influence the final color. For this technique, please return (store in gl_FragColor):

$$ c = k_w c_w + (1 - k_w)c_c, \quad\mathrm{where}\quad k_w = \frac{1}{2}(1 + \vec{n}\cdot\vec{l}), $$

where $c_w$ is the warm color, $c_c$ is the cooler color, $\vec{n}$ is the unit normal (normal) and $\vec{l}$ is the unit vector from the surface to the light. I used $c_w = (0.8, 0.6, 0.6)$ and $c_c = (0.4, 0.4, 0.7)$ in the right-most image below, but feel free to experiment here. Again, please take a screenshot of a model of your choice and add the image to your repl, naming the file coolwarm.png (or .jpg).

Submission

The initial submission for the lab is due on Wednesday 11/01 at 11:59pm EDT. I will then provide feedback within 1 week so you can edit your submission.

When you and your partner are ready, please submit the assignment on replit and be sure to upload all three images described above. I will then make comments (directly on your repl) and enter your current grade status at the top of the index.html file.

Please also remember to submit your reflection for this week in this Google Form.


© Philip Claude Caplan, 2023 (Last updated: 2023-10-26)