where f is the original image,
k the 3 × 3 kernel,
g the filtered image and
\(N = \mathrm{norm}\).
The demo evaluates that equation on all three colour
channels independently, so hues survive blurs and emboss.
2 Why the norm matters
Without the divisor N,
even a simple blur would brighten the image – nine positive numbers
sum to something bigger than the originals.
Setting N = ∑ k(i,j)
preserves the average brightness.
Box‑Blur with norm = 9 keeps mid‑grey unchanged.
3 Negative weights & edges
Kernels whose weights sum to 0 act as high‑pass filters:
positive cells amplify intensity jumps while negative cells suppress
flat regions. That makes edges pop out:
$$\small
\left[\begin{array}{rrr}
-1&-1&-1\\[-4pt]-1&\phantom{-}8&-1\\[-4pt]-1&-1&-1
\end{array}\right] * f
\;=\;\text{bright outline}
$$
Flip all signs and you get a dark outline instead (our
Outline preset).
4 Separable kernels
Some blurs (e.g. Gaussian) can be written as the outer product
of two 1‑D vectors:
Convolving first along x, then along y runs in
\(2 × 3\) operations per pixel instead of \(3 × 3 = 9\).
Our demo keeps the maths transparent, but real‑time filters
(camera apps, GPUs) always exploit that trick.
5 Stability & clamping
After summation we clamp each channel to 0‒255.
That prevents overflow (“wraparound”) and underflow
(“negative light”). With float textures you’d omit
the clamp to keep full dynamic range for later passes.