Eventhough gamma-correct rendering is absolutely crucial in order to get good and realistic lighting results, many programmers (sadly) still don’t care about it. Today, I want to show how different the visual results of lighting in gamma-space vs. linear-space actually are, and what you can do about it.
If you haven’t read anything about gamma, gamma-correction, gamma-space, etc. yet, here’s a very quick run-down of basic facts:
- The intensity of light generated by a physical device (your monitor) usually is not a linear function in terms of the applied input signal, e.g. the response of a CRT monitor follows approximately a pow(x, 2.2) curve.
- Most of the images you are viewing, e.g. pictures on the internet, JPEGs stored on your PC, etc. are stored in gamma-corrected space, so they end up looking “nice” on your monitor.
- Rendering, lighting, etc. cares about intensities, not some gamma-corrected values. Hence, all lighting should be done in a linear space, where the mathematics behind the equations actually make sense (1+1 = 2).
It is absolutely crucial that everybody understands the first two points – we’ll come to linear-space lighting in a minute.
- Because the response curve of a CRT monitor (and LCD/LED, for that matter) follows a pow(x, 2.2)-curve, doubling the input values does not lead to double the intensity seen on the monitor. This means that if you e.g. output 0.2 from your renderer, outputting 0.4 instead will not lead to double the intensity seen on your monitor.
- If you could see the actual values stored in e.g. JPEG pictures, all the pictures would look wrong, because they are stored in gamma-corrected space. Gamma-corrected space means that a pow(x, 1.0/2.2) operation has been applied to all values before storing them, so that the CRT gamma-curve and the gamma-correction applied to the pictures cancel out each other, leading to a linear response.
So, what does that mean, visually? Because a picture says more than a thousand words, let us consider two point lights slightly overlapping each other, and see what the difference between gamma-space and linear-space lighting is:
The above shows two point lights without any attenuation, each at 10% intensity (the output from the shader was 0.1). As should be clear, the overlapping portion should be twice as bright, but it actually isn’t, because the pow(x, 2.2)-curve caused by the monitor messes with the results.
If we apply a gamma-correction step (raising all values to 1.0/2.2) before outputting the values, the result looks like this:
The above is what this simple lighting example should look like in your renderer! Because lighting usually is done in linear space (e.g. adding different light source intensities in the shader, accumulating lighting using blending), the results needs to be gamma-corrected before they are sent to the monitor.
If you’re still not convinced yet, let us add simple N.L lighting and attenuation to our light sources. First the incorrect version, just adding two points lights at 25% intensity without applying any gamma-correction:
This is probably what you are used to seeing, and boy is it wrong! What the lighting actually should look like is the following, gamma-corrected version of lighting done in linear space:
We’ve only talked about diffuse lighting in this example, the difference with specular lighting is even bigger, because the monitor’s gamma-curve messes with the lights’ falloff, the color of the specular highlight, and more. Make sure to check out this excellent post if you’re looking for more examples.
In essence, if you’re working on PC/Xbox360/PS3 and you don’t care about gamma-correct rendering, all the lighting just looks wrong, and artists will have a hard time getting realistic and good-looking results, because incorrectly rendered images cannot be compensated by e.g. applying color correction in a post-process step. Certain parts of the image will always be too dark or too bright, depending on the “correction” done.
Furthermore, more advanced features like HDR, physically-based rendering, and image-based lighting are almost impossible to get right because non-linear lighting will not look correct under varying lighting conditions.
If you care about being gamma-correct, how can it be done using modern graphics hardware? It’s not that hard, there’s two major things you have to look out for:
- Before storing final values in the back buffer, apply a gamma-correction step to them, e.g. do pow(x, 1.0/2.2) as the very final step in your final shader. Of course, a better alternative is to use a sRGB-backbuffer format and let the hardware do the conversion for you, for free. In Direct3D 9, this used to be a separate render state, in Direct3D 11 just use one of the sRGB formats.
- Make sure to undo gamma-correction when using images such as diffuse/albedo textures in your shader. Again, this can be done by applying pow(x, 2.2) when reading from textures, but hardware offers dedicated sRGB texture formats for this purpose as well – use them, they are free.
As a final note, make sure to undo gamma-correction only for images which actually store brightness/intensity data, such as albedo textures. You certainly shouldn’t use sRGB textures for normal maps, alpha-channels, high-dynamic range data (e.g. 16-bit formats), etc., but this is something that can be dealt with off-line, in the asset conditioning pipeline.