Getting Started with WebGL: Building Interactive 3D Scenes in the Browser

Team 5 min read

#webgl

#graphics

#webdev

#tutorial

Introduction

WebGL puts the power of GPU-accelerated graphics in the browser. It lets you render 2D and 3D scenes with shaders, buffers, and a programmable pipeline—without plugins. This guide walks through the basics and presents a minimal, runnable example to get you building interactive graphics quickly.

What you need to get started

  • A modern browser with WebGL support (Chrome, Firefox, Edge, or Safari).
  • A basic understanding of JavaScript.
  • A canvas element to host WebGL’s drawing surface.

Optional, but helpful:

  • A simple text editor or IDE.
  • Familiarity with GLSL (the shading language used by WebGL).

A minimal WebGL program: the core idea

WebGL programs follow a tight loop: you feed data to the GPU via buffers, write small programs called shaders to describe how vertices become pixels, and then render each frame. The core components are:

  • Vertex shader: processes vertex data and positions it on screen.
  • Fragment shader: computes the color of each pixel.
  • Buffers: hold vertex data (positions, colors, texture coordinates).
  • Program: a pair of compiled shaders linked together for execution by the GPU.

A minimal, working example

Here is a self-contained example that draws a colorful triangle and animates its color over time. Copy this into an HTML file with a canvas element or adapt as a module in your setup.

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8" />
  <title>WebGL Triangle</title>
  <style> canvas { width: 100%; height: 100%; display: block; } </style>
</head>
<body>
<canvas id="glcanvas" width="640" height="480"></canvas>
<script>
const canvas = document.getElementById('glcanvas');
const gl = canvas.getContext('webgl');
if (!gl) {
  alert('WebGL not supported in this browser.');
  throw new Error('WebGL not supported');
}

// Vertex shader: positions vertices in clip space
const vertexShaderSource = `
attribute vec2 aPosition;
void main() {
  gl_Position = vec4(aPosition, 0.0, 1.0);
}
`;

// Fragment shader: uses a uniform color that we'll animate
const fragmentShaderSource = `
precision mediump float;
uniform vec4 uColor;
void main() {
  gl_FragColor = uColor;
}
`;

// Compile a shader
function compileShader(gl, type, source) {
  const shader = gl.createShader(type);
  gl.shaderSource(shader, source);
  gl.compileShader(shader);
  if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
    console.error('Shader compile failed:', gl.getShaderInfoLog(shader));
    gl.deleteShader(shader);
    return null;
  }
  return shader;
}

// Link program
function createProgram(gl, vsSource, fsSource) {
  const vs = compileShader(gl, gl.VERTEX_SHADER, vsSource);
  const fs = compileShader(gl, gl.FRAGMENT_SHADER, fsSource);
  const program = gl.createProgram();
  gl.attachShader(program, vs);
  gl.attachShader(program, fs);
  gl.linkProgram(program);
  if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
    console.error('Program link failed:', gl.getProgramInfoLog(program));
    gl.deleteProgram(program);
    return null;
  }
  return program;
}

const program = createProgram(gl, vertexShaderSource, fragmentShaderSource);
gl.useProgram(program);

// Vertex data: a single triangle in clip space
const vertices = new Float32Array([
  0.0,  0.5,
 -0.5, -0.5,
 0.5, -0.5
]);

const buffer = gl.createBuffer();
gl.bindBuffer(gl.ARRAY_BUFFER, buffer);
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);

const aPosition = gl.getAttribLocation(program, 'aPosition');
gl.enableVertexAttribArray(aPosition);
gl.vertexAttribPointer(aPosition, 2, gl.FLOAT, false, 0, 0);

const uColor = gl.getUniformLocation(program, 'uColor');

// Resize canvas to display size and set viewport
function resize() {
  const displayWidth  = canvas.clientWidth;
  const displayHeight = canvas.clientHeight;
  if (canvas.width !== displayWidth || canvas.height !== displayHeight) {
    canvas.width = displayWidth;
    canvas.height = displayHeight;
  }
  gl.viewport(0, 0, canvas.width, canvas.height);
}
window.addEventListener('resize', resize);
resize();

let start = performance.now();

function render(now) {
  const t = (now - start) / 1000;

  // Animate color over time
  const r = 0.5 + 0.5 * Math.sin(t);
  const g = 0.5 + 0.5 * Math.cos(t * 0.8);
  const b = 0.5;
  gl.uniform4f(uColor, r, g, b, 1.0);

  gl.clearColor(0.0, 0.0, 0.0, 1.0);
  gl.clear(gl.COLOR_BUFFER_BIT);
  gl.drawArrays(gl.TRIANGLES, 0, 3);

  requestAnimationFrame(render);
}
requestAnimationFrame(render);
</script>
</body>
</html>

The shader pipeline in a sentence

  • Vertex shaders transform vertex positions from model space to clip space, shaping where vertices appear.
  • Fragment shaders compute the color of each pixel, allowing lighting, textures, and effects.
  • Uniforms provide per-frame values (like colors) that can animate without changing vertex data.
  • Buffers stream vertex data into the GPU, enabling efficient rendering of shapes and models.

Extending beyond the triangle: building a scene

  • Add more geometry: expand your vertex buffer with additional shapes and use indices to reuse vertices.
  • Introduce colors per vertex: pass a color attribute or compute normals for lighting in the fragment shader.
  • Bring in textures: load an image, create a texture, and sample it in the fragment shader.
  • Implement a simple camera: move from 2D to pseudo-3D by introducing a projection or view transform in the shader.
  • Optimize: reuse programs and buffers, minimize state changes, and batch draw calls for performance.

Practical tips for success

  • Start simple: validate each piece (context creation, shader compilation, drawing) before adding features.
  • Use a robust shader error check path during development to catch syntax and type errors early.
  • Prefer requestAnimationFrame for rendering loops to synchronize with display refresh rate and save power.
  • Keep a responsive canvas: handle DPR and resizing so your scene looks consistent on all screens.

Next steps

  • Experiment with additional shapes and colors to get comfortable with the WebGL pipeline.
  • Explore WebGL2 for advanced features like vertex array objects, transform feedback, and easier matrices.
  • Look into small libraries for helper math (matrices/quaternions) and texture loading to accelerate your projects.