47#include <Video/VideoEnums.hpp>
64static inline ::Video::Tonemap resolveAutoTonemap(
int color_trc)
68 constexpr int TRC_SMPTE2084 = 16;
69 constexpr int TRC_ARIB_STD_B67 = 18;
71 if(color_trc == TRC_SMPTE2084)
72 return ::Video::Tonemap::BT_2390;
73 else if(color_trc == TRC_ARIB_STD_B67)
74 return ::Video::Tonemap::Clamp;
76 return ::Video::Tonemap::Clamp;
88static inline bool isLuminanceBasedTonemap(::Video::Tonemap mode)
92 case ::Video::Tonemap::BT_2390:
93 case ::Video::Tonemap::BT_2446:
94 case ::Video::Tonemap::Reinhard:
119static constexpr auto TONEMAP_BT2390 = R
"_(
120// BT.2390 EETF — operates on max-RGB channel, preserves hue ratios.
121// Input: linear light, 1.0 = content peak.
122// Output: linear light, [0, 1].
124// PQ forward: linear [0,1] (where 1.0 = 10000 nits) -> PQ [0,1]
125float pqForward(float Y) {
126 float Ym1 = pow(max(Y, 0.0), 0.1593017578125);
127 return pow((0.8359375 + 18.8515625 * Ym1) / (1.0 + 18.6875 * Ym1), 78.84375);
130// PQ inverse: PQ [0,1] -> linear [0,1] (where 1.0 = 10000 nits)
131float pqInverse(float N) {
132 float Nm = pow(max(N, 0.0), 1.0 / 78.84375);
133 return pow(max(Nm - 0.8359375, 0.0) / (18.8515625 - 18.6875 * Nm), 1.0 / 0.1593017578125);
136// BT.2390 hermite spline
137// Maps [KS, srcPeakPQ] -> [KS, dstPeakPQ] with slope=1 at KS, slope=0 at end.
138float bt2390_eetf(float E, float KS, float srcPeakPQ, float dstPeakPQ) {
139 if (E < KS) return E;
140 if (srcPeakPQ <= KS) return dstPeakPQ;
142 // Normalize to [0,1] in the [KS, srcPeakPQ] range
143 float t = (E - KS) / (srcPeakPQ - KS);
147 // Hermite basis functions:
148 // h00(t) = 2t³ - 3t² + 1 (value at t=0: 1, at t=1: 0)
149 // h10(t) = t³ - 2t² + t (tangent at t=0: 1, at t=1: 0)
150 // h01(t) = -2t³ + 3t² (value at t=0: 0, at t=1: 1)
152 // P(t) = h00(t)*KS + h10(t)*(srcPeakPQ-KS) + h01(t)*dstPeakPQ
154 // At t=0: P = KS (continuity with linear segment)
155 // At t=1: P = dstPeakPQ (reaches target peak)
156 // dP/dt at t=0 = (srcPeakPQ-KS) -> dP/dE = 1 (slope continuity)
157 // dP/dt at t=1 = 0 (smooth roll-off)
158 float p = (2.0 * t3 - 3.0 * t2 + 1.0) * KS
159 + (t3 - 2.0 * t2 + t) * (srcPeakPQ - KS)
160 + (-2.0 * t3 + 3.0 * t2) * dstPeakPQ;
165vec3 tonemap(vec3 color) {
166 const float srcPeakNits = contentPeakNits;
167 const float dstPeakNits = sdrPeakNits;
169 // Convert nits to PQ domain
170 float srcPeakPQ = pqForward(srcPeakNits / 10000.0);
171 float dstPeakPQ = pqForward(dstPeakNits / 10000.0);
173 // Knee start: BT.2390 formula (in absolute PQ domain)
174 // Spec defines KS = 1.5*maxLum - 0.5 in normalized space (srcPeak=1.0).
175 // In absolute PQ: KS = 1.5*dstPeakPQ - 0.5*srcPeakPQ
176 float KS = 1.5 * dstPeakPQ - 0.5 * srcPeakPQ;
178 // Operate on max-RGB to preserve hue ratios
179 float maxC = max(color.r, max(color.g, color.b));
180 if (maxC <= 0.0) return color;
182 // Convert maxC from normalized (1.0=peak) to absolute PQ
183 float absLinear = maxC * srcPeakNits / 10000.0;
184 float pqVal = pqForward(absLinear);
187 float mappedPQ = bt2390_eetf(pqVal, KS, srcPeakPQ, dstPeakPQ);
189 // Convert back to linear, normalize to [0,1] relative to target peak
190 float mappedLinear = pqInverse(mappedPQ);
191 float dstLinear = pqInverse(dstPeakPQ);
192 float ratio = mappedLinear / max(dstLinear, 1e-10);
194 // Scale all channels by the same ratio
195 float scale = ratio / maxC;
196 return clamp(color * scale, 0.0, 1.0);
211static constexpr auto TONEMAP_BT2446A = R
"_(
212// BT.2446-inspired log tone curve with chroma scaling
213// Input: linear light, 1.0 = content peak.
214// Output: linear light [0,1].
216vec3 tonemap(vec3 color) {
217 const float srcPeak = contentPeakNits;
218 const float dstPeak = sdrPeakNits;
220 // BT.2020 luminance coefficients (input is BT.2020 primaries)
221 const vec3 lumaCoeff = vec3(0.2627, 0.6780, 0.0593);
223 // Convert to absolute nits
224 vec3 absColor = color * srcPeak;
227 float Y = dot(absColor, lumaCoeff);
228 if (Y <= 0.0) return vec3(0.0);
230 // Normalized luminance (0-1 range relative to srcPeak)
231 float Yn = Y / srcPeak;
233 // Parametric log compression: rho adapts to source peak
234 float rho = 1.0 + 32.0 * pow(srcPeak / 10000.0, 1.0 / 2.4);
235 float Yt = log(1.0 + (rho - 1.0) * Yn) / log(rho);
237 // Scale to destination peak
238 float Yout = Yt * dstPeak;
240 // Chroma scaling: scale proportionally to luminance change
241 float chromaScale = Yout / Y;
243 vec3 result = absColor * chromaScale;
246 return clamp(result, 0.0, 1.0);
259static constexpr auto TONEMAP_REINHARD = R
"_(
260// Reinhard tone mapping (extended with white point)
261// Input: linear light in BT.2020 primaries, 1.0 = content peak.
262// Output: linear light [0,1].
264vec3 tonemap(vec3 color) {
265 vec3 c = color * (contentPeakNits / sdrPeakNits);
267 // BT.2020 luminance coefficients
268 const vec3 lumaCoeff = vec3(0.2627, 0.6780, 0.0593);
269 float Lin = dot(c, lumaCoeff);
270 if (Lin <= 0.0) return vec3(0.0);
272 float whitePoint = contentPeakNits / sdrPeakNits;
273 float wp2 = whitePoint * whitePoint;
276 float Lout = (Lin * (1.0 + Lin / wp2)) / (1.0 + Lin);
278 return clamp(c * (Lout / Lin), 0.0, 1.0);
291static constexpr auto TONEMAP_HABLE = R
"_(
292// Hable / Uncharted 2 filmic tone mapping
293// Input: linear light, 1.0 = content peak.
294// Output: linear light [0,1].
296vec3 hableCurve(vec3 x) {
297 const float A = 0.15; // Shoulder strength
298 const float B = 0.50; // Linear strength
299 const float C = 0.10; // Linear angle
300 const float D = 0.20; // Toe strength
301 const float E = 0.02; // Toe numerator
302 const float F = 0.30; // Toe denominator
303 return ((x * (A * x + C * B) + D * E) / (x * (A * x + B) + D * F)) - E / F;
306vec3 tonemap(vec3 color) {
307 vec3 c = color * (contentPeakNits / sdrPeakNits);
309 float exposureBias = 2.0;
310 vec3 mapped = hableCurve(c * exposureBias);
312 float W = contentPeakNits / sdrPeakNits;
313 vec3 whiteScale = 1.0 / hableCurve(vec3(W));
315 return clamp(mapped * whiteScale, 0.0, 1.0);
327static constexpr auto TONEMAP_ACES2 = R
"_(
328// ACES filmic tone mapping (Hill fit)
329// Input: linear light in BT.709/sRGB primaries, 1.0 = content peak.
330// Output: linear light [0,1] in BT.709/sRGB primaries.
332vec3 tonemap(vec3 color) {
333 vec3 c = color * (contentPeakNits / sdrPeakNits);
335 // sRGB/BT.709 -> AP1
336 const mat3 ACESInputMat = mat3(
337 0.59719, 0.07600, 0.02840,
338 0.35458, 0.90834, 0.13383,
339 0.04823, 0.01566, 0.83777
342 // AP1 -> sRGB/BT.709
343 const mat3 ACESOutputMat = mat3(
344 1.60475, -0.10208, -0.00327,
345 -0.53108, 1.10813, -0.07276,
346 -0.07367, -0.00605, 1.07602
349 vec3 v = ACESInputMat * c;
350 vec3 a = v * (v + 0.0245786) - 0.000090537;
351 vec3 b = v * (0.983729 * v + 0.4329510) + 0.238081;
353 v = ACESOutputMat * v;
355 return clamp(v, 0.0, 1.0);
368static constexpr auto TONEMAP_AGX = R
"_(
369// Minimal AgX tone mapping (algebraic, no LUT)
370// Input: linear light in BT.709/sRGB primaries, 1.0 = content peak.
371// Output: linear light [0,1] in BT.709/sRGB primaries.
373vec3 agxDefaultContrastApprox(vec3 x) {
376 return + 15.5 * x4 * x2
385vec3 agx(vec3 color) {
386 // sRGB/BT.709 linear -> AgX log space
387 const mat3 agxTransform = mat3(
388 0.842479062253094, 0.0423282422610123, 0.0423756549057051,
389 0.0784335999999992, 0.878468636469772, 0.0784336,
390 0.0792237451477643, 0.0791661274605434, 0.879142973793104
393 vec3 val = agxTransform * color;
394 val = max(val, 1e-10);
396 const float minEV = -12.47393;
397 const float maxEV = 4.026069;
398 val = clamp(log2(val), minEV, maxEV);
399 val = (val - minEV) / (maxEV - minEV);
401 val = agxDefaultContrastApprox(val);
405vec3 agxEotf(vec3 color) {
406 // AgX -> sRGB/BT.709 linear
407 const mat3 agxInvTransform = mat3(
408 1.19687900512017, -0.0528968517574562, -0.0529716355144438,
409 -0.0980208811401368, 1.15190312990417, -0.0980434501171241,
410 -0.0990297440797205, -0.0989611768448433, 1.15107367264116
412 return agxInvTransform * color;
415vec3 tonemap(vec3 color) {
416 vec3 c = color * (contentPeakNits / sdrPeakNits);
421 return clamp(val, 0.0, 1.0);
434static constexpr auto TONEMAP_PBR_NEUTRAL = R
"_(
435// Khronos PBR Neutral tone mapping (2024)
436// Input: linear light, 1.0 = content peak.
437// Output: linear light [0,1].
439vec3 pbrNeutralToneMapping(vec3 color) {
440 const float startCompression = 0.8 - 0.04;
441 const float desaturation = 0.15;
443 float x = min(color.r, min(color.g, color.b));
444 float offset = (x < 0.08) ? x - 6.25 * x * x : 0.04;
447 float peak = max(color.r, max(color.g, color.b));
448 if (peak < startCompression) return color;
450 float d = 1.0 - startCompression;
451 float newPeak = 1.0 - d * d / (peak + d - startCompression);
452 color *= newPeak / peak;
454 float g = 1.0 - 1.0 / (desaturation * (peak - newPeak) + 1.0);
455 return mix(color, vec3(newPeak), g);
458vec3 tonemap(vec3 color) {
459 vec3 c = color * (contentPeakNits / sdrPeakNits);
460 return clamp(pbrNeutralToneMapping(c), 0.0, 1.0);
468static constexpr auto TONEMAP_CLAMP = R
"_(
469vec3 tonemap(vec3 color) {
470 return clamp(color, 0.0, 1.0);
478static inline QString tonemapConstants(
float contentPeakNits = 1000.0f,
float sdrPeakNits = 203.0f)
481const float contentPeakNits = %1;
482const float sdrPeakNits = %2;
484 .arg(contentPeakNits, 0, 'f', 1)
485 .arg(sdrPeakNits, 0,
'f', 1);
492static inline QString tonemapShader(
493 ::Video::Tonemap mode,
494 float contentPeakNits = 1000.0f,
495 float sdrPeakNits = 203.0f)
498 shader.reserve(4096);
500 shader += tonemapConstants(contentPeakNits, sdrPeakNits);
504 case ::Video::Tonemap::BT_2390: shader += TONEMAP_BT2390;
break;
505 case ::Video::Tonemap::BT_2446: shader += TONEMAP_BT2446A;
break;
506 case ::Video::Tonemap::Reinhard: shader += TONEMAP_REINHARD;
break;
507 case ::Video::Tonemap::Hable: shader += TONEMAP_HABLE;
break;
508 case ::Video::Tonemap::ACES2: shader += TONEMAP_ACES2;
break;
509 case ::Video::Tonemap::AgX: shader += TONEMAP_AGX;
break;
510 case ::Video::Tonemap::PBR_Neutral: shader += TONEMAP_PBR_NEUTRAL;
break;
511 case ::Video::Tonemap::Auto:
514 case ::Video::Tonemap::Clamp:
515 default: shader += TONEMAP_CLAMP;
break;
Graphics rendering pipeline for ossia score.
Definition Filter/PreviewWidget.hpp:12