Draw with a shader
Paint a canvas on the GPU with a fragment shader.For a GPU-drawn visual, declare a canvas with type: "webgpu" and write a fragment
shader instead of a paint file. Msh compiles it to WGSL and a WebGL2 fallback, so it runs
everywhere. Editing the shader hot-reloads only that canvas.
Declare a GPU canvas in ui.ts
Canvas({ id: "viz", type: "webgpu", width: "fill", height: Theme.sizing(20) });Write the shader
Make canvases/viz/shader.ts. The body must begin with "use gpu". f.uv is 0..1
across the canvas; g is the per-frame globals.
import { defineShader, vec4f } from "@msh/device-sdk/device/shader";
export default defineShader((f, g) => {
"use gpu";
return vec4f(f.uv.x, f.uv.y, g.time, 1.0); // g.time animates
});Note
Import only from @msh/device-sdk/device/shader, never typegpu. It
re-exports defineShader, the constructors (vec2f/vec4f/...), and the
std math functions.
The globals are all named: g.time, g.resolution, g.dpr, g.pointer /
g.pointerActive, transport (g.bpm, g.beat, g.beatPhase, g.playing), and the node
palette (g.accent, g.background, g.foreground).
Read the device's params (audio-reactive shaders) by declaring them in opts.params and
taking the third argument p. Each is a live f32, the param's modulation-folded value.
export default defineShader(
(f, g, p) => {
"use gpu";
return vec4f(p.gain * f.uv.x, p.cutoff, g.time, 1.0);
},
{ params: ["gain", "cutoff"] },
);A shader can also sample channels (opts.channels, read with channel("name", x)),
buckets (opts.images, read with image("name", uv)), and the previous frame for
feedback (opts.feedback: true, read with prev(uv)). To stream data into a bucket, see
Stream data to a shader.
Full globals, macros, and rules in the GPU shader canvas reference.