WebGPU First Triangle

The triangle is the "Hello World" of GPU programming.

Drawing one triangle uses every core part of WebGPU. Once it works, everything else in this tutorial is a variation of it.

The result of this lesson, running live.

The Steps

Every WebGPU program follows the same steps:

  1. Get a device
  2. Connect the device to a canvas
  3. Write the shaders
  4. Create a pipeline
  5. Record and submit a render pass

Step 1: Get a Device

The adapter is the GPU. The device is your own connection to it. You create everything else from the device.

const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();

Step 2: Configure the Canvas

WebGPU draws into a normal <canvas> element. Ask it for a "webgpu" context and tell it which device and colour format to use.

const canvas = document.querySelector("canvas");
canvas.width = canvas.clientWidth * devicePixelRatio;
canvas.height = canvas.clientHeight * devicePixelRatio;

const context = canvas.getContext("webgpu");
const format = navigator.gpu.getPreferredCanvasFormat();
context.configure({ device, format });
Note Setting canvas.width and canvas.height to the displayed size times devicePixelRatio keeps the result sharp on high-resolution screens.

Clearing the Canvas

Before drawing a triangle, try the simplest possible WebGPU program: fill the canvas with one colour.

GPU work is recorded into a command encoder, then submitted to the device queue. A render pass says which texture to draw into and what to do with it first.

Example

Clear the canvas to blue:

const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({
  colorAttachments: [{
    view: context.getCurrentTexture().createView(),
    clearValue: { r: 0.1, g: 0.3, b: 0.6, a: 1 },
    loadOp: "clear",
    storeOp: "store"
  }]
});
pass.end();
device.queue.submit([encoder.finish()]);
Edit & Run »
PropertyDescription
viewThe texture to draw into. getCurrentTexture() returns the canvas texture for this frame.
clearValueThe colour to fill with. Each channel goes from 0 to 1.
loadOp"clear" fills with clearValue. "load" keeps what was there.
storeOp"store" keeps the result. "discard" throws it away.

Step 3: Write the Shaders

Shaders are small programs that run on the GPU. They are written in WGSL. A triangle needs two:

  • The vertex shader runs once per corner and returns its position.
  • The fragment shader runs once per pixel inside the triangle and returns its colour.
@vertex
fn vertexMain(@builtin(vertex_index) vertexIndex: u32) -> @builtin(position) vec4f {
  let corners = array(
    vec2f( 0.0,  0.6),
    vec2f(-0.6, -0.5),
    vec2f( 0.6, -0.5)
  );
  return vec4f(corners[vertexIndex], 0.0, 1.0);
}

@fragment
fn fragmentMain() -> @location(0) vec4f {
  return vec4f(0.49, 0.95, 0.61, 1.0);
}

Positions are in clip space: x goes from −1 (left) to 1 (right), and y from −1 (bottom) to 1 (top), whatever the canvas size.

Note The corners are written inside the shader for now. In Lesson 4 you will send them from JavaScript in a vertex buffer.

Step 4: Create a Pipeline

A render pipeline combines the shaders with the settings needed to run them. You create it once and reuse it every frame.

const shaderModule = device.createShaderModule({ code: shaderCode });

const pipeline = device.createRenderPipeline({
  layout: "auto",
  vertex: { module: shaderModule, entryPoint: "vertexMain" },
  fragment: { module: shaderModule, entryPoint: "fragmentMain", targets: [{ format }] }
});

The format in targets must match the canvas format.

Step 5: Draw

Inside the render pass, set the pipeline and draw 3 vertices. The vertex shader runs 3 times, with vertex_index 0, 1 and 2.

Example

Draw a green triangle:

const encoder = device.createCommandEncoder();
const pass = encoder.beginRenderPass({
  colorAttachments: [{
    view: context.getCurrentTexture().createView(),
    clearValue: { r: 0.05, g: 0.06, b: 0.07, a: 1 },
    loadOp: "clear",
    storeOp: "store"
  }]
});
pass.setPipeline(pipeline);
pass.draw(3);
pass.end();
device.queue.submit([encoder.finish()]);
Edit & Run »
Tip If nothing appears, open the browser console. WebGPU prints a detailed message explaining which call was invalid and why.

Exercises

1. Make the triangle orange.

Show answer

Change the fragment shader's return value:

return vec4f(1.0, 0.55, 0.1, 1.0);

2. Make the triangle point down.

Show answer

Flip the sign of every y value:

vec2f( 0.0, -0.6),
vec2f(-0.6,  0.5),
vec2f( 0.6,  0.5)

3. Which shader runs once per pixel?

Show answer

The fragment shader.

Summary

  • Adapter → device → canvas context connects JavaScript, the GPU and the page.
  • The vertex shader places corners; the fragment shader colours pixels.
  • A pipeline is created once. Each frame you encode a render pass and submit it.

Official documentation