XNA 4.0 Gamma Corrected Pipeline


This post will show how to implement a Gamma Corrected pipeline  in XNA 4.0 (PC and Xbox).

Normally we dont pay too much attention to gamma correction, the reason in that we can achieve good results without it. But when we want professional quality, gamma correction becomes a must have feature.

For those that dont know what gamma correction is, i suggest this and this classic wiki page =P

In XNA 3.1 (PC version only) we could use some DirectX 9c instructions to configure the texture sampler to automatically convert the texture from SRGB to Linear space on hardware. We also could set the render surface to be SRGB (convert the output to SRGB), so the gamma correction pipeline was pretty simple and fully done in hardware. More informations here.

In XNA 4.0 we cant use these “configurations” anymore (cause it is not Compatible with Xbox) >.<. The obvious aproach is to make all the convertions on the shaders but it is slow and involves changing some shaders.

My idea is to shift some of the work to the pre process phase and minimize changes to the existing code. The idea is:

  • In a pre process phase, we convert the textures (2D and cubemaps) to the linear space and create the mipmaps in this space
  • Use those converted textures in shaders calculations
  • After all processing, we apply a post processing to convert the colors from the Linear space to the SRGB

The problems with this aproach are:

  • We use the format 10R10B10G2A (constant alpha) or 8R8G8B8A (variable alpha) to store the linear space. Sometimes we can have precision problems. I tryied using float point texture BUT the XNA 4.0 does not have filtering in these textures. We would have to use Pointer filter in all textures and this is not acceptable.
  • We still applying Blending and Multisample in SRGB space, this is theoricaly wrong

The results i got is far better than not using Gamma Correction, so i suggest you to use it =P.

Pipeline Processor

I implemented two pipeline processors (one for the texture2D and other for textureCube). You can choose the output format (8R8G8B8A or 10R10G10B2A).

The code is not optimized, i just wanted it to work =P

The first one is the Texture Processor for the Texture Cube and the next is for the 2D Textures

TextureCube

using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Graphics.PackedVector;
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
using System.ComponentModel;
namespace ContentLibrary
{

    [ContentProcessor(DisplayName = "Gamma Decode TextureCube Processor")]
    class ContentProcessor2 : ContentProcessor
    {

        [DisplayName("Encode to SRGB after mipmap")]
        [DefaultValue(false)]
        public bool EncodeAfter
        {
            get;
            set;
        }

        public override TextureCubeContent Process(TextureCubeContent input, ContentProcessorContext context)
        {
//            System.Diagnostics.Debugger.Launch();
            TextureCubeContent tc = new TextureCubeContent();
            tc.Name = input.Name;
            tc.Identity = input.Identity;
            int i = 0;
            foreach (var item in input.Faces)
            {
                PixelBitmapContent bmpInput = (PixelBitmapContent)item[0];

                // Create Intermediate Content
                Texture2DContent texMipMap = new Texture2DContent();

                // Add decoded Vector4
                texMipMap.Mipmaps.Add(Decode2(bmpInput));

                // Generate Mip Maps
                texMipMap.GenerateMipmaps(true);
                MipmapChain mc = new MipmapChain();
                // Convert each bitmap to Gamma Encoded SurfaceFormat.Color
                for (int mi = 0; mi < texMipMap.Mipmaps.Count; mi++)
                {
                    // Get Mip Map
                    PixelBitmapContent bmpMipMap = (PixelBitmapContent)texMipMap.Mipmaps[mi];
                    if (EncodeAfter)
                    {
                        PixelBitmapContent bmpColor = Encode2(bmpMipMap);
                        mc.Add(bmpColor);
                    }
                    else
                    {
                        mc.Add(bmpMipMap);
                    }
                }                

                tc.Faces[i++] = mc;
            }

         return tc;
        }

        PixelBitmapContent Encode2(PixelBitmapContent bmpMipMap)
        {
            // Create Color Bitmap of Equal Size
            PixelBitmapContent bmpColor = new PixelBitmapContent(bmpMipMap.Width, bmpMipMap.Height);

            // Convert each pixel to gamma encoded color
            for (int y = 0; y < bmpMipMap.Height; y++)
            {

                for (int x = 0; x < bmpMipMap.Width; x++)
                {

                    // Get Input Pixel
                    Rgba1010102 CmipMap = bmpMipMap.GetPixel(x, y);

                    // Set Output Pixel
                    bmpColor.SetPixel(x, y, GammaEncodeColor2(CmipMap.ToVector4()));

                }//for (x)

            }//for (y)

            return bmpColor;

        }//method

        public PixelBitmapContent Decode2(PixelBitmapContent bmpInput)
        {
            // Decoded Bitmap
            PixelBitmapContent bmpDecoded = new PixelBitmapContent(bmpInput.Width, bmpInput.Height);

            // Convert each pixel to gamma decoded float
            for (int y = 0; y < bmpInput.Height; y++)
            {

                for (int x = 0; x < bmpInput.Width; x++)
                {

                    // Get Input Pixel
                    Color Cinput = bmpInput.GetPixel(x, y);

                    // Set Output Pixel
                    bmpDecoded.SetPixel(x, y, GammaDecodeColor2(Cinput.ToVector4()));

                }//for (x)

            }//for (y)

            return bmpDecoded;

        }

        Rgba1010102 GammaEncodeColor2(Vector4 vec)
        {
            Vector4 resp = new Vector4(1);
            resp.X = (float)Math.Pow(vec.X, 1.0 / 2.2);
            resp.Y = (float)Math.Pow(vec.Y, 1.0 / 2.2);
            resp.Z = (float)Math.Pow(vec.Z, 1.0 / 2.2);
            return new Rgba1010102(resp); ;
        }

        static Rgba1010102 GammaDecodeColor2(Vector4 vec)
        {
            Vector4 resp = new Vector4(1);
            resp.X = (float)Math.Pow(vec.X, 2.2);
            resp.Y = (float)Math.Pow(vec.Y, 2.2);
            resp.Z = (float)Math.Pow(vec.Z, 2.2);
            return new Rgba1010102(resp);
        }

    }//class
    // - - - - - - - - - - - - - - - - - - - -
}//namespace
// - - - - - - - - - - - - - - - - - - - -

Texture2D

using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;
using Microsoft.Xna.Framework.Graphics.PackedVector;
using Microsoft.Xna.Framework.Content.Pipeline;
using Microsoft.Xna.Framework.Content.Pipeline.Graphics;
using System.ComponentModel;
namespace ContentLibrary
{

    [ContentProcessor(DisplayName = "Gamma Decode Texture Processor")]
    class GameTextureProcessor : ContentProcessor
    {

        [DisplayName("Encode to SRGB after mipmap")]
        [DefaultValue(false)]
        public bool EncodeAfter
        {
            get;
            set;
        }

        [DisplayName("Use RGBA10")]
        [DefaultValue(false)]
        public bool UseRGBA10
        {
            get;
            set;
        }

        // - - - - - - - - - - - - - - - - - - - -
        public override Texture2DContent Process(Texture2DContent texInput, ContentProcessorContext context)
        {

            //System.Diagnostics.Debugger.Launch();

            // Input Bitmap
            PixelBitmapContent bmpInput = (PixelBitmapContent)texInput.Mipmaps[0];
            if (!UseRGBA10)
            {
                // Gamma Decode
                PixelBitmapContent bmpDecoded = Decode(bmpInput);

                // Create Intermediate Content
                Texture2DContent texMipMap = new Texture2DContent();

                // Add decoded Vector4
                texMipMap.Mipmaps.Add(bmpDecoded);

                // Generate Mip Maps
                texMipMap.GenerateMipmaps(true);

                // Create Output Content
                Texture2DContent texOutput = new Texture2DContent();

                // Passthrough Properties
                texOutput.Name = texInput.Name;
                texOutput.Identity = texInput.Identity;

                // Convert each bitmap to Gamma Encoded SurfaceFormat.Color
                for (int mi = 0; mi < texMipMap.Mipmaps.Count; mi++)
                {

                    // Get Mip Map
                    PixelBitmapContent bmpMipMap = (PixelBitmapContent)texMipMap.Mipmaps[mi];

                    // Create Color Bitmap of Equal Size
                    if (EncodeAfter)
                    {
                        PixelBitmapContent bmpColor = Encode(bmpMipMap);
                        texOutput.Mipmaps.Add(bmpColor);
                    }
                    else
                    {
                        texOutput.Mipmaps.Add(bmpMipMap);
                    }

                }//for

                // Return Gamma Encoded Texture with Linear Filtered Mip Maps
                return texOutput;
            }
            else
            {
                // Gamma Decode
                PixelBitmapContent bmpDecoded = Decode2(bmpInput);

                // Create Intermediate Content
                Texture2DContent texMipMap = new Texture2DContent();

                // Add decoded Vector4
                texMipMap.Mipmaps.Add(bmpDecoded);

                // Generate Mip Maps
                texMipMap.GenerateMipmaps(true);

                // Create Output Content
                Texture2DContent texOutput = new Texture2DContent();

                // Passthrough Properties
                texOutput.Name = texInput.Name;
                texOutput.Identity = texInput.Identity;

                // Convert each bitmap to Gamma Encoded SurfaceFormat.Color
                for (int mi = 0; mi < texMipMap.Mipmaps.Count; mi++)
                {

                    // Get Mip Map
                    PixelBitmapContent bmpMipMap = (PixelBitmapContent)texMipMap.Mipmaps[mi];

                    // Create Color Bitmap of Equal Size
                    if (EncodeAfter)
                    {
                        PixelBitmapContent bmpColor = Encode2(bmpMipMap);
                        texOutput.Mipmaps.Add(bmpColor);
                    }
                    else
                    {
                        texOutput.Mipmaps.Add(bmpMipMap);
                    }

                }//for

                // Return Gamma Encoded Texture with Linear Filtered Mip Maps
                return texOutput;
            }

        }//method
        // - - - - - - - - - - - - - - - - - - - -

        public PixelBitmapContent Decode2(PixelBitmapContent bmpInput)
        {
            // Decoded Bitmap
            PixelBitmapContent bmpDecoded = new PixelBitmapContent(bmpInput.Width, bmpInput.Height);

            // Convert each pixel to gamma decoded float
            for (int y = 0; y < bmpInput.Height; y++)
            {

                for (int x = 0; x < bmpInput.Width; x++)
                {

                    // Get Input Pixel
                    Color Cinput = bmpInput.GetPixel(x, y);

                    // Set Output Pixel
                    bmpDecoded.SetPixel(x, y, GammaDecodeColor2(Cinput.ToVector4()));

                }//for (x)

            }//for (y)

            return bmpDecoded;

        }
        PixelBitmapContent Decode(PixelBitmapContent bmpInput)
        {

            // Decoded Bitmap
            PixelBitmapContent bmpDecoded = new PixelBitmapContent(bmpInput.Width, bmpInput.Height);            

            // Convert each pixel to gamma decoded float
            for (int y = 0; y < bmpInput.Height; y++)
            {

                for (int x = 0; x < bmpInput.Width; x++)
                {

                    // Get Input Pixel
                    Color Cinput = bmpInput.GetPixel(x, y);

                    // Set Output Pixel
                    bmpDecoded.SetPixel(x, y, GammaDecodeColor(Cinput.ToVector4()));

                }//for (x)

            }//for (y)

            return bmpDecoded;

        }//method
        // - - - - - - - - - - - - - - - - - - - -
        PixelBitmapContent Encode(PixelBitmapContent bmpMipMap)
        {
            // Create Color Bitmap of Equal Size
            PixelBitmapContent bmpColor = new PixelBitmapContent(bmpMipMap.Width, bmpMipMap.Height);

            // Convert each pixel to gamma encoded color
            for (int y = 0; y < bmpMipMap.Height; y++)
            {

                for (int x = 0; x < bmpMipMap.Width; x++)
                {

                    // Get Input Pixel
                    Color CmipMap = bmpMipMap.GetPixel(x, y);                    

                    // Set Output Pixel
                    bmpColor.SetPixel(x, y, GammaEncodeColor(CmipMap.ToVector4()));

                }//for (x)

            }//for (y)

            return bmpColor;

        }//method

        PixelBitmapContent Encode2(PixelBitmapContent bmpMipMap)
        {
            // Create Color Bitmap of Equal Size
            PixelBitmapContent bmpColor = new PixelBitmapContent(bmpMipMap.Width, bmpMipMap.Height);

            // Convert each pixel to gamma encoded color
            for (int y = 0; y < bmpMipMap.Height; y++)
            {

                for (int x = 0; x < bmpMipMap.Width; x++)
                {

                    // Get Input Pixel
                    Rgba1010102 CmipMap = bmpMipMap.GetPixel(x, y);

                    // Set Output Pixel
                    bmpColor.SetPixel(x, y, GammaEncodeColor2(CmipMap.ToVector4()));

                }//for (x)

            }//for (y)

            return bmpColor;

        }//method        

        Color GammaEncodeColor(Vector4 vec)
        {
            Vector4 resp = new Vector4(1);
            resp.X = (float)Math.Pow(vec.X, 1.0 / 2.2);
            resp.Y = (float)Math.Pow(vec.Y, 1.0 / 2.2);
            resp.Z = (float)Math.Pow(vec.Z, 1.0 / 2.2);
            return Color.FromNonPremultiplied(resp); ;
        }

        Rgba1010102 GammaEncodeColor2(Vector4 vec)
        {
            Vector4 resp = new Vector4(1);
            resp.X = (float)Math.Pow(vec.X, 1.0 / 2.2);
            resp.Y = (float)Math.Pow(vec.Y, 1.0 / 2.2);
            resp.Z = (float)Math.Pow(vec.Z, 1.0 / 2.2);
            return new Rgba1010102(resp); ;
        }

        Color GammaDecodeColor(Vector4 vec)
        {
            Vector4 resp = new Vector4(1);
            resp.X = (float)Math.Pow(vec.X, 2.2);
            resp.Y = (float)Math.Pow(vec.Y, 2.2);
            resp.Z = (float)Math.Pow(vec.Z, 2.2);
            return Color.FromNonPremultiplied(resp); ;
        }

        Rgba1010102 GammaDecodeColor2(Vector4 vec)
        {
            Vector4 resp = new Vector4(1);
            resp.X = (float)Math.Pow(vec.X, 2.2);
            resp.Y = (float)Math.Pow(vec.Y, 2.2);
            resp.Z = (float)Math.Pow(vec.Z, 2.2);
            return new Rgba1010102(resp);
        }

        // - - - - - - - - - - - - - - - - - - - -
    }//class
    // - - - - - - - - - - - - - - - - - - - -
}//namespace
// - - - - - - - - - - - - - - - - - - - -

The code is very simple and easy to follow =P
The next stage is the post process that converts the Linear space to the SRGB. It is pretty simple and is listed below:

float4 PixelShader1( float2 Tex : TEXCOORD ) : COLOR0
{
float3 Color =  tex2D(baseSampler, Tex);
Color = pow(Color, 1.0/2.2));
return float4(Color,1);
}

Just applying the formula =P
Below you can see a simple screenshot with the diference of using and not using Gamma correction

This scene uses simple illumination, the first picture in this post shows the huge difference between using and not using gamma correction (the(a) picture has gamma correction).

I used the aproach i explained before in the PloobsEngine, and we good good results =P

A good source of information about this matter is this article from GPU Gems.

Special thanks to skytiger and his post about this same trouble.

Its all for today. =P

, , , , , , , ,