Bloom

In this post, we’ll be adding a Bloom Post Processing effect. We’ll start of simple by adding in some new folders. In the “ProjectVanquish” project, under the “Renderers” folder, create a new folder called “PostProcessing”. In the “ProjectVanquishTestContent” project, under the “Shaders” folder, create a new one called “PostProcessing”. Then inside the new folder, create another called “Bloom”.

Now that’s out of the way, let’s create the “Bloom” class. Add a new class to the “PostProcessing” folder in the “ProjectVanquish” project and name it “Bloom”.

Add the following namespaces:

using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Content;
using Microsoft.Xna.Framework.Graphics;

Now we can add our variables:

Effect bloomEffect, blurEffect, finalEffect;
RenderTarget2D bloomRT, blurHRT, blurVRT;
QuadRenderer fullscreenQuad;

Nothing out of the ordinary there. Moving onto the constructor, we will instantiate these new variables ready for use in the “Draw” method:

public Bloom(GraphicsDevice device, ContentManager content)
{
    bloomEffect = content.Load<Effect>("Shaders/PostProcessing/Bloom/Bloom");
    blurEffect = content.Load<Effect>("Shaders/PostProcessing/Bloom/Blur");
    finalEffect = content.Load<Effect>("Shaders/PostProcessing/Bloom/Final");

    bloomRT = new RenderTarget2D(device, device.PresentationParameters.BackBufferWidth,
                             device.PresentationParameters.BackBufferHeight,
                             false, SurfaceFormat.Color, DepthFormat.None);
    blurHRT = new RenderTarget2D(device, device.PresentationParameters.BackBufferWidth, 
                             device.PresentationParameters.BackBufferHeight,
                             false, SurfaceFormat.Color, DepthFormat.None);
    blurVRT = new RenderTarget2D(device, device.PresentationParameters.BackBufferWidth, 
                             device.PresentationParameters.BackBufferHeight,
                             false, SurfaceFormat.Color, DepthFormat.None);

    fullscreenQuad = new QuadRenderer(device);
}

We’ll finish off the class before we create our new Effects. We’ll create our last method for the class, “Draw”. In this code, we’ll take our RenderTarget output from the “SSAORenderer” and we’ll apply a two pass blur and then combine the final result:

public void Draw(GraphicsDevice device, RenderTarget2D sceneRT)
{
    // Bloom pass
    device.SetRenderTarget(bloomRT);
    device.Clear(Color.Black);
    device.Textures[0] = sceneRT;
    bloomEffect.CurrentTechnique.Passes[0].Apply();
    fullscreenQuad.Draw();
    device.SetRenderTarget(null);

    // Horizontal pass
    device.SetRenderTarget(blurHRT);
    device.Clear(Color.Black);
    device.Textures[0] = bloomRT;
    blurEffect.CurrentTechnique.Passes[0].Apply();
    fullscreenQuad.Draw();
    device.SetRenderTarget(null);

    // Vertical pass
    device.SetRenderTarget(blurVRT);
    device.Clear(Color.Black);
    device.Textures[0] = blurHRT;
    blurEffect.CurrentTechnique.Passes[0].Apply();
    fullscreenQuad.Draw();
    device.SetRenderTarget(null);

    // Final pass
    device.Clear(Color.Black);
    device.Textures[0] = blurVRT;
    finalEffect.Parameters["ColorMap"].SetValue(sceneRT);
    finalEffect.CurrentTechnique.Passes[0].Apply();
    fullscreenQuad.Draw();
}

That is the “Bloom” class complete. We’ll quickly add this to the “DeferredRenderer” class so we can then move onto the Effects. In the “DeferredRenderer” class, declare the namespace:

using ProjectVanquish.Renderers.PostProcessing;

Now we can declare two new variables:

RenderTarget2D bloomRT;
Bloom bloom;

In the constructor, we can instantiate the new class:

bloomRT = new RenderTarget2D(device, backbufferWidth, backbufferHeight, false, 
                             SurfaceFormat.Color, DepthFormat.Depth24Stencil8);
bloom = new Bloom(device, content);

Lastly, we just need to add the “Draw” call in the “Draw” method. We also need to alter the “SSAORenderer” call:

ssaoRenderer.Draw(device, renderTargets, sceneRT, sceneManager, bloomRT);
bloom.Draw(device, bloomRT);

That’s all of the class code complete. Now we can move onto the Shaders, starting with the “Bloom”. Create a new Effect file in the “Bloom” folder called “Bloom”. Remove all default contents and replace with the following:

sampler TextureSampler : register(s0);

float Threshold = 0.3;

float4 PixelShaderFunction(float2 texCoord : TEXCOORD0) : COLOR0
{
    float4 Color = tex2D(TextureSampler, texCoord);
    
    // Get the bright areas that is brighter than Threshold and return it.
    return saturate((Color - Threshold) / (1 - Threshold));
}

technique Bloom
{
    pass Pass1
    {
        PixelShader = compile ps_3_0 PixelShaderFunction();
    }
}

Quite straight forward. It gets the bright areas that are brighter than the Threshold and saturates it. Create a new file called “Blur” and remove the default contents. Replace with:

// 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 PixelShaderFunction(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;
}

technique Blur
{
	pass Pass1
	{
		PixelShader = compile ps_3_0 PixelShaderFunction();
	}
}

This will apply a Blur effect to the RenderTarget. Lastly, we create a new Effect file called “Final”. Once again, remove the default contents and replace with:

sampler BloomSampler : register(s0);

// Our original SceneTexture
texture ColorMap;

// Create a sampler for the ColorMap texture using lianear filtering and clamping
sampler ColorMapSampler = sampler_state
{
   Texture = <ColorMap>;
   MinFilter = Linear;
   MagFilter = Linear;
   MipFilter = Linear;   
   AddressU  = Clamp;
   AddressV  = Clamp;
};

// Controls the Intensity of the bloom texture
float BloomIntensity = 1.3;

// Controls the Intensity of the original scene texture
float OriginalIntensity = 1.0;

// Saturation amount on bloom
float BloomSaturation = 1.0;

// Saturation amount on original scene
float OriginalSaturation = 1.0;

float4 AdjustSaturation(float4 color, float saturation)
{
    // We define gray as the same color we used in the grayscale shader
    float grey = dot(color, float3(0.3, 0.59, 0.11));
    
    return lerp(grey, color, saturation);
}

float4 PixelShaderFunction(float2 texCoord : TEXCOORD0) : COLOR0
{
	// Get our bloom pixel from bloom texture
	float4 bloomColor = tex2D(BloomSampler, texCoord);

	// Get our original pixel from ColorMap
	float4 originalColor = tex2D(ColorMapSampler, texCoord);
    
    // Adjust color saturation and intensity based on the input variables to the shader
	bloomColor = AdjustSaturation(bloomColor, BloomSaturation) * BloomIntensity;
	originalColor = AdjustSaturation(originalColor, OriginalSaturation) * OriginalIntensity;
    
    // make the originalColor darker in very bright areas, avoiding these areas look burned-out
    originalColor *= (1 - saturate(bloomColor));
    
    // Combine the two images.
    return originalColor + bloomColor;
}

technique BloomFinal
{
    pass Pass1
    {
        PixelShader = compile ps_3_0 PixelShaderFunction();
    }
}

This combines the bloom texture with the original scene texture. BloomIntensity, OriginalIntensity, BloomSaturation and OriginalSaturation is used to control the blooming effect. This Bloom effect is based on the sample found at create.msdn.com.

In the screenshot below, I’ve added an extra 2 Point Lights: