Currently I use the following formula to gamma correct colors (convert them from RGB to sRGB color space) after the lighting pass:
output = pow(color, vec3(1.0/2.2));
Is this formula the correct formula for gamma correction? I ask because I have encountered a few people saying that its not, and that the correct formula is more complicated and has something to do with power 2.4 rather than 2.2. I also heard something that the three color R, G and B should have different weights (something like 0.2126, 0.7152, 0.0722).
I am also curious which function does OpenGL use when GL_FRAMEBUFFER_SRGB
is enabled.
Edit: This is one of many topics covered in Guy Davidson's talk "Everything you know about color is wrong". The gamma correction function is covered here, but the whole talk is related to color spaces including sRGB and gamma correction.
Gamma correction may have any value, but considering linear RGB / non-linear sRGB conversion, 2.2 is an approximate, so that your formula may be considered both wrong and correct: https://en.wikipedia.org/wiki/SRGB#Theory_of_the_transformation
Real sRGB transfer function is based on 2.4 gamma coefficient and has discontinuity at dark values like this:
float Convert_sRGB_FromLinear (float theLinearValue) {
return theLinearValue <= 0.0031308f
? theLinearValue * 12.92f
: powf (theLinearValue, 1.0f/2.4f) * 1.055f - 0.055f;
}
float Convert_sRGB_ToLinear (float thesRGBValue) {
return thesRGBValue <= 0.04045f
? thesRGBValue / 12.92f
: powf ((thesRGBValue + 0.055f) / 1.055f, 2.4f);
}
In fact, you may find even more rough approximations in some GLSL code using 2.0 coefficient instead of 2.2 and 2.4, so that to avoid usage of expensive pow()
(x*x
and sqrt()
are used instead). This is to achieve maximum performance (in context of old graphics hardware) and code simplicity, while sacrificing color reproduction. Practically speaking, the sacrifice is not that noticeable, and most games apply additional tone-mapping and user-managed gamma correction coefficient, so that result is not directly correlated to sRGB standard.
GL_FRAMEBUFFER_SRGB
and sampling from GL_SRGB8
textures are expected to use more correct formula (in case of texture sampling it is more likely pre-computed lookup table on GPU rather than real formula as there are only 256 values to convert). See, for instance, comments to GL_ARB_framebuffer_sRGB extension:
Given a linear RGB component, cl, convert it to an sRGB component, cs, in the range [0,1], with this pseudo-code: if (isnan(cl)) { /* Map IEEE-754 Not-a-number to zero. */ cs = 0.0; } else if (cl > 1.0) { cs = 1.0; } else if (cl < 0.0) { cs = 0.0; } else if (cl < 0.0031308) { cs = 12.92 * cl; } else { cs = 1.055 * pow(cl, 0.41666) - 0.055; } The NaN behavior in the pseudo-code is recommended but not specified in the actual specification language. sRGB components are typically stored as unsigned 8-bit fixed-point values. If cs is computed with the above pseudo-code, cs can be converted to a [0,255] integer with this formula: csi = floor(255.0 * cs + 0.5)
Here is another article describing sRGB usage in OpenGL applications, which you may find useful: https://unlimited3d.wordpress.com/2020/01/08/srgb-color-space-in-opengl/