OCCT 3D Viewer becomes sRGB-aware

Forums: 

Before talking about sRGB, lets figure out what was wrong with old OCCT 3D Viewer. Take a look onto a pair of screenshots:

Sphere lighting before (left) and after (right) switching to sRGB-aware renderer.

The left image demonstrates a soft transition of a light into a shadow, while the right one shows more rapid transition. Left image might look softer, but which one is closer to real life?

Moon photo from wikipedia.

Photo of the moon might be not a perfect reference, but smooth shadow transition demonstrated by old OCCT 3D Viewer was wrong and has to be fixed. This might look not that critical for not physically-based Phong shading model, as it is non-realistic anyway, but becomes more important for further implementation of PBR shading models in OCCT 3D Viewer conformant to other 3D renderers.

Why the issue occurred in first place and how it was solved is described in further chapters.

RGB color model

RGB (red+green+blue) is the most commonly used color model, and computer graphics is not an exception. Mixture of red, green and blue (also called primaries for RGB) with different proportions allows defining any other color distinguishable by the human eye, which is known to everybody from early school days.

Artists, however, usually aware of much more terms with and without RGB in name - sRGB, AdobeRGB, scRGB, DCI-P3, Rec. 2020, wide-gamut RGB. And developers of graphics applications, including 3D Viewer, should be aware of them too.

Color spaces comparison chart from Wikipedia.

sRGB and Standard RGB color space

While picking a pure green color in a paint program, ask yourself - is it really the very extent of green color you ever seen, or there are more “greener” shades in the world? The correct answer would be no, the green color you see on the screen is not the very extent, although it might be close to what the human eye may distinguish in case of a good monitor.

The same is true for all 3 primary components, which absolute values define the range of color values, or coverage, usually displayed as a triangle. AdobeRGB, sRGB and others are color spaces defining different extremes for RGB primaries. CRT, LCD, OLED displays also rely on RGB triad to represent a color, but each physical display actually has its own unique RGB color space. Predefined profiles in a monitor allow representing standardized color spaces like AdobeRGB or sRGB on a calibrated device. This representation might be incomplete, so that you may see labels like “87% sRGB coverage” meaning that display may physically represent only 87% of colors from sRGB color space, while others will be usually clamped.

Color reproduction is a great concern for artists, as color distortion might ruin aspects and hide important details when displayed on various displays. Color Profiles (or ICC profiles) have been designed to carefully reproduce the color, by embedding profile into source (image), output (display or printer) and providing conversion rules between them. The color conversion is usually defined through another standardized color space XYZ, and is quite a complicated process.

As there are more than one RGB color space, you may ask, which colors do you enter in applications like Paint? Professional image and video processing software is aware of color spaces, so a user may specify color space explicitly, but simple software has no such options.

sRGB defines a standard RGB color space, to be used "by default" - by monitor, printer and even internet content, so that it is reproduced properly (although printers use another color model called CMYK). Displays with extended output color range (covering AdobeRGB or similar) are mostly used by experienced users working with professional software, which is aware of color profiles. But even these devices provide an sRGB color profile.

sRGB linearization

Due to the nonlinear nature of color perception by human’s eye, storing linear RGB color values within 8-bit (255 gradations) per channel image formats (24-bit per pixel or smaller) leads to visually lower precision. To improve visual quality, sRGB was defined with gamma correction coefficient, making its values distributed nonlinearly. At the same time, image formats with more than 8 bits per component do not suffer from this precision issue, and gamma shift becomes redundant.

sRGB gamma shift is defined as an exponent function with approximately 2.2 coefficient. The exact formula listed below is slightly more complicated to handle the shades of dark values linearly:

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);
}

Formula above expects well-defined values, while some OpenGL extensions also map NaN input values to zero and clamp output to [0, 1] range. Software just displaying sRGB content on sRGB display may pass-through RGB values as is. Within 3D renderer, however, input colors are altered by lighting and blending equations. These equations make sense only when applied to linear (RGB) values.

RGB and color blending in 3D rendering

Result of lighting, blending and some other color math on nonlinear sRGB values is clearly wrong, but still may be acceptable to some extent (until you look deeper in detail to realize why it is wrong). Therefore, for proper rendering of the scene where colors are defined using sRGB values, the gamma shift should be first eliminated, to convert color to linearized RGB value before any math.

The tricky point to realize is that linearization of sRGB values still gives color within sRGB color space (not AdobeRGB, scRGB or any other RGB color space), just with linear properties. Although practically speaking it is none of concern to a GLSL program as long as RGB values are linear (in any color space), and appropriate (reverse) conversion is done when displaying the result on sRGB monitor.

The following video demonstrates in an entertaining manner which kind of visual defects might appear when sRGB linearization is ignored in the rendering pipeline.

In earlier days of GLSL, most code sensible to color manipulations performed sRGB linearization explicitly like this:

void main() {
  vec3 aColorL = texture2D(uLeftSampler,  vec2(0.0, 0.0)).rgb;
  vec3 aColorR = texture2D(uRightSampler, vec2(0.0, 0.0)).rgb;
  // linearize values before math
  aColorL = pow(aColorL, vec3(2.2));
  aColorR = pow(aColorR, vec3(2.2));
  // perform color math
  vec3 aColor = uMultR * aColorR + uMultL * aColorL;
  // de-linearize before writing into FBO
  gl_FragColor = vec4(pow(aColor, 1.0/vec3(2.2))), 1.0);
}

As you may see, code just pow(2.2) input color, performs math, and then pow(1/2.2) before writing the result. 2.2 is a good approximated exponent for sRGB conversion, used quite intensively in graphics. Sometimes you can find (x*x) with sqrt(x) combination in code, giving still good enough linearization but avoids expensive pow() calls.

Beware, that when working with RGBA values, only RGB should be passed through sRGB linearization, while alpha remains linear!

Color pickers

Most applications provide color input in non-linear sRGB color space. This can be easily assumed when you see input controls with [0..255] integer range. HEX encoding is another popular RGB(A) representation of nonlinear sRGB values, known as web colors. Here you can see screenshots from several popular editors:

Color pickers in Inkscape, Gimp, MS Paint

User interface of other editors goes further and provides clear means of color space of picked color. As you may see Krita and Blender display linear RGB values within floating number input. Developer should NOT be mistaken in an attempt to convert integer sRGB values by simply using 255 as a denominator, as sRGB linearization formula should be also applied.

Color pickers in Krita and Blender

Here is sample output for one color:

  • HEX
    #ae2fb8
  • sRGB non-linear 0..255
    Red 173, Green 48, Blue 184
  • sRGB non-linear float
    Red 0.678, Green 0.188, Blue 0.721
  • RGB linear float
    Red 0.423, Green 0.028, Blue 0.479

Linear Gradients

Let's take a look at how color gradient looks when applied to linear and nonlinear sRGB values:

Linear interpolation of nonlinear color values gives an unexpected muddy result within red-to-blue gradient, while interpolation of linear RGB values gives a clean color. Here you will find more images comparing linear gradients of different color spaces. At the same time, grayscale gradient visually looks more uniform in case of interpolation of sRGB values - which might be confusing, but rather expected as human eye perception of intensity is non-linear.

SVG format allows defining interpolation scheme through color-interpolation property, which gives an idea, that interpolation of nonlinear color values still may be used in some contexts. One obvious use case is defining a gradient of grayscale values.

Color operations

What kind of math operations can be done on RGB values?

  • Linear RGB.
    Two colors in linear RGB can be mutually added and multiplied.
  • Gamma-shifted.
    Two colors with gamma-shifted RGB values can be mutually multiplied, but not added.
  • Nonlinear sRGB.
    Neither multiplication, nor addition can be done.

Note that more operations are applicable to trivially gamma-shifted RGB values. This is because true sRGB color space has a breaking point for small values (see formula above).

Hardware OpenGL support

OpenGL support of sRGB color space consists of two parts: sampling of sRGB textures with automatic linearization and writing into sRGB frame buffer with automatic de-linearization.

There features require OpenGL 3.1+ and OpenGL ES 3.0+ hardware. Desktop software reached OpenGL 3.3 several years ago (Intel was the slowest vendor providing most recent OpenGL versions to integrated graphics). Statistics also shows that most Android devices already support OpenGL ES 3.0 or greater, so that mobile hardware should not be a problem here. It should be noted, though, that many old devices supporting only OpenGL ES 2.0 are still in use.

While making OCCT renderer sRGB-aware, it was decided to target OpenGL 3.2+ / OpenGL ES 3.0+ devices supporting sRGB offscreen framebuffer format. Limited compatibility with obsolete OpenGL versions is done by de-linearization of colors before passing them to OpenGL. Expectedly, this trick produces visually different image, but it is considered acceptable as a fallback solution for outdated devices.

Porting OCCT – challenges

Open CASCADE Technology (OCCT) used classical Blinn-Phong shading model defined by the very first versions of OpenGL. At these days, graphics hardware had no programmable cores, and even haven’t dedicated hardware for computing T&L (transformation and lighting) – it was done mostly on CPU, which was rather slow to think about such nonsense like real-time physically correct lighting.

OCCT reflected this pipeline and passed through colors to OpenGL as is, even when it is migrated to programmable pipeline implementing Phong shading using GLSL programs.

Framework defines two classes Quantity_Color and Quantity_ColorRGBA defining floating point RGB(A) colors. The definition includes an enumeration of named colors (filled with X11 colors), commonly used across the code. As you may expect, the named colors have been actually defined in nonlinear sRGB color space.

So that one of the tricky steps in migrating into proper sRGB handling by 3D renderer is defining how Quantity_Color should be treated – in linear or nonlinear sRGB color space? Stating Quantity_Color being linear RGB color means that existing software should be ported taking into account this breaking change. Although in many cases there will be no visible difference (colors with maximum/minimum values) or application users will not notice any change (apart from slightly changed look).

Finally, it was decided declaring Quantity_Color values being in linear RGB color space, so that no conversion is necessary to pass values to GLSL programs. Color class now supports Quantity_TOC_sRGB for passing / receiving nonlinear sRGB values with appropriate conversion.

The change affected not only Visualization, but also Data Exchange component. Unlike video/image files, most file formats for 3D assets and CAD data lack information about RGB color space, leaving the room for ambiguity.

glTF 2.0 is one of the few trying to clarify this aspect in format specifications. Old file formats (including STEP, IGES and OBJ) are now considered to store sRGB color values, so that OCCT performs appropriate color conversion on import/export steps.

Image below shows how standard OCCT materials looked before (OCCT 7.3.0) and after (OCCT 7.4.1dev) migration to sRGB-aware rendering pipeline.

Common material properties like diffuse, ambient, specular, and emissive colors should be defined in linear RGB color space for lighting math, yet direct conversion of standard OCCT materials for proper rendering into sRGB framebuffer required manual adjustments. Applications defining custom materials might also require adjustments after porting to the new version of OCCT.

Blending in linear RGB color space also changed behavior of semitransparent materials, as well as text rendering on various backgrounds.

Per-vertex attributes are interpreted in linear RGB color space (with no conversion in GLSL program). On screenshots above you can see the visual difference - the new version looks brighter due to gamma shift applied on result.

Porting OCCT-based application shortlist

Upgrade guide aggregates all important changes in OCCT, which might affect OCCT-based applications. However, lets make a short list related to porting to sRGB-aware renderer:

  • If application uses standard OCCT materials (Graphic3d_NameOfMaterial enumeration) - no extra action is required.
  • If application defines colors from predefined list (Quantity_NameOfColor enumeration) - no extra action is required.
  • If application defines Quantity_Color from floating point values - consider specifying Quantity_TOC_sRGB instead of Quantity_TOC_RGB to preserve old colors. Use also Quantity_TOC_sRGB to pass color values to GUI
  • If shading doesn't look good in application - consider adjusting material or lighting properties.

While porting onto new version of OCCT, application developers should understand, that there is no way to achieve previous shading results in new renderer. sRGB gamma correction is a power function, which means that no matter how light sources or materials are changed, the result will no more look the same - it should be just accepted as is. Adjustments should be better focused on achieving good-looking result rather than making them close to old renderer.