2
$\begingroup$

I would like a help to understand the following line of code:

return float3(pow(color.r, 2.2), pow(color.g, 2.2), pow(color.b, 2.2));

I've seen some places calling it gamma correction, but I don't know if it's correct.

If it really is a gamma correction how does it work?


No Correction

With Correction

With Correction

I liked the artistic result of this, I will keep it. I asked the question to understand better.

$\endgroup$
3
  • $\begingroup$ Yes, Wikipedia explains this pretty well. Anyway what part of taking the power of 2.2 do you not understand $\endgroup$
    – joojaa
    Mar 21, 2021 at 22:00
  • $\begingroup$ Where does 2.2 come from? Can I vary the value in some way to obtain other results? $\endgroup$ Mar 21, 2021 at 22:42
  • $\begingroup$ 2.2 is the gamma of sRGB specification. Although its linear near black for a short segment so. Yes you can vary it. $\endgroup$
    – joojaa
    Mar 22, 2021 at 4:56

4 Answers 4

3
$\begingroup$

The actual color of a pixel, outputted on a monitor, does not linearly depend on the applied voltage signal for that pixel. For CRT monitors, the actual color is approximately proportional to the applied voltage raised to the power of a so-called gamma value, which depends on the monitor. This gamma value typically lies between 2.2 and 2.5 for CRT monitors. (Hence, the ubiquitous appearances of the magical constant 2.2 in rendering applications.)

$$L_\mathrm{actual} \sim V^\gamma$$

To bypass this gamma value in your computations, you need to gamma correct the computed colors of each pixel before presenting. By raising the computed color to a power of the reciprocal of that same gamma value, the computed color becomes proportional to the actual color.

$$V \sim L_\mathrm{computed}^{1/\gamma} \\ L_\mathrm{actual} \sim L_\mathrm{computed}$$

So in general gamma correction is a technique that adapts the computed colors to the transfer function of the monitor used for outputting. Both CRT and non-CRT monitors each have their own transfer function. This means that the final rendering pass should adapt the computed colors, based on the used monitor, to obtain correct actual colors.

These linear-to-gamma color space conversions seem like unnecessary overhead from a monitor construction point of view. It is physically perfectly possible to construct a CRT monitor with a gamma value of exactly one, ensuring that the computed color is already proportional to the actual color, and eliminating the need for gamma correction.

From a perceptual point of view, removing the need for gamma correction and using a monitor where computed colors are proportional to the corresponding actual colors is actually a bad idea. Typically, actual colors are represented with 8 (or 10) bits for each of the red, green and blue channel. This quantization only supports 256 (or 1024) different colors. Here, a 0 value represents completely black and a 255 (or a 1023) value represents completely white. But what about the intermediate values? If a linear encoding is used (i.e. a gamma value of 1), the majority of values would be perceptually very close to white and a very small minority would be perceptually close to black. By using a gamma encoding (e.g., a gamma value of 2.2), the distribution is perceptually more linear (i.e. equidistant intervals between black and white).

$\endgroup$
1
  • $\begingroup$ It is important when talking about gamma correction, to differentiate between perceived color and perceived brightness. $\endgroup$
    – pmw1234
    Apr 13, 2021 at 14:19
1
$\begingroup$

Welcome to the world of 8-bit graphics! Other answers here are excellent, and most of what you need to know is described well on Wikipedia but let me take you on a human-friendly journey of understanding that I wish someone would have taken me on when I was younger.

The first realization that you need to make is that a pixel with RGB values 128, 128, 128 isn't "half as bright" as a pixel that is outputting 255, 255, 255 (white). It's not even close!

You can check this by making a checkerboard image of black and white pixels (which, on average, will be 50% as bright as a white image, by definition) and compare the brightness of that to a swatch of 128, 128, 128. You'll find the swatch is much too dark. A value that is closest to 50% is actually somewhere between 188 and 189 but also depends on your display.

a test pattern that reveals the gamma issue

It's very important to look at this picture with 1:1 scaling.

If you can "get" this (notice the effect) then you are ready to begin understanding why the 128, 128, 128 swatch seems too dark.

Most software developers discover this property by accident. If you just perform arithmetic on RGB color values in the intuitive (but naïve) way, like for example to compute an average color of two pixels as in (a+b)/2, you'll find that it doesn't work. Things look too dark. :( In fact, you can completely finish that awesome first-person shooter 3D engine you've always aspired to create, only to discover that all your graphics look too dark and shadowy.

This is because the RGB codes do not correspond to brightness levels in a linear way.

RGB codes are non-linear. You should have the moment of realization where you ask yourself, "but wait, (a+b)/2 is the formula for average, so why doesn't it produce an average color?" Or maybe it will occur to you by asking "Why, when I compute (0+255)/2 and get 128 (rounding up of course) do I not get a color that is half as bright as white?"

Once you get that feeling where it seems like arithmetic is broken, you'll be ready to understand that the encoding used for all pictures in typical image files are actually encoded in a non-linear color space such as sRGB. To do arithmetic on colors correctly you must:

  1. convert the non-linear RGB color codes to a linear color space
  2. then you can do normal arithmetic, like computing averages or whatever
  3. then you must finally convert the result back to the original non-linear RGB encoding to be displayed by your display device or stored in your image file or texture or whatever.

Ever notice how it's easy to tell the difference between 1 pound and 2 pounds, but it's hard to tell the difference between having 101 pounds and 102 pounds? They're both different by 1 pound, but the relative difference is twice as heavy for the small weights, but 102 is barely a 1% relative increase from 101 -- very difficult to notice.

Well your eyes work the same way with light! It's very easy to tell the difference (in a room with no other light sources) between the brightness from 1 light bulb vs the light from 2 light bulbs. But it's much harder to tell the difference in brightness between 101 light bulbs and 102 light bulbs.

Here's where engineers got clever with 8-bit graphics...

Since, with 8-bit graphics, we only have 256 different codes we can store, why waste so many of those different codes on intensities people can't even tell apart? Instead, the designers choose 256 different codes where each successive code is an equal relative increase in brightness compared to the previous code. All codes are equally useful, and it's much more efficient, right?

If you want to get a tiny bit mathematical now, it means that the intensity of light you get from using intensity n+1 divided by the intensity of light you get from using an intensity of n is a constant. (obviously something special happens for 0. Any amount of light will always be infinitely brighter than no light, relatively speaking).

And now that we have our toe dipped into the waters of the mathematical ocean, you're finally ready to at least get a hint of what the word gamma means.

The actual term gamma refers to the gamma function, which is a function with that nifty property where f(x+1) = k * f(x). To get from an intensity to a code you first raise the linear intensity (a number between 0 and 1) to the power of 2.2, which will also be a number between 0 and 1, then spread that range out over the 256 code values available to you. To perform the reverse conversion, you first convert your RGB coded number between 0 to 255 to a number between 0 and 1, and then "undo" the gamma function by raising it to the power of 1/2.2 to arrive at a linear intensity between 0.0 and 1.0. Historically the exponent 2.2 was chosen because it coincidentally matches some properties of old CRT monitors (the ratio of power of the electron beam to intensity of the light produced is similarly non-linear).

In practice the internet actually uses a slightly smarter color space called sRGB. It's still a non-linear function and it's very close to gamma 2.2, but has some nice properties where there aren't so many really dark values in the low end -- again, trying to make more efficient use of the 256 levels of intensity available to us in 8-bit graphics.

So let's circle back to your actual question:

return float3(pow(color.r, 2.2), pow(color.g, 2.2), pow(color.b, 2.2));

This is half of a color conversion. It is assuming that we have linear intensity values stored in color and it is applying the gamma function (raising it to the power of 2.2) to get closer to the Normalized (0.0 to 1.0) encoded values that are suitable for putting into a picture file or displaying on the screen. OpenGL will unnormalize these values and write a corresponding 8-bit integer representation of that value in the range 0-255.

This works great for what I would call "synthetic" colors. Like if you are simulating some light source or the attenuation from angle of incidence or something. But if you were reading values from an image file, then you would furthermore need to use a sampler that performed the inverse of this correction. It's important to get the sampler to do it because there's hardware built in to your GPU to do things like bi-linear interpolation (sample 4 pixels and compute an intermediate value). Without activating that inverse correction factor, you are at risk of doing the broken arithmetic on your color values and textures get weird artifacts in them caused by introducing colors that are too dark.

Here's an example of reducing that previous image in size ever so slightly using an incorrect technique - that is quite frankly found everywhere in the world even though it's wrong.

Incorrectly scaled image

This image was rescaled naïvely without gamma correction and will look bad, with artifacts that are too dark.

So the next generation of graphics developers all have a responsibility to learn the basics so that we don't get dark looking graphics everywhere that are filled with artifacts. My personal pet peeve is sci-fi movies where the stars in the black sky throb bright and dark as the camera pans slowly because the people that rendered it didn't get the gamma conversions correct!

I hope this helps. I need to give a disclaimer: I've intentionally used some terms incorrectly like brightness and intensity (just think "shades of gray") because you need to begin your journey with familiar language -- then learn the scientifically correct terms later where you will learn about things like luma, luminosity, luminous intensity, brightness, perceived brightness, etc. etc. After you get the terminology right, the next dragon you face is named color. Color is hard to get right and human perception makes it especially complex. I've only scratched the surface here.

$\endgroup$
1
  • 1
    $\begingroup$ This is a really good answer. $\endgroup$
    – joojaa
    Apr 20, 2021 at 4:10
1
$\begingroup$

Gamma correction originated as a way of correcting the output of a CRT to be a better fit for the human visual system. Modern monitors don't need to do it, but, they followed the CRT and there were millions of CRT's that all had gamma correction and most signals already had gamma correction in them. Today we have a chicken and egg problem...but reversed. Millions of clucking hardware chickens all speaking gamma...

The flip side is that us humans don't think well in the land of exponential values, but we do think well in terms of linear values. So software for graphics code tends to remove the gamma then we can multiply add and manipulate in an intuitive manner, and convert it back to gamma for display.

Modern hardware has specialty components that do this on the fly so the generally accepted practice is to set up the graphics hardware to handle this so we don't have to worry about it as programmers. But sometimes it can be useful.


Updates based on comments (after rereading this I added a few more updates)

Gamma correction has a second benefit for low precision formats like the common sRGB 8 bit format. (invented in the 90's to match hardware of the day) It redistributes the available values placing more values in the "dim" region where human's can better perceive changes in brightness. This redistribution is why gamma correction is often called gamma compression.

This is not a correction for human vision, it is a correction for the limitations of the 8 bit format. Higher precision formats do not need to store gamma correction and (generally) have gamma applied to them only when converting to a lower precision format.

Here is a breakdown of common formats with respect to gamma:

  • 8 bit formats like sRGB - Gamma is an essential component of this format
  • 10 bit formats like HRD10 - Usable without gamma, but adding gamma improves this format considerably.
  • 12 bit formats like Dolby Vision - Gamma is starting to be redundant from 12 bits and up. There is enough separation between linear color brightness that it is increasingly difficult for to see the color separation, but most people can still detect some banding in the dark colors.
  • 14 bits formats like RAW formats from professional cameras. Professional quality monitors that use 3D look up tables to do color matching begin to give truly magnificent images. At this point most specs include phrases like "A gamma of 2.2 is applied since it is expected that images will be viewed on gamma corrected devices". An 8 bit image next to a 14 bit can be perspective changing for viewers.
  • 16 bit I don't have a hardware example, but some software allow images to be edited in 16bit formats.

The very amazing human eye can perceive 1,000,000 nits of difference in brightness by dynamically adapting itself to the available lighting. But is especially adapted to see more changes in brightness in dark situations. The CRT when it was invented struggled to display 100 nits of brightness. Leaving 999,900 nits on the table. So gamma was applied to the signal to take advantage of our ability to see better in low lighting. This was a limitation of the CRT not our vision.

It is common for modern monitors to reach 300-500 nits, with some models getting up to 1000 nits or more. That extra available brightness means that gamma is not really needed for anything but historical compatibility. (this is a bit of an overstatement, monitors really need to get into the 10,000 nit range before gamma starts becoming redundant)


When working with image data it is best to configure the hardware to handle gamma correction for us and use methods such as tone mapping to achieve a specialized appearance. This allows the hardware to load images that are stored as a variety of color formats such as sRGB and HDR into a common space that is well defined, where code can then perform its calculations and send that result to the hardware where it is converted to a format that is specific to the device being used to display it. (which is often a gamma corrected format)

I am not tying to say that this image is "wrong", everyone should take the time to create images like this to better understand gamma correction.

$\endgroup$
2
  • 1
    $\begingroup$ You missed the fact that gamma compression/decompression actually exploits peculiarities of the human visual system to reduce banding. It's used for non-uniform quantisation in order to be somewhat in line with Weber's law and human perception. $\endgroup$
    – lightxbulb
    Mar 23, 2021 at 9:23
  • $\begingroup$ Confusingly, "gamma" refers to two distinct things: nonlinearity in human perception, and nonlinearity in hardware devices such as sensors or displays. Thankfully, display vendors and file format designers have agreed on the same standard, sRGB, so that the operation of decompressing an image file and the operation of preparing the image for display cancel each other out. $\endgroup$
    – Pseudonym
    Apr 13, 2021 at 1:55
0
$\begingroup$

Beside, note that the pow 2.2 is for decoding some sRGB encoded texture. The final encoding to sRGB at the end of shader would be pow 1./2.2 .

Also, note that gamma correction is an approximation of the sRGB correction. But in practice even doing square and sqrt instead is quite good (and cheaper than pow ) ;-)

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.