CS559 Spring 2021 Sample Solution - Workbook 11
Written by CS559 course staff
Now that we know what shaders are, let’s look at some simple ones!
A Few Practical Issues
Here is a first program - it makes a very boring yellow material that we apply to a sphere and a plane.
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.
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.
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):
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 (
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
Line 3 declares the
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
viewMatrix and multiplied them together.
A few things to notice:
- 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).
- GLSL is picky about numbers.
1is an integer,
1.0is a float. It is a type error to give an integer where a float is required.
- Because THREE wrote them for us, we don’t see the
modelViewMatrix. But be aware that they are there.
The Fragment Shader
Now, here is the fragment shader (shaders/11-02-01.fs):
This just sets the pixel’s color to yellow. It uses the special output variable
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:
- 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.
- Create a new THREE
ShaderMaterialthat uses the text as the shader source code. THREE will run the GLSL compiler on each.
- 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.
The line of interest is:
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:
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 helper function passes parameters through, so in 11-02-02.js (11-02-02.html) we write:
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.
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:
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.
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.
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
We’ll talk more about GLSL on Next: GLSL and THREE .