Skip to content
Sample of Tile Blur (WebGPU separable Gaussian tile-shared-memory blur)

anadol_webgpu · tileBlur

Tile Blur (WebGPU separable Gaussian tile-shared-memory blur)

The WebGPU separable Gaussian blur primitive: a workgroup of TILE×TILE threads loads a tile+halo of the source image into workgroup-shared memory, workgroupBarrier(), convolves each pixel with a 1D Gaussian along the row, writes to a ping-pong texture; then a second compute pass applies the column-direction Gaussian on the intermediate texture. The 1D factorisation reduces an O(K²) 2D kernel cost to O(2K) per pixel — the canonical GPU tutorial pattern. Here numpy implements the same algorithm (1D conv along rows then columns) on a synthesised textured image; each step adds high-frequency detail so the blur evolves over time, and sigma can drift (or oscillate via a sine envelope).

Original: https://lisyarus.github.io/blog/posts/compute-blur.html

▶ Try the live demo (JS port) ▶ Run reference.py via Pyodide 10 formulas have instant JS ports. The Pyodide path runs the actual Python in your browser — first load is ~6 MB, then instant.

Family
anadol_webgpu
Source
anadol · webgpu
Evolution
compute
Render
blurred_texture
Complexity
low
Agents
1
Year
2024
Author
WebGPU compute primitives (concept)

Math essence

For each axis (x, y) apply 1D Gaussian convolution with kernel k_i = exp(-(i-µ)²/(2σ²)) / Σ k_j. Padded to keep spatial extent unchanged. Two passes replace O(K²) cost with O(2K). Step adds Gaussian perturbation plus two 'ink blot' disk features and oscillates sigma ±1.5 around the base.

Parameters

Name Type Default Range Description
dim int 512 128–2048 Square image side length.
sigma float 4 0.5–50 Gaussian sigma (px).
blur_step float 0 -1–1 Per-step sigma delta.
sigma_min float 0.5 0–10 Lower bound on dynamic sigma.
sigma_max float 24 1–100 Upper bound on dynamic sigma.
noise_scale float 0.15 0–1 Per-step detail amplitude.
workgroup_tile int 16 4–64 WebGPU equivalent workgroup tile size.

Reference

First 60 lines of reference.py. Full source: library_external/anadol_webgpu/tileBlur/reference.py

"""tileBlur — WebGPU separable tile-shared-memory Gaussian blur.

The classical GPU image-blur pattern:

  1. Horizontal pass: each workgroup (TILE x TILE) loads a tile of
     the input image plus a halo of border pixels into
     workgroup-shared memory, workgroupBarrier(), convolves each pixel
     with a 1D Gaussian kernel along the x-axis, writes to a ping-pong
     texture.
  2. Vertical pass: same algorithm along the y-axis reading the
     intermediate texture as input.

The 1D factorisation reduces an O(K²) kernel to O(2K) texture
accesses per pixel, which is exactly why "separable Gaussian blur"
is the canonical GPU tutorial.

Here we implement the algorithm in numpy using two passes of 1D
Gaussian convolution along rows then columns; the input image is a
synthesised textured pattern so the blur result is interesting.
Each step "tick" adds a small noise perturbation to the source so
the blur evolves over time.

Reference:
- https://www.mysimulator.uk/content/articles/webgpu-compute.html
  ("2D convolution using workgroup tiles ... separable Gaussian kernel
  in shared memory")
- https://lisyarus.github.io/blog/posts/compute-blur.html
  ("Compute shaders in graphics: Gaussian blur")
"""
from __future__ import annotations

from typing import ClassVar

import numpy as np


def _gauss_kernel_1d(sigma: float, radius: int | None = None) -> np.ndarray:
    if radius is None:
        radius = max(1, int(np.ceil(3.0 * sigma)))
    x = np.arange(-radius, radius + 1, dtype=np.float64)
    k = np.exp(-(x * x) / (2.0 * sigma * sigma))
    k /= k.sum()
    return k.astype(np.float64)


def _separable_gaussian_blur(img: np.ndarray, sigma_x: float, sigma_y: float) -> np.ndarray:
    """Apply a separable 2D Gaussian blur: 1D along rows then along cols."""
    H, W, C = img.shape
    out = img.astype(np.float32)

    if sigma_x > 0:
        kx = _gauss_kernel_1d(sigma_x)
        r = kx.size // 2
        # pad each row
        pad = np.zeros((H, r, C), dtype=np.float32)
        padded = np.concatenate([pad, out, pad], axis=1)
        tmp = np.zeros_like(out)
        for i in range(kx.size):
            tmp += kx[i] * padded[:, i:i + W, :]
        out = tmp

Try running it locally

The reference implementation is self-contained Python with numpy. Install the package and run:

python library_external/anadol_webgpu/tileBlur/reference.py

Need help? See Learn for the standard environment setup, or Playground for an in-browser sketchbook.

More from anadol · webgpu

Sample of 2D FFT Texture Spectrum (WebGPU butterfly FFT)

2D FFT Texture Spectrum (WebGPU butterfly FFT)

anadol_webgpu · fft2dTexture

The WebGPU butterfly 2D FFT pattern: stage by stage, a @compute shader reads two texels per output (input[p] and twiddle-weighted input[q]) and writes the combined result; after log2(N) horizontal stages, transpose, and log2(N) vertical stages, the 2D FFT is complete.

View →
Sample of Compute Particle System (WebGPU @compute pattern)

Compute Particle System (WebGPU @compute pattern)

anadol_webgpu · computeParticleSystem

The canonical WebGPU 'compute-to-render' pattern: a @compute shader updates N particles in a storage buffer (one thread per particle, simple Verlet-style integration: v += gravity; v *= drag; p += v; bounce off walls; life++; respawn if life>max), and a render pass consumes the same buffer as vertex data to draw point sprites.

View →
Sample of Data Particle Field (Refik Anadol — Machine Hallucinations / AI Data Sculptures)

Data Particle Field (Refik Anadol — Machine Hallucinations / AI Data Sculptures)

anadol_webgpu · dataParticleField

Inspired by Refik Anadol Studio's AI Data Sculptures and 'data universe' portraits (e.g.

View →
Sample of Dreamscape Field (Refik Anadol — Dreams / Nature Dreams)

Dreamscape Field (Refik Anadol — Dreams / Nature Dreams)

anadol_webgpu · dreamscapeField

Inspired by Refik Anadol Studio's Dreams series (Nature Dreams, Urban Dreams, Space Dreams and the MoMA Fluid Dreams algorithm): a smooth low-frequency Perlin noise field drives slow global drift while a finer detail octave adds turbulent perturbation — Anadol's fluid solver aesthetic realised with cheap numpy.

View →
Sample of Latent Walk (Refik Anadol — Unsupervised / Machine Hallucinations)

Latent Walk (Refik Anadol — Unsupervised / Machine Hallucinations)

anadol_webgpu · latentWalk

Inspired by Refik Anadol Studio's Unsupervised — Machine Hallucinations — MoMA (2022): a chain of N points is sampled from a synthetic 64-dimensional latent space (a smoothed random walk) and projected to 2D via a fixed random matrix; the path is integrated step-by-step with spherical-linear interpolation between adjacent chain points, so the rendered trail visualises the path the 'latent space browser' would take through the embedding.

View →
Sample of Parallel Reduction (WebGPU workgroup-shared-memory tree reduction)

Parallel Reduction (WebGPU workgroup-shared-memory tree reduction)

anadol_webgpu · parallelReduction

The classic WebGPU parallel-reduction pattern: load N values into workgroup-shared memory, workgroupBarrier(), tree-reduce by stride (shared[lid.x] = op(shared[lid.x], shared[lid.x + stride]); stride /= 2), workgroupBarrier(), and emit per-workgroup partials from thread 0.

View →

See all anadol_webgpu artworks →