These are the key things that would make Gaussian Blur very fast on the GPU:
- Separated horizontal and vertical passes
- Remove tail parts of a large Gaussian number since they contribute next to nothing to the target pixel
- Use binomial coefficients as Gaussian number since binomial coefficients (or Pascal's triangle) is the discrete form of Gaussian function
- Use bilinear sampling from hardware to reduce number of samples you sample from the texture
The first two are quite obvious and so I won't explain them in detail. A good suggestion on how to "chop off" the tails of Gaussian numbers would be anything smaller than 1/255.0 because each channel is likely from 0 - 255. The last two are some what less intuitive. Basically the reason you want to use binomial coefficients is because mathematically, as you get closer to the bottom of the Pascal's triangle, the closer you are to the Gaussian function. In fact, binomial distribution is normal distribution with p = 0.5. To convert back and forth, use:
sigma^2 = 0.25n
mu = 0.5n
where n is the number of terms in the binomial distribution. This tells us, if we want a Gaussian filter with say sigma of 10, we will need 400 term binomial distribution! The center number is actually so large (1.029525991*10^118) that the edge (1/(2.5822498*10^120)) would basically be zero. However, if we apply our threshold of 1/255.0, this suddenly drops from 400 terms to merely 43 terms! So in conclusion, you want to use binomial distribution because:
- The human eye cannot tell the difference between sigma of 1 and 1.75 when blurring with Gaussian. This means we can skip them. This works well in using binomial distribution because we don't need it to be continuous.
- You can adjust the number of terms (hopefully with a script) without visually tell a difference between a full-blown Gaussian filter V.S. reduced binomial filter.
- Statically bake these terms into the shader (or code) and therefore no dynamic calculation is needed.
Finally the last optimization is actually a bit harder to explain ... We know that bilinear sampling simply means that the hardware fetch the pixel weighted by their texture coordinates. So if we ask for a color at texture coordinate that's between two pixels, we get 50% of each. The goal is to calculate a texture coordinate such that we get X% of left and Y% of right where X and Y are Gaussian (or binomial) weights.
To actually answer your question, to compute the weighted sample, you calculate the weights (like described above), and then you offset by that number of terms. Say you use 43 terms, that means you offset by 21 pixels left and 21 pixels right of the target pixel. Since we work in texture coordinates, we first calculate how wide each pixel is: (in GLSL)
vec2 texCoordOffset = vec2(1.0, 0.0) / vec2(TEXTURE_WIDTH, TEXTURE_HEIGHT);
//If vertical, the above should be vec2(0.0, 1.0)
vec4 outputColor = vec4(0.0, 0.0, 0.0, 0.0);
for(int i = 0; i < 21; ++i)
outputColor += binomWeights[i] * texture(TEXTURE_0, uv - vec2(1.0, 0.0) * binomOffsets[i]);
outputColor += binomWeights[i] * texture(TEXTURE_0, uv + vec2(1.0, 0.0) * binomOffsets[i]);
The weights are simply 1/sum_of_binomial; For example, a 5 term binomial distribution would be 1, 4, 6, 4, 1 and their sum is 16 so the weights should be: 1/16, 4/16, 6/16, 4/16, 1/16; The texture offsets should be: (2*(1/16) + (1*(4/16)) / (1/16 + 4/16). The 2 represents 2 pixels from target pixel and 1 represents 1 pixels from target pixel. This gives you a number in texture coordinate and in this case it is 1.2; That is if we fetch at 1.2 pixels away from the target pixel (center), we get 80% of pixel that's 1 away from center and only 20% of pixel that's 2 away from center.
I hope this isn't too confusing ...