For an in-depth overview of WGSL see Tour of WGSL. There’s also the actual WGSL spec though it can be hard to process since it’s written for language lawyers 😂
This article assumes you already know how to program so that that just by looking at examples of WGSL you should be likely be able to kind of grok what you see. It’s probably way too terse but hopefully it can give you some help in understanding and writing WGSL shader programs.
Unlike JavaScript, WGSL requires knowing the types of every variable, struct field, function parameter and function return type. If you’ve used typescript, rust, C++, C#, Java, Swift, Kotlin, etc then you’re used to this.
The plain types in WGSL are:
i32 a 32 bit signed integeru32 a 32 bit unsigned integerf32 a 32 bit floating point numberbool a boolean valuef16 a 16 bit floating point number (this is an optional feature you need to check for and request)In JavaScript you can declare variables and functions like this.
var a = 1;
let c = 3;
function d(e) { return e * 2; }
In WGSL the full form of those would be
var a: f32 = 1;
let c: f32 = 3;
fn d(e: f32) -> f32 { return e * 2; }
The important thing to note from above is having to add : <type> like : f32
for the variable declarations and -> <type> to function declarations.
WGSL has a shortcut for variables. Similar to typescript, if you don’t declare the type of the variable then it automatically becomes the type of the expression on the right.
fn foo() -> bool { return false; }
var a = 1; // a is an i32
let b = 2.0; // b is an f32
var c = 3u; // c is a u32
var d = foo(); // d is bool
Further, being strictly type means you often have to convert types.
let a = 1; // a is an i32 let b = 2.0; // b is a f32 *let c = a + b; // ERROR can't add an i32 to an f32
The fix is to convert one to the other.
let a = 1; // a is an i32 let b = 2.0; // b is a f32 let c = f32(a) + b; // ok
but!, WGSL has what are called “AbstractInt” and “AbstractFloat”. You can think of them as numbers that have not yet decided their type. These are compile time only features.
let a = 1; // a is an i32 let b = 2.0; // b is a f32 *let c = a + b; // ERROR can't add an i32 to an f32 let d = 1 + 2.0; // d is a f32
2i // i32 3u // u32 4f // f32 4.5f // f32 5h // f16 5.6h // f16 6 // AbstractInt 7.0 // AbstractFloat
let var and const mean different things in WGSL vs JavaScriptIn JavaScript var is a variable with function scope. let is a variable with block scope. const is a constant variable (can’t be changed) [1] with block scope.
In WGSL all variables have block scope. var is a variable that has storage and so is mutable. let is a constant value.
fn foo() {
let a = 1;
* a = a + 1; // ERROR: a is a constant expression
var b = 2;
b = b + 1; // ok
}
const is not a variable, it’s a compile time constant. You can
not use const for something that happens at runtime.
const one = 1; // ok
const two = one * 2; // ok
const PI = radians(180.0); // ok
fn add(a: f32, b: f32) -> f32 {
* const result = a + b; // ERROR! const can only be used with compile time expressions
return result;
}
WGSL has 3 vector types vec2, vec3, and vec4. Their basic style is vec?<type>
so vec2<i32> (a vector of two i32s), vec3<f32> (a vector of 3 f32s), vec4<u32>(a vector of 4 u32s),
vec3<bool> a vector of 3 boolean values.
Examples:
let a = vec2<i32>(1, -2); let b = vec3<f32>(3.4, 5.6, 7.8); let c = vec4<u32>(9, 10, 11, 12);
You can access the values inside a vector with various accessors.
let a = vec4<f32>(1, 2, 3, 4); let b = a.z; // via x,y,z,w let c = a.b; // via r,g,b,a let d = a[2]; // via array element accessors
Above, b, c, and d are all the same. They are all accessing the 3rd element of a. They are all ‘3’.
You can also access more than 1 element.
let a = vec4<f32>(1, 2, 3, 4); let b = a.zx; // via x,y,z,w let c = a.br; // via r,g,b,a let d = vec2<f32>(a[2], a[0]);
Above, b, c, and d are all the same. They are all a vec2<f32>(3, 1).
You can also repeat elements.
let a = vec4<f32>(1, 2, 3, 4); let b = vec3<f32>(a.z, a.z, a.y); let c = a.zzy;
Above b and c are the same. They both vec3<f32> who contents is 3, 3, 2.
There are shortcuts for the base types. Change the <i32> => i, <f32> => f, <u32> to u and <f16> to h so
let a = vec4<f32>(1, 2, 3, 4); let b = vec4f(1, 2, 3, 4);
a and b are the same type.
vectors can be constructed with smaller types.
let a = vec4f(1, 2, 3, 4); let b = vec2f(2, 3); let c = vec4f(1, b, 4); let d = vec4f(1, a.yz, 4); let e = vec4f(a.xyz, 4); let f = vec4f(1, a.yzw);
a, c, d, e and f are the same.
You can do math on vectors.
let a = vec4f(1, 2, 3, 4); let b = vec4f(5, 6, 7, 8); let c = a + b; // c is vec4f(6, 8, 10, 12) let d = a * b; // d is vec4f(5, 12, 21, 32) let e = a - b; // e is vec4f(-4, -4, -4, -4)
Many functions also work on vectors.
let a = vec4f(1, 2, 3, 4); let b = vec4f(5, 6, 7, 8); let c = mix(a, b, 0.5); // c is vec4f(3, 4, 5, 6) let d = mix(a, b, vec4f(0, 0.5, 0.5, 1)); // d is vec4f(1, 4, 5, 8)
WGSL has a bunch of matrix types. Matrices are arrays of vectors.
The format is mat<numVectors>x<vectorSize><<type>> so for example
mat3x4<f32> is an array of 3 vec4<f32>s. Like vectors, matrices
have the same shortcuts
let a: mat4x4<f32> = ... let b: mat4x4f = ...
a and b are the same type.
You can reference a vector of a matrix with array syntax.
let a = mat4x4f(...); let b = a[2]; // b is a vec4f of the 3rd vector of a
The most common matrix type for 3D computation is mat4x4f and can be multiplied directly
with a vec4f to produce another vec4f.
let a = mat4x4f(....); let b = vec4f(1, 2, 3, 4); let c = a * b; // c is a vec4f and the result of a * b
Arrays in WGSL are declared with the array<type, numElements> syntax.
let a = array<f32, 5>; // an array of five f32s let b = array<vec4f, 6>; // an array of six vec4fs
But there’s also the array constructor. It takes any number of arguments
and returns an array. The arguments must all be of the same type.
let arrOf3Vec3fsA = array(vec3f(1,2,3), vec3f(4,5,6), vec3f(7,8,9)); let arrOf3Vec3fsB = array<vec3f, 3>(vec3f(1,2,3), vec3f(4,5,6), vec3f(7,8,9));
Above arrOf3Vec3fsA is the same as arrOf3Vec3fsB.
Unfortunately, as of version 1 of WGSL there is no way to get the size of fixed size array.
Arrays that are at the root scope storage declarations or as the last field in a root scope struct are the only arrays that can be specified with no size.
struct Stuff {
color: vec4f,
size: f32,
verts: array<vec3f>,
};
@group(0) @binding(0) var<storage> foo: array<mat4x4f>;
@group(0) @binding(1) var<storage> bar: Stuff;
The number of elements in foo and in bar.verts is defined by the settings
of the bind group used at runtime. You can query this size in your WGSL with
arrayLength.
@group(0) @binding(0) var<storage> foo: array<mat4x4f>; @group(0) @binding(1) var<storage> bar: Stuff; ... let numMatrices = arrayLength(&foo); let numVerts = arrayLength(&bar.verts);
Functions in WGSL follow the pattern fn name(parameters) -> returnType { ..body... }.
fn add(a: f32, b: f32) -> f32 {
return a + b;
}
WGSL programs need an entry point. An entry point is designated by either @vertex, @fragment or @compute.
@vertex fn myFunc(a: f32, b: f32) -> @builtin(position): vec4f {
return vec4f(0, 0, 0, 0);
}
@group(0) @binding(0) var<uniforms> uni: vec4f;
vec4f fn foo() {
return uni;
}
@vertex fn vs1(): @builtin(position) vec4f {
return vec4f(0);
}
@vertex fn vs2(): @builtin(position) vec4f {
return foo();
}
Above uni is not accessed by vs1 and so will not show up as a required
binding if you use vs1 in a pipeline. vs2 does reference uni indirectly through
calling foo so it will show up as a required binding when using vs2 in a pipeline.
The word attributes has 2 meanings in WebGPU. One is vertex attributes which is covered in the article on vertex buffers.
The other is in WGSL where an attribute starts with @.
@location(number)@location(number) is used to defined inputs and outputs of shaders.
For a vertex shader, inputs are defined by the @location attributes
of the entry point function of the vertex shader.
@vertex vs1(@location(0) foo: f32, @location(1) bar: vec4f) ...
struct Stuff {
@location(0) foo: f32,
@location(1) bar: vec4f,
};
@vertex vs2(s: Stuff) ...
Both vs1 and vs2 declare inputs to the vertex shader on locations 0 and 1 which need to be supplied by vertex buffers.
For inter stage variables, @location attributes define the location where the variables are passed between shaders.
struct VSOut {
@builtin(position) pos: vec4f,
@location(0) color: vec4f,
@location(1) texcoords: vec2f,
};
struct FSIn {
@location(1) uv: vec2f,
@location(0) diffuse: vec4f,
};
@vertex fn foo(...) -> VSOut { ... }
@fragment fn bar(moo: FSIn) ...
Above, the vertex shader foo passes color as vec4f on location(0) and texcoords as a vec2f on location(1).
The fragment shader bar receives them as uv and diffuse because their locations match.
For fragment shaders, @location specifies which GPURenderPassDescriptor.colorAttachment to store the result in.
struct FSOut {
@location(0) albedo: vec4f;
@location(1) normal: vec4f;
}
@fragment fn bar(...) -> FSOut { ... }
@builtin(name)The @builtin attribute is used to specify that a particular variable’s value comes
from a built-in feature of WebGPU.
@vertex fn vs1(@builtin(vertex_index) foo: u32, @builtin(instance_index) bar: u32) ... {
...
}
Above foo gets its value from the builtin vertex_index and bar gets its value from the builtin instance_index.
struct Foo {
@builtin(vertex_index) vNdx: u32,
@builtin(instance_index) iNdx: u32,
}
@vertex fn vs1(blap: Foo) ... {
...
}
Above blap.vNdx gets its value from the builtin vertex_index and blap.iNdx gets its value from the builtin instance_index.
| Builtin Name | Stage | IO | Type | Description |
|---|---|---|---|---|
| vertex_index | vertex | input | u32 |
Index of the current vertex within the current API-level draw command,
independent of draw instancing.
For a non-indexed draw, the first vertex has an index equal to the For an indexed draw, the index is equal to the index buffer entry for the
vertex, plus the |
| instance_index | vertex | input | u32 |
Instance index of the current vertex within the current API-level draw command.
The first instance has an index equal to the |
| position | vertex | output | vec4<f32> | Output position of the current vertex, using homogeneous coordinates. After homogeneous normalization (where each of the x, y, and z components are divided by the w component), the position is in the WebGPU normalized device coordinate space. See WebGPU § 3.3 Coordinate Systems. |
| fragment | input | vec4<f32> | Framebuffer position of the current fragment in framebuffer space. (The x, y, and z components have already been scaled such that w is now 1.) See WebGPU § 3.3 Coordinate Systems. | |
| front_facing | fragment | input | bool | True when the current fragment is on a front-facing primitive. False otherwise. |
| frag_depth | fragment | output | f32 | Updated depth of the fragment, in the viewport depth range. See WebGPU § 3.3 Coordinate Systems. |
| local_invocation_id | compute | input | vec3<u32> | The current invocation’s local invocation ID, i.e. its position in the workgroup grid. |
| local_invocation_index | compute | input | u32 | The current invocation’s local invocation index, a linearized index of the invocation’s position within the workgroup grid. |
| global_invocation_id | compute | input | vec3<u32> | The current invocation’s global invocation ID, i.e. its position in the compute shader grid. |
| workgroup_id | compute | input | vec3<u32> | The current invocation’s workgroup ID, i.e. the position of the workgroup in the compute shader grid. |
| num_workgroups | compute | input | vec3<u32> | The dispatch size, vec<u32>(group_count_x, group_count_y, group_count_z), of the compute shader dispatched by the API. |
| sample_index | fragment | input | u32 | Sample index for the current fragment.
The value is least 0 and at most sampleCount-1, where sampleCount is the MSAA sample count specified for the GPU render pipeline. See WebGPU § 10.3 GPURenderPipeline. |
| sample_mask | fragment | input | u32 | Sample coverage mask for the current fragment.
It contains a bitmask indicating which samples in this fragment are covered
by the primitive being rendered. See WebGPU § 23.3.11 Sample Masking. |
| fragment | output | u32 | Sample coverage mask control for the current fragment.
The last value written to this variable becomes the shader-output mask.
Zero bits in the written value will cause corresponding samples in
the color attachments to be discarded. See WebGPU § 23.3.11 Sample Masking. |
Like most computer languages, WGSL has flow control statements.
for (var i = 0; i < 10; i++) { ... }
if (i < 5) {
...
} else if (i > 7) {
..
} else {
...
}
var j = 0;
while (j < 5) {
...
j++;
}
var k = 0;
loop {
k++;
if (k >= 5) {
break;
}
}
var k = 0;
loop {
k++;
if (k >= 5) {
break;
}
}
var k = 0;
loop {
k++;
break if (k >= 5);
}
for (var i = 0; i < 10; ++i) {
if (i % 2 == 1) {
continue;
}
...
}
for (var i = 0; i < 10; ++i) {
if (i % 2 == 1) {
continue;
}
...
continuing {
// continue goes here
...
}
}
if (v < 0.5) {
discard;
}
discard exits the shader. It can only be used in a fragment shader.
var a : i32;
let x : i32 = generateValue();
switch x {
case 0: { // The colon is optional
a = 1;
}
default { // The default need not appear last
a = 2;
}
case 1, 2, { // Multiple selector values can be used
a = 3;
}
case 3, { // The trailing comma is optional
a = 4;
}
case 4 {
a = 5;
}
}
switch only works with u32 or i32 and cases must be constants.
| Name | Operators | Associativity | Binding |
|---|---|---|---|
| Parenthesized | (...) |
||
| Primary | a(), a[], a.b |
Left-to-right | |
| Unary | -a, !a, ~a, *a, &a |
Right-to-left | All above |
| Multiplicative | a * b, a / b, a % b |
Left-to-right | All above |
| Additive | a + b, a - b |
Left-to-right | All above |
| Shift | a << b, a >> b |
Requires parentheses | Unary |
| Relational | a < b, a > b, a <= b, a >= b, a == b, a != b |
Requires parentheses | All above |
| Binary AND | a & b |
Left-to-right | Unary |
| Binary XOR | a ^ b |
Left-to-right | Unary |
| Binary OR | a | b |
Left-to-right | Unary |
| Short-circuit AND | a && b |
Left-to-right | Relational |
| Short-circuit OR | a || b |
Left-to-right | Relational |
See the WGSL Function reference.
if, while, switch, break-if expressions don’t need parenthesizes.if a < 5 {
doTheThing();
}
Many languages have a ternary operator condition ? trueExpression : falseExpression
WGSL does not. WGSL does have select.
let a = select(falseExpression, trueExpression, condition);
++ and -- are statements, not expressions.Many languages have pre-increment and post-increment operators.
// JavaScript let a = 5; let b = a++; // b = 5, a = 6 (post-increment) let c = ++a; // c = 7, a = 7 (pre-increment)
WGSL has neither. It just has the increment and decrement statements.
// WGSL var a = 5; a++; // is now 6 *++a; // ERROR: no such thing as pre-increment *let b = a++; // ERROR: a++ is not an expression, it's a statement
+=, -= are not expressions, they’re assignment statements// JavaScript let a = 5; a += 2; // a = 7 let b = a += 2; // a = 9, b = 9
// WGSL let a = 5; a += 2; // a is 7 *let b = a += 2; // ERROR: a += 2 is not an expression
In some languages but not WGSL.
var color = vec4f(0.25, 0.5, 0.75, 1); *color.rgb = color.bgr; // ERROR color = vec4(color.bgr, color.a); // Ok
Note: there is a proposal to add this feature.
__ is a special variable you can assign to to make something appear used but not actually use it.
@group(0) @binding(0) var<uniforms> uni1: vec4f;
@group(0) @binding(0) var<uniforms> uni2: mat4x4f;
@vertex fn vs1(): @builtin(position) vec4f {
return vec4f(0);
}
@vertex fn vs2(): @builtin(position) vec4f {
_ = uni1;
_ = uni2;
return vec4f(0);
}
Above neither uni1 nor uni2 are accessed by vs1 and so they will not show up as a required
bindings if you use vs1 in a pipeline. vs2 does reference both uni1 and uni2
so they will both show up as a required bindings when using vs2 in a pipeline.
Copyright © 2023 World Wide Web Consortium. W3C® liability, trademark and permissive document license rules apply.
Variables in JavaScript hold base types of undefined, null, boolean, number, string, reference-to-object.
It can be confusing for people new to programming that const o = {name: 'foo'}; o.name = 'bar'; works because o was declared as const.
The thing is, o is const. It is a constant reference to the object. You can not change which object o references. You can change object itself. ↩︎