Gaussian Blur

Separable Gaussian blur

Quick Start

import { createGaussianBlur } from './webgpu-market/gaussian-blur/gaussian-blur';

const blur = createGaussianBlur(device, { format: 'rgba8unorm' });

const output = device.createTexture({
  size: [1024, 768],
  format: 'rgba8unorm',
  usage: GPUTextureUsage.RENDER_ATTACHMENT | GPUTextureUsage.TEXTURE_BINDING,
});

blur.apply(sourceTexture, output, { radius: 8, sigma: 4.0 });
// output now contains the blurred result

blur.destroy();
Source
// Separable Gaussian blur fragment shader
//
// Samples along a single axis (horizontal or vertical) using
// precomputed kernel weights. Uses hardware texture sampling
// for bilinear filtering and edge clamping.

struct Uniforms {
  direction: vec2f,   // (1,0) for horizontal, (0,1) for vertical
  texel_size: vec2f,  // 1.0 / texture dimensions
  radius: i32,
  _pad0: i32,
  _pad1: i32,
  _pad2: i32,
}

// Kernel weights stored as an array of f32.
// Max 33 weights (center + up to 32 on each side, but weights are symmetric).
@group(0) @binding(0) var<uniform> u: Uniforms;
@group(0) @binding(1) var<storage, read> weights: array<f32>;
@group(0) @binding(2) var source_tex: texture_2d<f32>;
@group(0) @binding(3) var source_sampler: sampler;

struct VertexOutput {
  @builtin(position) position: vec4f,
  @location(0) uv: vec2f,
}

// Fullscreen triangle — 3 vertices cover the entire screen
@vertex
fn vs(@builtin(vertex_index) i: u32) -> VertexOutput {
  let uv = vec2f(f32((i << 1u) & 2u), f32(i & 2u));
  var out: VertexOutput;
  out.position = vec4f(uv * 2.0 - 1.0, 0.0, 1.0);
  out.uv = vec2f(uv.x, 1.0 - uv.y);
  return out;
}

@fragment
fn fs(in: VertexOutput) -> @location(0) vec4f {
  // Center sample (weight at index 0)
  var color = textureSample(source_tex, source_sampler, in.uv) * weights[0];

  // Symmetric samples on both sides of center
  let step = u.direction * u.texel_size;

  for (var i = 1i; i <= u.radius; i++) {
    let offset = step * f32(i);
    let w = weights[i];
    color += textureSample(source_tex, source_sampler, in.uv + offset) * w;
    color += textureSample(source_tex, source_sampler, in.uv - offset) * w;
  }

  return color;
}
Documentation

Gaussian Blur

Separable Gaussian blur using two render passes. Takes a source GPUTexture and writes the blurred result to a caller-provided target GPUTexture. Uses fragment shaders with hardware texture sampling for bilinear filtering and edge clamping.

API

createGaussianBlur(device, options?)

Returns a GaussianBlur instance.

Option Type Default Description
format GPUTextureFormat 'rgba8unorm' Texture format (must match source)

blur.apply(source, target, options?)

Blurs the source texture and writes the result to the target texture.

  • sourceGPUTexture to read from (must have TEXTURE_BINDING usage)
  • targetGPUTexture to write to (must have RENDER_ATTACHMENT usage)
Option Type Default Description
radius number 8 Blur radius in pixels (0–32)
sigma number radius / 2 Gaussian standard deviation

When radius is 0, the source is copied to the target without blurring.

blur.destroy()

Releases internal textures and buffers. Does not destroy source or target textures.

Further Reading

Further Reading

Resources on Gaussian blur, separable convolutions, and GPU image filtering.

Core Theory

  • Heckbert, "Filtering by Repeated Integration" (SIGGRAPH 1986) Foundational paper on efficient image filtering techniques, including the separability of Gaussian kernels that makes two-pass blur possible. https://dl.acm.org/doi/10.1145/15886.15921

  • Deriche, "Recursively Implementing the Gaussian and Its Derivatives" (1993) Introduces recursive (IIR) Gaussian filtering that achieves O(1) cost per pixel regardless of kernel size. A useful alternative for very large radii. https://inria.hal.science/inria-00074778/document

GPU Implementation

Post-Processing Applications

Bloom

General References