# Box Blur: the Window Goes 2D

*Task 3 of 5 · [Convolution & Filters](https://gpu.rocks/learn/convolution-and-filters-66933805.md) · GPU.js Learn*

Take the sliding window into two dimensions and you have image filtering. A
**3×3 box blur** is the simplest case: every output pixel is the plain
average of the 3×3 patch centered on it — nine reads, per color channel, per pixel.
131,072 threads each do their nine reads at once.

Same edge problem, now on four sides: clamp *both* coordinates into
`0…this.constants.last` before indexing. Average red, green and blue
separately and hand the result to `this.color()`.

**Array layout in gpu.js**
Image data comes in row-major: `image[y][x]` is the pixel in row *y*,
column *x*, and each pixel is an `[r, g, b, a]` array with channels from
0 to 1. Mind the inversion that catches everyone — sizes are given width-first
(`output: [width, height]`), but indexing runs row-first, so this thread's own
pixel is `image[this.thread.y][this.thread.x]`. Swap those two and you read the
transpose of your image. Three-dimensional data follows the same rule:
`output: [w, h, d]` is indexed `[z][y][x]`.

## Figures

- **nine reads and an average; off the edge, the border pixel answers twice**

## Goal

**Goal:** blur `inputImage` with a 3×3 box filter — each
painted pixel is the average of its 3×3 neighborhood, edges clamped.

## Requirements

- Loop over the 3×3 neighborhood (a double `for` loop over `dy`, `dx`)
- Clamp both sample coordinates to `0…this.constants.last`
- Accumulate red, green and blue separately, then paint `this.color(r/9, g/9, b/9, 1)`

## Hint 1 — the neighborhood loop

`for (let dy = 0; dy < 3; dy++)` nested with
`dx`, and the sample position is
`this.thread.y + dy - 1`, `this.thread.x + dx - 1` — the
`- 1` centers the window on this thread's pixel.

## Hint 2 — clamp, then read

```js
let sy = this.thread.y + dy - 1;
if (sy < 0) sy = 0;
if (sy > this.constants.last) sy = this.constants.last;
```

— same for
`sx` — then `const pixel = image[sy][sx];` and add
`pixel[0]`, `pixel[1]`, `pixel[2]` into three
running sums.

## Hint 3 — the finish

After the loops: `this.color(r / 9, g / 9, b / 9, 1);` —
nine samples went in, so divide by nine on the way out.

## Same idea elsewhere

Blur passes ship in every production toolkit — Metal Performance Shaders'
`MPSImageBox`, NVIDIA's NPP filtering routines, WebGPU post-processing
chains. The fast ones exploit that a box blur is *separable*: a horizontal pass
then a vertical pass — six reads per pixel instead of nine.

## Starter code

```js
// Nine reads per pixel, averaged per channel. 131,072 threads at once.
const gpu = new GPU({ mode });

const blur = gpu.createKernel(function (image) {
  // TODO: average the 3×3 neighborhood around this pixel.
  // Clamp sample coordinates to 0…this.constants.last on both axes.
  const pixel = image[this.thread.y][this.thread.x];
  this.color(pixel[0], pixel[1], pixel[2], 1);
}, {
  output: [128, 128],
  graphical: true,
  constants: { last: 127 },
});

await blur(inputImage);
render(blur.canvas);
```

---

Interactive version: https://gpu.rocks/learn/convolution-and-filters-66933805/3

[Previous task](https://gpu.rocks/learn/convolution-and-filters-66933805/2.md) · [Next task](https://gpu.rocks/learn/convolution-and-filters-66933805/4.md)
