Into Vertex Shaders Addendum 2: A Brief Look at GLSL

Szenia Zadvornykh
10 min readJun 18, 2017

This post will cover some of the basics of writing shaders in GLSL. For a better look at how shaders work, check out my other post in this series.

Shaders in WebGL are written in a language called OpenGL Shading Language, GLSL for short. As the name suggests, it is part of the OpenGL graphics standard, which WebGL is derived from.

If you are looking for WebGL resources, and your search yields no satisfactory results, consider expanding it to include OpenGL. OpenGL has a bigger scope and feature set, but there is still a lot of overlap.

GLSL Syntax

When writing GLSL the first thing you need to remember is to always end a line with a semicolon. If you forget one, your shader will likely crash, but rarely in a way that clearly points to a missing semicolon. As shaders can get highly complex, debugging a error (for hours) only to find out that it was caused by a missing semicolon can be very frustrating (or so I’ve heard…).

The other major difference from JavaScript is the fact that GLSL is strongly typed. GLSL provides a number of built in “classes” for mathematical objects like vectors and matrices. Trying to operate on classes that have no valid interactions, like a 2D vector and a 3D vector, will throw an error.

Critically, this also applies to numbers. There are two types of numbers in GLSL: integers and floats. Integers are round numbers (1, 2, 3, 4, etc). Floats are numbers with a fractal component (1.125, 2.0, 3.142, etc). You cannot use integers and floats in the same operation. This would be heresy!

2.0 + 2.0; // ok!
2 + 2; // ok!
2.0 + 2; // NOT OK!

Next we will take a look at a basic vertex shader, and dissect it word by word.

attribute vec3 position;uniform mat4 modelViewMatrix;
uniform mat4 projectionMatrix;
void main() {
mat4 mvpMatrix = modelViewMatrix * projectionMatrix;
gl_Position = mvpMatrix * vec4(position, 1.0);
}

Shaders have two main parts. The first part (above the main function) is where we declare our inputs (parameters). The second part (inside the main function) is the entry point of the shader. This is where the math happens. Trying to use parameters you have not declared will throw an error. The order of the parameters is important as well, as you cannot reference a parameter before it has been declared.

The void keyword implies that the main method does not have a return value. Instead we use gl_Position to represent the output of the vertex shader. You must declare a gl_Position inside the main function, which makes sense since this is kinda our purpose here. Failure to do so will throw an error. The equivalent of gl_Position in fragment shaders is gl_FragColor Note that the shader execution does not halt after these keywords are used.

Parameter qualifiers

Declaring a parameter has the following syntax:

{qualifier} {type} {name}attribute vec3 position;
uniform mat4 modelViewMatrix;

The qualifier specifies the source of the parameter. This basically tells the GPU where to look for the data associated with the parameter. It’s important to correctly declare the qualifier, as having incorrect qualifiers will cause the GPU to look in the wrong place, which would be rude.

In vertex shaders, the following qualifiers are available:

  • attribute: This is an attribute of the vertex for which the shader is executed. The data will be different for each execution.
  • uniform: This is a uniform value, and will contain the same data for each execution of the shader.
  • varying: This is a secondary output of the vertex shader that will be passed to the fragment shader. The fragment shader can then access an interpolated value for each fragment in the face.
  • const: This is a “static” value. It’s part of the shader, and cannot be set from outside. This is typically used for values that will never change, like the value of PI. Note that an error will be thrown if you declare a const without assigning it a value.

Fragment shaders have access to all of these, except for attribute. Since the fragment shader cannot access vertex attributes, the varying qualifier is used to pass attribute data along. Note that if you are using uniforms, you must declare them in both the vertex shader and the fragment shader. This also applies to uniforms and constants, since shader steps have their own isolated execution scopes.

For variables inside the main function the qualifier should be omitted. These then behave similarly to variables in JavaScript.

Types

Whenever you declare a variable, you must also specify its type. As mentioned before, GLSL has a number of “classes” to math with.

  • Numbers: int, float.
  • Vectors: vec2, vec3, vec4.
  • Matrices: mat2, mat3, mat4.

It’s important to make sure the data you supply to the shader matches the type you specified. Failure to do so will, you guessed it, throw an error.

All of these types behave the same as numbers do in JavaScript. The main advantage of this is that we can use mathematical operators on them, like below:

mat4 mvpMatrix = modelViewMatrix * projectionMatrix;

Note that WebGL does not support the modulus operator (%).

This makes it much easier to express mathematical operations, which is pretty cool. This power does have limits however: you can only use operators on appropriate types.

gl_Position = mvpMatrix * vec4(position, 1.0);

In this line we convert our position attribute (which is a vec3) to a vec4 in order to multiply it by the mvpMatrix (which is a mat4). We have to do this because you cannot multiply a mat4 and a vec3. Trying to do so will throw an error. As a rule, the size of vectors and matrices being operated on must always be the same.

Vector Constructors

vec4(position, 1.0) is a constructor for a vec4. GLSL constructors like this offer quite a lot of flexibility. In this example, the x, y, and z components of the position vector are destructured, and passed to the new vector. Then the fourth component (w) is set to 1.0. Note that this is a float, not an integer. Writing vec4(position, 1) would throw an error, since you cannot put an integer into a float vector. They just don’t get along.

Below are some other ways a vector can be constructed.

vec4 p = vec4(1.0, 2.0, 3.0, 4.0);
vec4 p = vec4(myVec2, 3.0, 4.0);
vec4 p = vec4(myVec2, myVec2);
vec4 p = vec4(1.0, myVec3);

The same logic applies to other the vector types. Trying to create a vector with an incorrect number of values will throw an error.

Vector Components

Once constructed, the vector components can be accessed the same way as in JavaScript: p.x, p.y, p.z and p.w. Since vectors are also used to represent colors, the corresponding components can likewise be accessed using p.r, p.g, p.b and p.a.

You can also access multiple components at once, which implicitly creates a vector of the appropriate size.

vec4 p = vec4(myVec4.xyz, 1.0);

Components can be accessed in any given order, which is referred to as swizzling. So flexible!

vec4 p = vec4(myVec4.zxy, 1.0);

GLSL Arrays

If you want to get fancy, you can also declare arrays. These arrays are also typed, which means they can only contain members of the same type. The line below creates an array of 6 floats.

float myArray[](1.0, 2.0, 3.0, 4.0, 5.0, 6.0);

Declaring an array you intend to populate from outside the shader looks like this:

uniform float myArray[6];

Members of arrays can be accessed the same way as in JavaScript.

float a = myArray[0]; // a === 1.0

Functions

GLSL also supports functions!

float decimate(float x) {
return x * 0.9;
}
void main() {
float x = 1.0;

x = decimate(x);

// x is now 0.9!
}

As you can see, the syntax is fairly similar to JavaScript. The main difference is that functions, like variables, must declare a type. The value you return must then be of the same type as specified. If your function does not return a value, the return type must be specified as void. Any functions you declare should be placed above the main function (otherwise you cannot use them inside the main function).

GLSL functions are scoped similarly to JavaScript. If you declare a variable inside the function, you can only access it inside that function. Note however that functions in GLSL cannot be nested.

Argument Qualifiers

If you want to get really fancy, you can use special qualifiers that change how arguments for a function behave.

void decimate(inout float x) {
x *= 0.9;
}

Whoa! What’s happening here?

By default, any arguments you pass to a function are passed by value. This essentially means that the value is copied into the argument, and the variable you passed to the function cannot be modified by it.

Using the inout qualifier, the argument is instead passed by reference, which means that the original variable can be changed directly.

void main() {
float x = 1.0;

decimate(x);

// x is now 0.9!
}

One use case for this is creating functions that “return” multiple values.

void decimateTwo(inout float x, inout float y) {
x *= 0.9;
y *= 0.9;
}

Another qualifier you can use is out.

void decimate(in float x, out float y) {
y = x * 0.9;
}
void main() {
float x = 1.0;
float y = 100.0;
decimate(x, y);

// x is still 1.0!
// y is now 0.9!
}

When using out, the initial value of the argument is ignored, but it can be set by the function. The in qualifier used for the first argument is the default, so it is usually omitted.

Optional Arguments?

Before we answer that, I should note that names of variables must be unique. Using the same name more than once in the same scope will throw an error.

Functions have a slight exception to this rule: their arguments are part of the function signature, which means that you can declare multiple functions with the same name, as long as they have different arguments. Functions do not support optional arguments, but the same effect can be achieved using different signatures (albeit with some additional overhead).

float easeQuadIn(float t) {
return t * t;
}
float easeQuadIn(float t, float b, float c, float d)
{
return b + easeQuadIn(t / d) * c;
}

In this example, I use two different signatures for an easing function. The first takes an argument t between 0.0 and 1.0, and returns an eased value. The second uses the four classic Penner easing arguments, and calls the first function internally. Because these functions have different arguments, their signatures are different, and they can coexist within the same scope.

The order in which these functions are declared is important. The second easing function uses the first, which would not be possible if they were declared the other way around.

Other than the number of arguments, the type of the arguments is also part of the signature, meaning you can declare multiple functions with the same name that operate on different types.

vec3 decimate(vec3 x){
return x * 0.9;
}

Built-in functions

Aside from custom functions, GLSL also provides a number of functions that cover common mathematical operations. The types mentioned so far do not have any methods themselves. Instead you use the built in functions to operate on variables.

One of these functions is mix, which linearly interpolates between two values based on a float value between 0.0 and 1.0.

vec3 mixed = mix(v0, v1, 0.5);

Many of these built-in functions operate on several types. Check here for a full list of functions available in OpenGL. Note however that some of these may not work in WebGL. For a list of functions (and other things) that are definitely available, check the last two pages of this handy reference card.

Loops & Conditions

GLSL supports loops and conditional statements. The syntax for these is similar to JavaScript (but since GLSL is strongly typed, there is no need for the strict equality operator (===)). Using loops and conditionals is however generally discouraged, as it can create complications for how the shader is executed depending on your hardware.

Defines

Finally you may encounter defines. These are usually declared at the very start of a shader. They are used for values that are set once when the shader compiles, and do not change afterward. One of the defines Three.js uses is NUM_DIR_LIGHTS, which is set to the number of directional lights added to the scene. This is then used to correctly set the size of an array where these lights are stored.

#define NUM_DIR_LIGHTS 6;uniform DirectionalLight directionalLights[ NUM_DIR_LIGHTS ];

In Three.js defines like this are dynamically determined and injected into the shader. If you want to add your own (using a THREE.ShaderMaterial), you can use the defines array in the constructor.

Shaders in Three.js

In WebGL, shaders are sent to the GPU as strings before being compiled. While this can be a little cumbersome to work with, it does offer a lot of flexibility. There are a number of ways you can deal with shaders, as long as they can be converted to strings in the end.

One way is to use HTML script tags, and send their textContent to be compiled. You can see an (old) example of this approach here.

Another is to use an array of strings, and concatenate them into a single string delimited by new-line characters. This is the basis for how shaders are used in Three.js. Three.js has multiple materials, all of which have their own corresponding shaders. However, some functionality is shared between different materials. To facilitate this, Three.js shaders are broken down into chunks, and combined in different ways for the final material shaders. My Three.js extension builds on this principle by injecting my own chunks into the mix.

If you can use ES6 for your project, you can use a multi-line template string instead of relying on arrays. This is a cleaner approach, and is much easier to maintain and edit.

Depending on your development environment, there are also ways to load standalone *.glsl files, and bundle their content with your JavaScript.

This concludes my brief overview of some of the features and constraints of GLSL. There are more things to know than covered here, but this overview should help you deal with some of the quirks of GLSL you may encounter when exploring the endless possibilities of shaders.

--

--

Szenia Zadvornykh

Creative coder with an interest in art and game design. Tech Lead @dpdk NYC.