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.

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:
- convert the non-linear RGB color codes to a linear color space
- then you can do normal arithmetic, like computing averages or whatever
- 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.

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.