CS114 Project 1: WebGL

Due: Thursday Apr 11, 2019 (23:59 pm Pacific Time)

Overview

Download the project codebase here. Please use Piazza for questions and discussions.

The main focus of this project is using WebGL's fragment shader to implement a number of shading models:

To finish all non-optional tasks, the only file you will need to modify is proj1_student.html which contains all the shader code. We recommend using text editors such as vim, emacs, Sublime, or Notepad++ for coding and Google Chrome to run your code (although recent versions of Mozilla Firefox and Microsoft Edge should also work). Remember to keep the console open for seeing Javascript/GL error messages.

The vertex shader has been provided for you:

<!-- Common vertex shader for all shading models -->
<script id="shader-vs" type="x-shader/x-vertex">
    uniform mat4 uMVMatrix;             // Model-view matrix
    uniform mat4 uPMatrix;              // Projection matrix
    uniform mat4 uNMatrix;              // Normal matrix

    attribute vec3 aVertexPosition;     // Vertex position in object space
    attribute vec3 aVertexNormal;       // Vertex normal in object space

    varying vec3 vPosition;             // Vertex position (camera space)
    varying vec3 vNormal;               // vertex normal (camera space)

    void main(void) {
        vec4 camSpacePosition = uMVMatrix * vec4(aVertexPosition, 1.0);
        vPosition = vec3(camSpacePosition);

        gl_Position = uPMatrix * camSpacePosition;

        vec4 camSpaceNormal = uNMatrix * vec4(aVertexNormal, 0.0);
        vNormal = vec3(camSpaceNormal);
    }
</script>

Besides clip-space position of each vertex, the vertex shader also computes camera-space position (vPosition) and normal (vNormal) which will be useful for implementing shading models.

The only fragment shader provided to you simply returns white (i.e., vec4(1.0, 1.0, 1.0, 1.0)) for all fragments:

<script id="shader-fs0" type="x-shader/x-fragment">
    precision mediump float;

    varying vec3 vPosition;             // Vertex position (camera space)
    varying vec3 vNormal;               // Vertex normal (camera space)

    void main(void) {
        // Dummy variable to ensure the use of all vertex attributes.
        vec4 zero = vec4(vPosition + vNormal - vPosition - vNormal, 0.0);

        gl_FragColor = zero + vec4(1.0, 1.0, 1.0, 1.0);
    }
</script>

Do not worry about the dummy variable zero. It is used to ensure that all vertex attributes are acutally used: certain WebGL compilers automatically remove inactive attributes, causing GL Errors when assigning attribute data. You can simply leave zero as is and add it to the fragment color for shaders.

As a result, you will see boring renderings like this:

teapot_white

During this project, you will write fragment shaders generating much more interesting images!

Useful reference: OpenGL ES Shading Language built-in functions

Task 1: Normal, Viewing, and Lighting Directions

To warm up, you first task is to write three very simple shaders that visualize fragment position as well as normal and lighting directions (all in camera space). In proj1_student.html, please use the fragment shader with id shader-fs1-1, namely the <script> element starting with

<script id="shader-fs1-1" type="x-shader/x-fragment">

for position, shader-fs1-2 for normal, and shader-fs1-3 for lighting visualizations.

For your convenience, all needed uniform/varying variables have been pre-declared. You only need to complete the void main() function of each shader. This is also the case for Tasks 2 and 3.

1-1. Position

Let $\mathrm{p}$ be the camera-space position of a fragment (which is stored in a varying variable named vPosition), simply set the fragment color (i.e., gl_FragColor) to $$\mathrm{fragColor} = | \mathrm{p} |,$$ where $| \cdot |$ denotes the absolute-value (i.e., abs()) operation.

Notice that gl_FragColor has the type of vec4 (i.e., 4D vector) while vPosition is vec3. Thus, you will need to use vec4(vPosition, 1.0) to cast vPosition into a 4D vector.

With a correct implementation, you should see the following:

teapot_pos

1-2. Normal Direction

Let $\mathrm{n}$ be a fragment's camera-space normal direction (stored in varying-variable vNormal), set the fragment color to $$\mathrm{fragColor} = \left| \hat{\mathrm{n}} \right|,$$ where $\hat{\mathrm{n}} := \mathrm{n}/ \|\mathrm{n}\|$ with $\| \cdot \|$ denoting the length of a vector. In other words, you need to set the fragment color to (the absolute value of) the normalized version of $\mathrm{n}$ (hint: using the normalize() function for simpler code).

With a correct implementation, you should see the following:

teapot_norm

1-3. Lighting Direction

In this project, the scene only contains one point light source. Let $\mathrm{p}_{\mathrm{light}}$ be its camera-space position (stored in uniform-variable uLightPos). For a fragment with position $\mathrm{p}$, the lighting direction (i.e., the direction from which light comes) is $\mathrm{i} := \mathrm{p}_{\mathrm{light}} - \mathrm{p}$. You need to set the fragment color to $$\mathrm{fragColor} = \left| \hat{\mathrm{i}} \right|,$$ where $\hat{\mathrm{i}} := \mathrm{i}/\|\mathrm{i}\|$. With a correct implementation, you should see the following:

teapot_light

Task 2: Diffuse (Lambertian) Shading Model

The first actual model you need to implement is the Diffuse Shading Model. Please modify the fragment shader with id shader-fs2.

Besides input (uniform/varying) variables used in Task 1, this shader takes three extra uniform variables as input:

Under this shading model while following the notations in Task 1, the fragment color is given by $$ \mathrm{fragColor} = k_d \left(I \cdot \max(\langle \hat{\mathrm{n}}, \hat{\mathrm{i}} \rangle, 0) + c_0\right), $$ where $\langle \cdot , \cdot \rangle$ denotes the dot product of two vectors, and $I$ is the "intensity" of incoming light and equals $$ I := \frac{I_0}{\| \mathrm{i} \|^2/5 + 5}, $$ where $\mathrm{i}$ is the (unnormalized) lighting direction defined in Task 1-3.

With a correct implementation, you should see the following (with default lighting/shading parameters):

teapot_diffuse

Task 3: Phong and Blinn-Phong Shading Models

Now it is time to add some highlights! For this task, you will implement the Phong and the Blinn-Phong, two of the earliest shading models designed for this purpose.

3-1. Phong Shading

For each fragment, the viewing $\hat{\mathrm{o}}$ and reflected $\hat{\mathrm{r}}$ directions (in camera space) are given by $$ \hat{\mathrm{o}} = - \frac{\mathrm{p}}{\| \mathrm{p} \|}, \quad \hat{\mathrm{r}} = 2 \hat{\mathrm{n}} \langle \hat{\mathrm{n}}, \hat{\mathrm{i}} \rangle - \hat{\mathrm{i}}.$$ The fragment color under the Phong shading is $$ \mathrm{fragColor} = \begin{cases} k_d \left( I \cdot \langle \hat{\mathrm{n}}, \hat{\mathrm{i}} \rangle + c_0 \right) + I \cdot \max(\langle \hat{\mathrm{o}}, \hat{\mathrm{r}} \rangle, 0)^{\alpha}, && \langle \hat{\mathrm{n}}, \hat{\mathrm{i}} \rangle > 0,\\ k_d\ c_0, && \mathrm{otherwise}, \end{cases} $$ where $\alpha$ is accessible via a uniform variable named uExponent.

Please edit the fragment shader with id shader-fs3-1 to implement this shading model. You should see the following with default parameter values:

teapot_phong

3-2. Blinn-Phong Shading

For each fragment, the half vector $\hat{\mathrm{h}}$ (between $\hat{\mathrm{i}}$ and $\hat{\mathrm{o}}$) is $$ \hat{\mathrm{h}} := \frac{\hat{\mathrm{i}} + \hat{\mathrm{o}}}{\left\| \hat{\mathrm{i}} + \hat{\mathrm{o}} \right\|}. $$ The fragment color under the Blinn-Phong shading is then given by $$ \mathrm{fragColor} = \begin{cases} k_d \left( I \cdot \langle \hat{\mathrm{n}}, \hat{\mathrm{i}} \rangle + c_0 \right) + I \cdot \langle \hat{\mathrm{n}}, \hat{\mathrm{h}} \rangle^{\alpha}, && \langle \hat{\mathrm{n}}, \hat{\mathrm{i}} \rangle > 0,\\ k_d\ c_0, && \mathrm{otherwise}, \end{cases} $$ where $\alpha$ is accessible via a uniform variable named uExponent.

Please edit the fragment shader with id shader-fs3-2 to implement this shading model. You should see the following with default parameter values:

teapot_blinnphong

Task 4: (Isotropic) Microfacet Shading Model

Finally, let us look at a more advanced model is more physically meaningful (and accurate) than the Phong and the Blinn-Phong models. Microfacet shading models treat the surface of an object as a collection of micro-mirrors characterized by the statistical distribution of their normal directions. You can refer to this paper for more details, but for this task, the model boils down to computing $$ \mathrm{fragColor} = \begin{cases} I \cdot \langle \hat{\mathrm{n}}, \hat{\mathrm{i}} \rangle \left( k_d + \displaystyle\frac{F(\hat{\mathrm{i}}, \hat{\mathrm{h}})\ D(\hat{\mathrm{h}})\ G(\hat{\mathrm{i}}, \hat{\mathrm{o}}, \hat{\mathrm{h}})}{4 \langle \hat{\mathrm{n}}, \hat{\mathrm{i}} \rangle \langle \hat{\mathrm{n}}, \hat{\mathrm{o}} \rangle}\right) + k_d\ c_0, && \langle \hat{\mathrm{n}}, \hat{\mathrm{i}} \rangle > 0,\\ k_d\ c_0, && \mathrm{otherwise}, \end{cases} $$ where

Please edit the fragment shader with id shader-fs4 to implement this model. You should see the following with default parameter values:

teapot_microfacet

Hint: let $\mathrm{u}_1$ and $\mathrm{u}_2$ be two unit vectors and $\theta$ be the angle between them, then it holds that $$ \cos\theta = \langle \mathrm{u}_1, \mathrm{u}_2 \rangle, \qquad \tan^2\theta = \frac{\sin^2 \theta}{\cos^2 \theta} = \frac{1}{\cos^2\theta} - 1 = \frac{1}{\langle \mathrm{u}_1, \mathrm{u}_2 \rangle^2} - 1. $$

Extra Credit: Customizing Specular Color

Warning: This optional task requires significantly more coding than the previous ones. Please ensure the correctness of your implementation for all previous tasks before trying this one!

If you are interested in digging deeper into the provided codebase and doing some general HTML/Javascript programming, consider making the specular color $k_s$, which is currently fixed to one (i.e., white), customizable:

In this case, the shading models for Tasks 3 and 4 become $$ \begin{align} \mathrm{fragColor}_{\mathrm{Phong}} &= \begin{cases} k_d \left( I \cdot \langle \hat{\mathrm{n}}, \hat{\mathrm{i}} \rangle + c_0 \right) + k_s\ I \cdot \max(\langle \hat{\mathrm{o}}, \hat{\mathrm{r}} \rangle, 0)^{\alpha}, && \langle \hat{\mathrm{n}}, \hat{\mathrm{i}} \rangle > 0,\\ k_d\ c_0, && \mathrm{otherwise}; \end{cases}\\[10pt] \mathrm{fragColor}_{\mathrm{Blinn-Phong}} &= \begin{cases} k_d \left( I \cdot \langle \hat{\mathrm{n}}, \hat{\mathrm{i}} \rangle + c_0 \right) + k_s\ I \cdot \langle \hat{\mathrm{n}}, \hat{\mathrm{h}} \rangle^{\alpha}, && \langle \hat{\mathrm{n}}, \hat{\mathrm{i}} \rangle > 0,\\ k_d\ c_0, && \mathrm{otherwise}; \end{cases}\\[10pt] \mathrm{fragColor}_{\mathrm{microfacet}} &= \begin{cases} I \cdot \langle \hat{\mathrm{n}}, \hat{\mathrm{i}} \rangle \left( k_d + k_s \displaystyle\frac{F(\hat{\mathrm{i}}, \hat{\mathrm{h}})\ D(\hat{\mathrm{h}})\ G(\hat{\mathrm{i}}, \hat{\mathrm{o}}, \hat{\mathrm{h}})}{4 \langle \hat{\mathrm{n}}, \hat{\mathrm{i}} \rangle \langle \hat{\mathrm{n}}, \hat{\mathrm{o}} \rangle}\right) + k_d\ c_0, && \langle \hat{\mathrm{n}}, \hat{\mathrm{i}} \rangle > 0,\\ k_d\ c_0, && \mathrm{otherwise}. \end{cases} \end{align} $$

You will need to add one uniform (RGB) variable to each corresponding fragment shader to handle $k_s$. In addition, you should add an additional color picker. This can be achieved by: