A game about forced loneliness, made by TACStudios
1// Important! This file assumes Color.hlsl and ACES.hlsl has been already included. 2#ifndef HDROUTPUT_INCLUDED 3#define HDROUTPUT_INCLUDED 4 5#include "Packages/com.unity.render-pipelines.core/Runtime/PostProcessing/HDROutputDefines.cs.hlsl" 6 7#if defined(HDR_COLORSPACE_CONVERSION_AND_ENCODING) 8#define HDR_COLORSPACE_CONVERSION 9#define HDR_ENCODING 10#endif 11 12int _HDRColorspace; 13int _HDREncoding; 14 15// A bit of nomenclature that will be used in the file: 16// Gamut: It is the subset of colors that is possible to reproduce by using three specific primary colors. 17// Rec709 (ITU-R Recommendation BT709) is a HDTV standard, in our context, we mostly care about its color gamut (https://en.wikipedia.org/wiki/Rec._709). The Rec709 gamut is the same as BT1886 and sRGB. 18// Rec2020 (ITU-R Recommendation BT2020) is an UHDTV standard. As above, we mostly reference it w.r.t. the color gamut. (https://en.wikipedia.org/wiki/Rec._2020). Nice property is that all primaries are on the locus. 19// DCI-P3 (or just P3): is a gamut used in cinema grading and used by iPhone for example. 20// ACEScg: A gamut that is larger than Rec2020. 21// ACES2065-1: A gamut that covers the full XYZ space, part of the ACES specs. Mostly used for storage since it is harder to work with than ACEScg. 22// WCG: Wide color gamut. This is defined as a color gamut that is wider than the Rec709 one. 23// LMS: A color space represented by the response of the three cones of human eye (responsivity peaks Long, Medium, Short) 24// OETF (Optical Eelectro Transfer Function): This is a function to goes from optical (linear light) to electro (signal transmitted to the display). 25// EOTF (Eelectro Optical Transfer Function): The inverse of the OETF, used by the TV/Monitor. 26// EETF (Eelectro-Electro Transfer Function): This is generally just a remapping function, we use the BT2390 EETF to perform range reduction based on the actual display. 27// PQ (Perceptual Quantizer): the EOTF used for HDR10 TVs. It works in the range [0, 10000] nits. Important to keep in mind that this represents an absolute intensity and not relative as for SDR. Sometimes this can be referenced as ST2084. As OETF we'll use the inverse of the PQ curve. 28// scRGB: a wide color gamut that uses same color space and white point as sRGB, but with much wider coordinates. Used on windows when 16 bit depth is selected. Most of the color space is imaginary colors. Works differently than with PQ (encoding is linear). 29// G22 (Gamma 2.2): the EOTF used for exact gamma 2.2 curve. 30 31// -------------------------------- 32// Perceptual Quantizer (PQ) / ST 2084 33// -------------------------------- 34 35#define MAX_PQ_VALUE 10000 // 10k nits is the maximum supported by the standard. 36 37#define PQ_N (2610.0f / 4096.0f / 4.0f) 38#define PQ_M (2523.0f / 4096.0f * 128.0f) 39#define PQ_C1 (3424.0f / 4096.0f) 40#define PQ_C2 (2413.0f / 4096.0f * 32.0f) 41#define PQ_C3 (2392.0f / 4096.0f * 32.0f) 42 43float LinearToPQ(float value, float maxPQValue) 44{ 45 value /= maxPQValue; 46 float Ym1 = PositivePow(value, PQ_N); 47 float n = (PQ_C1 + PQ_C2 * Ym1); 48 float d = (1.0f + PQ_C3 * Ym1); 49 return PositivePow(n / d, PQ_M); 50} 51 52float LinearToPQ(float value) 53{ 54 return LinearToPQ(value, MAX_PQ_VALUE); 55} 56 57float3 LinearToPQ(float3 value, float maxPQValue) 58{ 59 float3 outPQ; 60 outPQ.x = LinearToPQ(value.x, maxPQValue); 61 outPQ.y = LinearToPQ(value.y, maxPQValue); 62 outPQ.z = LinearToPQ(value.z, maxPQValue); 63 return outPQ; 64} 65 66float3 LinearToPQ(float3 value) 67{ 68 return LinearToPQ(value, MAX_PQ_VALUE); 69} 70 71float PQToLinear(float value) 72{ 73 float Em2 = PositivePow(value, 1.0f / PQ_M); 74 float X = (max(0.0, Em2 - PQ_C1)) / (PQ_C2 - PQ_C3 * Em2); 75 return PositivePow(X, 1.0f / PQ_N); 76} 77 78float PQToLinear(float value, float maxPQValue) 79{ 80 return PQToLinear(value) * maxPQValue; 81} 82 83float3 PQToLinear(float3 value, float maxPQValue) 84{ 85 float3 outLinear; 86 outLinear.x = PQToLinear(value.x, maxPQValue); 87 outLinear.y = PQToLinear(value.y, maxPQValue); 88 outLinear.z = PQToLinear(value.z, maxPQValue); 89 return outLinear; 90} 91 92float3 PQToLinear(float3 value) 93{ 94 float3 outLinear; 95 outLinear.x = PQToLinear(value.x); 96 outLinear.y = PQToLinear(value.y); 97 outLinear.z = PQToLinear(value.z); 98 return outLinear; 99} 100 101 102// -------------------------------------------------------------------------------------------- 103 104// -------------------------------- 105// Color Space transforms 106// -------------------------------- 107// As any other space transform, changing color space involves a change of basis and therefore a matrix multiplication. 108// Note that Rec2020 and Rec2100 share the same color space. 109// This section should be kept in sync with Runtime\Utilities\ColorSpaceUtils.cs 110 111float3 RotateRec709ToRec2020(float3 Rec709Input) 112{ 113 static const float3x3 Rec709ToRec2020Mat = float3x3( 114 115 0.627402, 0.329292, 0.043306, 116 0.069095, 0.919544, 0.011360, 117 0.016394, 0.088028, 0.895578 118 ); 119 120 return mul(Rec709ToRec2020Mat, Rec709Input); 121} 122 123float3 RotateRec709ToP3D65(float3 Rec709Input) 124{ 125 static const float3x3 Rec709ToP3D65Mat = float3x3( 126 0.822462, 0.177538, 0.000000, 127 0.033194, 0.966806, 0.000000, 128 0.017083, 0.072397, 0.910520 129 ); 130 131 return mul(Rec709ToP3D65Mat, Rec709Input); 132} 133 134float3 RotateRec2020ToRec709(float3 Rec2020Input) 135{ 136 static const float3x3 Rec2020ToRec709Mat = float3x3( 137 1.660496, -0.587656, -0.072840, 138 -0.124547, 1.132895, -0.008348, 139 -0.018154, -0.100597, 1.118751 140 ); 141 return mul(Rec2020ToRec709Mat, Rec2020Input); 142} 143 144float3 RotateRec2020ToP3D65(float3 Rec2020Input) 145{ 146 static const float3x3 Rec2020ToP3D65Mat = float3x3( 147 1.343578, -0.28218, -0.0613986, 148 -0.065298, 1.075788, -0.010491, 149 0.002822, -0.019599, 1.016777 150 ); 151 return mul(Rec2020ToP3D65Mat, Rec2020Input); 152} 153 154float3 RotateP3D65ToRec2020(float3 P3D65Input) 155{ 156 static const float3x3 P3D65ToRec2020Mat = float3x3( 157 0.753833, 0.198597, 0.04757, 158 0.045744, 0.941777, 0.012479, 159 -0.001210, 0.017602, 0.983609 160 ); 161 162 return mul(P3D65ToRec2020Mat, P3D65Input); 163} 164 165float3 RotateRec709ToOutputSpace(float3 Rec709Input) 166{ 167 if (_HDRColorspace == HDRCOLORSPACE_REC2020) 168 { 169 return RotateRec709ToRec2020(Rec709Input); 170 } 171 else if (_HDRColorspace == HDRCOLORSPACE_P3D65) 172 { 173 return RotateRec709ToP3D65(Rec709Input); 174 } 175 else // HDRCOLORSPACE_REC709 176 { 177 return Rec709Input; 178 } 179} 180 181 182float3 RotateRec2020ToOutputSpace(float3 Rec2020Input) 183{ 184 if (_HDRColorspace == HDRCOLORSPACE_REC2020) 185 { 186 return Rec2020Input; 187 } 188 else if (_HDRColorspace == HDRCOLORSPACE_P3D65) 189 { 190 return RotateRec2020ToP3D65(Rec2020Input); 191 } 192 else // HDRCOLORSPACE_REC709 193 { 194 return RotateRec2020ToRec709(Rec2020Input); 195 } 196} 197 198 199float3 RotateRec2020ToLMS(float3 Rec2020Input) 200{ 201 static const float3x3 Rec2020ToLMSMat = 202 { 203 0.412109375, 0.52392578125, 0.06396484375, 204 0.166748046875, 0.720458984375, 0.11279296875, 205 0.024169921875, 0.075439453125, 0.900390625 206 }; 207 208 return mul(Rec2020ToLMSMat, Rec2020Input); 209} 210 211float3 Rotate709ToLMS(float3 Rec709Input) 212{ 213 static const float3x3 Rec709ToLMSMat = 214 { 215 0.412109375, 0.52392578125, 0.06396484375, 216 0.166748046875, 0.720458984375, 0.11279296875, 217 0.024169921875, 0.075439453125, 0.900390625 218 }; 219 return mul(Rec709ToLMSMat, Rec709Input); 220} 221 222// Ref: ICtCp Dolby white paper (https://www.dolby.com/us/en/technologies/dolby-vision/ictcp-white-paper.pdf) 223float3 RotatePQLMSToICtCp(float3 LMSInput) 224{ 225 static const float3x3 PQLMSToICtCpMat = float3x3( 226 0.5f, 0.5f, 0.0f, 227 1.613769f, -3.323486f, 1.709716f, 228 4.378174f, -4.245605f, -0.1325683f 229 ); 230 231 return mul(PQLMSToICtCpMat, LMSInput); 232} 233 234float3 RotateLMSToICtCp(float3 lms) 235{ 236 float3 PQLMS = LinearToPQ(max(0.0f, lms)); 237 return RotatePQLMSToICtCp(PQLMS); 238} 239 240float3 RotateRec2020ToICtCp(float3 Rec2020) 241{ 242 float3 lms = RotateRec2020ToLMS(Rec2020); 243 float3 PQLMS = LinearToPQ(max(0.0f, lms)); 244 return RotatePQLMSToICtCp(PQLMS); 245} 246 247 248 249float3 RotateOutputSpaceToICtCp(float3 inputColor) 250{ 251 // TODO: Do the conversion directly from Rec709 (bake matrix Rec709 -> XYZ -> LMS) 252 if (_HDRColorspace == HDRCOLORSPACE_REC709) 253 { 254 inputColor = RotateRec709ToRec2020(inputColor); 255 } 256 else if (_HDRColorspace == HDRCOLORSPACE_P3D65) 257 { 258 inputColor = RotateP3D65ToRec2020(inputColor); 259 } 260 261 return RotateRec2020ToICtCp(inputColor); 262} 263 264float3 RotateLMSToXYZ(float3 LMSInput) 265{ 266 static const float3x3 LMSToXYZMat = float3x3( 267 2.07018005669561320f, -1.32645687610302100f, 0.206616006847855170f, 268 0.36498825003265756f, 0.68046736285223520f, -0.045421753075853236f, 269 -0.04959554223893212f, -0.04942116118675749f, 1.187995941732803400f 270 ); 271 return mul(LMSToXYZMat, LMSInput); 272} 273 274float3 RotateXYZToRec2020(float3 XYZ) 275{ 276 static const float3x3 XYZToRec2020Mat = float3x3( 277 1.71235168f, -0.35487896f, -0.25034135f, 278 -0.66728621f, 1.61794055f, 0.01495380f, 279 0.01763985f, -0.04277060f, 0.94210320f 280 ); 281 282 return mul(XYZToRec2020Mat, XYZ); 283} 284 285float3 RotateXYZToRec709(float3 XYZ) 286{ 287 return mul(XYZ_2_REC709_MAT, XYZ); 288} 289 290float3 RotateXYZToP3D65(float3 XYZ) 291{ 292 return mul(XYZ_2_P3D65_MAT, XYZ); 293} 294 295float3 RotateXYZToOutputSpace(float3 xyz) 296{ 297 if (_HDRColorspace == HDRCOLORSPACE_REC2020) 298 { 299 return RotateXYZToRec2020(xyz); 300 } 301 else if (_HDRColorspace == HDRCOLORSPACE_P3D65) 302 { 303 return RotateXYZToP3D65(xyz); 304 } 305 else // HDRCOLORSPACE_REC709 306 { 307 return RotateXYZToRec709(xyz); 308 } 309} 310 311float3 RotateRec709ToXYZ(float3 rgb) 312{ 313 static const float3x3 Rec709ToXYZMat = float3x3( 314 0.412391f, 0.357584f, 0.180481, 315 0.212639, 0.715169, 0.0721923, 316 0.0193308, 0.119195, 0.950532 317 ); 318 return mul(Rec709ToXYZMat, rgb); 319} 320 321float3 RotateRec2020ToXYZ(float3 rgb) 322{ 323 static const float3x3 Rec2020ToXYZMat = float3x3( 324 0.638574, 0.144617, 0.167265, 325 0.263367, 0.677998, 0.0586353, 326 0.0f, 0.0280727, 1.06099 327 ); 328 329 return mul(Rec2020ToXYZMat, rgb); 330} 331 332float3 RotateP3D65ToXYZ(float3 rgb) 333{ 334 static const float3x3 P3D65ToXYZMat = float3x3( 335 0.486571, 0.265668, 0.198217, 336 0.228975, 0.691739, 0.079287, 337 0, 0.045113, 1.043944 338 ); 339 340 return mul(P3D65ToXYZMat, rgb); 341} 342 343float3 RotateOutputSpaceToXYZ(float3 rgb) 344{ 345 if (_HDRColorspace == HDRCOLORSPACE_REC2020) 346 { 347 return RotateRec2020ToXYZ(rgb); 348 } 349 else if (_HDRColorspace == HDRCOLORSPACE_P3D65) 350 { 351 return RotateP3D65ToXYZ(rgb); 352 } 353 else // HDRCOLORSPACE_REC709 354 { 355 return RotateRec709ToXYZ(rgb); 356 } 357} 358 359 360float3 RotateICtCpToPQLMS(float3 ICtCp) 361{ 362 static const float3x3 ICtCpToPQLMSMat = float3x3( 363 1.0f, 0.0086051456939815f, 0.1110356044754732f, 364 1.0f, -0.0086051456939815f, -0.1110356044754732f, 365 1.0f, 0.5600488595626390f, -0.3206374702321221f 366 ); 367 368 return mul(ICtCpToPQLMSMat, ICtCp); 369} 370 371float3 RotateICtCpToXYZ(float3 ICtCp) 372{ 373 float3 PQLMS = RotateICtCpToPQLMS(ICtCp); 374 float3 LMS = PQToLinear(PQLMS, MAX_PQ_VALUE); 375 return RotateLMSToXYZ(LMS); 376} 377 378float3 RotateICtCpToRec2020(float3 ICtCp) 379{ 380 return RotateXYZToRec2020(RotateICtCpToXYZ(ICtCp)); 381} 382 383float3 RotateICtCpToRec709(float3 ICtCp) 384{ 385 return RotateXYZToRec709(RotateICtCpToXYZ(ICtCp)); 386} 387 388float3 RotateICtCpToP3D65(float3 ICtCp) 389{ 390 return RotateXYZToP3D65(RotateICtCpToXYZ(ICtCp)); 391} 392 393float3 RotateICtCpToOutputSpace(float3 ICtCp) 394{ 395 if (_HDRColorspace == HDRCOLORSPACE_REC2020) 396 { 397 return RotateICtCpToRec2020(ICtCp); 398 } 399 else if (_HDRColorspace == HDRCOLORSPACE_P3D65) 400 { 401 return RotateICtCpToP3D65(ICtCp); 402 } 403 else // HDRCOLORSPACE_REC709 404 { 405 return RotateICtCpToRec709(ICtCp); 406 } 407} 408 409 410// -------------------------------------------------------------------------------------------- 411 412// -------------------------------- 413// OETFs 414// -------------------------------- 415// The functions here are OETF, technically for applying the opposite of the PQ curve, we are mapping 416// from linear to PQ space as this is what the display expects. 417// See this desmos for comparisons https://www.desmos.com/calculator/5jdfc4pgtk 418#define PRECISE_PQ 0 419#define ISS_APPROX_PQ 1 420#define GTS_APPROX_PQ 2 421 422#define OETF_CHOICE GTS_APPROX_PQ 423 424// What the platforms expects as SDR max brightness (different from paper white brightness) in linear encoding 425#if defined(SHADER_API_METAL) 426 #define SDR_REF_WHITE 100 427#else 428 #define SDR_REF_WHITE 80 429#endif 430 431// Ref: [Patry 2017] HDR Display Support in Infamous Second Son and Infamous First Light 432// Fastest option, but also the least accurate. Behaves well for values up to 1400 nits but then starts diverging. 433// IMPORTANT! It requires the input to be scaled from [0 ... 10000] to [0...100]! 434float3 PatryApproxLinToPQ(float3 x) 435{ 436 return (x * (x * (x * (x * (x * 533095.76 + 47438306.2) + 29063622.1) + 575216.76) + 383.09104) + 0.000487781) / 437 (x * (x * (x * (x * 66391357.4 + 81884528.2) + 4182885.1) + 10668.404) + 1.0); 438} 439 440// Ref: [Uchimura and Suzuki 2018] Practical HDR and Wide Color Techniques in Gran Turismo Sport 441// Slower than Infamous approx, but more precise ( https://www.desmos.com/calculator/up4wwozghk ) in the full [0... 10 000] range, but still faster than reference 442// IMPORTANT! It requires the input to be scaled from [0 ... 10000] to [0...100]! 443float3 GTSApproxLinToPQ(float3 inputCol) 444{ 445 float3 k = PositivePow((inputCol * 0.01), PQ_N); 446 return (3.61972*(1e-8) + k * (0.00102859 + k * (-0.101284 + 2.05784 * k))) / 447 (0.0495245 + k * (0.135214 + k * (0.772669 + k))); 448} 449 450// IMPORTANT! This wants the input in [0...10000] range, if the method requires scaling, it is done inside this function. 451float3 OETF(float3 inputCol, float maxNits) 452{ 453 if (_HDREncoding == HDRENCODING_LINEAR) 454 { 455 // IMPORTANT! This assumes that the maximum nits is always higher or same as the reference white. Seems like a sensible choice, but revisit if we find weird use cases (just min with the the max nits). 456 // We need to map the value 1 to [reference white] nits. 457 return inputCol / SDR_REF_WHITE; 458 } 459 else if (_HDREncoding == HDRENCODING_PQ) 460 { 461 #if OETF_CHOICE == PRECISE_PQ 462 return LinearToPQ(inputCol); 463 #elif OETF_CHOICE == ISS_APPROX_PQ 464 return PatryApproxLinToPQ(inputCol * 0.01f); 465 #elif OETF_CHOICE == GTS_APPROX_PQ 466 return GTSApproxLinToPQ(inputCol * 0.01f); 467 #endif 468 } 469 else if (_HDREncoding == HDRENCODING_GAMMA22) 470 { 471 return LinearToGamma22(inputCol / (float3)maxNits); // Usually used to encode into UNORM output 0->1 where 1 is the max display brightness, this will be very device specific so use our maxNits. 472 } 473 else if (_HDREncoding == HDRENCODING_S_RGB) 474 { 475 return inputCol / (float3)maxNits; // Usually used to encode into UNORM output 0->1 where 1 is the max display brightness, this will be very device specific so use our maxNits. 476 } 477 else 478 { 479 return inputCol; 480 } 481} 482 483#define LIN_TO_PQ_FOR_LUT GTS_APPROX_PQ // GTS is close enough https://www.desmos.com/calculator/up4wwozghk 484float3 LinearToPQForLUT(float3 inputCol) 485{ 486#if LIN_TO_PQ_FOR_LUT == PRECISE_PQ 487 return LinearToPQ(inputCol); 488#elif LIN_TO_PQ_FOR_LUT == ISS_APPROX_PQ 489 return PatryApproxLinToPQ(inputCol * 0.01f); 490#elif LIN_TO_PQ_FOR_LUT == GTS_APPROX_PQ 491 return GTSApproxLinToPQ(inputCol * 0.01f); 492#endif 493} 494 495// -------------------------------------------------------------------------------------------- 496 497// -------------------------------- 498// Inverse OETFs 499// -------------------------------- 500// The functions here are the inverse of our OETF, to allow reading in data we have processed through our OETF. 501// This is not the same as the EOTF as that is not always a direct inverse of the OETF so that an end to end transfer may not be linear. 502// We pass the HDR Encoding value manually into this function rather than reading from _HDREncoding as that is the currnet destination encoding 503// and we may be reading from a different encoding as part of translating between encodings. 504 505float3 InverseOETF(float3 inputCol, float maxNits, int hdrEncoding) 506{ 507 if (hdrEncoding == HDRENCODING_LINEAR) 508 { 509 // IMPORTANT! This assumes that the maximum nits is always higher or same as the reference white. Seems like a sensible choice, but revisit if we find weird use cases (just min with the the max nits). 510 // We need to map the value 1 to [reference white] nits. 511 return inputCol * SDR_REF_WHITE; 512 } 513 else if (hdrEncoding == HDRENCODING_PQ) 514 { 515 // For the time being always use the reference spec for computing the inverse. For the optimized version of LinearToPQ we would need to fit our own PQToLinear version. 516 return PQToLinear(inputCol); 517 } 518 else if (hdrEncoding == HDRENCODING_GAMMA22) 519 { 520 return Gamma22ToLinear(inputCol) * maxNits; 521 } 522 else if (hdrEncoding == HDRENCODING_S_RGB) 523 { 524 return inputCol * maxNits; 525 } 526 else 527 { 528 return inputCol; 529 } 530} 531 532// -------------------------------------------------------------------------------------------- 533 534// -------------------------------- 535// Range reduction 536// -------------------------------- 537// This section of the file concerns the way we map from full range to whatever range the display supports. 538// Also note, we always tonemap luminance component only, so we need to reach this point after we converted 539// to a format such as ICtCp or YCbCr 540// See https://www.desmos.com/calculator/pqc3raolms for plots 541#define RANGE_REDUCTION HDRRANGEREDUCTION_BT2390LUMA_ONLY 542 543// Note this takes x being in [0...10k nits] 544float ReinhardTonemap(float x, float peakValue) 545{ 546 float m = MAX_PQ_VALUE * peakValue / (MAX_PQ_VALUE - peakValue); 547 return x * m / (x + m); 548} 549 550/// BT2390 EETF Helper functions 551float T(float A, float Ks) 552{ 553 return (A - Ks) / (1.0f - Ks); 554} 555 556float P(float B, float Ks, float L_max) 557{ 558 float TB2 = T(B, Ks) * T(B, Ks); 559 float TB3 = TB2 * T(B, Ks); 560 561 return lerp((TB3 - 2 * TB2 + T(B, Ks)), (2.0f * TB3 - 3.0f * TB2 + 1.0f), Ks) + (-2.0f * TB3 + 3.0f*TB2)*L_max; 562} 563 564 565// Ref: https://www.itu.int/dms_pub/itu-r/opb/rep/R-REP-BT.2390-4-2018-PDF-E.pdf page 21 566// This takes values in [0...10k nits] and it outputs in the same space. PQ conversion outside. 567// If we chose this, it can be optimized (a few identity happen with moving between linear and PQ) 568float BT2390EETF(float x, float minLimit, float maxLimit) 569{ 570 float E_0 = LinearToPQ(x); 571 // For the following formulas we are assuming L_B = 0 and L_W = 10000 -- see original paper for full formulation 572 float E_1 = E_0; 573 float L_min = LinearToPQ(minLimit); 574 float L_max = LinearToPQ(maxLimit); 575 float Ks = 1.5f * L_max - 0.5f; // Knee start 576 float b = L_min; 577 578 float E_2 = E_1 < Ks ? E_1 : P(E_1, Ks, L_max); 579 float E3Part = (1.0f - E_2); 580 float E3Part2 = E3Part * E3Part; 581 float E_3 = E_2 + b * (E3Part2 * E3Part2); 582 float E_4 = E_3; // Is like this because PQ(L_W)= 1 and PQ(L_B) = 0 583 584 return PQToLinear(E_4, MAX_PQ_VALUE); 585} 586 587 588float3 PerformRangeReduction(float3 input, float minNits, float maxNits) 589{ 590 float3 ICtCp = RotateOutputSpaceToICtCp(input); // This is in PQ space. 591 float linearLuma = PQToLinear(ICtCp.x, MAX_PQ_VALUE); 592#if RANGE_REDUCTION == HDRRANGEREDUCTION_REINHARD_LUMA_ONLY 593 linearLuma = ReinhardTonemap(linearLuma, maxNits); 594#elif RANGE_REDUCTION == HDRRANGEREDUCTION_BT2390LUMA_ONLY 595 linearLuma = BT2390EETF(linearLuma, minNits, maxNits); 596#endif 597 ICtCp.x = LinearToPQ(linearLuma); 598 599 600 return RotateICtCpToOutputSpace(ICtCp); // This moves back to linear too! 601} 602 603// TODO: This is very ad-hoc and eyeballed on a limited set. Would be nice to find a standard. 604float3 DesaturateReducedICtCp(float3 ICtCp, float lumaPre, float maxNits) 605{ 606 float saturationAmount = min(1.0f, ICtCp.x / max(lumaPre, 1e-6f)); // BT2390, but only when getting darker. 607 //saturationAmount = min(lumaPre / ICtCp.x, ICtCp.x / lumaPre); // Actual BT2390 suggestion 608 saturationAmount *= saturationAmount; 609 //saturationAmount = pow(smoothstep(1.0f, 0.4f, ICtCp.x), 0.9f); // A smoothstepp-y function. 610 ICtCp.yz *= saturationAmount; 611 return ICtCp; 612} 613 614float LumaRangeReduction(float input, float minNits, float maxNits, int mode) 615{ 616 float output = input; 617 if (mode == HDRRANGEREDUCTION_REINHARD) 618 { 619 output = ReinhardTonemap(input, maxNits); 620 } 621 else if (mode == HDRRANGEREDUCTION_BT2390) 622 { 623 output = BT2390EETF(input, minNits, maxNits); 624 } 625 626 return output; 627} 628 629float3 HuePreservingRangeReduction(float3 input, float minNits, float maxNits, int mode) 630{ 631 float3 ICtCp = RotateOutputSpaceToICtCp(input); 632 633 float lumaPreRed = ICtCp.x; 634 float linearLuma = PQToLinear(ICtCp.x, MAX_PQ_VALUE); 635 linearLuma = LumaRangeReduction(linearLuma, minNits, maxNits, mode); 636 ICtCp.x = LinearToPQ(linearLuma); 637 ICtCp = DesaturateReducedICtCp(ICtCp, lumaPreRed, maxNits); 638 639 return RotateICtCpToOutputSpace(ICtCp); 640} 641 642float3 HueShiftingRangeReduction(float3 input, float minNits, float maxNits, int mode) 643{ 644 float3 hueShiftedResult = input; 645 if (mode == HDRRANGEREDUCTION_REINHARD) 646 { 647 hueShiftedResult.x = ReinhardTonemap(input.x, maxNits); 648 hueShiftedResult.y = ReinhardTonemap(input.y, maxNits); 649 hueShiftedResult.z = ReinhardTonemap(input.z, maxNits); 650 } 651 else if(mode == HDRRANGEREDUCTION_BT2390) 652 { 653 hueShiftedResult.x = BT2390EETF(input.x, minNits, maxNits); 654 hueShiftedResult.y = BT2390EETF(input.y, minNits, maxNits); 655 hueShiftedResult.z = BT2390EETF(input.z, minNits, maxNits); 656 } 657 return hueShiftedResult; 658} 659 660// Ref "High Dynamic Range color grading and display in Frostbite" [Fry 2017] 661float3 FryHuePreserving(float3 input, float minNits, float maxNits, float hueShift, int mode) 662{ 663 float3 ictcp = RotateOutputSpaceToICtCp(input); 664 665 // Hue-preserving range compression requires desaturation in order to achieve a natural look. We adaptively desaturate the input based on its luminance. 666 float saturationAmount = pow(smoothstep(1.0, 0.3, ictcp.x), 1.3); 667 float3 col = RotateICtCpToOutputSpace(ictcp * float3(1, saturationAmount.xx)); 668 669 // Only compress luminance starting at a certain point. Dimmer inputs are passed through without modification. 670 float linearSegmentEnd = 0.25f; 671 672 // Hue-preserving mapping 673 float maxCol = max(col.x, max(col.y, col.z)); 674 float mappedMax = maxCol; 675 if (maxCol > linearSegmentEnd) 676 { 677 mappedMax = LumaRangeReduction(maxCol, minNits, maxNits, mode); 678 } 679 680 float3 compressedHuePreserving = col * mappedMax / maxCol; 681 682 // Non-hue preserving mapping 683 float3 perChannelCompressed = 0; 684 perChannelCompressed.x = col.x > linearSegmentEnd ? LumaRangeReduction(col.x, minNits, maxNits, mode) : col.x; 685 perChannelCompressed.y = col.y > linearSegmentEnd ? LumaRangeReduction(col.y, minNits, maxNits, mode) : col.y; 686 perChannelCompressed.z = col.z > linearSegmentEnd ? LumaRangeReduction(col.z, minNits, maxNits, mode) : col.z; 687 688 // Combine hue-preserving and non-hue-preserving colors. Absolute hue preservation looks unnatural, as bright colors *appear* to have been hue shifted. 689 // Actually doing some amount of hue shifting looks more pleasing 690 col = lerp(perChannelCompressed, compressedHuePreserving, 1-hueShift); 691 692 float3 ictcpMapped = RotateOutputSpaceToICtCp(col); 693 694 // Smoothly ramp off saturation as brightness increases, but keep some even for very bright input 695 float postCompressionSaturationBoost = 0.3 * smoothstep(1.0, 0.5, ictcp.x); 696 697 // Re-introduce some hue from the pre-compression color. Something similar could be accomplished by delaying the luma-dependent desaturation before range compression. 698 // Doing it here however does a better job of preserving perceptual luminance of highly saturated colors. Because in the hue-preserving path we only range-compress the max channel, 699 // saturated colors lose luminance. By desaturating them more aggressively first, compressing, and then re-adding some saturation, we can preserve their brightness to a greater extent. 700 ictcpMapped.yz = lerp(ictcpMapped.yz, ictcp.yz * ictcpMapped.x / max(1e-3, ictcp.x), postCompressionSaturationBoost); 701 702 col = RotateICtCpToOutputSpace(ictcpMapped); 703 704 return col; 705} 706 707float3 PerformRangeReduction(float3 input, float minNits, float maxNits, int mode, float hueShift) 708{ 709 float3 outputValue = input; 710 bool reduceLuma = hueShift < 1.0f; 711 bool needHueShiftVersion = hueShift > 0.0f; 712 713 if (mode == HDRRANGEREDUCTION_NONE) 714 { 715 outputValue = input; 716 } 717 else 718 { 719 float3 huePreserving = reduceLuma ? HuePreservingRangeReduction(input, minNits, maxNits, mode) : 0; 720 float3 hueShifted = needHueShiftVersion ? HueShiftingRangeReduction(input, minNits, maxNits, mode) : 0; 721 722 if (reduceLuma && !needHueShiftVersion) 723 { 724 outputValue = huePreserving; 725 } 726 else if (!reduceLuma && needHueShiftVersion) 727 { 728 outputValue = hueShifted; 729 } 730 else 731 { 732 // We need to combine the two cases 733 outputValue = lerp(huePreserving, hueShifted, hueShift); 734 } 735 } 736 737 return outputValue; 738} 739 740// -------------------------------------------------------------------------------------------- 741 742// -------------------------------- 743// Public facing functions 744// -------------------------------- 745// These functions are aggregate of most of what we have above. You can think of this as the public API of the HDR Output library. 746// Note that throughout HDRP we are assuming that when it comes to the final pass adjustements, our tonemapper has *NOT* 747// performed range reduction and everything is assumed to be displayed on a reference 10k nits display and everything post-tonemapping 748// is in either the Rec 2020 or Rec709 color space. The Rec709 version just rotate to Rec2020 before going forward if required by the output device. 749 750float3 HDRMappingFromRec2020(float3 Rec2020Input, float paperWhite, float minNits, float maxNits, int reductionMode, float hueShift, bool skipOETF = false) 751{ 752 float3 outputSpaceInput = RotateRec2020ToOutputSpace(Rec2020Input); 753 float3 reducedHDR = PerformRangeReduction(outputSpaceInput * paperWhite, minNits, maxNits, reductionMode, hueShift); 754 755 if (skipOETF) return reducedHDR; 756 757 return OETF(reducedHDR, maxNits); 758} 759 760float3 HDRMappingFromRec709(float3 Rec709Input, float paperWhite, float minNits, float maxNits, int reductionMode, float hueShift, bool skipOETF = false) 761{ 762 float3 outputSpaceInput = RotateRec709ToOutputSpace(Rec709Input); 763 float3 reducedHDR = PerformRangeReduction(outputSpaceInput * paperWhite, minNits, maxNits, reductionMode, hueShift); 764 765 if (skipOETF) return reducedHDR; 766 767 return OETF(reducedHDR, maxNits); 768} 769 770 771float3 HDRMappingACES(float3 aces, float hdrBoost, float minNits, float maxNits, int reductionMode, bool skipOETF = false) 772{ 773 aces = (aces * hdrBoost * 0.01f); 774 half3 oces = RRT(half3(aces)); 775 776 float3 AP1ODT = 0; 777 778 // This is a static branch. 779 if (reductionMode == HDRRANGEREDUCTION_ACES1000NITS) 780 { 781 AP1ODT = ODT_1000nits_ToAP1(oces); 782 } 783 else if (reductionMode == HDRRANGEREDUCTION_ACES2000NITS) 784 { 785 AP1ODT = ODT_2000nits_ToAP1(oces); 786 } 787 else if (reductionMode == HDRRANGEREDUCTION_ACES4000NITS) 788 { 789 AP1ODT = ODT_4000nits_ToAP1(oces); 790 } 791 792 float3 linearODT = 0; 793 if (_HDRColorspace == HDRCOLORSPACE_REC2020) 794 { 795 const float3x3 AP1_2_Rec2020 = mul(XYZ_2_REC2020_MAT, mul(D60_2_D65_CAT, AP1_2_XYZ_MAT)); 796 linearODT = mul(AP1_2_Rec2020, AP1ODT); 797 } 798 else if (_HDRColorspace == HDRCOLORSPACE_P3D65) 799 { 800 const float3x3 API1_2_P3D65 = mul(XYZ_2_P3D65_MAT, mul(D60_2_D65_CAT, AP1_2_XYZ_MAT)); 801 linearODT = mul(API1_2_P3D65, AP1ODT); 802 } 803 else // HDRCOLORSPACE_REC709 804 { 805 const float3x3 AP1_2_Rec709 = mul(XYZ_2_REC709_MAT, mul(D60_2_D65_CAT, AP1_2_XYZ_MAT)); 806 linearODT = mul(AP1_2_Rec709, AP1ODT); 807 } 808 809 if (skipOETF) return linearODT; 810 811 return OETF(linearODT, maxNits); 812} 813 814// -------------------------------------------------------------------------------------------- 815 816// -------------------------------- 817// UI Related functions 818// -------------------------------- 819 820float3 ProcessUIForHDR(float3 uiSample, float paperWhite, float maxNits) 821{ 822 uiSample.rgb = RotateRec709ToOutputSpace(uiSample.rgb); 823 uiSample.rgb *= paperWhite; 824 825 return uiSample.rgb; 826} 827 828float3 SceneUIComposition(float4 uiSample, float3 sceneColor, float paperWhite, float maxNits) 829{ 830 // Undo the pre multiply. 831 uiSample.rgb = uiSample.rgb / (uiSample.a == 0.0f ? 1.0 : uiSample.a); 832 uiSample.rgb = ProcessUIForHDR(uiSample.rgb, paperWhite, maxNits); 833 return uiSample.rgb * uiSample.a + sceneColor.rgb * (1.0f - uiSample.a); 834} 835 836// -------------------------------------------------------------------------------------------- 837 838#endif