Color Spaces, Gamma & HDR

Monitors expect sRGB-encoded values (≈ gamma 2.2); lighting math must be done in linear space.

20.1 Linear vs sRGB

Monitors expect sRGB-encoded values (≈ gamma 2.2); lighting math must be done in linear space.

Color textures (albedo) are usually sRGB → load with GL_SRGB8_ALPHA8 so sampling returns linear values automatically.

Data textures (normal, roughness, metallic, height) are linear → load with GL_RGBA8.

Output: render to an sRGB framebuffer (glEnable(GL_FRAMEBUFFER_SRGB)) or convert manually:

vec3 linearToSRGB(vec3 c) {
    return mix(1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055, c * 12.92, lessThanEqual(c, vec3(0.0031308)));
}
vec3 srgbToLinear(vec3 c) {
    return mix(pow((c + 0.055) / 1.055, vec3(2.4)), c / 12.92, lessThanEqual(c, vec3(0.04045)));
}
// approximation: pow(c, vec3(1.0 / 2.2)) and pow(c, vec3(2.2))

20.2 HDR rendering & tone mapping

Render lighting into a floating-point framebuffer (RGBA16F), then map HDR values to [0, 1]:

Other operators: Uncharted 2 (Hable filmic), AgX, Khronos PBR Neutral, Lottes. Auto-exposure: compute average log-luminance via mipmaps or a compute reduction, adapt over time.

vec3 reinhard(vec3 c) { return c / (1.0 + c); }

vec3 acesFilm(vec3 x) {                       // Narkowicz ACES approximation
    const float a = 2.51, b = 0.03, c = 2.43, d = 0.59, e = 0.14;
    return clamp((x * (a * x + b)) / (x * (c * x + d) + e), 0.0, 1.0);
}

vec3 hdr = texture(uHDRScene, vUV).rgb;
vec3 mapped = acesFilm(hdr * uExposure);      // exposure control (manual or auto from average luminance)
outColor = vec4(linearToSRGB(mapped), 1.0);

20.3 Other color operations

float luma = dot(c, vec3(0.2126, 0.7152, 0.0722));          // relative luminance (Rec. 709)
vec3 saturated = mix(vec3(luma), c, uSaturation);
vec3 contrasted = (c - 0.5) * uContrast + 0.5;
vec3 hsv2rgb(vec3 c) {
    vec3 p = abs(fract(c.xxx + vec3(0.0, 2.0 / 3.0, 1.0 / 3.0)) * 6.0 - 3.0);
    return c.z * mix(vec3(1.0), clamp(p - 1.0, 0.0, 1.0), c.y);
}
// Dithering to remove banding: add ±0.5/255 of blue noise before quantization