I'm writing a Gaussian blur shader and it's coming along pretty well. However, when I run it on a circle, the result is lumpy.

enter image description here

I'm using a simple nested for loop to create the blur. Here's the code in it's current form:

int samples = 10;
int sampledist = 1;
float rt_h = 720; // render target height
float rt_w = 1280; // render target height

vec4 effect(vec4 col, Image tex, vec2 texcoord, vec2 screencoord) 
   vec3 final = vec3(1.0, 0.0, 0.0);
   final = Texel(tex, texcoord.xy).rgb;
   for (int i=-samples; i<samples; i++) 
      for (int j=-samples; j<samples; j++) 
         final += Texel(tex, texcoord.xy + vec2(i*sampledist, j*sampledist)/vec2(rt_w, rt_h)).rgb * .01;
    return vec4(final, 1.0);

(Note: I'm using a variation of the regular GLSL language for Love2D, so the above code isn't completely valid GLSL shader code)

What is wrong with my algorithm?

  • \$\begingroup\$ Only took a quick scan but it looks like you're only blurring along the x and y axes, which would make areas of diagonal pixels appear brighter due to both the x and y axes being sampled. \$\endgroup\$ – RandyGaul Jan 13 '14 at 1:22

There are two problems here:

  1. You're including the center texel twice in the filter. You first initialize final as

    final = Texel(tex, texcoord.xy).rgb;

    but the same texel also gets sampled in the loop when i and j are both zero. That accounts for why you can still see the image of the unblurred circle mixed in with the blur.

  2. You're not doing a Gaussian blur. Since you give every sample the same weight, .01, you're just doing a box blur. This accounts for why the blur is "lumpy" as you put it - the results look different when blurring the part of the circle that's aligned with the box, versus the part of the circle that's at a 45-degree angle to the box. To do a Gaussian blur you would need to calculate the weights using a Gaussian distribution, like

    weight = exp(-(i*i + j*j) / (2.0 * sigma*sigma))

    where sigma is the standard deviation (usually set to 1/3 to 1/4 of the filter radius in pixels). In practice this is usually precomputed and the values are hardcoded into the shader, since evaluating that formula in real-time is rather expensive. You have to normalize the total weight to 1.0, as well.

For completeness, I'll also note that you're doing the blur in the non-separable 2D fashion, looping over both x and y inside the pixel shader. That works, but it's quite slow, especially for larger filters. For separable filters (which box and Gaussian both are, but most other shapes are not) you can get a huge speedup by doing the filter in two passes: a 1D filter on each row, then take the result of that and run another 1D filter on each column.

  • \$\begingroup\$ Thanks for the awesome explanation. However, I'm still doing something wrong, because this is what I'm getting: i.imgur.com/e0N6Wm2.png \$\endgroup\$ – CharlesL Jan 13 '14 at 22:04
  • \$\begingroup\$ @CharlesL Hmm, are your weights normalized to a total of 1.0? That looks like maybe the center region of the blur is getting clipped to a maximum value, but the edges look OK. \$\endgroup\$ – Nathan Reed Jan 14 '14 at 5:38
  • \$\begingroup\$ How should I normalize it? I tried using normalize() but that didn't help. \$\endgroup\$ – CharlesL Jan 14 '14 at 17:23
  • \$\begingroup\$ @CharlesL I mean that the total weight has to be 1.0. You add up all the weights, then divide all of them by the sum. That makes their sum 1.0. \$\endgroup\$ – Nathan Reed Jan 14 '14 at 17:52

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.