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:
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.
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,
};
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 |
// 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).
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);
}
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.
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
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();
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.
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)
// 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)
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)
// 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
// 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)); }
% 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.