WGSL Basics

WGSL (WebGPU Shading Language) is the language for programs that run on the GPU.

It looks a bit like Rust and a bit like TypeScript. Every value has a type, and the GPU runs your code for many vertices or pixels at the same time.

Circles drawn entirely by a fragment shader function.

WGSL Types

WGSL has a small set of basic types:

TypeDescriptionExample
f3232-bit floating-point number1.5
i3232-bit signed integer-3
u3232-bit unsigned integer7u
booltrue or falsetrue
vec2f, vec3f, vec4f2, 3 or 4 floatsvec3f(1.0, 0.5, 0.0)
mat4x4f4×4 matrix of floatsUsed for 3D transforms
array<T, N>Fixed-size arrayarray<f32, 4>
Warning WGSL never converts types for you. 1.0 + 2 is fine because 2 is a flexible literal, but adding an f32 variable to an i32 variable is an error. Convert explicitly with f32(value).

Variables: let and var

Use let for values that never change, and var for values you will change:

let radius = 0.5;
var total = 0.0;
total = total + radius;

The type is usually worked out from the value. You can also write it:

let count: u32 = 4u;
var color: vec3f = vec3f(1.0, 0.0, 0.0);

Vectors

Vectors are everywhere in shaders: positions, colours and directions. You can read their parts with .x .y .z .w or .r .g .b .a, and combine them freely. This is called swizzling.

let color = vec4f(1.0, 0.5, 0.2, 1.0);
let red = color.r;
let rgb = color.rgb;
let flipped = color.bgr;
let position = vec4f(rgb, 1.0);

Maths works on every component at once: vec2f(1.0, 2.0) * 2.0 is vec2f(2.0, 4.0).

Structs

A struct groups values together. Structs are the usual way to send several values from the vertex shader to the fragment shader.

Fields marked @location(n) are passed along and smoothly blended across the triangle. This blending is called interpolation.

Example

Give each corner its own colour:

struct VertexOutput {
  @builtin(position) position: vec4f,
  @location(0) color: vec3f,
}

@vertex
fn vertexMain(@builtin(vertex_index) vertexIndex: u32) -> VertexOutput {
  let corners = array(vec2f(0.0, 0.7), vec2f(-0.7, -0.6), vec2f(0.7, -0.6));
  let colors = array(vec3f(1.0, 0.3, 0.3), vec3f(0.3, 1.0, 0.4), vec3f(0.3, 0.5, 1.0));
  var output: VertexOutput;
  output.position = vec4f(corners[vertexIndex], 0.0, 1.0);
  output.color = colors[vertexIndex];
  return output;
}

@fragment
fn fragmentMain(input: VertexOutput) -> @location(0) vec4f {
  return vec4f(input.color, 1.0);
}
Edit & Run »

Built-in Values

Values marked @builtin(...) come from, or go to, the GPU itself:

Built-inShaderDescription
vertex_indexVertex (input)Which vertex is being processed: 0, 1, 2, …
instance_indexVertex (input)Which copy is being drawn (see Instancing)
positionVertex (output)The clip-space position of the vertex
positionFragment (input)The pixel coordinate being coloured
global_invocation_idCompute (input)Which thread is running (see Compute shaders)

Built-in Functions

WGSL comes with many maths functions. These are the ones you will use most:

FunctionDescription
mix(a, b, t)Blends from a to b as t goes from 0 to 1
clamp(x, low, high)Keeps x between low and high
smoothstep(edge0, edge1, x)A smooth 0-to-1 step between two edges
length(v)The length of a vector
normalize(v)The same direction with length 1
dot(a, b)How much two directions agree. Used for lighting.
sin(x), cos(x)Trigonometry, in radians
max(a, b), min(a, b)The larger or smaller value

Example

Use mix() to make a sky gradient:

@fragment
fn fragmentMain(input: VertexOutput) -> @location(0) vec4f {
  let bottomColor = vec3f(0.05, 0.1, 0.3);
  let topColor = vec3f(0.95, 0.55, 0.3);
  let color = mix(bottomColor, topColor, input.uv.y);
  return vec4f(color, 1.0);
}
Edit & Run »
Note This example draws one big triangle that covers the whole canvas, and passes a uv coordinate from 0 to 1 across it. This "full-screen triangle" trick is used in many effects, including post-processing.

Functions

Write your own functions with fn. Parameters and the return value must have types:

fn circle(point: vec2f, center: vec2f, radius: f32) -> f32 {
  let distanceToCenter = length(point - center);
  return 1.0 - smoothstep(radius - 0.01, radius + 0.01, distanceToCenter);
}

Functions marked @vertex, @fragment or @compute are entry points: the places JavaScript can start the shader from.

Loops and Conditions

if, for and while work like in JavaScript, but conditions must be bool and braces are required.

Example

Draw five circles in a loop:

var color = vec3f(0.05, 0.06, 0.07);
for (var index = 0; index < 5; index++) {
  let ordinal = f32(index);
  let center = vec2f(aspect * 0.5 + (ordinal - 2.0) * 0.3, 0.5 + 0.2 * sin(ordinal * 1.3));
  let shade = vec3f(0.3 + ordinal * 0.15, 0.9 - ordinal * 0.12, 0.6);
  color = mix(color, shade, circle(point, center, 0.12));
}
Edit & Run »
Tip In the full example, the canvas aspect ratio is written into the shader text with a JavaScript template string. In Lesson 5 you will learn the proper way to send values like this: uniforms.

Exercises

1. Which keyword declares a value that can change?

Show answer

var. Values declared with let cannot change.

2. What does vec4f(0.2, 0.4, 0.6, 1.0).zyx return?

Show answer

vec3f(0.6, 0.4, 0.2)

3. Fix this line: let half = count / 2.0; where count is an i32.

Show answer
let half = f32(count) / 2.0;

4. In the gradient example, swap the two colours so the sky is darker at the top.

Show answer
let color = mix(topColor, bottomColor, input.uv.y);

Summary

  • WGSL is strictly typed: f32, i32, u32, bool, vectors and matrices.
  • let is constant, var can change.
  • Structs with @location fields carry data from the vertex shader to the fragment shader.
  • Built-in functions like mix, smoothstep and length do most of the work.

Official documentation