Color Spaces, Gamma & HDR
Lighting math must happen in linear space.
21.1 Linear vs sRGB
Lighting math must happen in linear space. Color textures are stored in sRGB: use DXGI_FORMAT_*_SRGB so the hardware converts on sample and on render-target write. Manual conversion:
Data textures (normals, roughness, masks) must use non-sRGB formats.
float3 SRGBToLinear(float3 c)
{
return select(c <= 0.04045, c / 12.92, pow((c + 0.055) / 1.055, 2.4)); // HLSL 2021 select
}
float3 LinearToSRGB(float3 c)
{
return select(c <= 0.0031308, c * 12.92, 1.055 * pow(c, 1.0 / 2.4) - 0.055);
}21.2 HDR rendering & tone mapping
Render into R16G16B16A16_FLOAT (or R11G11B10_FLOAT), then tone map to the display:
Other operators: Uncharted 2 (Hable), AgX, Khronos PBR Neutral, Lottes. For HDR displays, output scRGB (FP16, linear, 1.0 = 80 nits) or HDR10 (R10G10B10A2 with PQ/ST.2084 encoding and Rec.2020 primitives) with IDXGISwapChain3::SetColorSpace1.
float3 Reinhard(float3 x) { return x / (1.0 + x); }
float3 ACESFilm(float3 x) // Krzysztof Narkowicz's ACES approximation
{
const float a = 2.51, b = 0.03, c = 2.43, d = 0.59, e = 0.14;
return saturate((x * (a * x + b)) / (x * (c * x + d) + e));
}
Texture2D<float4> gHDR : register(t0);
cbuffer Tone : register(b0) { float gExposure; };
float4 ToneMapPS(float4 pos : SV_Position) : SV_Target
{
float3 hdr = gHDR.Load(int3(pos.xy, 0)).rgb * gExposure;
float3 ldr = ACESFilm(hdr);
return float4(ldr, 1); // write to an _SRGB back buffer view → hardware gamma encode
}21.3 Other color operations
float Luminance(float3 c) { return dot(c, float3(0.2126, 0.7152, 0.0722)); }
float3 Saturation(float3 c, float s) { return lerp(Luminance(c).xxx, c, s); }
float3 Contrast(float3 c, float k) { return (c - 0.5) * k + 0.5; }
float3 HueShift(float3 c, float a) // Rodrigues rotation around the gray axis
{
const float3 k = 0.57735;
float s, co; sincos(a, s, co);
return c * co + cross(k, c) * s + k * dot(k, c) * (1 - co);
}