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
Related posts:








