GameComponents: the Bloom post-processing filter

If you played a random game for the last 2 or 3 years, you probably have already seen the incredibly popular (and very beautiful) Bloom post-processing filter in action. In this article, I'll show you how Bloom works and how to implement it as a DrawableGameComponent in XNA.

Oblivion made this effect incredibly popular: all latest games seem to implement it in some way or another.

Oblivion screenshot
Oblivion screenshot with Bloom.

The Bloom filter is a post-processing filter, that is, it will work after you rendered your scene normally and will simply transform the pixels of the backbuffer before they are presented on the screen. Bloom takes all highlights of your rendered scene and superimposes them, also making them more intense: this gives your scene a good feeling of bright lighting and overexposure. Even though the effect is not as correct (nor as good-looking) as HDR, it's cheap and easy to do.  :)

How does Bloom work?

The Bloom pipeline

Once your backbuffer is ready and all models have been drawn, you'll have to take the backbuffer as a texture and render it to a secondary rendering surface (which usually is smaller than the backbuffer in order to save memory and to speed up the effect - this doesn't matter in terms of quality since you will be blurring the image anyway). This step is done using a pixel shader that extracts the highlights of the original image (a simple threshold).

The "highlights" image will pass through a second blurring step (again, using a pixel shader that renders on a rendering surface). You can use real Gaussian blurring or an approximation thereof.

When the blurred image is ready, you'll only have to compose the image back on the backbuffer by superimposing the original and the modified images. This last step can be executed in a single step while blurring the image.

There are several parameters you can tweak in order to improve the look of your scene. For instance, you can adjust "how much" of the original image you want to keep and how intense the bloom image should be. You could desaturate or oversaturate one of the images before overimposing it (as seen in the Bloom sample on XNA.com). The simplest variation of the bloom filter can be seen in this article on Ziggyware.

The code

As I told you in the previous article, all modules of your XNA game should be enclosed in a GameComponent subclass. Therefore, we'll implement the Bloom post-processing effect as a DrawableGameComponent. First of all, we define the interface that the component will expose:

public interface IBloomService { RenderTarget2D RenderTarget { get; } RenderTarget2D FinalRenderTarget { get; set; } bool BloomEnabled { get; set; } float BlurPower { get; set; } float BaseIntensity { get; set; } float BloomIntensity { get; set; } float BaseSaturation { get; set; } float BloomSaturation { get; set; } float BloomThreshold { get; set; } }

That's a lot of properties!  :D
Notice that RenderTarget and FinalRenderTarget are RenderTarget2D instances that will be assigned by your Game instance before the first frame is rendered: RenderTarget gets the texture on which the scene should be drawn and that will be blurred by the Bloom effect. FinalRenderTarget sets the final target on which the Bloom component will output the image: usually it will simply be "null" (that is, the bloom filter will simply output to the backbuffer), but you could also specify a different RenderTarget2D instance in order to chain more than one post-processing effect together. These chained effects are completely indepent and don't have to know about each other (your Game instance simply has to hook them together correctly).

The rest of the code is quite simple. Let's take a look to the pixel shader "BloomExtract" that extracts the highlights from the origial render:

sampler TextureSampler : register(s0); float BloomThreshold; float4 PixelShader(float2 texCoord : TEXCOORD0) : COLOR0 { // Look up the original image color. float4 c = tex2D(TextureSampler, texCoord); // Adjust it to keep only values brighter than the specified threshold. return saturate((c - BloomThreshold) / (1 - BloomThreshold)); }

This shader takes a TextureSampler as input (the original backbuffer) and a threshold (between 0.0f and 1.0f). Each pixel is copied to the output render target only if its value is greater than the threshold.

The "Bloom" pixel shader then has to compose the original image and the blurred one back to the output buffer. This effect uses a simplified version of Gaussian blurring:

//Original image sampler TextureSampler : register(s0); //Blurred image sampler BloomSampler : register(s1); //Power: sampling range multiplier (used in pseudo gaussian blurring) float BlurPower; //Intensity of the base render and the bloom render float BaseIntensity, BloomIntensity; //Saturation scaling float BaseSaturation, BloomSaturation; //Range of offsets to sample from (pseudo gaussian blurring) const float2 offsets[12] = { -0.326212, -0.405805, -0.840144, -0.073580, -0.695914, 0.457137, -0.203345, 0.620716, 0.962340, -0.194983, 0.473434, -0.480026, 0.519456, 0.767022, 0.185461, -0.893124, 0.507431, 0.064425, 0.896420, 0.412458, -0.321940, -0.932615, -0.791559, -0.597705, }; struct PixelShaderInput { float2 TexCoord : TEXCOORD0; }; float4 AdjustSaturation(float4 color, float saturation) { //The constants 0.3, 0.59, and 0.11 are chosen because the //human eye is more sensitive to green light, and less to blue. float grey = dot(color, float3(0.3, 0.59, 0.11)); return lerp(grey, color, saturation); } float4 bloomEffect(PixelShaderInput Input) : COLOR0 { //Get base color (from render target) float4 original = tex2D(TextureSampler, Input.TexCoord); //Compute bloom color after gaussian filtering float4 sum = tex2D(BloomSampler, Input.TexCoord); for(int i = 0; i < 12; i++){ sum += tex2D(BloomSampler, Input.TexCoord + BlurPower * offsets[i]); } sum /= 13; //Adjust intensity and saturation original = AdjustSaturation(original, BaseSaturation) * BaseIntensity; sum = AdjustSaturation(sum, BloomSaturation) * BloomIntensity; return sum + original; }

The shader gets a pixel from the original scene and a blurred pixel from the "highlights-only" texture (the blurring is done by taking 13 pixels of the image in a circular radius and computing the average value). The two color values are multiplied by the Intensity parameters and eventually de/over-saturated before overimposing them (simple additive blending).

As I said, the Bloom effect is a rather easy and computationally inexpensive post-processing step, but the effect will usually be very pleasing in most games.

Source code

Download the Bloom Component source code (.rar).