Program Structure

You may need to read the article about shaders before this one.


Most of the WebGL API is dedicated to setting up state for shader programs. There are very few functions that actually rasterize.

Initialization

The initialization step is executed once at the beginning of the program. It is typically used to get the rendering context, compile shaders, link shader programs, and prepare static data.

Rasterization

The rasterization step is executed once for each frame, meaning once every time that the rendering context is updated. The frame rate is the speed at which new frames are rasterized. The frame rate typically depends on how fast the GPU can execute the given shader programs, up to a certain cap.

The viewport specifies the affine transformation of the horizontal and vertical axes from clip space to screen space, which is a coordinate system that represents the rendering context in the ranges [0,x][0,x] and [0,y][0,y] for horizontal and vertical coordinates, respectively, where xx is the width of the rendering context in pixels and yy is the height of the rendering context in pixels. In other words, the viewport determines how clip space coordinates are mapped to pixels.

The rasterization step is typically used to clear and resize the viewport, modify dynamic data, and rasterize primitives.

The requestAnimationFrame function can be used to execute the rasterization step as fast as the GPU can (up to a certain cap). Since the frame rate depends on the user's hardware, it is sometimes necessary to calculate it to make animations consistent across all devices. The following example calculates the frame rate in frames per second as frameRate:

let then = 0;

function rasterizationStep(now: number): void {
	requestAnimationFrame(rasterizationStep);

	const deltaTime: number = now - then;
	then = now;
	const frameRate = 1000 / deltaTime;
}
requestAnimationFrame(rasterizationStep);

Resizing the Viewport

It is a good idea to resize the viewport every frame in case the rendering context resizes. If the viewport is not updated to match the rendering context, the rasterized primitives will appear blurry and/or stretched.

The rendering context has two sizes: the physical size of the canvas, which is set by CSS, and the size of the draw buffer in pixels. If the size of the draw buffer does not match the physical size of the rendering context, the rasterized primitives will appear blurry and/or stretched.

The following code resizes the drawing buffer to match the canvas, then resizes the viewport to match the drawing buffer with viewport:

canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;

gl.viewport(0, 0, canvas.width, canvas.height);

Clearing Color Buffers

Before rasterizing a new frame, color buffers should be cleared of the fragments from the previous frame. First, a value can be set for fragments to be cleared to with clearColor. Then, WebGL needs to be told which buffers to clear with clear. For example, in order to clear the viewport to transparent black fragments, the following code would be used:

gl.clearColor(0, 0, 0, 0);
gl.clear(gl.COLOR_BUFFER_BIT);

Cheat Sheet

If you're following this tutorial series in order, you won't know what most of the following cheat sheet means yet.

A typical program that uses the WebGL API follows this structure:

  • Initialization step:
    • Get the rendering context.
    • Compile shaders.
    • Link shader programs.
    • Look up variable locations.
    • Create and fill buffers.
    • Create VAOs. For each attribute in each VAO:
      • Enable the attribute.
      • Bind a buffer to the attribute.
      • Specify the layout of the buffer.
    • Create and fill textures.
  • Rasterization step:
    • Resize the drawing buffer.
    • Resize the viewport.
    • Set global state.
    • Clear color buffers.
    • For each VAO:
      • Use its shader program.
      • Bind the VAO.
      • Set uniforms.
      • Assign textures to texture units.
      • Rasterize.

The next article is about attributes.