Dynamic depth of field on the GPU – part 3 of n

As of the end of part 2 (and using information from part 1, which you should totally read), it’s time to implement the actual blurring effect. This uses code that I acquired somewhere on the internet, but I can’t remember the exact attribution; so, if I got it off you, please let me know!

What you will need is:

  • a framebuffer object containing your final render as a texture
  • the texture we made in part 2, which contains each fragment’s difference in linear depth from the calculated focal length
  • two framebuffer objects each of which is the same size as your main framebuffer
  • (you can cut this down to one with a bit of clever fiddling
  • a shader which does a gaussian blur, which I’m going to explain

What we’re going to do is blur the image according to the values in the depth texture. Because fragment shaders generally like to do everything backwards, the way to do this is generate the blurred image, and then blend it with the original image, so that bigger differences in the depth texture give more of the blurred image.

Okay, here’s the implementation details:

Step 3: Depth blur

It’s most efficient to do blurs as a two-step, “ping-pong” process: first, blur the original texture horizontally and put the result in one of your extra framebuffers. Then blur the horizontally blurred texture vertically, putting the result of this operation back into the first buffer. We’re actually going to mix in the original image at both stages, so that by the time we’re done we’ve got the final, combined image.

As mentioned previously, you can (theoretically) increase the efficiency of texture lookups in the fragment shader by generating the texture coordinates in the vertex shader and passing them through. We can make things easier for ourselves by using a vec2 uniform which allows us to switch between horizontally and vertically oriented arrays of texture coordinates – a value of (1.0, 0.0) multiplied by a series of offsets gives us a horizontal series of coordinates, a value of (0.0, 1.0) gives us a vertical series.

Here’s the vertex shader code:

#version 330
 
layout (location = 0) in vec4 position;
layout (location = 1) in vec4 textureCoordinate;
layout  (location = 2)  in vec4 normal;
 
uniform vec2 axisStrengths;
uniform float viewWidth;
uniform float viewHeight;
 
out vec2 v_texCoord;
out vec2 v_blurTexCoords[14];
 
void main() 
{
    gl_Position = position;
 
    vec2 framebuffer_dimension = vec2(viewWidth, viewHeight);
    v_texCoord = vec2(textureCoordinate.xy);
    vec2 blurVector = 2.0/framebuffer_dimension * axisStrengths;
 
    v_texCoord = vec2(textureCoordinate.xy);
 
     v_blurTexCoords[ 0] = v_texCoord + vec2(-7.0 * blurVector);
     v_blurTexCoords[ 1] = v_texCoord + vec2(-6.0 * blurVector);
     v_blurTexCoords[ 2] = v_texCoord + vec2(-5.0 * blurVector);
     v_blurTexCoords[ 3] = v_texCoord + vec2(-4.0 * blurVector);
     v_blurTexCoords[ 4] = v_texCoord + vec2(-3.0 * blurVector);
     v_blurTexCoords[ 5] = v_texCoord + vec2(-2.0 * blurVector);
     v_blurTexCoords[ 6] = v_texCoord + vec2(-1.0 * blurVector);
     v_blurTexCoords[ 7] = v_texCoord + vec2(1.0 * blurVector);
     v_blurTexCoords[ 8] = v_texCoord + vec2(2.0 * blurVector);
     v_blurTexCoords[ 9] = v_texCoord + vec2(3.0 * blurVector);
     v_blurTexCoords[10] = v_texCoord + vec2(4.0 * blurVector);
     v_blurTexCoords[11] = v_texCoord + vec2(5.0 * blurVector);
     v_blurTexCoords[12] = v_texCoord + vec2(6.0 * blurVector);
     v_blurTexCoords[13] = v_texCoord + vec2(7.0 * blurVector);
}

and here’s the fragment shader:

#version 330
 
/////////////////////////////////////////////////
// 7x1 gaussian blur fragment shader
/////////////////////////////////////////////////
 
uniform sampler2D originalTextureSource;
uniform sampler2D textureSource;
uniform sampler2D depthTextureSource;
 
in vec2 v_texCoord;
in vec2 v_blurTexCoords[14];
 
out vec4 fragColour;
 
void main ()
{
    float centralFragDepth = texture(depthTextureSource, v_texCoord).b;
    vec3 blurColour = vec3(0.0);
 
    vec3 originalColour = texture(originalTextureSource, v_texCoord).xyz;
 
    blurColour += (texture(textureSource, v_blurTexCoords[ 0])*0.0044299121055113265).xyz;
    blurColour += (texture(textureSource, v_blurTexCoords[ 1])*0.00895781211794).xyz;
    blurColour += (texture(textureSource, v_blurTexCoords[ 2])*0.0215963866053).xyz;
    blurColour += (texture(textureSource, v_blurTexCoords[ 3])*0.0443683338718).xyz;
    blurColour += (texture(textureSource, v_blurTexCoords[ 4])*0.0776744219933).xyz;
    blurColour += (texture(textureSource, v_blurTexCoords[ 5])*0.115876621105).xyz;
    blurColour += (texture(textureSource, v_blurTexCoords[ 6])*0.147308056121).xyz;
    blurColour += (texture(textureSource, v_texCoord         )*0.159576912161).xyz;
    blurColour += (texture(textureSource, v_blurTexCoords[ 7])*0.147308056121).xyz;
    blurColour += (texture(textureSource, v_blurTexCoords[ 8])*0.115876621105).xyz;
    blurColour += (texture(textureSource, v_blurTexCoords[ 9])*0.0776744219933).xyz;
    blurColour += (texture(textureSource, v_blurTexCoords[10])*0.0443683338718).xyz;
    blurColour += (texture(textureSource, v_blurTexCoords[11])*0.0215963866053).xyz;
    blurColour += (texture(textureSource, v_blurTexCoords[12])*0.00895781211794).xyz;
    blurColour += (texture(textureSource, v_blurTexCoords[13])*0.0044299121055113265).xyz;
 
    vec3 outputColour = mix(originalColour, blurColour, clamp(abs(centralFragDepth), 0.0, 1.0));
 
    fragColour = vec4(outputColour.xyz, 1.0);
}

and finally, here’s some drawing code which demonstrates how to wrangle your framebuffers and texture units. This uses the same technique of drawing a single fullscreen quad as part 2.

            glUseProgram(gaussianBlurProgram);
            glBindFramebuffer(GL_FRAMEBUFFER, horizontalBlurFBO);
            glViewport(0.0, 0.0, mainFramebufferWidth, mainFramebufferHeight);
            glClearColor(0.0, 0.0, 0.0, 0.0);
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
            glUniform2f(gaussianBlurUniforms[kGBScaleUniform], 1.0f, 0.0f);
            glActiveTexture(GL_TEXTURE2);
            glBindTexture(GL_TEXTURE_2D, unblurredFBOTexture);
            glActiveTexture(GL_TEXTURE1);
            glBindTexture(GL_TEXTURE_2D, unblurredFBOTexture);
            glActiveTexture(GL_TEXTURE0);
            glBindTexture(GL_TEXTURE_2D, depthVSCentreTexture);
            glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, 0);
 
            glBindFramebuffer(GL_FRAMEBUFFER, verticalBlurFBO);
            glViewport(0.0, 0.0, mainFramebufferWidth, mainFramebufferHeight);
            glClearColor(0.0, 0.0, 0.0, 0.0);
            glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
            glUniform2f(gaussianBlurUniforms[kGBScaleUniform], 0.0f, 1.0f);
            glActiveTexture(GL_TEXTURE1);
            glBindTexture(GL_TEXTURE_2D, horizontalBlurFBOTexture);
            glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_SHORT, 0);

The names of the uniforms don’t matter; what matters is that you notice that I’m keeping the unblurred texture bound throughout both steps so that the original image can be blended with the blurred image at each step.

Okay, that’s pretty much it. Good luck!

Leave a Reply

Your email address will not be published. Required fields are marked *