Task 3 of 5
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().
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].
inputImage with a 3×3 box filter — each
painted pixel is the average of its 3×3 neighborhood, edges clamped.for loop over dy, dx)0…this.constants.lastthis.color(r/9, g/9, b/9, 1)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.
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.
After the loops: this.color(r / 9, g / 9, b / 9, 1); —
nine samples went in, so divide by nine on the way out.
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.
This page is an interactive exercise — the editor, the GPU runner and your saved progress need JavaScript. The text above is the full brief.