Page 10-2: Starting with Simple Shaders

Page 10-2: Starting with Simple Shaders

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.

Box 1: A First Shader Pair

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

I like to put shaders in separate files (so we can edit them more easily). The shaders for this example are in the files Shaders/s0.vs and Shaders/s0.fs (I am using vs for fragment shader and fs for vertex shader). My program loads these files (alternatively, you could put the shader source code into a string in JavaScript - but this means your GLSL program is mixed into your JavaScript program).

Here is the vertex shader (Shaders/s0.vs has comments in it):

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.

Shader programs take their inputs and outputs through variables. Here there are three input variables (position, projectionMatrix, and modelViewMatrix). These variables are how the host program passes information to the shader. They are set up by THREE for us. If you want to see the list of variables THREE sets up automatically for us, check this page. Under the hood, THREE has actually added lines to our GLSL program that declare these variables. If you're unfortunate and have a compiler error, you will get a program listing that includes this.

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).

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.

Now, here is the fragment shader (Shaders/s0.fs):

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.

Box 2: Using these 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. (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 I find it convenient and 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.

In file 2-1-simple.js there is a simple scene that uses the shaders from the previous box.

The line of interest is:

let shaderMat = shaderMaterial("./Shaders/s0.vs","./Shaders/s0.fs",{side:T.DoubleSide});

But the real action happens in the s0.vs and s0.fs files.

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

Box 3: 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 s1.vs and s1.fs (which we'll use in this box), the vertex shader doesn't change (since it doesn't use the color). I could have used s0.vs, but I added different comments.

The fragment shader s1.fs is changed slightly:

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 the JavaScript (2-2-uniforms.js) we write:

let mat1 = shaderMaterial("./Shaders/s1.vs","./Shaders/s1.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.JS'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 example in this box, 2-2-uniforms.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.

Box 4: 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:

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.

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

 varying vec2 v_uv;

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

These shaders are in s2.vs and s2.fs (with some extra comments). The program (2-3-varying.js) looks like:

Summary: The basics of shaders

In this box, 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 the next page.