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 workgroup 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. ↩︎