Passes
A pass is a render-pass section inside a frame. It has one target, one clear color, and any number of draw calls. Open a pass by hand when you want to composite multiple draws into the same render target — here, an ocean and a boat rendered straight to the canvas:
import { init, effect, frame, surface } from "vgpu";
const gpu = await init();
const canvas = document.querySelector("canvas")!;
const canvasSurface = surface(gpu, canvas);
const oceanSource = `
@fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
let wave = sin(uv.x * 24.0) * 0.01;
let depth = smoothstep(0.4 + wave, 1.0, uv.y);
return vec4f(0.1, 0.3 + depth * 0.2, 0.55 + depth * 0.3, 1.0);
}
`;
// Draws only the hull pixels; discards everything else.
const boatSource = `
@fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
let inHull = abs(uv.x - 0.5) < 0.12 && abs(uv.y - 0.42) < 0.05;
if (!inHull) { discard; }
return vec4f(0.45, 0.26, 0.13, 1.0);
}
`;
// ---cut---
const ocean = effect(gpu, oceanSource);
const boat = effect(gpu, boatSource);
frame(gpu, (currentFrame) => {
currentFrame.pass({ target: canvasSurface, clear: [0, 0, 0, 1] }, (pass) => {
pass.draw(ocean); // fill the canvas with water
pass.draw(boat); // paint the boat on top — same target
});
});Both draws share one render pass and one target. Order inside the pass is paint order: the ocean fills the canvas first, then the boat draws on top of it.
Good to know:
FramePass.draw()accepts a fullscreenEffector an explicitDraw. Usedraw(gpu)when you need meshes, vertex counts, instancing, or raw bind groups.
One shader? Draw it directly
Now add postprocessing. The pass is the same — the only change is its target: an offscreen Target with the same size as the canvas. Then the postprocessing effect (bound with set({ src: scene })) needs no pass ceremony to reach the screen:
import { init, effect, frame, sampler, surface, target } from "vgpu";
const gpu = await init();
const canvas = document.querySelector("canvas")!;
const canvasSurface = surface(gpu, canvas);
const ocean = effect(gpu, `
@fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
let wave = sin(uv.x * 24.0) * 0.01;
let depth = smoothstep(0.4 + wave, 1.0, uv.y);
return vec4f(0.1, 0.3 + depth * 0.2, 0.55 + depth * 0.3, 1.0);
}
`);
const boat = effect(gpu, `
@fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
let inHull = abs(uv.x - 0.5) < 0.12 && abs(uv.y - 0.42) < 0.05;
if (!inHull) { discard; }
return vec4f(0.45, 0.26, 0.13, 1.0);
}
`);
const postSource = `
@group(0) @binding(0) var src: texture_2d<f32>;
@group(0) @binding(1) var samp: sampler;
@fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
let base = textureSampleLevel(src, samp, uv, 0.0);
let vignette = 1.0 - 0.4 * length(uv - vec2f(0.5));
return vec4f(base.rgb * vignette, 1.0);
}
`;
// ---cut---
const scene = target(gpu, { size: [canvasSurface.size[0], canvasSurface.size[1]] });
const postprocessing = effect(gpu, postSource);
postprocessing.set({
src: scene,
samp: sampler(gpu, { minFilter: 'linear', magFilter: 'linear' }),
}); // the offscreen scene becomes the post input
frame(gpu, (currentFrame) => {
currentFrame.pass({ target: scene, clear: [0, 0, 0, 1] }, (pass) => {
pass.draw(ocean);
pass.draw(boat);
});
});
postprocessing.draw(canvasSurface); // rendering an effect creates a passThe one-shot draw() runs after the frame has submitted, so the scene is already rendered when postprocessing reads it. Under the hood it creates an encoder, opens a render pass on canvasSurface, encodes the draw, and submits — the same GPU work you would write by hand with frame.pass.
Good to know:
frame.pass()always needs a target. Use a canvas-backedSurfacefromsurface(gpu, canvas)or an offscreenTargetfromtarget(gpu, { size }).
Good to know: a pass takes more than a target and a clear color.
FramePassOptionsalso setsclearDepth(0for reversed-Z),clearStencil, aviewportorscissorrectangle for split-screen and partial redraws,depthReadOnlyto depth-test while sampling the depth texture, atimerspan for GPU timing, andvisibilityfor occlusion queries.