Skip to content

WebGL - Basics

WebGL (Web Graphics Library) is a JavaScript API for rendering interactive 2D and 3D graphics within any compatible web browser without the use of plug-ins. It’s essentially a low-level API that provides GPU acceleration for graphics rendering directly in the browser, based on the OpenGL ES standard.

To draw anything with WebGL, you need to provide data (like vertex positions and colors) and programs (shaders) that tell the GPU how to process this data and turn it into pixels on the screen. This chapter covers the fundamental concepts.

WebGL – Coordinate System (Normalized Device Coordinates)

Section titled “WebGL – Coordinate System (Normalized Device Coordinates)”

WebGL operates in a 3D coordinate system. After your vertex data is processed by the vertex shader, WebGL expects the coordinates to be within a specific range known as ‘Clip Space’. Coordinates outside this space are ‘clipped’ (not drawn). Clip space coordinates are then transformed into Normalized Device Coordinates (NDC), which range from -1 to +1 on all three axes (X, Y, Z).

Imagine the visible area as a cube centered at (0,0,0):

  • X-axis: -1 is the left edge, +1 is the right edge.
  • Y-axis: -1 is the bottom edge, +1 is the top edge.
  • Z-axis: -1 is the furthest visible point, +1 is the nearest visible point. (Note: This Z-direction might seem counter-intuitive initially, but it relates to how projection matrices are typically set up).

Anything outside this [-1, +1] cube in NDC will not be rendered. Your shader programs are responsible for transforming your model’s coordinates into this Clip Space, which then becomes NDC before finally being mapped to screen pixels (window coordinates).

Once you obtain the WebGL rendering context from an HTML canvas element, you can start using the WebGL API via JavaScript.

Here are key terms:

A vertex is a point in 2D or 3D space, typically defined by its position coordinates (X, Y, and optionally Z). Vertices are the fundamental building blocks of geometric shapes (like triangles, lines, or points). Beyond position, vertices can also have other associated data like color, texture coordinates, or normals (vectors indicating surface direction, used for lighting).

To define a triangle with corners at (0.0, 0.5), (-0.5, -0.5), and (0.5, -0.5) in 2D space (Z is implicitly 0), you’d store these coordinates, usually in a JavaScript Array or TypedArray:

// Using a standard JavaScript Array (often converted to TypedArray later)
const triangleVertices = [
0.0, 0.5, // Top vertex
-0.5, -0.5, // Bottom-left vertex
0.5, -0.5 // Bottom-right vertex
];
// Using Float32Array (preferred for WebGL buffers)
const triangleVerticesTyped = new Float32Array([
0.0, 0.5,
-0.5, -0.5,
0.5, -0.5
]);

This data needs to be transferred to the GPU using WebGL Buffers.

Often, multiple shapes share vertices. For example, a square can be drawn using two triangles that share two vertices. Instead of repeating vertex data, we can list each unique vertex once and then specify the order in which to draw them using indices. An index is simply a numerical reference (like 0, 1, 2…) to a vertex in the vertex list.

Consider a square with vertices: 0=(-0.5, 0.5), 1=(0.5, 0.5), 2=(0.5, -0.5), 3=(-0.5, -0.5). We can draw it with two triangles: (0, 3, 1) and (1, 3, 2).

const squareVertices = new Float32Array([
-0.5, 0.5, // Vertex 0
0.5, 0.5, // Vertex 1
0.5, -0.5, // Vertex 2
-0.5, -0.5 // Vertex 3
]);
// Indices refer to the positions in squareVertices
const squareIndices = new Uint16Array([
0, 3, 1, // First triangle (Top-left, Bottom-left, Top-right)
1, 3, 2 // Second triangle (Top-right, Bottom-left, Bottom-right)
]);

Using indices reduces memory usage and can improve rendering performance. Index data is stored in specific Index Buffers on the GPU.

WebGL requires numerical data (like vertex coordinates or indices) to be in a binary format for efficient transfer to the GPU. JavaScript Typed Arrays (Float32Array, Uint16Array, etc.) provide this mechanism, representing raw binary data buffers.

Buffers are chunks of memory allocated on the GPU to store rendering data. Key types include:

  • Vertex Buffer Objects (VBOs): Store vertex attribute data (positions, colors, texture coordinates, normals). Typically bound to gl.ARRAY_BUFFER.
  • Index Buffer Objects (IBOs): Store vertex indices. Typically bound to gl.ELEMENT_ARRAY_BUFFER.
  • Framebuffers: Represent destinations for rendering, like the screen (default framebuffer) or offscreen textures (custom framebuffers).

You create buffers, bind them, and then upload your TypedArray data into them using WebGL API calls.

Setting up how vertex attributes (like position, color) read data from VBOs involves several WebGL calls (vertexAttribPointer, enableVertexAttribArray). A Vertex Array Object (VAO) encapsulates all this configuration state. By binding a VAO, you restore all the associated attribute settings with a single call. VAOs are mandatory in WebGL 2 and highly recommended for performance in WebGL 1 (via extension or standard in WebGL 2).

A mesh is a collection of vertices, edges, and faces (typically triangles) that defines the shape of a 3D object. In WebGL, you typically render meshes by providing vertex and (optionally) index data in buffers and then making draw calls.

WebGL provides two main draw calls:

  • gl.drawArrays(mode, first, count): Draws primitives using sequential vertices from the currently bound VBOs, starting at index first and using count vertices.
  • gl.drawElements(mode, count, type, offset): Draws primitives using indices from the currently bound IBO. count is the number of indices to use, type is the data type of the indices (e.g., gl.UNSIGNED_SHORT), and offset is the byte offset into the IBO.

The mode parameter in both functions specifies the type of primitive to draw (e.g., gl.TRIANGLES, gl.LINES, gl.POINTS). Complex 3D models are usually constructed as meshes of triangles.