Bloom on Windows Phone

Originally posted to Shawn Hargreaves Blog on MSDN, Thursday, January 19, 2012

Ok, silliness aside, is it possible to implement an efficient bloom effect on Windows Phone without custom shaders?

Bloom consists of three operations:

  1. Identify which parts of the image should be bloomed
  2. Blur those parts
  3. Combine blurred bloomy bits with the original image

There are many choices for how each operation should be implemented, the combination of which determines the visual result.  In the XNA bloom sample:

  1. Bright areas are identified by subtracting a threshold value, then scaling back up to preserve full color range: (value - threshold) / (1 - threshold)
  2. Blur is applied using a two pass separable Gaussian filter
  3. To avoid excessively bright areas, the final combine operation is: (base * (1 – bloom)) + bloom  (done in a pixel shader that also provides brightness and saturation adjustment for both images)

There is no good way to adjust saturation without shaders, but everything else in this design can be done with simple alpha blending operations:

using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace BloomPostprocess
{
    public class BloomComponent : DrawableGameComponent
    {
        // Adjust these values to change visual appearance.
        const float BloomThreshold = 0.25f;
        const float BloomIntensity = 1.5f;
        const int BlurPasses = 4;


        // result = source - destination
        static BlendState extractBrightColors = new BlendState
        {
            ColorSourceBlend = Blend.One,
            AlphaSourceBlend = Blend.One,

            ColorDestinationBlend = Blend.One,
            AlphaDestinationBlend = Blend.One,

            ColorBlendFunction = BlendFunction.Subtract,
            AlphaBlendFunction = BlendFunction.Subtract,
        };


        // result = source + destination
        static BlendState additiveBlur = new BlendState
        {
            ColorSourceBlend = Blend.One,
            AlphaSourceBlend = Blend.One,

            ColorDestinationBlend = Blend.One,
            AlphaDestinationBlend = Blend.One,
        };


        // result = source + (destination * (1 - source))
        static BlendState combineFinalResult = new BlendState
        {
            ColorSourceBlend = Blend.One,
            AlphaSourceBlend = Blend.One,

            ColorDestinationBlend = Blend.InverseSourceColor,
            AlphaDestinationBlend = Blend.InverseSourceColor,
        };


        SpriteBatch spriteBatch;

        RenderTarget2D scene;
        RenderTarget2D halfSize;
        RenderTarget2D quarterSize;
        RenderTarget2D quarterSize2;


        public BloomComponent(Game game)
            : base(game)
        { }


        protected override void LoadContent()
        {
            spriteBatch = new SpriteBatch(GraphicsDevice);

            PresentationParameters pp = GraphicsDevice.PresentationParameters;

            int w = pp.BackBufferWidth;
            int h = pp.BackBufferHeight;

            scene = new RenderTarget2D(GraphicsDevice, w, h, false, pp.BackBufferFormat, pp.DepthStencilFormat, pp.MultiSampleCount, RenderTargetUsage.DiscardContents);
            halfSize = new RenderTarget2D(GraphicsDevice, w / 2, h / 2, false, pp.BackBufferFormat, DepthFormat.None);
            quarterSize = new RenderTarget2D(GraphicsDevice, w / 4, h / 4, false, pp.BackBufferFormat, DepthFormat.None);
            quarterSize2 = new RenderTarget2D(GraphicsDevice, w / 4, h / 4, false, pp.BackBufferFormat, DepthFormat.None);
        }


        public void BeginDraw()
        {
            if (Visible)
            {
                GraphicsDevice.SetRenderTarget(scene);
            }
        }


        public override void Draw(GameTime gameTime)
        {
            // Shrink to half size.
            GraphicsDevice.SetRenderTarget(halfSize);
            DrawSprite(scene, BlendState.Opaque);

            // Shrink again to quarter size, at the same time applying the threshold subtraction.
            GraphicsDevice.SetRenderTarget(quarterSize);
            GraphicsDevice.Clear(new Color(BloomThreshold, BloomThreshold, BloomThreshold));
            DrawSprite(halfSize, extractBrightColors);

            // Kawase blur filter (see http://developer.amd.com/media/gpu_assets/Oat-ScenePostprocessing.pdf)
            for (int i = 0; i < BlurPasses; i++)
            {
                GraphicsDevice.SetRenderTarget(quarterSize2);
                GraphicsDevice.Clear(Color.Black);

                int w = quarterSize.Width;
                int h = quarterSize.Height;

                float brightness = 0.25f;

                // On the first pass, scale brightness to restore full range after the threshold subtraction.
                if (i == 0)
                    brightness /= (1 - BloomThreshold);

                // On the final pass, apply tweakable intensity adjustment.
                if (i == BlurPasses - 1)
                    brightness *= BloomIntensity;

                Color tint = new Color(brightness, brightness, brightness);

                spriteBatch.Begin(0, additiveBlur);

                spriteBatch.Draw(quarterSize, new Vector2(0.5f, 0.5f), new Rectangle(i + 1, i + 1, w, h), tint);
                spriteBatch.Draw(quarterSize, new Vector2(0.5f, 0.5f), new Rectangle(-i, i + 1, w, h), tint);
                spriteBatch.Draw(quarterSize, new Vector2(0.5f, 0.5f), new Rectangle(i + 1, -i, w, h), tint);
                spriteBatch.Draw(quarterSize, new Vector2(0.5f, 0.5f), new Rectangle(-i, -i, w, h), tint);
                
                spriteBatch.End();

                Swap(ref quarterSize, ref quarterSize2);
            }

            // Combine the original scene and bloom images.
            GraphicsDevice.SetRenderTarget(null);
            DrawSprite(scene, BlendState.Opaque);
            DrawSprite(quarterSize, combineFinalResult);
        }


        void DrawSprite(Texture2D source, BlendState blendState)
        {
            spriteBatch.Begin(0, blendState);
            spriteBatch.Draw(source, GraphicsDevice.Viewport.Bounds, Color.White);
            spriteBatch.End();
        }


        static void Swap<T>(ref T a, ref T b)
        {
            T tmp = a;
            a = b;
            b = tmp;
        }
    }
}

Notes:

So how does it look?

With BloomThreshold = 0.25f,  BloomIntensity = 1.5f,  BlurPasses = 4:

image

With BloomThreshold = 0.5f,  BloomIntensity = 3,  BlurPasses = 6:

image

With BloomThreshold = 0,  BloomIntensity = 1.5f,  BlurPasses = 3:

image

Blog index   -   Back to my homepage