Task 5 of 5
The payoff, and the histogram everybody has actually seen: an image's tone histogram — how many pixels are dark, how many mid, how many bright. Every photo editor draws one, because it tells you a shot is underexposed before your eyes do.
Two kernels, and the reason for two is worth a sentence. Luminance is a per-pixel calculation and there are 4,096 pixels — but there are 32 bins, so a single histogram kernel would recompute every pixel's luminance 32 times over, once per bin thread. Compute it once into a 64 × 64 map, then histogram the map. Map first, bin second; the map pass is 4,096 luminance evaluations instead of 131,072.
Luminance runs 0 … 1, so 32 bins over that range is a bin every 0.03125 — the same
clamped floor as task 3, with lo = 0 and span = 1
doing nothing visible. And the same smoke alarm: 4,096 pixels in, 4,096 counted out.
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].
photo, histogram
it into 32 tone bins, and log the total.luminance: output: [64, 64], each cell 0.299r + 0.587g + 0.114b of that pixelhistogram: output: [32], each thread scans the whole mapMath.min(bins - 1, Math.floor(l * bins))console.log the total of the 32 counts — it must be 4096Straight out of any grayscale kernel — read this thread's pixel and return a number instead of painting it:
const pixel = photo[this.thread.y][this.thread.x];
return 0.299 * pixel[0] + 0.587 * pixel[1] + 0.114 * pixel[2];The histogram kernel has 32 threads and a 64 × 64 map, so each thread runs two nested loops over the map. Both bounds are constants, which is what the WebGL backend needs:
for (let y = 0; y < this.constants.size; y++) {
for (let x = 0; x < this.constants.size; x++) {
const bin = Math.min(
this.constants.bins - 1,
Math.floor(map[y][x] * this.constants.bins)
);
if (bin === this.thread.x) count++;
}
}Once it runs, look at the counts: the first bins and the last bins are empty. This image never gets truly black or truly white — which is precisely the thing a tone histogram exists to tell you.
This page is an interactive exercise — the editor, the GPU runner and your saved progress need JavaScript. The text above is the full brief.