1#version 450
2// Capture-only SDR conversion of the HDR composition image (linear BT.709
3// in units of SDR reference white, values beyond [0, 1] on both sides).
4// Presentation never uses this pass. The mapping is deterministic and
5// mirrored by captureCurve in capture.go for tests:
6//
7// 1. Gamut: a component below zero (a BT.2020 color outside BT.709) is
8// brought to zero by mixing the pixel toward its own luminance: hue
9// direction and luminance are kept, chroma is reduced just enough.
10// 2. Shoulder, on the largest component so hue is kept: identity up to
11// the knee k = 0.9 (SDR content below 90 % white is exact), then the
12// rational curve k + (1 - k) * t / (1 + t) with t = (m - k) / s,
13// s = 0.5, which reaches 1 only asymptotically: diffuse white lands
14// at 0.917 (sRGB 245), 2x white at 251, 5x (1000 nits) at 254; the
15// order of highlights is kept instead of clipping.
16// 3. sRGB encoding, opaque.
17layout(set=0, binding=0) uniform sampler2D imageHDR;
18layout(location=0) out vec4 color;
19
20const float knee = 0.9;
21const float scale = 0.5;
22const vec3 luma709 = vec3(0.2126, 0.7152, 0.0722);
23
24float shoulder(float m) {
25 float t = (m - knee) / scale;
26 return m <= knee ? m : knee + (1.0 - knee) * t / (1.0 + t);
27}
28
29vec3 encodeSRGB(vec3 v) {
30 return mix(v * 12.92, 1.055 * pow(v, vec3(1.0 / 2.4)) - 0.055, greaterThan(v, vec3(0.0031308)));
31}
32
33void main() {
34 vec2 uv = gl_FragCoord.xy / vec2(textureSize(imageHDR, 0));
35 vec3 c = texture(imageHDR, uv).rgb;
36 // NaN is black before any clamp (clamping NaN is undefined);
37 // infinities are bounded to the half range.
38 c = mix(c, vec3(0.0), isnan(c));
39 c = clamp(c, vec3(-65504.0), vec3(65504.0));
40 float y = max(dot(c, luma709), 0.0);
41 float lo = min(min(c.r, c.g), c.b);
42 if (lo < 0.0) {
43 // c + t * (y - c) == 0 for the lowest component: t = -lo / (y - lo).
44 float t = -lo / max(y - lo, 1e-6);
45 c = mix(c, vec3(y), clamp(t, 0.0, 1.0));
46 }
47 c = max(c, vec3(0.0));
48 float m = max(max(c.r, c.g), c.b);
49 if (m > knee) {
50 c *= shoulder(m) / m;
51 }
52 color = vec4(encodeSRGB(clamp(c, vec3(0.0), vec3(1.0))), 1.0);
53}