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.