Tuesday, September 21, 2010

Gaussian Blur Shader (GLSL)

A Gaussian blur is one of the most useful post-processing techniques in graphics yet I somehow find myself hard pressed to find a good example of a Gaussian blur shader floating around on the interwebs. I've included below a very flexible, separable Gaussian blur shader in GLSL. The theory behind its value generation can be found in GPU Gems 3; Chapter 40 ("Incremental Computation of the Gaussian" by Ken Turkowski).

Here's the extremely simple vertex shader; the assumption being that you'll be feeding a fullscreen quad to the vertex shader such that it's 4 vertices are positioned at the corners of the viewport:
void main() {
gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex;
gl_TexCoord[0] = gl_MultiTexCoord0;
}
The fragment shader is setup using macros separated based on the blur direction (i.e., horizontal/vertical) and the blur kernel size (currently 5, 7 and 9; but this can easily be extended). Here's the fragment shader:

uniform float sigma; // The sigma value for the gaussian function: higher value means more blur
// A good value for 9x9 is around 3 to 5
// A good value for 7x7 is around 2.5 to 4
// A good value for 5x5 is around 2 to 3.5
// ... play around with this based on what you need :)

uniform float blurSize; // This should usually be equal to
// 1.0f / texture_pixel_width for a horizontal blur, and
// 1.0f / texture_pixel_height for a vertical blur.

uniform sampler2D blurSampler; // Texture that will be blurred by this shader

const float pi = 3.14159265f;

// The following are all mutually exclusive macros for various
// seperable blurs of varying kernel size
#if defined(VERTICAL_BLUR_9)
const float numBlurPixelsPerSide = 4.0f;
const vec2 blurMultiplyVec = vec2(0.0f, 1.0f);
#elif defined(HORIZONTAL_BLUR_9)
const float numBlurPixelsPerSide = 4.0f;
const vec2 blurMultiplyVec = vec2(1.0f, 0.0f);
#elif defined(VERTICAL_BLUR_7)
const float numBlurPixelsPerSide = 3.0f;
const vec2 blurMultiplyVec = vec2(0.0f, 1.0f);
#elif defined(HORIZONTAL_BLUR_7)
const float numBlurPixelsPerSide = 3.0f;
const vec2 blurMultiplyVec = vec2(1.0f, 0.0f);
#elif defined(VERTICAL_BLUR_5)
const float numBlurPixelsPerSide = 2.0f;
const vec2 blurMultiplyVec = vec2(0.0f, 1.0f);
#elif defined(HORIZONTAL_BLUR_5)
const float numBlurPixelsPerSide = 2.0f;
const vec2 blurMultiplyVec = vec2(1.0f, 0.0f);
#else
// This only exists to get this shader to compile when no macros are defined
const float numBlurPixelsPerSide = 0.0f;
const vec2 blurMultiplyVec = vec2(0.0f, 0.0f);
#endif

void main() {

// Incremental Gaussian Coefficent Calculation (See GPU Gems 3 pp. 877 - 889)
vec3 incrementalGaussian;
incrementalGaussian.x = 1.0f / (sqrt(2.0f * pi) * sigma);
incrementalGaussian.y = exp(-0.5f / (sigma * sigma));
incrementalGaussian.z = incrementalGaussian.y * incrementalGaussian.y;

vec4 avgValue = vec4(0.0f, 0.0f, 0.0f, 0.0f);
float coefficientSum = 0.0f;

// Take the central sample first...
avgValue += texture2D(blurSampler, gl_TexCoord[0].xy) * incrementalGaussian.x;
coefficientSum += incrementalGaussian.x;
incrementalGaussian.xy *= incrementalGaussian.yz;

// Go through the remaining 8 vertical samples (4 on each side of the center)
for (float i = 1.0f; i <= numBlurPixelsPerSide; i++) {
avgValue += texture2D(blurSampler, gl_TexCoord[0].xy - i * blurSize *
blurMultiplyVec) * incrementalGaussian.x;
avgValue += texture2D(blurSampler, gl_TexCoord[0].xy + i * blurSize *
blurMultiplyVec) * incrementalGaussian.x;
coefficientSum += 2 * incrementalGaussian.x;
incrementalGaussian.xy *= incrementalGaussian.yz;
}

gl_FragColor = avgValue / coefficientSum;
}

2 comments:

Robin said...

Hi. Interesting article. I'm not sure why you need to execute `gl_Position = gl_ModelViewProjectionMatrix * gl_Vertex' though. If your quad vertices go from -1 to 1 and the intention is to fill the viewport, you don't need to transform them at all. Also, you won't need texture coordinates in your vertex, as these are simply output from the VS as position * 0.5 + 0.5.

Unknown said...

i am using same code for blurring, but the result of rendering is a black screen. how it can be like this.