RANDOM USER PROJECTS
An advanced rendering pipeline for Panda3D
Student Recreation of "Colossus"
Code3D is a tool for creating virtual training scenarios.

Panda3D Manual: Color Spaces


This page is not in the table of contents.

This page describes a future feature. It is not currently available in any version of Panda3D.

Contents

Introduction

By default, Panda3D and OpenGL (and most conventional graphics software) operate under the assumption that all input colors and output colors are in a linear color space. This means that they assume that adding two colors together produces a color with their combined luminosity, and that multiplying a color with a scalar value n produces a color that is n times as bright.

However, these assumptions are wrong. Historically, CRT monitors had a gamma of around 2.2, which means that the color values appear as if they were raised to the power of 2.2, so content produced was corrected for that gamma so that they could be viewed without the need for in-software gamma correction.

This practice was standardized in 1996 when the sRGB color space was established; it mimicked a gamma of 2.2 so that it could be viewed directly on CRT monitors. When other display devices were produced, they typically had compensating circuitry so they could view the same imagery. Today, almost all monitors are calibrated to the sRGB color space and the great majority of digital cameras shoot in sRGB.

Now, one might think that sRGB is merely an archaic artifact of the old days when we all had CRT monitors. However, as it turns out, the human eye also has a very non-linear perception of color. In particular, our eyes are better able to distinguish dark tones than they are able to distinguish bright tones. So it is more efficient to store images as sRGB, which gives higher resolution for the darker tones, so that the images are more optimized for human consumption.

Usually, this all works out just fine: when you view an sRGB image in a browser or image viewer, it is sent to the monitor without need for gamma correction, and the monitor displays it with a gamma of 2.2. However, there is a very important drawback: our textures and framebuffers are non-linear.

This has an important implication for 3D lighting and blending calculations. Since these involve linear calculations, the results will be wrong. This can be fairly dramatic! A light that is supposed to attenuate a colour value to half brightness will actually cause it to appear at 0.22x brightness, which means that darker shades will appear far too dark, not to mention that the transition between light and dark will appear unnatural.

It is easy to dismiss these since the lighting in your scene may look just fine to you. After all, you've probably adjusted all your light's attenuation to compensate for the darkness and you may find that you have to re-balance all your lights if you want to work with proper gamma-correction. Plus, you may have to get used to the way lighting behaves if you're used to those old games that didn't account for gamma correction.

Your results may look fine, but they're still wrong. Until you properly account for gamma correction, lights in your applications won't behave the way they should, or rather, as they do in real life. This is especially important for realistic rendering techniques like physically based lighting - the closer you get to realistic lighting, the more obvious these lighting issues will become.

Monitor Calibration

Of course, this all assumes that the monitor has been calibrated to the industry standard sRGB color space. This may not always be the case. Cheaper monitors may not have been calibrated correctly, or a monitor's gamma settings may have been altered.

Therefore, it is recommended that you not only let the user adjust the gamma in a settings menu, but it would be even better if there was a mechanism to let the user adjust the gamma based on test images. One common approach is to show three instances of a certain glyph at different brightnesses, and let the user adjust the gamma until the brightness of the glyphs match, or until the visibility of the glyphs matches a certain expectation.

Use the following image to find out if your monitor is sRGB corrected. Align the following image to the center of your monitor, then lean back and look at it straight-on. The top half is finely black/white checkered, and its brightness will be exactly the midpoint between black and white. The bottom half is what the midpoint of grey should look like when correctly calibrated. If both halves appear to be at the same brightness, your monitor is correctly sRGB-calibrated.

srgb-calibration-strip.png

Be sure to view this image at exactly 100% magnification! At any other magnification, the browser will perform gamma-naive scaling, and the top half will suddenly change to a dark grey.

Correction Features

To correct this, we have to make sure that our lighting and blending operations happen in linear space. These are the steps we have to do for that to happen:

  • The texture values we sample must have been converted to linear space.
  • The rendered pixels must be converted back to sRGB when displaying them on screen or writing them into the framebuffer.

Fortunately, modern graphics hardware has a range of facilities that make this easy for us, using two features available in both OpenGL and Direct3D: sRGB textures, and sRGB framebuffers. The first means that a texture can be marked as being in the sRGB color space, which causes OpenGL to automatically convert the data to linear space when sampling the texture. The second causes OpenGL to convert linear data back to sRGB when writing back into the frame buffer. These two features go hand-in-hand; enabling one but not the other will cause results that will appear even more incorrect.

The nice thing is that these operations are implemented in silico, so they are virtually free on hardware that supports it. It is supported in OpenGL 2 and Direct3D 9, although some vendors did not implement some things (like texture filtering) entirely in linear space; it is generally safe to assume that OpenGL 3 / Direct3D 10 hardware does it correctly.

The one thing that is not affected by color space conversion is flat colors. This means that you may have to adjust the colors of your lights, fog, and background color after enabling sRGB frame buffers. All colors in Panda3D are assumed to be in linear space.

sRGB Textures

As mentioned above, input textures need to be converted to linear space when they are sampled in order to have proper gamma corrections. This simply means that all textures storing color data need to be flagged as being in the sRGB color space; the graphics hardware does the rest.

Keep in mind that this only applies to textures storing color data. Normal maps and specular maps in particular do not store color data, and they are typically assumed to be linear. This also applies to any alpha channel, which is always linear, even if the red, green and blue channels are sRGB.

There are several ways to enable sRGB support on textures, depending on how those textures are loaded or created.

Firstly, when loading a texture in code, it is possible to specify the color space in the load call. An extra colorSpace keyword argument has been added to the loader.loadTexture call:

diffuseMap = loader.loadTexture("texture-diffuse.png", colorSpace=CS_srgb)
normalMap = loader.loadTexture("texture-normal.png", colorSpace=CS_linear)

This information can also be specified in an .egg file, using a syntax like the following:

<Texture> tex {
  image.png
  <Scalar> color-space { linear }
}

Finally, there is the option to set the default color space, which is used whenever color space information is omitted. Simply setting the default color space to sRGB should cause all textures without otherwise specified color space information to be loaded as sRGB. This can be done as follows:

default-texture-color-space sRGB

The most recommended approach is to set the default color space to sRGB and only explicitly overriding this to linear when loading normal map or specular map textures. This allows for easily turning off sRGB support by changing a single configuration variable, and also accounts for older model data or exporters that do not correctly respect color space settings.

When creating a texture procedurally, it is important to note that different formats are used for sRGB textures. In the setup_texture call, you should use one of the F_srgb, F_srgb_alpha, F_sluminance or F_sluminance_alpha formats. These are 8-bit per channel formats of which the color components must be specified in the sRGB color space, whereas the alpha is always assumed to be linear. There is no floating-point equivalent; when floating-point textures are desired, you should convert the data to a linear color space first.

The following setting enables driver support for sRGB textures:

textures-srgb true

This setting is strongly recommended, and is in fact the default setting. Panda3D can automatically convert textures from sRGB to linear space when the driver doesn't support sRGB textures or when this setting is disabled, but this comes at a cost: since linear textures do not optimally represent the range of colors perceived by the human eye, this is essentially a quality downgrade, and banding artifacts will be present when the texture is converted back to sRGB. That's why you should usually leave this setting on unless you suspect a driver bug.

Mip map generation is automatically done in linear space by Panda3D on linear textures. Since some AMD graphics cards I've tried have bugs causing them to perform the mip map generation in linear space (which causes them to appear too dark in the distance), Panda3D does not rely on driver generation of mipmaps by default.

Note: while some image formats (such as PNG) store color profile information, they are more often than not used incorrectly, and therefore Panda3D makes no attempt to interpret this metadata.

sRGB Framebuffers

The following Config.prc setting enables sRGB conversion on the main framebuffer:

framebuffer-srgb true

It's that simple - from that point on, all colors will be converted from linear space to sRGB before showing up in the window.

Enabling sRGB on a buffer or window you create at run-time can be done using the following FrameBufferProperties setting:

fbp = FrameBufferProperties()
fbp.set_srgb_color(True)

However, things get a bit more complicated when you have intermediate buffers. It is important to always note which color space your image is in. There are several cases to take into account here:

  • A shadow, normal or other non-color buffer is always linear. No sRGB is enabled.
  • An intermediate buffer storing 8-bit color data should be sRGB. Failure to do so will result in artifacts when converting the data back to sRGB for viewing.
  • An intermediate buffer storing floating-point color data should be linear. There is no point to the sRGB conversion.

When using post-processing passes, it may be tempting to enable sRGB correction only on the intermediate buffer and leaving it off when rendering the results into the window. This is technically possible, but the issue with that is that the post-processing filter would not be working in linear space, which may cause them to show the wrong results. Because sRGB conversion is practically free, we don't recommend this approach, but instead recommend to always enable sRGB support on the default window framebuffer using the above Config.prc variable.