Reference · WGSL · WebGPU

WGSL Shader Cheatsheet

Types, structs, address spaces, bind groups, entry points, and built-in functions for WebGPU vertex, fragment, and compute shaders.

Types & Constructors

WGSL is statically typed and, unlike GLSL, has no implicit conversions between numeric types — 1 (i32) and 1.0 (f32) are not interchangeable without an explicit cast.

Type Description Constructor example
bool Boolean var b: bool = true;
i32 32-bit signed integer var n: i32 = -42;
u32 32-bit unsigned integer var u: u32 = 42u;
f32 32-bit float var f: f32 = 1.0;
f16 ext 16-bit float — requires the f16 feature var h: f16 = 1.0h;
vec2<T> / vec3<T> / vec4<T> Vector of 2/3/4 components of type T (f32, i32, u32, bool) let v = vec3<f32>(1.0, 0.5, 0.0);
vec3f / vec3i / vec3u Type-alias shorthand for common vector types let v = vec3f(1.0, 0.5, 0.0);
mat2x2<f32> … mat4x4<f32> Column-major float matrix, NxM let m = mat4x4<f32>();
mat4x4f Shorthand alias for mat4x4<f32> var mvp: mat4x4f;
array<T, N> Fixed-size array; runtime-sized only as the last struct member in a storage buffer var a: array<f32, 4>;
texture_2d<f32> 2D sampled texture @group(0) @binding(0) var t: texture_2d<f32>;
sampler Texture sampler, declared separately from the texture @group(0) @binding(1) var s: sampler;
texture_storage_2d<format, access> Storage texture for compute read/write (no filtering) var img: texture_storage_2d<rgba8unorm, write>;

Vector construction and swizzling

Components can be read with .xyzw or .rgba swizzles, same as GLSL. WGSL also allows "splat" construction from a single scalar:

WGSL
let col  = vec4f(1.0, 0.5, 0.2, 1.0);
let rgb  = col.rgb;          // vec3f(1.0, 0.5, 0.2)
let half = vec3f(0.5);       // splat: (0.5, 0.5, 0.5)
let mixed = vec4f(rgb, 1.0); // concatenation constructor

// Swizzle assignment (write to multiple components)
var c = vec4f();
c.x = 1.0;
// note: WGSL does NOT allow c.yz = vec2f(...) swizzle-assignment on
// the left side of '=' — assign components individually instead

Structs & Arrays

Structs are the primary way to pass vertex attributes, varyings, and uniform data. Field order matters for buffer layout — WGSL uses the same std140/std430-derived alignment rules as WGPU's buffer binding layout.

WGSL Struct definitions
struct Particle {
  position: vec3f,
  // NOTE: vec3 has 16-byte alignment in uniform buffers — pad
  // explicitly or interleave with a scalar to avoid layout surprises
  life: f32,
  velocity: vec3f,
  mass: f32,
};

struct Uniforms {
  viewProjection: mat4x4f,
  time: f32,
  deltaTime: f32,
};

struct VertexOutput {
  @builtin(position) position: vec4f,
  @location(0) uv: vec2f,
  @location(1) normal: vec3f,
};
Alignment gotcha: vec3<f32> has the alignment of vec4<f32> (16 bytes) inside uniform and storage buffers. Interleaving a vec3f with a trailing f32 field (as above) packs them into one 16-byte slot — omit it and you silently misalign every field after it.

Address Spaces & Bindings

Every variable declared outside a function body lives in an explicit address space, which determines how it's bound from JavaScript via GPUBindGroupLayout.

Address space Mutability Typical use
function read/write, local Default for var inside a function body
private read/write, per-invocation Module-scope scratch state, not shared
workgroup read/write, shared Shared memory between invocations in one compute workgroup — used for tile reductions
uniform read-only Small, frequently-updated data (camera matrices, time) — max 64KB per binding on most devices
storage, read read-only Large read-only buffers (vertex pools, LUTs)
storage, read_write read/write Compute shader output buffers, particle state read-modify-write
WGSL Bind group declarations
// group index maps to setBindGroup(index, ...) in JS
// binding index maps to the entry's `binding` in GPUBindGroupLayoutDescriptor

@group(0) @binding(0) var<uniform> uniforms: Uniforms;
@group(0) @binding(1) var<storage, read> particlesIn: array<Particle>;
@group(0) @binding(2) var<storage, read_write> particlesOut: array<Particle>;
@group(1) @binding(0) var diffuseTex: texture_2d<f32>;
@group(1) @binding(1) var diffuseSampler: sampler;

var<workgroup> tile: array<f32, 64>; // shared within one workgroup

Built-in Functions

Trigonometric & exponential

Function Returns
sin(x), cos(x), tan(x) Component-wise trig
asin(x), acos(x), atan2(y, x) Inverse trig; note atan2, not atan(y,x) as in GLSL
pow(x, y), exp(x), log(x) Exponential / logarithm
sqrt(x), inverseSqrt(x) Square root; fast reciprocal square root (camelCase, unlike GLSL's inversesqrt)

Math & interpolation

Function Returns
abs(x) Absolute value component-wise
floor(x), ceil(x), round(x) Floor / ceil / round-to-even
fract(x) x − floor(x)
x % y Truncated modulo — WGSL uses the % operator, not a mod() function
min(x,y), max(x,y) Component-wise min/max
clamp(x, lo, hi) Clamp x to [lo, hi]
mix(a, b, t) Linear interpolation: a·(1−t) + b·t
step(edge, x) 0 if x < edge, else 1
smoothstep(lo, hi, x) Hermite cubic: 3t²−2t³ in [lo,hi]
sign(x) −1, 0 or +1
select(f, t, cond) Branchless choice: returns t if cond is true, else f — WGSL has no ternary ?: operator

Vector & geometry

Function Returns
length(v) Euclidean length √(x²+y²+…)
distance(a, b) length(a−b)
dot(a, b) Dot product scalar
cross(a, b) Cross product of vec3
normalize(v) Unit vector: v / length(v)
reflect(I, N) Reflection of incident I about normal N
refract(I, N, eta) Snell's law refraction; eta = n₁/n₂
faceForward(N, I, Nref) Returns N oriented to face away from I

Texture sampling

Function Returns
textureSample(t, s, uv) Filtered sample — fragment stage only (implicit derivatives required)
textureSampleLevel(t, s, uv, lod) Sample at explicit mip level — usable in any stage, including compute
textureLoad(t, coords, lod) Fetch exact texel by integer coordinates, no filtering
textureStore(t, coords, value) Write a texel to a storage texture — compute shaders only
textureDimensions(t) Returns vec2<u32> dimensions of the texture at mip 0

Synchronization (compute only)

Function Purpose
workgroupBarrier() Blocks until all invocations in the workgroup reach this point and all workgroup-memory writes are visible
storageBarrier() Ensures storage-buffer writes from this invocation are visible to other invocations in the workgroup
atomicAdd(&a, v) / atomicLoad / atomicStore Atomic read-modify-write on atomic<i32> or atomic<u32> in storage or workgroup memory

Vertex & Fragment Stages

Unlike GLSL, WGSL uses a single shader module with tagged entry points (@vertex, @fragment, @compute) rather than separate shader objects. All attribute and varying locations are explicit via @location(n).

WGSL Vertex + fragment in one module
struct Uniforms {
  viewProjection: mat4x4f,
  model: mat4x4f,
  time: f32,
};
@group(0) @binding(0) var<uniform> u: Uniforms;

struct VertexInput {
  @location(0) position: vec3f,
  @location(1) normal: vec3f,
  @location(2) uv: vec2f,
};

struct VertexOutput {
  @builtin(position) clipPos: vec4f,
  @location(0) uv: vec2f,
  @location(1) worldNormal: vec3f,
};

@vertex
fn vs_main(in: VertexInput) -> VertexOutput {
  var out: VertexOutput;
  var pos = in.position;
  pos.y += sin(pos.x * 3.0 + u.time) * 0.1; // simple wave displacement

  let world = u.model * vec4f(pos, 1.0);
  out.clipPos = u.viewProjection * world;
  out.uv = in.uv;
  out.worldNormal = normalize((u.model * vec4f(in.normal, 0.0)).xyz);
  return out;
}

@group(1) @binding(0) var diffuseTex: texture_2d<f32>;
@group(1) @binding(1) var diffuseSampler: sampler;

@fragment
fn fs_main(in: VertexOutput) -> @location(0) vec4f {
  let albedo = textureSample(diffuseTex, diffuseSampler, in.uv);

  let lightDir = normalize(vec3f(1.0, 2.0, 1.5));
  let diff = max(dot(in.worldNormal, lightDir), 0.0);
  let lit = albedo.rgb * (0.2 + 0.8 * diff);

  return vec4f(lit, albedo.a);
}
No gl_Position / gl_FragColor: WGSL uses @builtin(position) on the vertex output struct and @location(0) on the fragment return value instead of the implicit GLSL globals.

Compute Shaders

Compute shaders run in 3D grids of workgroups, each containing a fixed 3D grid of invocations declared with @workgroup_size(x, y, z). There is no compute stage in WebGL — this is WGSL/WebGPU exclusive and is the backbone of GPU particle systems, fluid solvers, and N-body simulations.

WGSL Particle integration compute pass
struct Particle {
  position: vec3f,
  life: f32,
  velocity: vec3f,
  mass: f32,
};

struct SimParams {
  deltaTime: f32,
  gravity: f32,
  particleCount: u32,
};

@group(0) @binding(0) var<uniform> params: SimParams;
@group(0) @binding(1) var<storage, read> particlesIn: array<Particle>;
@group(0) @binding(2) var<storage, read_write> particlesOut: array<Particle>;

@compute @workgroup_size(64)
fn cs_main(
  @builtin(global_invocation_id) gid: vec3<u32>,
  @builtin(local_invocation_index) lid: u32
) {
  let i = gid.x;
  if (i >= params.particleCount) {
    return; // guard: workgroup count is rounded up, may overshoot
  }

  var p = particlesIn[i];
  p.velocity.y -= params.gravity * params.deltaTime;
  p.position += p.velocity * params.deltaTime;
  p.life -= params.deltaTime;

  particlesOut[i] = p;
}

Dispatch sizing from JavaScript

JavaScript
const WORKGROUP_SIZE = 64;
const particleCount = 100_000;
const workgroupCount = Math.ceil(particleCount / WORKGROUP_SIZE);

const pass = encoder.beginComputePass();
pass.setPipeline(computePipeline);
pass.setBindGroup(0, bindGroup);
pass.dispatchWorkgroups(workgroupCount); // 1D dispatch; also accepts (x, y, z)
pass.end();
Workgroup size limits: The product x * y * z in @workgroup_size must not exceed maxComputeInvocationsPerWorkgroup (typically 256). 64 is a safe, portable default for 1D particle/array workloads.

JavaScript Setup

Minimal boilerplate to acquire a device, create a shader module from a WGSL string, and build a bind group layout that matches the @group/@binding annotations above.

JavaScript Device + pipeline bootstrap
if (!navigator.gpu) throw new Error("WebGPU not supported");

const adapter = await navigator.gpu.requestAdapter();
const device = await adapter.requestDevice();
const context = canvas.getContext("webgpu");
const format = navigator.gpu.getPreferredCanvasFormat();
context.configure({ device, format, alphaMode: "premultiplied" });

const module = device.createShaderModule({ code: wgslSource });

const pipeline = device.createRenderPipeline({
  layout: "auto", // let WebGPU infer bind group layout from WGSL bindings
  vertex: { module, entryPoint: "vs_main", buffers: [vertexBufferLayout] },
  fragment: {
    module,
    entryPoint: "fs_main",
    targets: [{ format }],
  },
  primitive: { topology: "triangle-list", cullMode: "back" },
});

const computePipeline = device.createComputePipeline({
  layout: "auto",
  compute: { module: computeModule, entryPoint: "cs_main" },
});

Shader Patterns

Hash / noise (no textures needed)

WGSL
// Simple 2D hash -> pseudo-random float in [0,1]
fn hash21(pIn: vec2f) -> f32 {
  var p = fract(pIn * vec2f(127.1, 311.7));
  p += dot(p, p + 19.19);
  return fract(p.x * p.y);
}

// Value noise: smooth interpolation between hashed lattice points
fn noise(p: vec2f) -> f32 {
  let i = floor(p);
  let f = fract(p);
  let u = f * f * (3.0 - 2.0 * f); // smoothstep
  return mix(
    mix(hash21(i),             hash21(i + vec2f(1.0, 0.0)), u.x),
    mix(hash21(i + vec2f(0.0, 1.0)), hash21(i + vec2f(1.0, 1.0)), u.x), u.y
  );
}

Signed distance fields (2D)

WGSL
fn sdCircle(p: vec2f, r: f32) -> f32 { return length(p) - r; }

fn sdBox(p: vec2f, b: vec2f) -> f32 {
  let d = abs(p) - b;
  return length(max(d, vec2f(0.0))) + min(max(d.x, d.y), 0.0);
}

// Smooth boolean union (k controls blend radius)
fn smin(a: f32, b: f32, k: f32) -> f32 {
  let h = clamp(0.5 + 0.5 * (b - a) / k, 0.0, 1.0);
  return mix(b, a, h) - k * h * (1.0 - h);
}

Ping-pong buffers (double-buffered simulation)

JavaScript
// Storage buffers can't be read and written in the same bind group slot
// safely, so alternate two buffers + two bind groups each frame.
let bufA = device.createBuffer({ size, usage: STORAGE | COPY_DST });
let bufB = device.createBuffer({ size, usage: STORAGE | COPY_DST });
let bindGroupAtoB = makeBindGroup(bufA, bufB); // read A, write B
let bindGroupBtoA = makeBindGroup(bufB, bufA); // read B, write A
let flip = false;

function frame() {
  const pass = encoder.beginComputePass();
  pass.setPipeline(computePipeline);
  pass.setBindGroup(0, flip ? bindGroupBtoA : bindGroupAtoB);
  pass.dispatchWorkgroups(workgroupCount);
  pass.end();
  flip = !flip;
}

Common WGSL utility snippets

WGSL
// Remap [a,b] -> [c,d]
fn remap(x: f32, a: f32, b: f32, c: f32, d: f32) -> f32 {
  return c + (d - c) * ((x - a) / (b - a));
}

// 2D rotation matrix
fn rot2(a: f32) -> mat2x2f {
  let c = cos(a);
  let s = sin(a);
  return mat2x2f(c, -s, s, c);
}

// Luminance (perceived brightness, Rec. 709 weights)
fn luma(rgb: vec3f) -> f32 {
  return dot(rgb, vec3f(0.2126, 0.7152, 0.0722));
}

// sRGB <-> linear (approximation via gamma 2.2)
fn toLinear(c: vec3f) -> vec3f { return pow(c, vec3f(2.2)); }
fn toSRGB(c: vec3f)   -> vec3f { return pow(c, vec3f(1.0 / 2.2)); }
GLSL → WGSL quick migration notes: no implicit int/float conversions · % replaces mod() · atan2(y,x) replaces atan(y,x) · select() replaces the ternary operator · functions and variables need explicit return and declaration types (fn f() -> f32, var x: f32) · every module can hold vertex, fragment, and compute entry points side by side.