Shaders

You may need to read the introduction article before this one.


WebGL runs on the GPU, so it needs to be provided with code that the GPU is capable of understanding. The code is supplied in the form of functions called shaders. There are two types of shaders, each with a specific purpose:

  • Vertex shaders run once for each vertex. Their purpose is to compute the position of the vertex.
  • Fragment shaders run once for each fragment (pixel). Their purpose is to compute the color of the fragment.

Whenever a primitive is rasterized, the vertex shader is first executed once for each vertex in the primitive. After all of the vertex positions have been computed, the fragment shader is then executed once for each fragment in the area that is bounded by those vertices.

Both types of shaders return vec4 values, which are vectors that contain four floating-point values. In order, the four values are referred to as x, y, z, and w.

  • The return value of the vertex shader represents the position of the vertex in clip space, which is a coordinate system that represents the rendering context in the range [1,1][-1,1]. Everything outside of clip space is clipped (not rasterized). The negative boundaries for each direction are left, down, and near, respectively. The first three values are automatically divided by the fourth.
  • The return value of the fragment shader represents the color of the fragment in color space, which organizes the red, green, blue, and alpha values of the color in the range [0,1][0,1].

GLSL

WebGL shaders are written in a strictly-typed language called GLSL. The specification for GLSL ES 3.00 (the version used by WebGL2) can be found here.

Every shader defines a function called main that is executed when the shader is executed. This method is responsible for setting the return value of the shader.

  • For vertex shaders, the return value is set with a special variable called gl_Position.
  • Fragment shaders can have many return values, so output variables must be defined using the out keyword.

The fragments returned by a fragment shader constitute a bitmap image that is stored in a portion of contiguous memory called a framebuffer. A framebuffer can have multiple regions that hold color data called color buffers. Each color buffer corresponds to an output variable in a fragment shader. The draw buffer is the color buffer that contains the fragments that are rasterized to the rendering context.

In order to specify that a shader should use version ES 3.00 of GLSL, its first line must be #version 300 es. If this is not the first line in a WebGL2 shader, it will not compile.

A minimal WebGL2 vertex shader looks like this:

#version 300 es

void main() {
	gl_Position = vec4(0, 0, 0, 1);
}

A minimal WebGL2 fragment shader looks like this:

#version 300 es

precision highp float;

out vec4 outColor;

void main() {
	outColor = vec4(0, 0, 0, 1);
}

The line precision highp float; in the fragment shader sets the precision of floating-point values. It is not necessary in the vertex shader because the vertex shader has a default precision for floating-point values.


The WebGL API can be used to compile shaders. First, a WebGLShader object must be created with createShader. Then, it must be assigned source code with shaderSource and compiled with compileShader. Optionally, the shader status can be checked with getShaderParameter and logged with getShaderInfoLog. For example:

const vertexShaderSource = `\
#version 300 es

void main() {
	gl_Position = vec4(0, 0, 0, 1);
}`;

const vertexShader: WebGLShader = gl.createShader(gl.VERTEX_SHADER);
gl.shaderSource(vertexShader, vertexShaderSource);
gl.compileShader(vertexShader);

const compileStatus: boolean = gl.getShaderParameter(
	vertexShader,
	gl.COMPILE_STATUS
);
if (!compileStatus) {
	const infoLog: string = gl.getShaderInfoLog(vertexShader);
	throw new Error(infoLog);
}

Shader Programs

One vertex shader and one fragment shader can be linked together to create a shader program, which can be used to rasterize primitives. The process for creating a shader program with the WebGL API is very similar to the process for creating a shader. First, a WebGLProgram must be created with createProgram. Then, it must be assigned a vertex shader and a fragment shader with attachShader and linked together with linkProgram. Optionally, the shader program status can be checked with getProgramParameter and logged with getProgramInfoLog. For example:

const program: WebGLProgram = gl.createProgram();
gl.attachShader(program, vertexShader);
gl.attachShader(program, fragmentShader);
gl.linkProgram(program);

const linkStatus: boolean = gl.getProgramParameter(program, gl.LINK_STATUS);
if (!linkStatus) {
	const infoLog: string = gl.getProgramInfoLog(program);
	throw new Error(infoLog);
}

The next article is about program structure.