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