Twitter  Facebook  Google+  YouTube  E-Mail  RSS
The One Man MMO Project
The story of a lone developer's quest to build an online world :: MMO programming, design, and industry commentary
Gamma Correct After 20 Years
By Robert Basler on 2012-10-14 00:21:07
Homepage: www.onemanmmo.com email:one at onemanmmo dot com

I've known for a while that my graphics renderer wasn't gamma correct. Up until a couple years ago, this wasn't an issue, then somebody noticed that we've all been doing lighting calculations wrong for the last 20 years. You've probably heard of gamma. In effect it is a brightness adjustment. Without gamma correctness everything renders a little too dark.

Textures typically are in a color space called sRGB with a gamma of 2.2, lighting is done in linear space (gamma of 1.0) and the screen most likely has a gamma of 2.2. The idea with gamma correctness is to make sure that the input textures and colors are converted to linear space before they are processed with the lighting in the shader, and that the result is converted back to gamma 2.2 when it is sent to the display.

Thursday evening I discovered this excellent OpenGL tutorial which explains most of what is needed to implement this in OpenGL. There were some details missing, so I'll fill those in here.

The first step was to modify the PNG reader to read the gamma value stored in the PNG. I discovered that not all PNG's have a gamma value stored in them. In Gimp there's a checkbox you need to check to have it write the gamma. In a PNG file you might find an sRGB chunk (which indicates the image is gamma 2.2) or a gAMA chunk which provides the gamma value. If there is no gamma stored in the file, I print a warning in the trace and guess that the gamma is 2.2 (which is a pretty safe guess.)

    png_read_png(png_ptr, info_ptr, pngTransforms, NULL);
int sRGBintent;
if ( png_get_sRGB( png_ptr, info_ptr, &sRGBintent ) != 0 )
{
bitmap.SetGamma( 2.2 );
}
else
{
double gamma;
if (png_get_gAMA(png_ptr, info_ptr, &gamma))
{
bitmap.SetGamma( 1.0 / gamma );
}
else
{
LogNp("PngFile::ReadBitmap WARNING PNG image has no gamma setting. Guessing it is 2.2.");
bitmap.SetGamma( 2.2 );
}
}


Now that I have the gamma for the texture, I need to tell OpenGL. If I select an RGB format, OpenGL assumes the gamma is 1.0. If I choose an SRGB format, it tells OpenGL that the gamma is 2.2. Once OpenGL knows if the texture is RGB or SRGB, OpenGL will do the gamma conversion automatically for you in hardware.

    float64 gamma = mBitmap[ bitmapIndex ].Get()-<GetGamma();
// sRGB image data (gamma of roughly 2.2)
// Linear is also ok (gamma of roughly 1.0)
if ( ( 2.1 < gamma ) && ( gamma < 2.3 ) )
{
switch( internalFormat )
{
case GL_RGBA: internalFormat = GL_SRGB_ALPHA; break;
case GL_RGB: internalFormat = GL_SRGB; break;
case GL_COMPRESSED_RGBA: internalFormat = GL_COMPRESSED_SRGB_ALPHA; break;
case GL_COMPRESSED_RGB: internalFormat = GL_COMPRESSED_SRGB; break;
default:
{
ASSERT( false );
break;
}
}
}


The next step is to tell OpenGL to convert the output color of the shader from linear to sRGB. For each of my materials, I can specify if the output from the material pass is linear or sRGB. If the output color value is linear I need to call

glEnable( GL_FRAMEBUFFER_SRGB );


And if the output color value is sRGB, the conversion isn't needed and I call

glDisable( GL_FRAMEBUFFER_SRGB );


The GL_FRAMEBUFFER_SRGB flag is a hint to the final step needed - a framebuffer that supports sRGB. Unfortunately, the standard method for setting up the OpenGL render context doesn't support sRGB. sRGB output is an extension to OpenGL. Usually ChoosePixelFormat selects the framebuffer format to use, but to get an sRGB framebuffer use wglChoosePixelFormatARB. Unfortunately, Windows has a limitation that you can only call SetPixelFormat on a window once, so in order to initialize an OpenGL render context to get access to wglChoosePixelFormatARB, I needed to create a dummy window before the main window. That window first needs a DummyWndProc which doesn't do anything.

    LRESULT CALLBACK DummyWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}


For my dummy OpenGL render context, I use my old render context creation code so that if the new code fails, I still have the best pixel format that doesn't support sRGB.

    WNDCLASS wc; 
const wchar* dummyclassname = L"dUmMyClAsSnAmE";
wc.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC | CS_DBLCLKS;
wc.lpfnWndProc = DummyWndProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = mConfig.instance;
wc.hIcon = NULL;
wc.hCursor = NULL;
wc.hbrBackground = NULL;
wc.lpszMenuName = NULL;
wc.lpszClassName = dummyclassname;
if ( !RegisterClass( &wc ) )
{
return false;
}
HWND hWnd=CreateWindowEx( WS_EX_APPWINDOW,
dummyclassname,
L"",
WS_POPUP |
WS_CLIPSIBLINGS |
WS_CLIPCHILDREN,
0, 0, // Window Position
1, 1, // Window size
NULL, // No Parent Window
NULL, // No Menu
mConfig.instance,
NULL );
if ( hWnd == NULL )
{
return false;
}
static PIXELFORMATDESCRIPTOR pfd = // pfd Tells Windows How We Want Things To Be
{
sizeof(PIXELFORMATDESCRIPTOR), // Size Of This Pixel Format Descriptor
1, // Version Number
PFD_DRAW_TO_WINDOW | // Format Must Support Window
PFD_SUPPORT_OPENGL | // Format Must Support OpenGL
PFD_DOUBLEBUFFER, // Must Support Double Buffering
PFD_TYPE_RGBA, // Request An RGBA Format
mConfig.mBits, // Select Our Color Depth
0, 0, 0, 0, 0, 0, // Color Bits Ignored
0, // No Alpha Buffer
0, // Shift Bit Ignored
0, // No Accumulation Buffer
0, 0, 0, 0, // Accumulation Bits Ignored
16, // 16Bit Z-Buffer (Depth Buffer)
0, // No Stencil Buffer
0, // No Auxiliary Buffer
PFD_MAIN_PLANE, // Main Drawing Layer
0, // Reserved
0, 0, 0 // Layer Masks Ignored
};
HDC dummyDc = GetDC( hWnd );
if ( dummyDc == NULL ) // Did We Get A Device Context?
{
return false;
}
GLuint pixelFormat; // Holds The Results After Searching For A Match
pixelFormat=ChoosePixelFormat(dummyDc,&pfd);
if ( pixelFormat == 0 ) // Did Windows Find A Matching Pixel Format?
{
return false;
}
if( !SetPixelFormat( dummyDc, pixelFormat, &pfd ) ) // Are We Able To Set The Pixel Format?
{
return false;
}
HGLRC hRc=wglCreateContext( dummyDc );
if ( hRc == NULL ) // Are We Able To Get A Rendering Context?
{
return false;
}
if( !wglMakeCurrent( dummyDc, hRc ) ) // Try To Activate The Rendering Context
{
return false;
}
OS::OpenGLExtensions::Initialize();


Here's the new goodness with wglChoosePixelFormatARB:

    int srgbPixelFormats[ 128 ];
UINT numSrgbFormats;
PFNWGLCHOOSEPIXELFORMATARBPROC wglChoosePixelFormatARB = (PFNWGLCHOOSEPIXELFORMATARBPROC)
wglGetProcAddress("wglChoosePixelFormatARB");
if ( wglChoosePixelFormatARB != NULL )
{
int attribList[ 64 ];
int index = 0;
attribList[ index++ ] = WGL_DRAW_TO_WINDOW_ARB;
attribList[ index++ ] = GL_TRUE;
attribList[ index++ ] = WGL_SUPPORT_OPENGL_ARB;
attribList[ index++ ] = GL_TRUE;
attribList[ index++ ] = WGL_DOUBLE_BUFFER_ARB;
attribList[ index++ ] = GL_TRUE;
attribList[ index++ ] = WGL_PIXEL_TYPE_ARB;
attribList[ index++ ] = WGL_TYPE_RGBA_ARB;
attribList[ index++ ] = WGL_COLOR_BITS_ARB;
attribList[ index++ ] = mConfig.mBits;
attribList[ index++ ] = WGL_DEPTH_BITS_ARB;
attribList[ index++ ] = 16;
attribList[ index++ ] = WGL_ACCELERATION_ARB;
attribList[ index++ ] = WGL_FULL_ACCELERATION_ARB;
bool tryingForSrgb = false;
if ( OS::OpenGLExtensions::GL_VERSION_3_0Enable() && OS::OpenGLExtensions::GL_ARB_framebuffer_sRGBEnable() )
{
attribList[ index++ ] = WGL_FRAMEBUFFER_SRGB_CAPABLE_ARB;
attribList[ index++ ] = GL_TRUE;
tryingForSrgb = true;
}
attribList[ index++ ] = 0; //End
if ( wglChoosePixelFormatARB( dummyDc, (const int*)attribList, NULL, ARRAY_SIZE( srgbPixelFormats ),
srgbPixelFormats, &numSrgbFormats ) )
{
if ( numSrgbFormats >= 1 )
{
pixelFormat = srgbPixelFormats[ 0 ];
mSrgbDisplay = tryingForSrgb;
}
}
}
// Get rid of the dummy window.
if ( hRc != NULL )
{
wglMakeCurrent(NULL,NULL);
wglDeleteContext(hRc);
hRc = NULL;
}
if ( dummyDc != NULL )
{
ReleaseDC(mConfig.wnd,dummyDc);
dummyDc = NULL;
}
OS::OpenGLExtensions::Shutdown();
DestroyWindow( hWnd );
UnregisterClass( dummyclassname, mConfig.instance );
// Create the REAL window render context
mDc = GetDC( mConfig.wnd );
if ( mDc == NULL )
{
return false;
}
if( !SetPixelFormat( mDc, pixelFormat, &pfd ) ) // Are We Able To Set The Pixel Format?
{
return false;
}
PIXELFORMATDESCRIPTOR pfdr;
pfdr.nSize = sizeof( pfdr );
pfdr.nVersion = 1;
if ( 0 == DescribePixelFormat( mDc, pixelFormat, sizeof( pfdr ), &pfdr ) )
{
return false;
}
Log( L"Selected PixelFormat {0}: r {1} g {2} b {3} a {4} depth {5}{6}{7}{8}{9} {10}",
pixelFormat %
pfdr.cRedBits %
pfdr.cGreenBits %
pfdr.cBlueBits %
pfdr.cAlphaBits %
pfdr.cDepthBits %
( ( pfdr.dwFlags & PFD_SUPPORT_OPENGL ) != 0 ? L" GL" : L"") %
( ( pfdr.dwFlags & PFD_DOUBLEBUFFER ) != 0 ? L" DOUBLE-BUFFER" : L"") %
( pfdr.iPixelType == PFD_TYPE_RGBA ? L" RGB" : L"") %
( ( pfdr.dwFlags & PFD_DRAW_TO_WINDOW ) != 0 ? L" WINDOW" : L"") %
( mSrgbDisplay ? L"SRGB" : L"" ) );
if ( ( pfdr.iPixelType != PFD_TYPE_RGBA ) ||
( pfdr.cRedBits < 5 ) ||
( pfdr.cGreenBits < 5 ) ||
( pfdr.cBlueBits < 5 ) )
{
return false;
}
mRc=wglCreateContext( mDc );
if ( mRc == NULL )
{
return false;
}
if( !wglMakeCurrent( mDc, mRc ) )
{
return false;
}
ShowWindow( mConfig.wnd,SW_SHOW );
SetForegroundWindow( mConfig.wnd ); // Slightly Higher Priority
SetFocus( mConfig.wnd ); // Sets Keyboard Focus To The Window
OS::OpenGLExtensions::Initialize();


And that's all there is to it.

New Comment

Cookie Warning

We were unable to retrieve our session cookie from your web browser. If pressing F5 once to reload this page does not get rid of this message, please read this to learn more.

You will not be able to post until you resolve this problem.

Comment (You can use HTML, but please double-check web link URLs and HTML tags!)
Your Name
Homepage (optional, don't include http://)
Email (optional, but automatically spam protected so please do)
What is 5 multiplied by 1? (What's this?)

  Admin Log In



[The Imperial Realm :: Miranda] [Blog] [Gallery] [About]
Terms Of Use & Privacy Policy