XNA Shader Programming – Tutorial 20, Depth of field

XNA Shader Programming
Tutorial 20 – Depth of Field
Hi, and welcome to Tutorial 20 of my XNA Shader Programming tutorial. This tutorial will cover a simple post process Depth of Field effect.
This shader is pretty fast, considering that a Depth of Field effect can be pretty heavy to calculate.

As the shader is a post process effect, it can easily be implemented in any scene, without having to reprogram a lot or program the scene around the effect.

 
Depth of Field
Depth of Field( DoF ) is an effect that you probably see every day. If you stare at something, you can see that the foreground and background of the object you are staring is getting unclear. This happens using a camera aslo, where you have to adjust the focus so the thing you want to take picture of looks clear and not blurry. I took a fast image with my camera just to show you the effect:
 
The front part of the bottle is in-focus, and the background is out-of-focus. The in-focus plane is called the in-focal-plane.

Now, how can we implement this in XNA?
First of all, we need a way to get the depth of any point in the scene. This is a perfect opportunity for the depth buffer to show us it’s magic skills( see tutorial 14 ).

 
Then we need two versions of our scene on a texture, one being the normal scene we can see when everything is in focus, and then a blurred version of the scene that will be seen when the whole scene is out of focus.

To blur a scene, we will need to implement a new post process shader! This shader will take the average of a number of the pixels neighbour pixels, and return that color as the color we want the player to see. The blur shader will have a distance variable that will be used to modify the texture coordinate we will use as a lookup texture containing our normal scene, so it takes the top-left, top-right, bottom-left and bottom-right pixel, adds them together and devides it by 4 so we get the average. More on this later!

On the image, you can see the blur shader in effect. The shader takes the average of the surrounding pixels with a distance( in this case 0.003 ) set in the shader.

Next, to blur the scene even better, we will use the blurred texture and render it to another texture using the same post process blur shader. This will result in the scene being blurred twice as much!
Another tip to blurring is to downscale a rendertexture multiple times, but I decided to use a shader for this instead!
 
Now, we got a few textures. One containing the scene, one containing the 2xBlurred version of the scene, and one depth texture, whats next? Is this all we need? Yeah! But, we need to find a way to show the blurred scene where the scene is out-of-focus, the normal scene when it’s in-focus, and a mix of those two where the scene is in-between.

For this, we will need something that describes where we want to have the focus point( distance from the eye ), the range of the focus( how quickly will we go from in-focus to out-of-focus? ), our cameras near clipping plane and the a modified version of the cameras far clipping plane. The modified Far clipping plane is calculated by taking the given Far clipping plane( say 300 ), and devide it by ( Far – Near ). If near is 1, we get 300/(300-1). This will convert the far clipping plane to a correct range [0,1] for use in the next equations.

The following equations will use a given distance and range to calculate what will be in-focus:

[20.1]
 
Here, we take the negative of our near clipping plane and multiply it with the modified far clipping plane, and deviding it by how far away the focal-point is, subracted by the modified far clipping plane. Next, we use this to calculate how far this point is from the distance point we set in the shader, fading out based on the Range value.
This returns a value between 0 and 1, where the value is 0 if it’s in focus, and 1 if it’s out-of-focus. I rendered the values here, where the range is pretty small( 10 ) and a distance of 70 units from the eye:
 
Now, we can use this texture to display the blurred scene where it’s white, the normal scene where it’s black, and a mix where it’s gray, using interpolation( lerp ).
 
Implementing the shader
We got three shaders in our application. The first one is a depth texture shader covered in tutorial 14, the 2nd is the blur post process shader, and the last one is the Depth of Field shader.

Let’s start with the blur-shader:
// The blur amount( how far away from our texel will we look up neighbour texels? )
float BlurDistance = 0.003f;

// This will use the texture bound to the object( like from the sprite batch ).
sampler ColorMapSampler : register(s0);
 
float4 PixelShader(float2 Tex: TEXCOORD0) : COLOR
{
    float4 Color;
 
    // Get the texel from ColorMapSampler using a modified texture coordinate. This
    // gets the texels at the neighbour texels and adds it to Color.
    Color  = tex2D( ColorMapSampler, float2(Tex.x+BlurDistance, Tex.y+BlurDistance));
    Color += tex2D( ColorMapSampler, float2(Tex.x-BlurDistance, Tex.y-BlurDistance));
    Color += tex2D( ColorMapSampler, float2(Tex.x+BlurDistance, Tex.y-BlurDistance));
    Color += tex2D( ColorMapSampler, float2(Tex.x-BlurDistance, Tex.y+BlurDistance));
    // We need to devide the color with the amount of times we added
    // a color to it, in this case 4, to get the avg. color
    Color = Color / 4; 
 
    // returned the blurred color
    return Color;
}
 
Here we take the average of the neighbour pixels, on a given range( 0.003 ), and return this, resulting in a blurred scene.
 
Next, we need to implement the Depth of Field shader.
We first need to declare a few variables that will be used to calculate whats in focus and whats not:
float Distance;
float Range;
float Near;
float Far;

Then we need the usual textures:
sampler SceneSampler : register(s0);
texture D1M;
sampler D1MSampler = sampler_state
{
   Texture = <D1M>;
   MinFilter = Linear;
   MagFilter = Linear;
   MipFilter = Linear;  
   AddressU  = Clamp;
   AddressV  = Clamp;
};

texture BlurScene;
sampler BlurSceneSampler = sampler_state
{
   Texture = <BlurScene>;
   MinFilter = Linear;
   MagFilter = Linear;
   MipFilter = Linear;  
   AddressU  = Clamp;
   AddressV  = Clamp;
};
 
Now, we are ready to start implementing the post process effect:
float4 PixelShader(float2 Tex: TEXCOORD0) : COLOR
{
 // Get the scene texel
 float4 NormalScene = tex2D(SceneSampler, Tex);
 
 // Get the blurred scene texel
 float4 BlurScene = tex2D(BlurSceneSampler, Tex);
 
 // Get the depth texel
 float  fDepth = tex2D(D1MSampler, Tex).r;
 
 // Invert the depth texel so the background is white and the nearest objects are black
 fDepth = 1 – fDepth;
 
 // Calculate the distance from the selected distance and range on our DoF effect, set from the application
 float fSceneZ = ( -Near * Far ) / ( fDepth – Far);
 float blurFactor = saturate(abs(fSceneZ-Distance)/Range);
 
 // Based on how far the texel is from "distance" in Distance, stored in blurFactor, mix the scene
 return lerp(NormalScene,BlurScene,blurFactor);
}

Whats happeing here is that we take the pixel color from the normal scene, the blurred scene and the inverse of the depth texture as we want the distant objects to be white and the near objects to be black before starting on the next calculations:
 float fSceneZ = ( -Near * Far ) / ( fDepth – Far);
 float blurFactor = saturate(abs(fSceneZ-Distance)/Range);

 
Here we use equation 20.1 to calculate whether the pixel is in-focus or not, placing the result for the current pixel in blurFactor.

now, using blurFactor, we can lerp between the color in the normal scene( will be fully visible if blurFactor is 0 ) and the blurred scene( will be fully visible if blurFactor is 1 ).
 return lerp(NormalScene,BlurScene,blurFactor);

And that’s it!

Using the shader

To use the shader, we will need to render the depth buffer texture, the normal scene texture and the blurred scene texture. Then we need to set the shaders parameters, so it contains the distance and range we want.

In this case, we know the camera is about 80 units away from the spinning squidlike objects center, so we set the distance to about 70, with a range of 10 giving us a rather small in-focus part.
void  SetShaderParameters( float fD, float fR, float nC, float fC )
{
    focusDistance = fD;
    focusRange = fR;
    nearClip = nC;
    farClip = fC;
    farClip = farClip / ( farClip – nearClip );

    effectPostDoF.Parameters["Distance"].SetValue(focusDistance);
    effectPostDoF.Parameters["Range"].SetValue(focusRange);
    effectPostDoF.Parameters["Near"].SetValue(nearClip);
    effectPostDoF.Parameters["Far"].SetValue(farClip);
}
 
That’s it for our 1-pass depth of field shader. As you can see, it’s pretty simple to make and don’t require too many calculations and performance loss!
In the example, you can use the XBox controler’s right thumbstick to change the distance and range values of the shader, and the left thumbstick to rotate the model.
 
This entry was posted in XNA Shader Tutorial. Bookmark the permalink.

7 Responses to XNA Shader Programming – Tutorial 20, Depth of field

  1. Dirk says:

    Hello!I implemented this shader into my game and it looks great. But there is one problem with it. All my semitransparent stuff (like particles) is blurred out, because I don\’t have a shader that gets the depth information out of the particles. I use quads like shown in the Microsoft quad sample.Do you have any idea how I can get semitransparent stuff rendered correct with your shader?Greetings!

  2. Pingback: Depth of Field in XNA 4 | Nelxon Studio

  3. Pingback: Depth of field, revisited! « Electronic Meteor

  4. Hemanshu says:

    This is an excellent article. I do have a question about the equation:

    Dz = -Near*Far/(D-far)

    I believe this is converting D from a 0-1 depth value(projection space) to world space. Is that accurate?

    I’d appreciate it if you could explain the math behind those equations or point me to some good reference for it. Like, I don’t know how this equation comes into play and how the modified version of Far was determined, etc.

    Thanks again though, this article is great!

  5. Pingback: Parallel-split shadow maps « Electronic Meteor

  6. Nicco Simone says:

    I don’t understand your shader…Especially where you say 0 <= 300/299 <= 1 in the calculation of the modified far plane.

  7. Raymond says:

    You may want to add this to your shader code:
    //Convert the Far distance to a value between [0-1]
    Far = Far / (Far – Near);

Leave a comment

This site uses Akismet to reduce spam. Learn how your comment data is processed.