Notes for February 4 class -- Introduction to Shaders

3D Coordinate system

WebGL exists in a 3D world:

  • x goes to the right
  • y goes up
  • z goes forward (out of the screen)
This is called a right hand coordinate system.

For now we are going to be doing all of our work starting with the x,y plane. Specifically we will be working with a square that extends from -1 → +1 in both x and y.

 

Square as a triangle strip

Our geometry will be a square.

In WebGL everything is made of triangles, so we will need two triangles.

We define these as a triangle strip.

In a triangle strip, every three successive vertices makes a new triangle, so we will need to specify a total of four vertices, as in the figure to the right.
 

Z-buffer algorithm

The GPU (Graphics Processing Unit) renders using a "Z-buffer algorithm"

For each animation frame, this algorithm consists of two successive steps:

  1. For each vertex:
    The GPU runs a vertex shader to:

    • Find which pixel of the image contains this vertex;
    • Set up "varying" values to be interpolated.

  2. For each triangle:
    The GPU interpolates from vertices to pixels.
    For each pixel:
    The GPU runs a fragment shader to:

    1. Compute color;

    2. If this is the nearest fragment at the pixel, replace color and depth at this pixel in the image.
 

Vector3 object

One data structure that will be very useful is a vector of length 3, which we can use to represent x,y,z coordinates, as seen in the figure to the right.

In Javascript we can define this object via a constructor, which contains all of its properties that can change per instance, as well as a prototype, which contains properties that do not change from one instance to another (such as functions to do such operations as setting values).

 

The Vector3 object will grow in capability as the semester progresses, but to the right is a starter version.

Note that the x,y and z properties, which change from instance to intance, are defined in the constructor itself.

The set property, which will be the same function for all instances, is defined in the prototype.

 
function Vector3() {
   this.x = 0;
   this.y = 0;
   this.z = 0;
}
Vector3.prototype = {
   set : function(x,y,z) {
      this.x = x;
      this.y = y;
      this.z = z;
   },
}

Uniform variables

GLSL (for "GL Shading Language") is the C-like language that is used for shaders on the GPU. One of its key constructs is a uniform variable.

Uniform variables on the GPU have the same value at every pixel. They can (and often do) change over time.

By convention, a uniform variable name starts with the letter 'u'.

For your assignment, I have create some useful uniform variables:

   float uTime;    // time elapsed, in seconds

   vec3  uCursor;  // mouse position and state
                   //    uCursor.x goes from -1 to +1
                   //    uCursor.y goes from -1 to +1
                   //    uCursor.z is 1 when mouse down, 0 when mouse up.
 

Vertex shaders

A vertex shader is a program that you (the application programmer/artist) writes which gets run on the GPU at every vertex. It is written in the special purpose language GLSL.

To the right is a very simple vertex shader program. An "attribute" is a constant value that is passed in from the CPU. In this case, it is aPosition, the x,y,z position of each vertex in the scene. wIt is of type vec3, which means that it consists of three GLSL floating point numbers.

 


   attribute vec3 aPosition;
   varying   vec3 vPosition;
   void main() {
      gl_Position = vec4(aPosition, 1.0);
      vPosition = aPosition;
   }

One of the most powerful things that a vertex shader can do is set "varying" variables. These values are then interpolated by the GPU across the pixels of any triangles that use this vertex. That interpolated value will then be available to fragment shaders at each pixel.

For example, "varying" variable vPosition is set by this vertex shader. By convention, the names of varying variables start with the letter 'v'.

This vertex shader does two things:

  • By setting gl_Position, it determines at which pixel of the image this vertex will appear.

  • It sets varying variable vPosition to equal the attribute position aPosition for this vertex.

Fragment shaders

A fragment shader is a program that you (the application programmer) writes which is run at every pixel.

Because pieces of different triangles can be visible at each pixel (eg: when triangles are very small, or pixels where an edge of one triangle partly obscures another triangle), in general we are really defining the colors of fragments of pixels, which is why these are called fragment shaders.

Since our vertex shader has set a value for vPosition, any fragment shader we write will be able to make use of this variable, whose values will now have been interpolated down to the pixel level.

To the right is a very simple fragment shader, which implements the abstract animation that I showed at the start of class. Note that it makes use of both the uTime and uCursor uniform variables, as it computes the color of this fragment by setting gl_FragColor.  
   precision mediump float;
   uniform float uTime;
   uniform vec3  uCursor;
   varying vec3  vPosition;
   void main() {
      float x = mod(2. * (vPosition.x - uCursor.x * uCursor.z), 1.);
      float y = mod(2. * (vPosition.y - uCursor.y * uCursor.z), 1.);
      gl_FragColor = vec4(x * .5 + .5, .5 + .5 * sin(3. * uTime), y * .5 + .5, 1.);
   }

To the right is the more elaborate fragment shader that we implemented in class. It produces an animated version of the below image.

 





   precision mediump float;
   uniform float uTime;
   uniform vec3  uCursor;
   varying vec3  vPosition;
   void main() {
      vec3 color = vec3(0., 0., 0.);                    // Set background color black.
      float x = vPosition.x;                            // Use only x and y coords of
      float y = vPosition.y;                            //   the square's geometry.
      float rr = (x * x + y * y) / pow(.5, 2.);         // Compute radius squared.
      if (rr < 1.) {                                    // If pixel is on sphere:
         float z = sqrt(1. - rr);                       //    compute z.
         float t = .2 + .5 * max(0., x + y + z);        //    do shading.
         float zSlice = 1. - 3.3 * x + .5 * sin(uTime); //    check for slice.
         if (zSlice < z) {                              //    If pixel is on slice:
            z = zSlice;                                 //       adjust z,
            t = z * z < 1. - rr ? .6 : 0.;              //       check for off shape,
         }                                              //       do flat shading.
         color = vec3(t, t, t);                         //    Make cool easter egg-like
         color.r *= 1. + .2 * sin(30. * (x + .5 * z + .03 * sin(20. * y))); // pattern.
      }
      gl_FragColor = vec4(color, 1.);
   }

Homework

Your assignment for this week is to start with the code in this zip file, which we went over in class, and replace the fragment shader with your own fascinating and wonderful fragment shader.

As you will see when you look at index.html, I took Connor's suggestion from class, and am avoiding the need for quoted strings when specifying shader programs, by describing them within special purpose HTML5 scripts. The vertex shader is in a script of type 'x-shader/x-vertex' and the fragment shader is in a script of type 'x-shader/x-fragment'.

I can pull out the shaders within these scripts, because these strings are just the values of their respective innerHTML properties.