Learn WGSL for WebGPU and Bevy
WGSL is the shading language of WebGPU, and it is what Bevy uses. It looks more like Rust than like HLSL or GLSL, and it is strict: it tells you off for sloppy types. ShaderBattle includes a WGSL translator and checker, so the lessons come out as natural WGSL and your mistakes get WGSL-style explanations.
Things to know about WGSL
- Functions look like
fn frag(uv: vec2f) -> vec4f { ... }; types come after names. - Use
letfor values that never change andvarfor ones you change later. - There is no
? :operator: writeselect(valueIfFalse, valueIfTrue, condition). - Function parameters are read-only, you cannot assign to two components at once (
p.xz = ...), and integers and floats never mix: usef32(i). - The remainder operator
%works on floats and keeps the sign of the left side, like HLSL'sfmod.
Cheat sheet
| Idea | Write |
|---|---|
| Vectors | vec2f vec3f vec4f (or vec3<f32>) |
| Declare a value | let d = length(p); (var if you change it later) |
| A function | fn circle(p: vec2f, r: f32) -> f32 { ... } |
| Matrices | mat2x2f, multiply with M * v (this site writes v * M for lesson rotations) |
| Blend | mix(a, b, t) |
| Fractional part | fract(x) |
| Clamp to 0..1 | saturate(x) |
| Remainder | a % b (keeps the sign of a) |
| Choose | select(b, a, c) (false value first) |
| Angle of a vector | atan2(y, x) |
| Time in seconds | time |
| Pixel position | uv (0..1, bottom-left) |
| Width / height | aspect |
| Entry point | fn frag(uv: vec2f) -> vec4f { return …; } |
Handy built-ins: sin cos abs sign floor ceil fract step smoothstep clamp min max pow exp sqrt length distance dot cross normalize reflect select
Gotchas
- Every pixel runs on its own, with no memory of other pixels.
- Functions can be declared in any order.
- Parameters are read-only: copy one into a var to change it (var p = pIn;).
- You cannot assign to two components at once (p.xz = …). Write p.x and p.z separately.
- Floats and ints never mix silently: f32(i) turns a loop counter into a float.
Example: a glowing ring
The same shader as it is written for WebGPU / Bevy:
fn frag(uv: vec2f) -> vec4f
{
let p = (uv - 0.5) * vec2f(aspect, 1.0);
let d = length(p);
let m = 1.0 - smoothstep(0.0, 0.02, abs(d - 0.3));
let bg = vec3f(0.08, 0.08, 0.12);
let fg = vec3f(0.4, 0.9, 1.0);
return vec4f(mix(bg, fg, m), 1.0);
} Exporting to WebGPU
Create the module with device.createShaderModule, draw 3 vertices with vs_main / fs_main, and write time and aspect into a uniform buffer at group 0, binding 0. In Bevy: use it as a Material2d fragment shader, move Uniforms to @group(2), and read the time from globals.time (#import bevy_sprite::mesh2d_view_bindings::globals).
// ShaderBattle: Glowing ring
struct Uniforms { time: f32, aspect: f32 };
@group(0) @binding(0) var<uniform> u: Uniforms;
// globals your code reads, filled in by fs_main
var<private> time: f32;
var<private> aspect: f32;
fn frag(uv: vec2f) -> vec4f
{
let p = (uv - 0.5) * vec2f(aspect, 1.0);
let d = length(p);
let m = 1.0 - smoothstep(0.0, 0.02, abs(d - 0.3));
let bg = vec3f(0.08, 0.08, 0.12);
let fg = vec3f(0.4, 0.9, 1.0);
return vec4f(mix(bg, fg, m), 1.0);
}
struct VertexOut { @builtin(position) pos: vec4f, @location(0) uv: vec2f };
// one triangle that covers the screen
@vertex fn vs_main(@builtin(vertex_index) i: u32) -> VertexOut
{
let p = vec2f(f32((i << 1u) & 2u), f32(i & 2u));
var o: VertexOut;
o.pos = vec4f(p * 2.0 - 1.0, 0.0, 1.0);
o.uv = p; // (0,0) bottom-left
return o;
}
@fragment fn fs_main(v: VertexOut) -> @location(0) vec4f
{
time = u.time;
aspect = u.aspect;
return frag(v.uv);
} Questions
Is the WGSL checked by a real compiler?
The editor uses its own checker, and every exported file was also validated with the browser's own WebGPU shader compiler.
Can I use it in Bevy?
Yes. The export notes how to move the uniforms to Bevy's bind groups and read the time from the globals.
Other languages
Unity (Built-in) · Unity (URP) · Unreal · Shadertoy / WebGL · OpenGL (LWJGL, raylib) · three.js · GameMaker · LÖVE (Lua) · Godot · Apple (iOS, macOS)