MotionGPU renders fullscreen fragment shaders written in WGSL (WebGPU Shading Language). This page covers the WGSL concepts you need for effective shader authoring, the conventions MotionGPU imposes, and common visual patterns.
The fragment contract
Every material must declare exactly this function:
fn frag(uv: vec2f) -> vec4ffn frag(uv: vec2f) -> vec4f- — normalized coordinates from
uvat the bottom-left to(0, 0)at the top-right.(1, 1) - Return value — an RGBA color. The alpha channel is typically for opaque output.
1.0
If this signature is missing,
defineMaterialCoordinate space
(0, 1) ───────── (1, 1)
│ │
│ Your canvas │
│ │
(0, 0) ───────── (1, 0)(0, 1) ───────── (1, 1)
│ │
│ Your canvas │
│ │
(0, 0) ───────── (1, 0)This is the standard GPU convention (Y-up), not the DOM convention (Y-down). When converting mouse coordinates from DOM events, flip Y:
1.0 - (clientY - top) / heightBuilt-in bindings
MotionGPU injects the frame uniforms plus any user uniforms/textures you declare. You do not declare these bindings yourself.
Frame uniforms (always available)
These are injected by the renderer and updated every frame:
| Name | Type | Description |
|---|---|---|
motiongpuFrame.time | f32 | requestAnimationFrame |
motiongpuFrame.delta | f32 | Time since last frame in seconds (clamped by maxDelta |
motiongpuFrame.resolution | vec2f | Canvas size in physical pixels (width, height) |
User uniforms
Any uniform you declare in
defineMaterial({ uniforms: { ... } })motiongpuUniformsdefineMaterial({
fragment: `
fn frag(uv: vec2f) -> vec4f {
let t = sin(motiongpuUniforms.uTime * 3.0);
return vec4f(t, uv.y, motiongpuUniforms.uIntensity, 1.0);
}
`,
uniforms: {
uTime: 0,
uIntensity: 0.8
}
});defineMaterial({
fragment: `
fn frag(uv: vec2f) -> vec4f {
let t = sin(motiongpuUniforms.uTime * 3.0);
return vec4f(t, uv.y, motiongpuUniforms.uIntensity, 1.0);
}
`,
uniforms: {
uTime: 0,
uIntensity: 0.8
}
});Here
uTimeuIntensityf32motiongpuUniformsUser textures
Textures declared in the material become
texture_2d<f32>samplerdefineMaterial({
fragment: `
fn frag(uv: vec2f) -> vec4f {
let color = textureSample(uAlbedo, uAlbedoSampler, uv);
return color;
}
`,
textures: {
uAlbedo: { filter: 'linear' }
}
});defineMaterial({
fragment: `
fn frag(uv: vec2f) -> vec4f {
let color = textureSample(uAlbedo, uAlbedoSampler, uv);
return color;
}
`,
textures: {
uAlbedo: { filter: 'linear' }
}
});For each texture named
uFoo- — the texture binding
uFoo: texture_2d<f32> - — the associated sampler
uFooSampler: sampler
WGSL quick reference
If you are new to WGSL, here are the most common types and functions used in fragment shaders:
Scalar and vector types
| Type | Description | Example |
|---|---|---|
f32 | 32-bit float | let x: f32 = 3.14; |
i32 | 32-bit signed integer | let n: i32 = 42; |
u32 | 32-bit unsigned integer | let n: u32 = 7u; |
bool | Boolean | let b: bool = true; |
vec2f | 2-component float vector | vec2f(1.0, 2.0) |
vec3f | 3-component float vector | vec3f(1.0, 0.5, 0.0) |
vec4f | 4-component float vector | vec4f(r, g, b, a) |
Common math functions
| Function | Description |
|---|---|
sin(x)cos(x)tan(x) | Trigonometry |
abs(x) | Absolute value |
min(a, b)max(a, b) | Min/max |
clamp(x, lo, hi) | Range clamping |
mix(a, b, t) | Linear interpolation |
step(edge, x) | Step function (0.0 or 1.0) |
smoothstep(lo, hi, x) | Hermite interpolation |
length(v) | Vector magnitude |
distance(a, b) | Distance between two points |
normalize(v) | Unit vector |
dot(a, b) | Dot product |
cross(a, b) | Cross product (vec3f only) |
floor(x)ceil(x) | Rounding |
fract(x) | Fractional part (x - floor(x) |
pow(x, y) | Power |
exp(x)log(x) | Exp / natural log |
sqrt(x) | Square root |
Common shader patterns
UV gradient
The simplest shader maps UV coordinates directly to colours:
fn frag(uv: vec2f) -> vec4f {
return vec4f(uv.x, uv.y, 0.5, 1.0);
}fn frag(uv: vec2f) -> vec4f {
return vec4f(uv.x, uv.y, 0.5, 1.0);
}Animated sine wave
fn frag(uv: vec2f) -> vec4f {
let wave = 0.5 + 0.5 * sin(motiongpuFrame.time * 2.0 + uv.x * 10.0);
return vec4f(vec3f(wave), 1.0);
}fn frag(uv: vec2f) -> vec4f {
let wave = 0.5 + 0.5 * sin(motiongpuFrame.time * 2.0 + uv.x * 10.0);
return vec4f(vec3f(wave), 1.0);
}Radial glow
fn frag(uv: vec2f) -> vec4f {
let center = vec2f(0.5, 0.5);
let dist = distance(uv, center);
let glow = 0.01 / (dist * dist + 0.001);
return vec4f(glow * 0.2, glow * 0.4, glow * 0.8, 1.0);
}fn frag(uv: vec2f) -> vec4f {
let center = vec2f(0.5, 0.5);
let dist = distance(uv, center);
let glow = 0.01 / (dist * dist + 0.001);
return vec4f(glow * 0.2, glow * 0.4, glow * 0.8, 1.0);
}Circle SDF (Signed Distance Field)
fn frag(uv: vec2f) -> vec4f {
let center = vec2f(0.5, 0.5);
let radius = 0.3;
let dist = length(uv - center) - radius;
let edge = smoothstep(0.005, 0.0, dist);
return vec4f(vec3f(edge), 1.0);
}fn frag(uv: vec2f) -> vec4f {
let center = vec2f(0.5, 0.5);
let radius = 0.3;
let dist = length(uv - center) - radius;
let edge = smoothstep(0.005, 0.0, dist);
return vec4f(vec3f(edge), 1.0);
}Aspect-ratio correction
The UV space is always
[0..1] × [0..1]fn frag(uv: vec2f) -> vec4f {
let aspect = motiongpuFrame.resolution.x / motiongpuFrame.resolution.y;
var corrected = uv;
corrected.x *= aspect;
// Now corrected.x ranges from 0 to aspect, corrected.y from 0 to 1
let dist = length(corrected - vec2f(aspect * 0.5, 0.5));
let circle = smoothstep(0.305, 0.3, dist);
return vec4f(vec3f(circle), 1.0);
}fn frag(uv: vec2f) -> vec4f {
let aspect = motiongpuFrame.resolution.x / motiongpuFrame.resolution.y;
var corrected = uv;
corrected.x *= aspect;
// Now corrected.x ranges from 0 to aspect, corrected.y from 0 to 1
let dist = length(corrected - vec2f(aspect * 0.5, 0.5));
let circle = smoothstep(0.305, 0.3, dist);
return vec4f(vec3f(circle), 1.0);
}Color output and color space
By default,
FragCanvasoutputColorSpace: 'srgb'If you are doing your own color management and want raw linear output, set
outputColorSpace="linear"FragCanvas