WGSL Fundamentals

The WebGPU Shading Language—built for safety and portability

WebGPU introduces its own shading language: WGSL. If you have written shaders before, you might wonder why we need yet another language. The answer lies in the constraints of the modern web platform.

Why a New Language

GPU shading languages have existed for decades. GLSL powers OpenGL and WebGL. HLSL is Microsoft's language for DirectX. Metal Shading Language runs on Apple hardware. Each evolved organically, accumulating quirks and undefined behaviors along the way.

WGSL exists because the web demands something different. Web browsers must run untrusted code safely. They must behave identically across all platforms. Neither GLSL nor HLSL were designed with these constraints in mind.

Consider what happens when a GLSL shader contains undefined behavior. On one GPU, it might produce garbage. On another, it might crash the driver. On a third, it might appear to work correctly while corrupting memory. This is acceptable in native development where you control the environment. It is unacceptable on the web where any page might run any shader.

WGSL eliminates undefined behavior by design. Every valid WGSL program has precisely defined semantics. If your shader compiles, it will behave identically on every browser, every operating system, every GPU. This is not a limitation—it is a feature.

The syntax draws from multiple sources. The type system resembles Rust. The function syntax echoes C. The built-in functions mirror those of other shading languages. The result is familiar enough to be learnable but strict enough to be safe.

GLSL to WGSL comparison

GLSLOpenGL / WebGL
float x = 1.0;
vec3 position = vec3(0.0);
const float PI = 3.14159;
WGSLWebGPU
var x: f32 = 1.0;
let position = vec3f(0.0);
const PI: f32 = 3.14159;
Key Differences
  • var for mutable, let for immutable
  • Type suffix on vector constructors (vec3f not vec3)
  • Type inference available with let

Click the tabs above to compare different GLSL and WGSL patterns.

Basic Types

WGSL provides four scalar types. These are the building blocks from which everything else is constructed.

f32 is a 32-bit floating-point number. Most shader math uses f32. Colors, positions, distances, angles—almost everything involves floats. The precision follows IEEE 754, which means consistent behavior across platforms.

i32 is a 32-bit signed integer. Use it for indices, counters, and integer arithmetic. The range spans from -2,147,483,648 to 2,147,483,647.

u32 is a 32-bit unsigned integer. It represents non-negative values from 0 to 4,294,967,295. Vertex indices and bit manipulation typically use u32.

bool represents true or false. Conditional logic and comparisons produce booleans. Unlike some languages, WGSL does not implicitly convert between bool and numeric types.

Type sizes and alignment

Scalars & Vectors
Matrices

Click a type to see alignment details. Alignment matters when laying out buffer data.

Vector types bundle scalars together. A vec2f holds two f32 values. A vec3f holds three. A vec4f holds four. The naming convention is vecNT where N is the component count and T is the element type suffix: f for f32, i for i32, u for u32.

let position: vec3f = vec3f(1.0, 2.0, 3.0);
let color: vec4f = vec4f(1.0, 0.0, 0.0, 1.0);  // red
let indices: vec3u = vec3u(0u, 1u, 2u);
wgsl

Vectors support component access through swizzling. Access individual components with x, y, z, w or equivalently r, g, b, a. Combine them to create new vectors.

Interactive: Vector swizzling

Source Vector
v = vec4f(
x
1.0
y
2.0
z
3.0
w
4.0
)
Result
v.xyzvec3f(1.0, 2.0, 3.0)
Creates a new vec3f from the selected components.

Click a swizzle pattern to see the result. Use xyzw or rgba notation interchangeably.

Matrix types store rectangular grids of floats. A mat4x4f is a 4×4 matrix of f32 values—16 floats total, arranged in four columns of four rows each. Matrices are essential for transformations: rotation, scaling, projection. WGSL stores matrices in column-major order, meaning each column is a vec4f.

let identity: mat4x4f = mat4x4f(
    1.0, 0.0, 0.0, 0.0,  // column 0
    0.0, 1.0, 0.0, 0.0,  // column 1
    0.0, 0.0, 1.0, 0.0,  // column 2
    0.0, 0.0, 0.0, 1.0   // column 3
);
wgsl

Variables and Let

WGSL distinguishes between mutable and immutable bindings. This distinction, borrowed from Rust, makes code intent clearer and enables compiler optimizations.

let creates an immutable binding. Once assigned, the value cannot change. Most local variables should be let bindings. If you do not need to modify a value, immutability signals that clearly.

let pi: f32 = 3.14159;
let doubled = pi * 2.0;  // type inferred as f32
wgsl

var creates a mutable binding. Use it when a value must change during execution. Loop counters, accumulators, and values computed incrementally require var.

var sum: f32 = 0.0;
sum = sum + 1.0;  // allowed because sum is var
sum += 2.0;       // compound assignment also allowed
wgsl

WGSL infers types when the initializer makes them unambiguous. You can write let x = 5.0; instead of let x: f32 = 5.0;. However, explicit types often improve readability, especially in complex shaders.

Numeric literals require attention. The literal 5 could be i32 or u32. The literal 5.0 is unambiguously f32. To be explicit, use suffixes: 5i for i32, 5u for u32, 5.0f for f32. When in doubt, be explicit.

Functions

Functions in WGSL follow a straightforward syntax. The keyword fn introduces a function, followed by the name, parameters, and return type.

fn add(a: f32, b: f32) -> f32 {
    return a + b;
}
 
fn square(x: f32) -> f32 {
    return x * x;
}
wgsl

Functions without a return type omit the arrow:

fn log_value(value: f32) {
    // side effects only, no return
}
wgsl

Entry points are special functions that the GPU calls. They are marked with attributes that declare their role in the pipeline. A vertex shader entry point processes each vertex. A fragment shader entry point processes each pixel. A compute shader entry point runs general computation.

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

@vertex
fn main(@builtin(vertex_index) idx: u32) -> VertexOutput {
var output: VertexOutput;
output.position = vec4f(0.0, 0.0, 0.0, 1.0);
output.color = vec3f(1.0, 0.0, 0.0);
return output;
}

The vertex shader above takes a vertex index and returns a position and color. The GPU invokes this function once per vertex, passing the index automatically.

Attributes

Attributes are annotations that provide metadata to the compiler. They appear before the item they modify, prefixed with @.

@vertex, @fragment, and @compute mark entry points. A shader module may contain multiple entry points of different types, or just one.

@vertex
fn vertex_main() -> @builtin(position) vec4f {
    return vec4f(0.0, 0.0, 0.0, 1.0);
}
 
@fragment
fn fragment_main() -> @location(0) vec4f {
    return vec4f(1.0, 0.0, 0.0, 1.0);
}
wgsl

@builtin connects parameters and return values to predefined pipeline inputs and outputs. The position builtin is special—the vertex shader must output it, and the rasterizer uses it to determine where fragments land.

Common builtins include:

  • vertex_index: the index of the current vertex (u32)
  • instance_index: the index of the current instance (u32)
  • position: clip-space position (vec4f), vertex output or fragment input
  • front_facing: whether the fragment is front-facing (bool)
  • frag_depth: overridden depth value (f32)

@location assigns a slot number for inter-stage communication and render target outputs. Vertex shader outputs at location 0 become fragment shader inputs at location 0. Fragment shader outputs go to render target attachments by location.

@group and @binding specify how resources (buffers, textures, samplers) connect to bind groups. These are covered in detail in the Bind Groups chapter.

Operators and Built-ins

WGSL supports the arithmetic operators you expect: +, -, *, / for basic math. The modulo operator % works on integers. Comparison operators <, >, <=, >=, ==, != produce booleans. Logical operators &&, ||, ! combine booleans.

Compound assignment operators modify variables in place: +=, -=, *=, /=. These are syntactic sugar—x += 1.0 means x = x + 1.0.

Vectors support element-wise operations. Adding two vec3f values adds their corresponding components. Multiplying a vec3f by a scalar multiplies each component.

let a = vec3f(1.0, 2.0, 3.0);
let b = vec3f(4.0, 5.0, 6.0);
let sum = a + b;        // vec3f(5.0, 7.0, 9.0)
let scaled = a * 2.0;   // vec3f(2.0, 4.0, 6.0)
wgsl

Built-in functions provide common mathematical operations:

let x = abs(-5.0);           // 5.0
let y = max(3.0, 7.0);       // 7.0
let z = clamp(5.0, 0.0, 1.0);// 1.0
let s = sin(radians(90.0));  // 1.0
let d = dot(a, b);           // 32.0 (1*4 + 2*5 + 3*6)
let c = cross(a, b);         // perpendicular vector
let n = normalize(a);        // unit vector in a's direction
let m = mix(0.0, 10.0, 0.5); // 5.0 (linear interpolation)
wgsl

The dot function computes the dot product of two vectors—essential for lighting calculations. The cross function produces a vector perpendicular to both inputs—fundamental for computing normals. The normalize function returns a unit vector—same direction, length of 1.

Structs

Structs group related data into named types. They are essential for organizing shader inputs, outputs, and uniform data.

struct Vertex {
    position: vec3f,
    normal: vec3f,
    uv: vec2f,
}
 
struct Material {
    color: vec4f,
    roughness: f32,
    metallic: f32,
}
wgsl

Struct instances are created by calling the struct name as a constructor:

let v = Vertex(
    vec3f(0.0, 1.0, 0.0),
    vec3f(0.0, 1.0, 0.0),
    vec2f(0.5, 0.5)
);
wgsl

When structs are used in uniform or storage buffers, memory layout matters. WGSL follows strict alignment rules. An f32 must be 4-byte aligned. A vec2f must be 8-byte aligned. A vec3f and vec4f must be 16-byte aligned. Structs themselves align to their largest member's alignment.

struct Uniforms {
    time: f32,           // offset 0, size 4
    // 12 bytes padding to align the next vec4f
    resolution: vec4f,   // offset 16, size 16
}
wgsl

The compiler inserts padding automatically, but you must account for it when creating buffers on the JavaScript side. Mismatched layouts cause garbage data.

When structs are used as inter-stage data (vertex shader outputs becoming fragment shader inputs), each member needs a location attribute:

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

The position uses a builtin because it has a special meaning to the rasterizer. The color and uv are user-defined data, passed via location slots.

Key Takeaways

  • WGSL is designed for safety and portability—every valid program behaves identically across platforms
  • Scalar types are f32, i32, u32, and bool; vectors bundle them (vec2f, vec3f, vec4f)
  • Use let for immutable bindings and var for mutable ones; prefer let when possible
  • Entry points are marked with @vertex, @fragment, or @compute attributes
  • @builtin connects to pipeline internals; @location assigns slots for custom data
  • Vectors support swizzling (v.xy, v.rgb) and element-wise operations
  • Structs group data; memory alignment matters for buffers