Blog & Insights

Thoughts, tutorials, and explorations at the intersection of mathematics, visualization, and web development.

Exploring WebGL Fundamentals

Published: March 25, 2025Category: Web Development

Exploring WebGL Fundamentals

WebGL (Web Graphics Library) is a JavaScript API that enables web browsers to render high-performance 3D and 2D graphics without plugins. By leveraging the GPU, WebGL brings hardware-accelerated graphics to the web platform, opening up possibilities for immersive games, data visualizations, and interactive experiences.

Understanding WebGL's Architecture

WebGL is based on OpenGL ES (Embedded Systems), a subset of OpenGL designed for mobile devices and embedded systems. It provides a low-level interface to the GPU, giving developers precise control over rendering.

WebGL 1.0 was released in 2011 and is based on OpenGL ES 2.0. WebGL 2.0, released in 2017, is based on OpenGL ES 3.0 and offers additional features like 3D textures, transform feedback, and instanced rendering.

The WebGL Pipeline

The WebGL rendering pipeline consists of several stages:

  1. JavaScript Code: Sets up the scene, geometry, and shaders
  2. Vertex Shader: Processes each vertex (position, color, etc.)
  3. Primitive Assembly: Forms primitives (points, lines, triangles)
  4. Rasterization: Converts primitives to fragments (pixels)
  5. Fragment Shader: Processes each fragment (color, depth, etc.)
  6. Framebuffer Operations: Performs tests and blending before writing to the framebuffer

Setting Up a WebGL Context

To start using WebGL, you need to obtain a WebGL context from a canvas element:

1// Get the canvas element
2const canvas = document.getElementById('webgl-canvas');
3
4// Get the WebGL context
5const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
6
7if (!gl) {
8  console.error('WebGL not supported');
9}
10

Clearing the Canvas

Before drawing, you typically clear the canvas:

1// Set clear color (RGBA)
2gl.clearColor(0.0, 0.0, 0.0, 1.0);
3
4// Clear the color buffer
5gl.clear(gl.COLOR_BUFFER_BIT);
6

Understanding Shaders

Shaders are small programs that run on the GPU. WebGL requires two types of shaders:

  1. Vertex Shader: Processes each vertex
  2. Fragment Shader: Processes each pixel

Shaders are written in GLSL (OpenGL Shading Language), a C-like language.

Vertex Shader Example

1// Vertex shader
2attribute vec4 a_position;
3attribute vec4 a_color;
4
5uniform mat4 u_modelViewMatrix;
6uniform mat4 u_projectionMatrix;
7
8varying vec4 v_color;
9
10void main() {
11  // Transform the vertex position
12  gl_Position = u_projectionMatrix * u_modelViewMatrix * a_position;
13  
14  // Pass the color to the fragment shader
15  v_color = a_color;
16}
17

Fragment Shader Example

1// Fragment shader
2precision mediump float;
3
4varying vec4 v_color;
5
6void main() {
7  // Set the fragment color
8  gl_FragColor = v_color;
9}
10

Compiling and Linking Shaders

To use shaders in WebGL, you need to compile and link them:

1// Create shader function
2function createShader(gl, type, source) {
3  const shader = gl.createShader(type);
4  gl.shaderSource(shader, source);
5  gl.compileShader(shader);
6  
7  // Check if compilation was successful
8  const success = gl.getShaderParameter(shader, gl.COMPILE_STATUS);
9  if (success) {
10    return shader;
11  }
12  
13  console.error(gl.getShaderInfoLog(shader));
14  gl.deleteShader(shader);
15}
16
17// Create program function
18function createProgram(gl, vertexShader, fragmentShader) {
19  const program = gl.createProgram();
20  gl.attachShader(program, vertexShader);
21  gl.attachShader(program, fragmentShader);
22  gl.linkProgram(program);
23  
24  // Check if linking was successful
25  const success = gl.getProgramParameter(program, gl.LINK_STATUS);
26  if (success) {
27    return program;
28  }
29  
30  console.error(gl.getProgramInfoLog(program));
31  gl.deleteProgram(program);
32}
33
34// Create shaders
35const vertexShader = createShader(gl, gl.VERTEX_SHADER, vertexShaderSource);
36const fragmentShader = createShader(gl, gl.FRAGMENT_SHADER, fragmentShaderSource);
37
38// Create program
39const program = createProgram(gl, vertexShader, fragmentShader);
40
41// Use the program
42gl.useProgram(program);
43

Working with Buffers and Attributes

WebGL uses buffers to store vertex data and attributes to access that data in shaders.

Creating and Binding Buffers

1// Create a buffer for positions
2const positionBuffer = gl.createBuffer();
3
4// Bind the buffer
5gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
6
7// Set data in the buffer
8const positions = [
9  -0.5, -0.5,  // Vertex 1
10   0.5, -0.5,  // Vertex 2
11   0.0,  0.5   // Vertex 3
12];
13gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(positions), gl.STATIC_DRAW);
14

Setting Up Attributes

1// Get the attribute location
2const positionAttributeLocation = gl.getAttribLocation(program, 'a_position');
3
4// Enable the attribute
5gl.enableVertexAttribArray(positionAttributeLocation);
6
7// Tell the attribute how to get data from the buffer
8gl.vertexAttribPointer(
9  positionAttributeLocation,
10  2,           // 2 components per vertex (x, y)
11  gl.FLOAT,    // Data type
12  false,       // Don't normalize
13  0,           // Stride (0 = auto)
14  0            // Offset
15);
16

Drawing with WebGL

WebGL provides several drawing functions, with the most common being drawArrays and drawElements.

Drawing with drawArrays

1// Draw a triangle
2gl.drawArrays(
3  gl.TRIANGLES,  // Primitive type
4  0,             // Starting index
5  3              // Number of vertices
6);
7

Drawing with drawElements

For more complex geometry, you can use indexed drawing:

1// Create an index buffer
2const indexBuffer = gl.createBuffer();
3gl.bindBuffer(gl.ELEMENT_ARRAY_BUFFER, indexBuffer);
4
5// Set index data
6const indices = [
7  0, 1, 2,  // First triangle
8  0, 2, 3   // Second triangle
9];
10gl.bufferData(gl.ELEMENT_ARRAY_BUFFER, new Uint16Array(indices), gl.STATIC_DRAW);
11
12// Draw with indices
13gl.drawElements(
14  gl.TRIANGLES,       // Primitive type
15  indices.length,     // Number of indices
16  gl.UNSIGNED_SHORT,  // Index type
17  0                   // Offset
18);
19

Transformations and Matrices

3D graphics rely heavily on matrix transformations for positioning, rotating, and scaling objects.

Creating Transformation Matrices

You can use libraries like gl-matrix to create transformation matrices:

1// Create matrices
2const modelViewMatrix = mat4.create();
3const projectionMatrix = mat4.create();
4
5// Set up model-view matrix
6mat4.translate(modelViewMatrix, modelViewMatrix, [0.0, 0.0, -5.0]);
7mat4.rotate(modelViewMatrix, modelViewMatrix, time, [0, 1, 0]);
8
9// Set up projection matrix
10mat4.perspective(projectionMatrix, 
11  45 * Math.PI / 180,  // Field of view
12  canvas.width / canvas.height,  // Aspect ratio
13  0.1,  // Near clipping plane
14  100.0  // Far clipping plane
15);
16

Passing Matrices to Shaders

1// Get uniform locations
2const modelViewMatrixLocation = gl.getUniformLocation(program, 'u_modelViewMatrix');
3const projectionMatrixLocation = gl.getUniformLocation(program, 'u_projectionMatrix');
4
5// Set uniform values
6gl.uniformMatrix4fv(modelViewMatrixLocation, false, modelViewMatrix);
7gl.uniformMatrix4fv(projectionMatrixLocation, false, projectionMatrix);
8

Textures and Texture Mapping

Textures allow you to apply images to 3D objects, adding detail and realism.

Loading and Creating Textures

1// Create a texture
2const texture = gl.createTexture();
3gl.bindTexture(gl.TEXTURE_2D, texture);
4
5// Fill the texture with a placeholder color until the image loads
6gl.texImage2D(
7  gl.TEXTURE_2D,
8  0,
9  gl.RGBA,
10  1,
11  1,
12  0,
13  gl.RGBA,
14  gl.UNSIGNED_BYTE,
15  new Uint8Array([255, 0, 255, 255])  // Magenta
16);
17
18// Load an image
19const image = new Image();
20image.src = 'texture.jpg';
21image.onload = function() {
22  // Now that the image has loaded, copy it to the texture
23  gl.bindTexture(gl.TEXTURE_2D, texture);
24  gl.texImage2D(
25    gl.TEXTURE_2D,
26    0,
27    gl.RGBA,
28    gl.RGBA,
29    gl.UNSIGNED_BYTE,
30    image
31  );
32  
33  // Check if the image is a power of 2 in both dimensions
34  if (isPowerOf2(image.width) && isPowerOf2(image.height)) {
35    // Generate mipmap for power-of-2 textures
36    gl.generateMipmap(gl.TEXTURE_2D);
37  } else {
38    // Set parameters for non-power-of-2 textures
39    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
40    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
41    gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
42  }
43};
44
45function isPowerOf2(value) {
46  return (value & (value - 1)) === 0;
47}
48

Using Textures in Shaders

Vertex Shader:

1attribute vec4 a_position;
2attribute vec2 a_texcoord;
3
4uniform mat4 u_modelViewMatrix;
5uniform mat4 u_projectionMatrix;
6
7varying vec2 v_texcoord;
8
9void main() {
10  gl_Position = u_projectionMatrix * u_modelViewMatrix * a_position;
11  v_texcoord = a_texcoord;
12}
13

Fragment Shader:

1precision mediump float;
2
3varying vec2 v_texcoord;
4
5uniform sampler2D u_texture;
6
7void main() {
8  gl_FragColor = texture2D(u_texture, v_texcoord);
9}
10

Advanced WebGL Techniques

Lighting

Implementing lighting in WebGL requires calculating normals and implementing lighting equations in shaders:

Vertex Shader with Lighting:

1attribute vec4 a_position;
2attribute vec3 a_normal;
3
4uniform mat4 u_modelViewMatrix;
5uniform mat4 u_projectionMatrix;
6uniform mat4 u_normalMatrix;  // Inverse transpose of model-view matrix
7
8uniform vec3 u_lightDirection;
9uniform vec4 u_lightColor;
10uniform vec4 u_ambientLight;
11uniform vec4 u_materialColor;
12
13varying vec4 v_color;
14
15void main() {
16  // Transform position
17  gl_Position = u_projectionMatrix * u_modelViewMatrix * a_position;
18  
19  // Transform normal
20  vec3 normal = normalize(vec3(u_normalMatrix * vec4(a_normal, 0.0)));
21  
22  // Calculate lighting
23  float nDotL = max(dot(normal, u_lightDirection), 0.0);
24  vec4 diffuse = u_lightColor * u_materialColor * nDotL;
25  vec4 ambient = u_ambientLight * u_materialColor;
26  
27  v_color = ambient + diffuse;
28}
29

Framebuffers for Off-screen Rendering

Framebuffers allow you to render to a texture instead of the canvas:

1// Create a framebuffer
2const framebuffer = gl.createFramebuffer();
3gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
4
5// Create a texture to render to
6const texture = gl.createTexture();
7gl.bindTexture(gl.TEXTURE_2D, texture);
8gl.texImage2D(
9  gl.TEXTURE_2D,
10  0,
11  gl.RGBA,
12  512,  // Width
13  512,  // Height
14  0,
15  gl.RGBA,
16  gl.UNSIGNED_BYTE,
17  null
18);
19gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.LINEAR);
20gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_S, gl.CLAMP_TO_EDGE);
21gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_WRAP_T, gl.CLAMP_TO_EDGE);
22
23// Create a depth buffer
24const depthBuffer = gl.createRenderbuffer();
25gl.bindRenderbuffer(gl.RENDERBUFFER, depthBuffer);
26gl.renderbufferStorage(gl.RENDERBUFFER, gl.DEPTH_COMPONENT16, 512, 512);
27
28// Attach the texture and depth buffer to the framebuffer
29gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, texture, 0);
30gl.framebufferRenderbuffer(gl.FRAMEBUFFER, gl.DEPTH_ATTACHMENT, gl.RENDERBUFFER, depthBuffer);
31
32// Check if framebuffer is complete
33if (gl.checkFramebufferStatus(gl.FRAMEBUFFER) !== gl.FRAMEBUFFER_COMPLETE) {
34  console.error('Framebuffer not complete');
35}
36
37// Render to the framebuffer
38gl.bindFramebuffer(gl.FRAMEBUFFER, framebuffer);
39gl.viewport(0, 0, 512, 512);
40
41// ... rendering code ...
42
43// Switch back to the canvas
44gl.bindFramebuffer(gl.FRAMEBUFFER, null);
45gl.viewport(0, 0, canvas.width, canvas.height);
46

Instanced Rendering (WebGL 2.0)

Instanced rendering allows you to draw many instances of the same geometry efficiently:

1// Set up instanced attributes
2const instanceOffsetLocation = gl.getAttribLocation(program, 'a_instanceOffset');
3const instanceBuffer = gl.createBuffer();
4gl.bindBuffer(gl.ARRAY_BUFFER, instanceBuffer);
5
6// Create instance data (e.g., positions for 100 instances)
7const instanceOffsets = new Float32Array(200);  // 2 components per instance * 100 instances
8for (let i = 0; i < 100; i++) {
9  instanceOffsets[i * 2] = Math.random() * 10 - 5;     // x offset
10  instanceOffsets[i * 2 + 1] = Math.random() * 10 - 5;  // y offset
11}
12gl.bufferData(gl.ARRAY_BUFFER, instanceOffsets, gl.STATIC_DRAW);
13
14// Set up the attribute
15gl.enableVertexAttribArray(instanceOffsetLocation);
16gl.vertexAttribPointer(instanceOffsetLocation, 2, gl.FLOAT, false, 0, 0);
17
18// Set the divisor to 1 (advance once per instance)
19gl.vertexAttribDivisor(instanceOffsetLocation, 1);
20
21// Draw 100 instances
22gl.drawArraysInstanced(gl.TRIANGLES, 0, 3, 100);
23

Performance Optimization

WebGL performance is critical for smooth animations and interactive applications. Here are some optimization techniques:

Minimize State Changes

WebGL state changes (like switching programs or textures) are expensive:

1// Bad: Switching programs in a loop
2for (let i = 0; i < objects.length; i++) {
3  gl.useProgram(objects[i].program);
4  // ... draw object ...
5}
6
7// Better: Group objects by program
8const objectsByProgram = groupObjectsByProgram(objects);
9for (const [program, programObjects] of Object.entries(objectsByProgram)) {
10  gl.useProgram(program);
11  for (const object of programObjects) {
12    // ... draw object ...
13  }
14}
15

Use Vertex Array Objects (VAOs)

VAOs store vertex attribute configurations, reducing setup overhead:

1// Create a VAO
2const vao = gl.createVertexArray();
3gl.bindVertexArray(vao);
4
5// Set up attributes
6gl.bindBuffer(gl.ARRAY_BUFFER, positionBuffer);
7gl.enableVertexAttribArray(positionAttributeLocation);
8gl.vertexAttribPointer(positionAttributeLocation, 3, gl.FLOAT, false, 0, 0);
9
10// ... set up other attributes ...
11
12// Unbind VAO
13gl.bindVertexArray(null);
14
15// Later, when drawing
16gl.bindVertexArray(vao);
17gl.drawArrays(gl.TRIANGLES, 0, vertexCount);
18

Use Typed Arrays

Typed arrays are more efficient than regular JavaScript arrays:

1// Bad: Regular array
2const positions = [];
3for (let i = 0; i < 1000; i++) {
4  positions.push(Math.random(), Math.random(), Math.random());
5}
6
7// Better: Typed array
8const positions = new Float32Array(3000);  // 3 components * 1000 vertices
9for (let i = 0; i < 1000; i++) {
10  positions[i * 3] = Math.random();     // x
11  positions[i * 3 + 1] = Math.random();  // y
12  positions[i * 3 + 2] = Math.random();  // z
13}
14

WebGL in the Real World

WebGL is used in a wide variety of applications:

3D Visualization Libraries

Libraries like Three.js, Babylon.js, and PlayCanvas provide high-level abstractions over WebGL:

1// Three.js example
2const scene = new THREE.Scene();
3const camera = new THREE.PerspectiveCamera(75, window.innerWidth / window.innerHeight, 0.1, 1000);
4const renderer = new THREE.WebGLRenderer();
5
6renderer.setSize(window.innerWidth, window.innerHeight);
7document.body.appendChild(renderer.domElement);
8
9const geometry = new THREE.BoxGeometry();
10const material = new THREE.MeshBasicMaterial({ color: 0x00ff00 });
11const cube = new THREE.Mesh(geometry, material);
12scene.add(cube);
13
14camera.position.z = 5;
15
16function animate() {
17  requestAnimationFrame(animate);
18  
19  cube.rotation.x += 0.01;
20  cube.rotation.y += 0.01;
21  
22  renderer.render(scene, camera);
23}
24
25animate();
26

Data Visualization

WebGL enables interactive visualization of large datasets:

1// Example using regl (a functional WebGL library)
2const regl = require('regl')();
3
4const drawPoints = regl({
5  frag: `
6    precision mediump float;
7    varying vec3 vColor;
8    void main() {
9      gl_FragColor = vec4(vColor, 1.0);
10    }
11  `,
12  vert: `
13    precision mediump float;
14    attribute vec3 position;
15    attribute vec3 color;
16    uniform mat4 projection, view;
17    varying vec3 vColor;
18    void main() {
19      vColor = color;
20      gl_PointSize = 5.0;
21      gl_Position = projection * view * vec4(position, 1);
22    }
23  `,
24  attributes: {
25    position: generatePositions(10000),  // Generate 10,000 data points
26    color: generateColors(10000)
27  },
28  count: 10000,
29  primitive: 'points'
30});
31
32regl.frame(({ time }) => {
33  regl.clear({
34    color: [0, 0, 0, 1],
35    depth: 1
36  });
37  
38  drawPoints({
39    projection: perspective(...),
40    view: lookAt(...)
41  });
42});
43

Conclusion

WebGL provides a powerful platform for creating high-performance graphics on the web. While it has a steep learning curve due to its low-level nature, the capabilities it offers are unmatched for web-based graphics.

Ready to dive deeper? Check out resources like WebGL Fundamentals and The Book of Shaders to continue your WebGL journey.

As browsers continue to evolve, WebGL will remain a cornerstone of web graphics, enabling ever more impressive visual experiences directly in the browser.