---
title: Effects
description: An effect is a full-screen fragment shader; chain effects by binding a target as another effect's input.
---

# Effects



An [`Effect`](/docs/reference/vgpu/effect#effect) is a full-screen fragment shader created with `effect(gpu, source)`. Its pipeline compiles lazily on first use; call `await effect.compile(target)` during load if you want to pre-warm it. See [Compilation](/docs/concepts/compilation) for the full pre-warm flow. Every draw fills the whole target — you only write the fragment.

Effects chain through targets: render one effect into an offscreen [`Target`](/docs/reference/vgpu/target#target), then bind that target as a texture input of the next effect with `set()`.

The `uv` varying that `effect(gpu)` injects is top-origin: `(0, 0)` is the
top-left corner and `v` grows downward — the same convention as WebGPU texture
coordinates, `@builtin(position)`, and `target.read()`. Sampling any texture
with this `uv` needs no flip: a pass that samples `src` at `uv` reproduces the
image exactly. If you are porting a WebGL or Shadertoy shader that assumes
`v` grows upward, invert once at the boundary (`1.0 - uv.y`) and keep
everything else flip-free.

```ts
import { init, effect, sampler, surface, target } from "vgpu";

const gpu = await init();
const canvas = document.querySelector("canvas")!;
const canvasSurface = surface(gpu, canvas);

// ---cut---
const sceneSource = `
  @fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
    return vec4f(uv, 1.0, 1.0);
  }
`;

// Post-processing: reads the scene texture and inverts its colors.
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);
    return vec4f(1.0 - base.rgb, 1.0);
  }
`;

const scene = target(gpu, { size: [1280, 720] });

const sceneEffect = effect(gpu, sceneSource);
const post = effect(gpu, postSource);
post.set({
  src: scene,
  samp: sampler(gpu, { minFilter: 'linear', magFilter: 'linear' }),
}); // the offscreen result becomes the post input

sceneEffect.draw(scene); // render the scene offscreen
post.draw(canvasSurface); // invert it onto the canvas
```

Reach for `textureLoad` only when you need exact texels or an unfilterable
format — for ordinary sampling, a filtering sampler is simpler and faster.

`post.set(...)` exposes the offscreen result and filtering sampler to WGSL as bindings named `src` and `samp`. Each one-shot `draw()` encodes and submits its own work immediately, in call order.

## Updating bindings

You can update bindings at any time by using `.set`.

`set()` writes immediately — there is no change detection, so every call is a
real GPU write. Match your calls to how often values actually change: constants
once at creation, size- and resolution-class uniforms at init and on resize,
and per-frame calls only for genuinely dynamic values like time or pointer
input. Rebinding the same resources is free — bind groups are cached by
resource identity — so this rule is purely about avoiding redundant writes.

One more rule keeps multi-pass frames predictable: a frame records into a
single command buffer, and `set()` writes land before any of it executes — so
re-recording the same effect with mutated uniforms makes every pass read the
final values. When two passes need different values (a horizontal and a
vertical blur, say), create two effects; they are cheap, and each owns its
uniforms.

```ts
import { clock, init, effect, surface } from "vgpu";

const gpu = await init();
const canvas = document.querySelector("canvas")!;
const canvasSurface = surface(gpu, canvas);

// ---cut---
const pulseSource = `
  struct Params { time: f32, width: f32, height: f32 }
  @group(0) @binding(0) var<uniform> params: Params;

  @fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
    let glow = sin(params.time) * 0.5 + 0.5;
    return vec4f(uv.x, uv.y, glow, 1.0);
  }
`;

const pulse = effect(gpu, pulseSource, {
  // initial uniform defaults
  set: {
    params: {
      time: 0,
      width: canvasSurface.size[0],
      height: canvasSurface.size[1]
    }
  },
});

// update uniforms before drawing
pulse.set({
  params: {
    time: clock(gpu).time,
  },
});

pulse.draw(canvasSurface);
```

You should also only update uniforms when they need to change, for example, react to canvas size changes:

```ts
import { clock, init, effect, surface } from "vgpu";

const gpu = await init();
const canvas = document.querySelector("canvas")!;
const canvasSurface = surface(gpu, canvas);
const pulse = effect(gpu, `
  struct Params { time: f32, width: f32, height: f32 }
  @group(0) @binding(0) var<uniform> params: Params;

  @fragment fn fs_main(@location(0) uv: vec2f) -> @location(0) vec4f {
    return vec4f(uv, sin(params.time) * 0.5 + 0.5, 1.0);
  }
`, { set: { params: { time: 0, width: canvasSurface.size[0], height: canvasSurface.size[1] } } });

// ---cut---
const unsubscribe = canvasSurface.onResize(({ width, height }) => {
  pulse.set({ params: { width, height } }); // partial update: time keeps its value
});
```

`onResize()` fires the callback once immediately with the current size, then again on every resize. It returns an `unsubscribe` function — call it when you tear the effect down.


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)