The Graphics Pipeline

The Graphics Pipeline from Vertices to Pixels

A draw call pushes vertices through a fixed sequence of stages. Two are programs you write in GLSL; the rest are fixed-function hardware that you only configure.

The WebGL 2 pipeline: green stages run your GLSL, grey stages are configured with gl.* calls
The WebGL 2 pipeline: green stages run your GLSL, grey stages are configured with gl.* calls

The vertex shader writes gl_Position in clip space. Primitive assembly builds points, lines or triangles and clips them; the divide by w and the viewport transform map them onto pixels. The rasterizer makes a fragment per covered pixel, interpolating the vertex shader's outputs, and the fragment shader colors it (or discards it) before depth, stencil and blending decide what reaches the framebuffer.

One triangle seen as rasterized fragments and as final interpolated pixelsHTMLLive
<!doctype html>
<style>
  body { margin: 0; font: 12px system-ui, sans-serif; background: #f7f4ee; color: #333; }
  .stages { display: flex; gap: 4px; max-width: 600px; margin-bottom: 6px; }
  .stages div { flex: 1; padding: 5px 2px; text-align: center; border-radius: 4px; background: #e6e1d6; }
  .stages .prog { background: #1f5f8b; color: #fff; }
  canvas { display: block; width: 100%; max-width: 600px; }
  p { margin: 4px 0; max-width: 600px; color: #555; }
</style>
<div class="stages">
  <div class="prog">vertex shader</div><div>primitive assembly + clip</div>
  <div>rasterizer</div><div class="prog">fragment shader</div><div>depth / blend &rarr; framebuffer</div>
</div>
<canvas id="view" width="600" height="280"></canvas>
<p>Left: the rasterizer makes one fragment per covered pixel of a 30&times;28 target. Right: the same draw at full resolution. Colours are the vertex shader's outputs, interpolated.</p>
<script>
const canvas = document.getElementById('view');
const gl = canvas.getContext('webgl2');
function compile(vsSource, fsSource) {
  const p = gl.createProgram();
  for (const [type, src] of [[gl.VERTEX_SHADER, vsSource], [gl.FRAGMENT_SHADER, fsSource]]) {
    const s = gl.createShader(type);
    gl.shaderSource(s, src.trim()); gl.compileShader(s);
    if (!gl.getShaderParameter(s, gl.COMPILE_STATUS)) throw new Error(gl.getShaderInfoLog(s));
    gl.attachShader(p, s);
  }
  gl.bindAttribLocation(p, 0, 'aPos');
  gl.bindAttribLocation(p, 1, 'aColor');
  gl.linkProgram(p);
  return p;
}
// The vertex shader writes gl_Position in clip space and passes a colour on.
const triangle = compile(`#version 300 es
in vec2 aPos; in vec3 aColor; out vec3 vColor;
uniform float uTime;
void main() {
  float c = cos(uTime), s = sin(uTime);
  vColor = aColor;
  gl_Position = vec4(mat2(c, s, -s, c) * aPos, 0.0, 1.0);
  gl_PointSize = 7.0;
}`, `#version 300 es
precision mediump float; in vec3 vColor; out vec4 outColor;
void main() { outColor = vec4(vColor, 1.0); }`);   // the fragment shader colours each fragment

// Shows the small render target with NEAREST filtering so each fragment is a visible block.
const show = compile(`#version 300 es
in vec2 aPos; out vec2 vUV;
void main() { vUV = aPos * 0.5 + 0.5; gl_Position = vec4(aPos, 0.0, 1.0); }`, `#version 300 es
precision mediump float; in vec2 vUV; uniform sampler2D uTex; out vec4 outColor;
void main() {
  vec2 cell = fract(vUV * vec2(30.0, 28.0));
  float gap = step(0.08, cell.x) * step(0.08, cell.y);
  outColor = mix(vec4(0.85, 0.83, 0.78, 1.0), texture(uTex, vUV), gap);
}`);

const triVAO = gl.createVertexArray();
gl.bindVertexArray(triVAO);
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([
  0.0, 0.8, 0.12, 0.37, 0.55,  -0.75, -0.6, 0.87, 0.47, 0.13,  0.75, -0.5, 0.71, 0.27, 0.18]), gl.STATIC_DRAW);
gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 20, 0);
gl.enableVertexAttribArray(1); gl.vertexAttribPointer(1, 3, gl.FLOAT, false, 20, 8);

const quadVAO = gl.createVertexArray();
gl.bindVertexArray(quadVAO);
gl.bindBuffer(gl.ARRAY_BUFFER, gl.createBuffer());
gl.bufferData(gl.ARRAY_BUFFER, new Float32Array([-1,-1, 1,-1, 1,1, -1,-1, 1,1, -1,1]), gl.STATIC_DRAW);
gl.enableVertexAttribArray(0); gl.vertexAttribPointer(0, 2, gl.FLOAT, false, 0, 0);

// A tiny 30x28 render target: its pixels are the fragments we want to see.
const tex = gl.createTexture();
gl.bindTexture(gl.TEXTURE_2D, tex);
gl.texImage2D(gl.TEXTURE_2D, 0, gl.RGBA8, 30, 28, 0, gl.RGBA, gl.UNSIGNED_BYTE, null);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MIN_FILTER, gl.NEAREST);
gl.texParameteri(gl.TEXTURE_2D, gl.TEXTURE_MAG_FILTER, gl.NEAREST);
const fbo = gl.createFramebuffer();
gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);
gl.framebufferTexture2D(gl.FRAMEBUFFER, gl.COLOR_ATTACHMENT0, gl.TEXTURE_2D, tex, 0);

function drawTriangle(t, mode = gl.TRIANGLES) {
  gl.useProgram(triangle);
  gl.uniform1f(gl.getUniformLocation(triangle, 'uTime'), t);
  gl.bindVertexArray(triVAO);
  gl.drawArrays(mode, 0, 3);
}
function frame(ms) {
  const t = ms * 0.0003;
  gl.bindFramebuffer(gl.FRAMEBUFFER, fbo);          // low-resolution pass
  gl.viewport(0, 0, 30, 28);
  gl.clearColor(1, 1, 1, 1); gl.clear(gl.COLOR_BUFFER_BIT);
  drawTriangle(t);

  gl.bindFramebuffer(gl.FRAMEBUFFER, null);
  gl.clearColor(0.97, 0.96, 0.93, 1); gl.clear(gl.COLOR_BUFFER_BIT);
  gl.viewport(0, 0, 295, 280);                      // left: fragments as blocks
  gl.useProgram(show); gl.bindVertexArray(quadVAO); gl.bindTexture(gl.TEXTURE_2D, tex);
  gl.drawArrays(gl.TRIANGLES, 0, 6);
  drawTriangle(t, gl.POINTS);                       // the three vertices on top
  gl.viewport(305, 0, 295, 280);                    // right: full resolution
  drawTriangle(t);
  requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
</script>