D3DBook:Color Filters

From GDWiki
Jump to: navigation, search

Contents

Color Filters

Color filters are by far the oldest post effect used in games. Impressive examples are the Call of Duty and Medal of Honor series. The main idea here is to give a winter scene -for example- a more grey-ish tint, a summer scene a slight yellow tint or a scene in the jungle a slight greenish/blueish tint. Color filters can also be triggered or changed based on the player being in a specific area. When the player leaves one area and goes to another, the color filters might change accordingly by being linearly interpolated between areas.

Most of the time, color filters in a post-processing pipeline work on RGB values. This color space is frequently used in most computer applications since no transform is required to display information on the screen. It is device dependent because different devices transform the linear-light intensity to a nonlinear video signal by gamma correction.

Decent post-processing pipelines offer the following features:

  • Adding and multiplying Color
  • Color saturation control
  • Contrast control
  • Gamma control

Gamma Control

Physical light intensity values behave linearly - if two sources of light shine on the same surface, their combined intensity is equal to the sum of the intensities of the two sources. For this reason such values are also known as "linear light" values. If we quantize linear light values into fixed-width steps, we have physically uniform encoding - each step increment has the same physical magnitude anywhere on the scale.

However when light values must be quantized using few bits, to minimize banding artifacts the most efficient encoding is one which is perceptually uniform - each step increment has the same perceptual magnitude anywhere on the scale. Since human perception of brightness is non-linear (the perceived brightness of combined sources is not equal to the sum of the perceived brightness of separate sources), perceptually uniform and physically uniform encodings are different.

To enable the use of 8-bit-per-channel display frame buffers without noticeable banding, a standard nonlinear transfer function is defined for converting linear light values into frame buffer values. After this conversion, the frame buffer values are in a (roughly) perceptually uniform space, also known as the sRGB color space [Stokes]. This is also called gamma correction or sRGB encoding.

In other words, the main purpose of gamma correction or sRGB encoding of colors is to optimize perceptual performance of a limited number of bits in each component in RGB color space.

In addition to frame buffers, sRGB encoding is commonly used for many textures stored in non-HDR formats. Textures authored by artists directly such as color maps are commonly gamma corrected. Even modern 8-bit image file formats (such as JPEG 2000 or PNG) default to the sRGB encoding.

The advantage of utilizing the available precision in a rendering pipeline more efficiently by encoding images or framebuffers to sRGB needs to be balanced against the fact that most color manipulation methods that are applied in a renderer become physically non-linear.

By removing sRGB encoding or gamma correction, the result of dynamic lighting, the addition of several light sources, shadowing, texture filtering and alpha blending will look more realistic and advanced color filters will provide predictable and consistent visual results (read also [Brown]).

Background

The sRGB color space is based on the monitor characteristics expected in a dimly lit office, and has been standardised by the IEC (as IEC 61966-1-2) [Stokes]. The sRGB transfer functions are usually designed to be close to a pure power function, which leads to a naming convention: the number K which most closely matches the transfer function with X1/K is the gamma value of the space. By this convention, sRGB is a gamma 2.2 space, and linear light is a gamma 1.0 space. In theory, a pure power function would work, but power functions have an infinite slope at zero, which causes practical difficulties. For this reason, the transfer function has a linear portion at the extreme low end. Figure 1 shows a power function:

Image:x22.jpg
Figure 1 - x2.2

Decent game engines receive color data from many sources (vertex color, ambient occlusion color, textures etc.) and they blend those values in different ways. To make the result of all those operations physically linear, the sRGB encoding or gamma correction need to be removed before any operation is applied.

Because the monitor expects a value that is sRGB encoded, a transfer function that converts from gamma 1.0 to gamma 2.2 is applied to the resulting color value before it is send to the framebuffer.

Gamma correction has a huge impact on the art workflow. Most game teams nowadays setup their art pipeline so that all assets are viewed and created with monitors and art packages assuming a gamma value of 2.2, whereas the renderer of the game runs all dynamic lighting, color operations, texture filtering (read more in [Brown]), and shadowing in linear gamma space.

This leads to visual inconsistencies between what the art team sees in their tools and what the renderer produces as the end result. Most of the time those inconsistencies are not as bad as the inconsistencies that would have been introduced by running the renderer with sRGB encoding. Some teams even switch to a workflow that assumes a linear gamma value by re-adjusting and re-calibrating their LCD screens and their art tools.

Implementation

Most video game consoles and PC graphics cards are able to convert textures internally from gamma 2.2 to gamma 1.0 while fetching the texture. Additionally there is functionality available to convert color values back to gamma 2.2 before they will be send to the display. That leaves game developers with the challenge to manually convert color values to gamma 1.0 that are fed to the renderer from other sources than a texture or provide backward compatibility in case graphics hardware allows to convert textures to gamma 1.0 without functionality to convert back to gamma 2.2.

The equation to convert from sRGB to linear gamma is

Image:ConvertsRGBToLinGamma.bmp
Equation 1

Figure 2 shows the resulting curve of equation 1.

Image:Togamma10.jpg
Figure 2- Transform from gamma 2.2 to linear gamma.

The equation to convert from linear gamma to gamma 2.2 is

Image:ConvertLinGammaTosRGB.bmp
Equation 2

Figure 3 shows the resulting curve of equation 2.

Image:Togamma22.jpg
Figure 3 - Transform linear gamma to gamma 2.2.

The effect of the above equations closely fits a straightforward gamma 2.2 curve with a slight offset (read more in [Stokes]). Converting into gamma 1.0 is done in HLSL like this:

float3 Color; 
Color = ((Color <= 0.03928) ? Color / 12.92 : pow((Color + 0.055) / 1.055, 2.4))

Converting from linear gamma to gamma 2.2 can be done with the following HLSL snippet:

float3 Color; 
Color = (Color <= 0.00304) ? Color * 12.92 : (1.055 * pow(Color, 1.0/2.4) - 0.055);

Simplifying this equation leads to color inaccuracies of colors that are smaller than 10 from the value range of 0..255 and therefore to banding effects in dark textures.

Typical simplifications remove the condition that differs between values lower than 0.03928 and values that are equal or higher than this number, the following simplified code can be used to convert from gamma 2.2 to gamma 1.0.

float3 Color;
Color = pow((Color + 0.055)/ 1.055, 2.4);

The code to convert from linear gamma to gamma 2.2 is:

Color = pow(1.055 * Color, 1.0/2.4)- 0.055;

Simplifying the gamma conversion even more, can be done by using a power value of 1/2.2 to convert to gamma 2.2 and a power value of 2.2 to convert to linear gamma.

All decent hardware offers gamma corrections from gamma 2.2 to gamma 1.0 and vice versa, while the conversion from gamma 2.2 to gamma 1.0 is only supported for textures, while they are fetched. All other color values need to be converted manually to gamma 1.0 in a pre-process or in the renderer.

Before values are send to the monitor, the graphics hardware can convert back to gamma 2.2. All conversions supported by hardware are free. ATI graphics hardware supports piecewise linear approximations to the gamma curve, while decent NVIDIA graphics hardware stick with the equations above.

Contrast Control

Background

Contrast control is inherently a luminance-only operation. The following equation produces good looking results:

Image:ContrastNew.gif
Equation 3

Figure 4 visualizes this equation for a value range of 0..1.

Image:ContrastNewEquation.jpg
Figure 4 - Visualization of a Contrast Operator based on a Cubic Polynomial

This contrast operator offers a visually more pleasant result than for example using a power function. Similar to all the other color filters, it assumes a gamma 1.0 color that is in the value range of 0..1. Therefore it can only be used after tone mapping is applied.

In HSB color space, when choosing complementary colors, fully saturated colors will offer the highest level of contrast. Choosing from tints or shades within the hue family reduces the overall contrast of the composition.

Implementation

A vectorized implementation of equation x is straightforward:

float3 Color = Color - Contrast * (Color - 1.0f) * Color *(Color - 0.5f);

Color Saturation

Background

To remove color from an image, first its color values -that are already in gamma 1.0- are converted to luminance values. Interpolating the then desaturated copy of the image with the original color image offers a seamless way to remove colors from an image.

The common way to transform a RGB value to luminance is taken from the standard matrix such as specified in [ITU1990] to convert from CIE XYZ color space to RGB. This matrix looks as shown in equation 4:

Image:ConversionFromRGBToXYZandBack.gif
Equation 4

The Y component in the XYZ color space represents luminance. Thus, a representation of luminance is obtained by computing a linear combination of red, green and blue components according to the middle row of the RGB-to-XYZ conversion matrix. Therefore luminance can be computed following [ITU1990] as follows:

Image:Luminance.bmp
Equation 5

Implementation

The color saturation approach is build on the result of the color conversion stage. Saturation is controlled by lerping between luminance and the original color.

float Lum = dot(Color,float3(0.2126, 0.7152, 0.0722));
 
float3 Color = lerp(Lum.xxx, Color, Sat)

The variable Sat holds the value the user has chosen.

Color Changes

Background

The simplest color filters just add or multiply RGB values.

Implementation

Multiplying an incoming color value with a value for each color channel and additionally adding a value to each of the color channels is done like this.

float3 Color = Color * ColorCorrect * 2.0 + ColorAdd;

ColorCorrect and ColorAdd are vectors that hold three values for each of the color channels.

Personal tools