WebGL fundamentals for game developers
WebGL gives you direct GPU access in the browser. It's the foundation for 3D (and performant 2D) web games. This tutorial covers the essentials.
1) Getting a WebGL context
js
const canvas = document.getElementById('game')
const gl = canvas.getContext('webgl2') || canvas.getContext('webgl')
if (!gl) {
// Fallback or error message
console.error('WebGL not supported')
}Prefer webgl2 when available—it's widely supported and offers better features.
2) The render pipeline (simplified)
- Vertex shader runs once per vertex. It positions your geometry.
- Fragment shader runs once per pixel. It colors the output.
- Buffers hold data (positions, UVs, colors).
- Textures are images sampled by shaders.
3) A minimal shader pair
Vertex shader:
glsl
#version 300 es
in vec2 a_position;
void main() {
gl_Position = vec4(a_position, 0.0, 1.0);
}Fragment shader:
glsl
#version 300 es
precision mediump float;
out vec4 fragColor;
void main() {
fragColor = vec4(1.0, 0.5, 0.2, 1.0); // orange
}4) Compiling and linking shaders
js
function createShader(gl, type, source) {
const shader = gl.createShader(type)
gl.shaderSource(shader, source)
gl.compileShader(shader)
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
console.error(gl.getShaderInfoLog(shader))
gl.deleteShader(shader)
return null
}
return shader
}
function createProgram(gl, vs, fs) {
const program = gl.createProgram()
gl.attachShader(program, vs)
gl.attachShader(program, fs)
gl.linkProgram(program)
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) {
console.error(gl.getProgramInfoLog(program))
return null
}
return program
}5) Creating buffers
js
const positions = new Float32Array([
-0.5, -0.5,
0.5, -0.5,
0.0, 0.5,
])
const buffer = gl.createBuffer()
gl.bindBuffer(gl.ARRAY_BUFFER, buffer)
gl.bufferData(gl.ARRAY_BUFFER, positions, gl.STATIC_DRAW)6) Connecting buffers to shaders
js
const positionLoc = gl.getAttribLocation(program, 'a_position')
gl.enableVertexAttribArray(positionLoc)
gl.vertexAttribPointer(positionLoc, 2, gl.FLOAT, false, 0, 0)7) The game loop
js
function render() {
gl.viewport(0, 0, canvas.width, canvas.height)
gl.clearColor(0, 0, 0, 1)
gl.clear(gl.COLOR_BUFFER_BIT)
gl.useProgram(program)
gl.drawArrays(gl.TRIANGLES, 0, 3)
requestAnimationFrame(render)
}
render()8) Loading textures
js
function loadTexture(gl, url) {
const texture = gl.createTexture()
gl.bindTexture(gl.TEXTURE_2D, texture)
// Placeholder pixel until image loads
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, 1, 1, 0, gl.RGBA, gl.UNSIGNED_BYTE,
new Uint8Array([255, 0, 255, 255]))
const img = new Image()
img.onload = () => {
gl.bindTexture(gl.TEXTURE_2D, texture)
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA, gl.RGBA, gl.UNSIGNED_BYTE, img)
gl.generateMipmap(gl.TEXTURE_2D)
}
img.src = url
return texture
}9) Common gotchas
- Power-of-two textures aren't strictly required in WebGL 2, but mipmaps work better with them.
- Lost context: handle
webglcontextlostevents—mobile browsers will evict contexts under memory pressure. - Float textures need extension checks (
EXT_color_buffer_float).
10) When to use a library
Raw WebGL is verbose. For most games, consider:
- Three.js — full-featured 3D
- PixiJS — fast 2D rendering
- PlayCanvas — game engine with editor
Use raw WebGL when you need:
- maximum control
- a custom renderer
- to learn how things really work