Page 2: Simple Shaders

CS559 Spring 2021 Sample Solution - Workbook 11

Written by CS559 course staff

You can try out the example solutions here. The interesting code is on page 6, page 7 and page 9.

Now that we know what shaders are, let’s look at some simple ones!

There are two things that go beyond theory here: (1) the shaders are written in a specific shader language (GLSL), and (2) there are the details of how we set them up and pass information to them from our “host program” (the JavaScript program we write). Fortunately, THREE.js will take care of #2, but that means we need to learn about its quirks.

A Few Practical Issues

Before we even look at the actual shader programs themselves, we need to think about how we’ll use them on our web pages in JavaScript programs.

Here is a first program - it makes a very boring yellow material that we apply to a sphere and a plane.

The web page is 11-02-01.html. The JavaScript code is in 11-02-01.js - which is pretty standard THREE code (it uses the class framework). The only thing new is that it uses a ShaderMaterial that loads in shaders from the files shaders/11-02-01.vs and shaders/11-02-01.fs. Over the course of this page, we will review all of this. The box is replicated below.

The shaders are written in a programming language called GLSL. As far as the web page and JavaScript are concerned, they are just text (strings). There are many different ways to store these strings. You could just put them as constants in your JavaScript program, or put them on the web page as a hidden block of text that the JavaScript program reads into a variable.

However, it is good practice to put shaders in separate files (so we can edit them more easily - you may want to use the Visual Studio Code Shader Languages extensions). These files can be read in by the JavaScript program. In this workbook, we will always name our shaders as “.vs” for vertex shaders and “.fs” for fragment shaders. The shaders for the first example below are in the files for_students/shaders/11-02-01.vs and for_students/shaders/11-02-01.fs (we are using .fs for fragment shader and .vs for vertex shader).

The CS559 Framework provides support for handling shaders (see the documentation). It loads the files, puts them into variables, and creates a THREE material from them. A nice feature of the framework is that it is asynchronous - your program can run while the files are read and processed. The Framework makes a simple yellow material for your object while the real textures load, and gives a simple red material as an “error”. If your object turns red, rather than showing its proper shader colors, you have an error. Check the console. Note: the framework captures errors in loading the shader file. If you have a compiler error, THREE catches it, and you won’t see your objects.

Once JavaScript has the string of the shader programs, it send them to the GLSL compiler. The GLSL compiler is part of the graphics library (OpenGL). We do not pre-compile the programs - they are compiled on demand. Somewhere in THREE it calls a function compileShader that is part of WebGL.

A warning: the GLSL compiler is notoriously bad about error messages. It sometimes stops at the first minor problem (it’s gotten a little better). Be sure to check the console for error messages.

A First Shader Pair

As you know, we always make shaders in pairs - we’ll need a vertex shader and a fragment shader.

Here is the vertex shader (shaders/11-02-01.vs has comments in it, and comments out what are lines 1-5 here):

1
2
3
4
5
6
7
8
9
uniform mat4 projectionMatrix;
uniform mat4 modelViewMatrix;
attribute vec3 position;

varying vec4 gl_Position;

void main() {
    gl_Position = projectionMatrix * modelViewMatrix * vec4( position, 1.0 );
}

Observe that a GLSL shader program (vertex or fragment) always is the main function, with type void. GLSL syntax is very C-like, so if you are a C programmer, this will look familiar.

GLSL shader programs take their inputs and outputs through “global” variables. By global, these look like global variables in C, and they are available across the file. You can see 3 variables declared on lines 1-3. (although, also note that those declarations are commented out)

This program has three input variables (position, projectionMatrix, and modelViewMatrix). These variables are how the host program passes information to the shader. If we were programming in WebGL directly, we would have to define these in our JavaScript program to tell WebGL how to move data from our JavaScript program to the Shader. Fortunately, THREE takes care of this for us.

In fact, when THREE loads our shader, it adds the declarations at the beginning. We actually leave out lines 1-3 since THREE adds them for us.

Errors in Shader Programs

This is an opportunity to look at what happens when things go wrong.

What if we didn’t comment out those lines 1-3 above? Since THREE has added code to our program that declares them, those lines would re-declare those variables. The GLSL compiler will complain that we are re-declaring the variables.

Try this: edit the file shaders/11-02-01.vs and uncomment the declarations (lines 1-3 of the code above are lines 8-10 of shaders/11-02-01.vs). You should notice that the yellow sphere and plane in the image box (above and below) disappear (you may need to “hard reload” the page). It might be easier to see this if you open the program 11-02-01.html in a separate tab/window.

The objects disappeared because their shaders won’t run. They generated compiler errors. If you look in the console, you’ll see error messages. There are actually a lot of them: we made one error (an error in the shader) and it creates other problems (e.g., there is no valid shader for the object).

THREE is nice enough to to tell us what the compiler’s error message is (“ERROR: 0:65: ‘modelViewMatrix’ : redefinition”), as well as give us the listing of the bad shader. If you look carefully, you’ll see the program is much longer than what we gave it: it includes all of THREE’s declarations at the top of the program. Indeed, we can see where THREE has uniform mat4 modelViewMatrix; (on line 15). What was line 1 of the listing above, and line 8 of our shader file is now line 65 of the “new” shader program that THREE has made.

Also, notice that you get the error multiple times. Because of how THREE works, we not only get an error message the first time we compile the program, but also later when we try to use the broken program (once for each object). And, since the box is duplicated twice on this page, that can cause another copy of the error messages.

You will make errors in your shaders, so learning to find the errors is a useful skill. Here, the error isn’t so hard to find - so fix it (comment out those lines that you uncommented).

But the lesson we interrupted: THREE adds a whole bunch of declarations to your program so that you don’t have to. This can actually get confusing.

If you want to see the list of variables THREE sets up automatically for us, check this page. There is a way to ask THREE not to add this for us, but we won’t use it in this workbook.

All of the other examples in this workbook will leave out things that THREE declares for us.

Variable Declarations and the Vertex Shader

I included the variable declarations (albeit commented out) this time because I want to introduce how to read them. GLSL is a very strongly typed language: you must declare the type of every variable, and it is strict about type errors. For example, you cannot assign an integer to float variable without an explicit cast.

float x = 7;            // this is an error - since 7 is an integer
float y = ((float) 7);  // we need an explicit cast
float z = 7.0;          // or we can just use the correct type

Notice that the variable declarations (lines 1-5) of the shader specify multiple pieces of information for each. If we look at line 1, reading backwards, we are declaring the variable called projectionMatrix which has the type mat4 (a 4x4 matrix). This variable has the qualifier uniform which tells GLSL what type of variable it is. Recall that a uniform variable is one that is constant over the entire group of triangles (which I sometimes call an “object”). Three will take care of passing the uniforms from our JavaScript program.

Line 3 declares the attribute position. Remember that attributes are variables that have a value for each vertex. Again, THREE takes care of getting the attribute from JavaScript to GLSL.

I (and THREE) use the attribute qualifier for attribute variables. Some examples use the qualifier in. It is unclear what the difference is. I use attribute out of habit.

The output of this shader program is setting the variable gl_Position. This is a special variable that all vertex shaders must set. It is of type vec4 (a homogeneous coordinate). Because it is a built in variable, we do not need to declare it (and we’re not allowed to re-declare it, which is why the declaration is commented out). For this first example, I included the declaration to remind you what the variable is, and that it is a varying variable, that is a property of each vertex that is interpolated across the triangle so that it can be used by the fragment shader.

This program takes the position of the vertex (which is a point in 3D), converts it to a homogeneous coordinate (adding a 1 for the w component). And then transforming it by the modeling matrix (the objects matrix that positions it in the world), the view matrix (the transformation that puts things in front of the camera), and the projection matrix (that makes things far away be smaller). The program uses modelViewMatrix, however it could have used modelMatrix and viewMatrix and multiplied them together.

A few things to notice:

  1. GLSL has nice matrix and vector types. And it can put them together in easy ways (we made a 4-vector by adding a number at the end of a 3-vector).
  2. GLSL is picky about numbers. 1 is an integer, 1.0 is a float. It is a type error to give an integer where a float is required.
  3. Because THREE wrote them for us, we don’t see the attribute declaration for position or the uniform declaration for projectionMatrix, and modelViewMatrix. But be aware that they are there.

The Fragment Shader

Now, here is the fragment shader (shaders/11-02-01.fs):

1
2
3
4
void main()
{
    gl_FragColor = vec4(0.8, 0.8, 0.4, 1);
}

This just sets the pixel’s color to yellow. It uses the special output variable gl_FragColor.

Note that in GLSL, colors range from 0-1 (not 0-255, as they do in “byte oriented” systems). Also, note that here I wrote “1” even though I should have written “1.0” - the vec4 (and other vec constructors) are one of the few places where integers can be used where floats are expected.

To make sure that you can read and edit the shaders, change the yellow color to cyan (blue-green). Yes, we give you points for doing this. Make sure you fixed any errors you put into the program (above) so we see a cyan sphere and plane, not a yellow one.

Using Shader Programs

Now that we’ve written the shaders, we need to use them in our THREE program. Basically, we need to make a new kind of material that has these two programs as part of it.

The steps would be:

  1. Read in the files as text. This must be asynchronous - since it may take time to load the files or fetch them from the web.
  2. Create a new THREE ShaderMaterial that uses the text as the shader source code. THREE will run the GLSL compiler on each.
  3. Attach that material to some THREE objects and see our shaders run!

To simplify steps 1 and 2, the CS559 Framework provides a utility that takes 2 URLS (file paths) and makes a ShaderMaterial. You don’t have to use it, but it’s convenient and we will use it for all the examples in the workbook.

There is also a step 2b: check to make sure there were no compilation errors. If there are, you’ll see them in the console. If your object doesn’t show up as expected, you should check.

11-02-01.js (11-02-01.html) is a simple scene that uses the shaders from the previous box.

The line of interest is:

1
2
3
let shaderMat = shaderMaterial("./shaders/11-02-01.vs", "./shaders/11-02-01.fs", {
  side: T.DoubleSide,
});

But the real action happens in the shaders/11-02-01.vs and shaders/11-02-01.fs files.

Make sure you understand all this before going on. Including the shader files.

Our Own Uniforms

In the first shaders, we only used THREE’s variables. Now we can add one of our own. We’ll still have a simple constant-color shader, but we’ll make that “constant” color be a value that we pass from our program via a uniform variable.

For shader pair shaders/11-02-02-1.vs and shaders/11-02-02-1.fs (which we’ll use in this box), the vertex shader doesn’t change (since it doesn’t use the color). We could have used shaders/11-02-01.vs, but we’ve added different comments.

The fragment shader shaders/11-02-02.fs is changed slightly:

1
2
3
4
5
uniform vec3 color;
void main()
{
    gl_FragColor = vec4(color,1.0);
}

Note that we had to declare a new variable (color) as a uniform. This is like a global variable that we set in our host program. It keeps its value for the set of triangles being drawn (the current THREE object).

The only thing remaining is to tell THREE to do the “host program” side of declaring the color variable and setting it to the correct value. We do this by giving the uniforms as a parameter to ShaderMaterial. The shaderMaterial helper function passes parameters through, so in 11-02-02.js (11-02-02.html) we write:

1
2
3
let mat1 = shaderMaterial("./shaders/11-02-02.vs", "./shaders/11-02-02.fs", {
  uniforms: { color: { value: new T.Vector3(0.4, 0.8, 0.8) } },
});

Note that we pass uniforms as a dictionary (hashmap) of variable names (color) and dictionaries with a value key. This is THREE’s format. The value of color is the 3-vector (.4,.8,.8). THREE takes care of the conversion between a JavaScript (THREE) Vector3 and a GLSL vec3. The dictionary of dictionaries is a weird THREE thing - and it is something I get wrong often.

The example in this box (11-02-02.html, 11-02-02.js) has three cubes. One uses the shaders from the previous box (yellow). The next uses this shader with the uniform to make a cyan cube. The third animates the uniform property to make a cube that changes color. Read this code and make sure you understand it before moving on. The shaders are shaders/11-02-02-1.vs and shaders/11-02-02-1.fs.

When you look at the code, you will notice that in addition to passing the color from JavaScript to GLSL, we also pass a float that is the “time.” Right now, this isn’t used for anything. In the code, we computed the color in JavaScript. We could have taken the time and computed the color from that. If you would like some bonus points, re-write the shader so it uses the time variable and causes the color to change accordingly. Change shaders/11-02-02-1.fs, and have it look different than the original (use different colors). Because the middle cube uses the same shader, its color may be different (but understand why its color stays constant).

Passing Attributes And Varying

In the previous box, we passed a value that was constant for the entire object. In this box, we’ll think about vertex properties.

In GLSL, a property of a vertex is called an attribute. Up until now, we’ve seen position. THREE set this up for us.

Setting up attributes is tricky because we also need to arrange for the triangle data to be organized correctly. For this workbook, we’ll let THREE take care of this, and only use the attributes that it has built in. Fortunately, it has the main ones we want (position, normal, texture coordinate, and per-vertex color). See the documentation for the full list.

Our vertex program has access to all of these attributes and can use them to compute properties it wants to pass along to the fragment shader. So, for example, let us send the texture coordinate to the fragment shader so it can use it to color the fragments. We need to extend the vertex shader slightly so it passes the value along:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
varying vec2 v_uv;

void main()
{
    // the main output of the shader (the vertex position)
    gl*Position = projectionMatrix * modelViewMatrix / vec4( position, 1.0 );

    // pass the texture coordinate as well
    v_uv = uv;
}

Note how we declare a new varying variable (v_uv) to pass information between the vertex shader and the fragment shader, and copy the attribute uv we get from THREE into it. The rasterizer will interpolate the values over the area of the triangle.

Because it connects to the JavaScript side (to something built in to THREE), THREE declares the attribute uv for us. We’re on our own to create the varying variables to communicate between our shaders.

The fragment shader is similarly modified - declaring the variable it expects to receive, and using it as two components of the color.

1
2
3
4
5
6
varying vec2 v_uv;

void main()
{
    gl_FragColor = vec4(v_uv, .5, 1);
}

These shaders are in shaders/11-02-03.vs and shaders/11-02-03.fs (with some extra comments). These are utilized in 11-02-03.js (11-02-03.html) and look like:

You might notice how the fragment shader uses the UV vector to make a color. It assigns the u value to red and the v value to green by building up the 4-vector from a 2-vector.

Summary: The basics of shaders

Here we saw some very simple (boring?) shaders. But hopefully, you got the basics of shaders, GLSL and how we fit them into THREE and the CS559 Framework. Make sure you understand how we pass data from JavaScript to our shaders!

We’ll talk more about GLSL on Next: GLSL and THREE .

Page 2 Rubric (6 points total)
Points (4):
Box 11-02-01
4 pt
change the color in 11-02-01.fs from yellow to something else
Bonus points (2):
Box 11-02-02
2 pt
implement the color animation inside the fragment shader using the provided time uniform