Shader Lab
04.Vignette

Vignette

Arc 2 · 2D Screen-Space · Lesson 4 of 35
coordinate signal color
0.70
0.30

Purpose

A post-FX effect is almost always a mask computed in the signal layer, multiplied onto a sampled image in the color layer — vignette is the smallest, cleanest instance of that pattern.

Key insight

The canvas above is already showing a rendered scene. To darken the edges, the shader does two jobs per pixel: it gets the scene's color, and separately computes a number — a mask — that's bright in the middle and dark near the corners. The mask comes from length(uv - 0.5) fed through a smoothstep. Then the two are multiplied.

This "sample + mask + multiply" recipe is the template for vignette, bloom, film grain, dirty-lens — every screen-darkener you'll ever direct. Everything different between them is how the mask is computed.

Break it

Drag darkness to 1 and softness to 0. The image becomes a crisp black frame surrounding a visible center — the mask is now rendering as a shape you can point at. Teaches: a vignette is a soft-edged shape you multiply onto the image. Soften the edge and it reads as mood; harden it and it reads as a port-hole.

Now drop darkness back to 0 — the scene underneath is untouched. The mask was never destructive; you were only deciding how much of it to apply.

Direct Claude

"darker corners" darkness up "cinematic edges" darkness up, softness wide "gentler falloff" softness up, darkness down "port-hole" softness down, darkness up "keep the mood, lift the corners" darkness down, softness unchanged
Meta-phrase you gain here: "mask-and-multiply." The recipe name for any edge-darkener, dirt overlay, or soft-frame effect. Every shader you direct that dims or tints specific regions is an instance of this pattern with a different mask.
Combines with: L7 (chromatic aberration often gets a vignette over it), L11 (dither the mask for a ditherpunk edge), L12 (a CRT's edge-darkening is a vignette).
the fragment shader running above
uniform float u_darkness;
uniform float u_softness;

// The synthetic scene we're "sampling" — a sunset gradient + mountains + grid.
vec3 scene(vec2 uv) {
  vec3 col = mix(vec3(0.18, 0.10, 0.28), vec3(0.95, 0.48, 0.32), uv.y);
  col = mix(col, vec3(0.25, 0.60, 0.85), smoothstep(0.55, 0.98, uv.y));
  float sun = 1.0 - smoothstep(0.09, 0.11, length(uv - vec2(0.5, 0.55)));
  col = mix(col, vec3(1.0, 0.93, 0.68), sun);
  float mntn = 0.40 + 0.05 * sin(uv.x * 18.0) + 0.08 * sin(uv.x * 5.0 + 2.0);
  col = mix(col, vec3(0.10, 0.06, 0.20), step(uv.y, mntn) * step(uv.y, 0.5));
  if (uv.y < 0.5) {
    float yy = (0.5 - uv.y);
    float hz = smoothstep(0.004, 0.0, abs(fract(pow(yy, 0.7) * 8.0 - u_time * 0.25) - 0.5));
    float vl = smoothstep(0.004, 0.0, abs(fract((uv.x - 0.5) / max(yy, 0.02) * 4.0) - 0.5));
    col = mix(col, vec3(0.95, 0.30, 0.75), max(hz, vl) * 0.9);
  }
  return col;
}

void main() {
  vec2 uv = gl_FragCoord.xy / u_resolution;

  // COLOR LAYER — get the scene.
  vec3 col = scene(uv);

  // SIGNAL LAYER — distance from center, through a smoothstep.
  // Hard edge at softness=0; gentle falloff at softness=0.5.
  float d = length(uv - vec2(0.5));
  float mask = 1.0 - smoothstep(0.35, 0.35 + u_softness + 0.001, d);

  // COLOR LAYER — blend the mask in. u_darkness controls how much.
  col *= mix(1.0, mask, u_darkness);

  gl_FragColor = vec4(col, 1.0);
}