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 Steps
Every WebGPU program follows the same steps:
- Get a device
- Connect the device to a canvas
- Write the shaders
- Create a pipeline
- 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 });
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 »
| Property | Description |
|---|---|
view | The texture to draw into. getCurrentTexture() returns the canvas texture for this frame. |
clearValue | The 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.
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 »
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
Go deeper with the specifications and reference documentation:
- developer.mozilla.org
MDN: GPUAdapter.requestDevice()
Getting a device, with the optional features and limits you can ask for.
- developer.mozilla.org
MDN: GPUCanvasContext.configure()
Connecting a canvas to the device, with format and alpha mode options.
- developer.mozilla.org
MDN: GPUDevice.createRenderPipeline()
Every field of the render pipeline descriptor.
- w3.org
WebGPU specification: render passes
How render passes, attachments, load and store operations work.