Shaders

This is a tutorial about programming shaders on the web.

Shaders are programs that run on the GPU: a processor that is designed specifically for graphics operations.

A diagram comparing drawing libraries to shaders

In most drawing libraries we draw one shape at a time. With shaders we go through every pixel on the screen and decide what color it should be.

WebGL is the interface for the GPU.

WebGL can be tricky to use because it matches how the GPU works: it can't decide what to do next on its own.

A diagram of WebGL interfacing with the GPU

Drawing with WebGL and shaders is a fundamentally different technique from most drawing libraries.

Threads

All processors work like a group of pipes.

The GPU is a processor with thousands of small pipes, or threads that work asynchronously.

Individual threads can't know what their neighbors are up to.

A diagram comparing processors to groups of pipes.

Rasterization

WebGL uses a rasterization algorithm to turn our model into pixels on the screen.

  1. It requires that all geometry be composed of triangles.

  2. It runs a vertex shader on each vertex of each triangle to position it on the screen.
  3. It removes all triangles that are covered or off-screen.

  4. It runs a fragment shader on every pixel contained in each of the remaining triangles.
A diagram of the rasterization algorithm.

In our demo, we will have three programs:

  1. A JavaScript program running in a browser. It initializes WebGL with programs 2 and 3.
  2. A vertex shader.
  3. A fragment shader.

0 — Setting up the site

Demo

Start by downloading the source of this page here.

Follow the instructions in the README to run the page locally.

The changes for each step are in the gray box:

1 — Initializing the context

Demo

To start drawing with our vertex and fragment shaders, we need to create a new WebGL context.

The context object contains all of the methods that we will use to set up the program. I did some of the boring error handling in the utils.

2 — Creating the buffers

Demo

We want the vertex shader to iterate over our vertex positions, but we haven't yet given it any data to work with.

We use a vertex buffer to store our vertex positions, and an element buffer to store the order in which to traverse the vertices.

Even though vertex positions are 3-dimensional vectors, both the vertex and element buffers are flat arrays. We will tell WebGL to read the buffers in chunks of three components later.

This won't have any visual effect because we haven't drawn the buffers to the canvas yet.

3 — Drawing triangles to the screen

Demo

We have created buffers for our vertex data, now its time to convert the data into pixels in the canvas.

The vertex shader receives the vertex positions from the vertex buffer in an attribute. An attribute is a variable declared in the vertex shader whose memory location can be accessed from the main script.

First we write each element of the vertex buffer to an attribute we declared in the vertex shader.

When the vertex shader is finished processing all of the vertex data, the fragment shader colors in the pixels inside each triangle.

4 — Perspective projection

Demo

The entire canvas is now blue because the edges of the cube line up with the corners of the canvas.

WebGL does not give you a 3d perspective by default; it just ignores the z axis. We need to do some math to project our 3d coordinates into the 2d space of the canvas.

The easiest way to do this is using matrix multiplication. Simply put, matrices perform different transformations on vectors when you multiply them together.

5 — Model rotation

Demo

At this point we can only see the front of the cube. Let's try rotating it.

We rotate the model in the same way we performed the perspective projection, with matrix multiplication.

6 — Animation

Demo

We can animate our scene by re-drawing the frame 60 times per second, and passing in a different global time variable for each frame.

WebGL supports passing global variables to shaders with uniforms. Uniforms are just constants that we set before running the vertex and fragment shaders.

7 — Color

Demo

We can add color to our cube by returning a different color for each pixel in the fragment shader.

We can use a varying to pass information from the vertex shader to the fragment shader. In this case, we give each vertex a color.

The varying will automatically interpolate between vertex values for the given fragment.

8 — ???

Demo

This one is just for fun.

More

Written and edited by Jonas Luebbers.