ACES.cginc 45.6 KB
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868 869 870 871 872 873 874 875 876 877 878 879 880 881 882 883 884 885 886 887 888 889 890 891 892 893 894 895 896 897 898 899 900 901 902 903 904 905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993 994 995 996 997 998 999 1000 1001 1002 1003 1004 1005 1006 1007 1008 1009 1010 1011 1012 1013 1014 1015 1016 1017 1018 1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034 1035 1036 1037 1038 1039 1040 1041 1042 1043 1044 1045 1046 1047 1048 1049 1050 1051 1052 1053 1054 1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070 1071 1072 1073 1074 1075 1076 1077 1078 1079 1080 1081 1082 1083 1084 1085 1086 1087 1088 1089 1090 1091 1092 1093 1094 1095 1096 1097 1098 1099 1100 1101 1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113 1114 1115 1116 1117 1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154 1155 1156 1157 1158 1159 1160 1161 1162 1163 1164 1165 1166 1167 1168 1169 1170 1171 1172 1173 1174 1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189 1190 1191 1192 1193 1194 1195 1196 1197 1198 1199 1200 1201 1202 1203 1204 1205 1206 1207 1208 1209 1210 1211 1212 1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224 1225 1226 1227 1228 1229 1230 1231 1232 1233 1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277 1278 1279 1280 1281 1282 1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302 1303 1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314 1315 1316 1317 1318 1319 1320 1321 1322 1323 1324 1325 1326 1327 1328 1329 1330 1331 1332 1333
#ifndef __ACES__
#define __ACES__

/**
 * https://github.com/ampas/aces-dev
 *
 * Academy Color Encoding System (ACES) software and tools are provided by the
 * Academy under the following terms and conditions: A worldwide, royalty-free,
 * non-exclusive right to copy, modify, create derivatives, and use, in source and
 * binary forms, is hereby granted, subject to acceptance of this license.
 *
 * Copyright 2015 Academy of Motion Picture Arts and Sciences (A.M.P.A.S.).
 * Portions contributed by others as indicated. All rights reserved.
 *
 * Performance of any of the aforementioned acts indicates acceptance to be bound
 * by the following terms and conditions:
 *
 * * Copies of source code, in whole or in part, must retain the above copyright
 * notice, this list of conditions and the Disclaimer of Warranty.
 *
 * * Use in binary form must retain the above copyright notice, this list of
 * conditions and the Disclaimer of Warranty in the documentation and/or other
 * materials provided with the distribution.
 *
 * * Nothing in this license shall be deemed to grant any rights to trademarks,
 * copyrights, patents, trade secrets or any other intellectual property of
 * A.M.P.A.S. or any contributors, except as expressly stated herein.
 *
 * * Neither the name "A.M.P.A.S." nor the name of any other contributors to this
 * software may be used to endorse or promote products derivative of or based on
 * this software without express prior written permission of A.M.P.A.S. or the
 * contributors, as appropriate.
 *
 * This license shall be construed pursuant to the laws of the State of
 * California, and any disputes related thereto shall be subject to the
 * jurisdiction of the courts therein.
 *
 * Disclaimer of Warranty: THIS SOFTWARE IS PROVIDED BY A.M.P.A.S. AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND
 * NON-INFRINGEMENT ARE DISCLAIMED. IN NO EVENT SHALL A.M.P.A.S., OR ANY
 * CONTRIBUTORS OR DISTRIBUTORS, BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
 * SPECIAL, EXEMPLARY, RESITUTIONARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * WITHOUT LIMITING THE GENERALITY OF THE FOREGOING, THE ACADEMY SPECIFICALLY
 * DISCLAIMS ANY REPRESENTATIONS OR WARRANTIES WHATSOEVER RELATED TO PATENT OR
 * OTHER INTELLECTUAL PROPERTY RIGHTS IN THE ACADEMY COLOR ENCODING SYSTEM, OR
 * APPLICATIONS THEREOF, HELD BY PARTIES OTHER THAN A.M.P.A.S.,WHETHER DISCLOSED OR
 * UNDISCLOSED.
 */

//#define CUSTOM_WHITE_POINT

/*
    Basic usage :

    half4 color = tex2D(_MainTex, i.uv);
    half3 aces = unity_to_ACES(color.rgb);
    half3 oces = RRT(aces);
    half3 odt = ODT_RGBmonitor_100nits_dim(oces);
    return half4(odt, color.a);

    If you want to customize the white point, uncomment the previous define and set uniforms accordingly:

    float whitePoint = 48f; // Default ACES value
    material.SetFloat("CINEMA_WHITE", whitePoint);
    material.SetFloat("CINEMA_DARK", whitePoint / 2400f);
 */

#include "Common.cginc"

#define ACEScc_MAX      1.4679964
#define ACEScc_MIDGRAY  0.4135884

//
// Precomputed matrices (pre-transposed)
// See https://github.com/ampas/aces-dev/blob/master/transforms/ctl/README-MATRIX.md
//
static const half3x3 sRGB_2_AP0 = {
    0.4397010, 0.3829780, 0.1773350,
    0.0897923, 0.8134230, 0.0967616,
    0.0175440, 0.1115440, 0.8707040
};

static const half3x3 sRGB_2_AP1 = {
    0.61319, 0.33951, 0.04737,
    0.07021, 0.91634, 0.01345,
    0.02062, 0.10957, 0.86961
};

static const half3x3 AP0_2_sRGB = {
    2.52169, -1.13413, -0.38756,
    -0.27648, 1.37272, -0.09624,
    -0.01538, -0.15298, 1.16835,
};

static const half3x3 AP1_2_sRGB = {
    1.70505, -0.62179, -0.08326,
    -0.13026, 1.14080, -0.01055,
    -0.02400, -0.12897, 1.15297,
};

static const half3x3 AP0_2_AP1_MAT = {
     1.4514393161, -0.2365107469, -0.2149285693,
    -0.0765537734,  1.1762296998, -0.0996759264,
     0.0083161484, -0.0060324498,  0.9977163014
};

static const half3x3 AP1_2_AP0_MAT = {
     0.6954522414, 0.1406786965, 0.1638690622,
     0.0447945634, 0.8596711185, 0.0955343182,
    -0.0055258826, 0.0040252103, 1.0015006723
};

static const half3x3 AP1_2_XYZ_MAT = {
     0.6624541811, 0.1340042065, 0.1561876870,
     0.2722287168, 0.6740817658, 0.0536895174,
    -0.0055746495, 0.0040607335, 1.0103391003
};

static const half3x3 XYZ_2_AP1_MAT = {
     1.6410233797, -0.3248032942, -0.2364246952,
    -0.6636628587,  1.6153315917,  0.0167563477,
     0.0117218943, -0.0082844420,  0.9883948585
};

static const half3x3 XYZ_2_REC709_MAT = {
     3.2409699419, -1.5373831776, -0.4986107603,
    -0.9692436363,  1.8759675015,  0.0415550574,
     0.0556300797, -0.2039769589,  1.0569715142
};

static const half3x3 XYZ_2_REC2020_MAT = {
     1.7166511880, -0.3556707838, -0.2533662814,
    -0.6666843518,  1.6164812366,  0.0157685458,
     0.0176398574, -0.0427706133,  0.9421031212
};

static const half3x3 XYZ_2_DCIP3_MAT = {
     2.7253940305, -1.0180030062, -0.4401631952,
    -0.7951680258,  1.6897320548,  0.0226471906,
     0.0412418914, -0.0876390192,  1.1009293786
};

static const half3 AP1_RGB2Y = half3(0.272229, 0.674082, 0.0536895);

static const half3x3 RRT_SAT_MAT = {
    0.9708890, 0.0269633, 0.00214758,
    0.0108892, 0.9869630, 0.00214758,
    0.0108892, 0.0269633, 0.96214800
};

static const half3x3 ODT_SAT_MAT = {
    0.949056, 0.0471857, 0.00375827,
    0.019056, 0.9771860, 0.00375827,
    0.019056, 0.0471857, 0.93375800
};

static const half3x3 D60_2_D65_CAT = {
     0.98722400, -0.00611327, 0.0159533,
    -0.00759836,  1.00186000, 0.0053302,
     0.00307257, -0.00509595, 1.0816800
};

//
// Unity to ACES
//
// converts Unity raw (sRGB primaries) to
//          ACES2065-1 (AP0 w/ linear encoding)
//
half3 unity_to_ACES(half3 x)
{
    x = mul(sRGB_2_AP0, x);
    return x;
}

//
// ACES to Unity
//
// converts ACES2065-1 (AP0 w/ linear encoding)
//          Unity raw (sRGB primaries) to
//
half3 ACES_to_unity(half3 x)
{
    x = mul(AP0_2_sRGB, x);
    return x;
}

//
// Unity to ACEScg
//
// converts Unity raw (sRGB primaries) to
//          ACEScg (AP1 w/ linear encoding)
//
half3 unity_to_ACEScg(half3 x)
{
    x = mul(sRGB_2_AP1, x);
    return x;
}

//
// ACEScg to Unity
//
// converts ACEScg (AP1 w/ linear encoding) to
//          Unity raw (sRGB primaries)
//
half3 ACEScg_to_unity(half3 x)
{
    x = mul(AP1_2_sRGB, x);
    return x;
}

//
// ACES Color Space Conversion - ACES to ACEScc
//
// converts ACES2065-1 (AP0 w/ linear encoding) to
//          ACEScc (AP1 w/ logarithmic encoding)
//
// This transform follows the formulas from section 4.4 in S-2014-003
//
half ACES_to_ACEScc(half x)
{
    if (x <= 0.0)
        return -0.35828683; // = (log2(pow(2.0, -15.0) * 0.5) + 9.72) / 17.52
    else if (x < pow(2.0, -15.0))
        return (log2(pow(2.0, -16.0) + x * 0.5) + 9.72) / 17.52;
    else // (x >= pow(2.0, -15.0))
        return (log2(x) + 9.72) / 17.52;
}

half3 ACES_to_ACEScc(half3 x)
{
    x = clamp(x, 0.0, HALF_MAX);

    // x is clamped to [0, HALF_MAX], skip the <= 0 check
    return (x < 0.00003051757) ? (log2(0.00001525878 + x * 0.5) + 9.72) / 17.52 : (log2(x) + 9.72) / 17.52;

    /*
    return half3(
        ACES_to_ACEScc(x.r),
        ACES_to_ACEScc(x.g),
        ACES_to_ACEScc(x.b)
    );
    */
}

//
// ACES Color Space Conversion - ACEScc to ACES
//
// converts ACEScc (AP1 w/ ACESlog encoding) to
//          ACES2065-1 (AP0 w/ linear encoding)
//
// This transform follows the formulas from section 4.4 in S-2014-003
//
half ACEScc_to_ACES(half x)
{
    // TODO: Optimize me
    if (x < -0.3013698630) // (9.72 - 15) / 17.52
        return (pow(2.0, x * 17.52 - 9.72) - pow(2.0, -16.0)) * 2.0;
    else if (x < (log2(HALF_MAX) + 9.72) / 17.52)
        return pow(2.0, x * 17.52 - 9.72);
    else // (x >= (log2(HALF_MAX) + 9.72) / 17.52)
        return HALF_MAX;
}

half3 ACEScc_to_ACES(half3 x)
{
    return half3(
        ACEScc_to_ACES(x.r),
        ACEScc_to_ACES(x.g),
        ACEScc_to_ACES(x.b)
    );
}

//
// ACES Color Space Conversion - ACES to ACEScg
//
// converts ACES2065-1 (AP0 w/ linear encoding) to
//          ACEScg (AP1 w/ linear encoding)
//
half3 ACES_to_ACEScg(half3 x)
{
    return mul(AP0_2_AP1_MAT, x);
}

//
// ACES Color Space Conversion - ACEScg to ACES
//
// converts ACEScg (AP1 w/ linear encoding) to
//          ACES2065-1 (AP0 w/ linear encoding)
//
half3 ACEScg_to_ACES(half3 x)
{
    return mul(AP1_2_AP0_MAT, x);
}

//
// Reference Rendering Transform (RRT)
//
//   Input is ACES
//   Output is OCES
//
half rgb_2_saturation(half3 rgb)
{
    const half TINY = 1e-10;
    half mi = Min3(rgb);
    half ma = Max3(rgb);
    return (max(ma, TINY) - max(mi, TINY)) / max(ma, 1e-2);
}

half rgb_2_yc(half3 rgb)
{
    const half ycRadiusWeight = 1.75;

    // Converts RGB to a luminance proxy, here called YC
    // YC is ~ Y + K * Chroma
    // Constant YC is a cone-shaped surface in RGB space, with the tip on the
    // neutral axis, towards white.
    // YC is normalized: RGB 1 1 1 maps to YC = 1
    //
    // ycRadiusWeight defaults to 1.75, although can be overridden in function
    // call to rgb_2_yc
    // ycRadiusWeight = 1 -> YC for pure cyan, magenta, yellow == YC for neutral
    // of same value
    // ycRadiusWeight = 2 -> YC for pure red, green, blue  == YC for  neutral of
    // same value.

    half r = rgb.x;
    half g = rgb.y;
    half b = rgb.z;
    half chroma = sqrt(b * (b - g) + g * (g - r) + r * (r - b));
    return (b + g + r + ycRadiusWeight * chroma) / 3.0;
}

half rgb_2_hue(half3 rgb)
{
    // Returns a geometric hue angle in degrees (0-360) based on RGB values.
    // For neutral colors, hue is undefined and the function will return a quiet NaN value.
    half hue;
    if (rgb.x == rgb.y && rgb.y == rgb.z)
        hue = 0.0; // RGB triplets where RGB are equal have an undefined hue
    else
        hue = (180.0 / UNITY_PI) * atan2(sqrt(3.0) * (rgb.y - rgb.z), 2.0 * rgb.x - rgb.y - rgb.z);

    if (hue < 0.0) hue = hue + 360.0;

    return hue;
}

half center_hue(half hue, half centerH)
{
    half hueCentered = hue - centerH;
    if (hueCentered < -180.0) hueCentered = hueCentered + 360.0;
    else if (hueCentered > 180.0) hueCentered = hueCentered - 360.0;
    return hueCentered;
}

half sigmoid_shaper(half x)
{
    // Sigmoid function in the range 0 to 1 spanning -2 to +2.

    half t = max(1.0 - abs(x / 2.0), 0.0);
    half y = 1.0 + sign(x) * (1.0 - t * t);

    return y / 2.0;
}

half glow_fwd(half ycIn, half glowGainIn, half glowMid)
{
    half glowGainOut;

    if (ycIn <= 2.0 / 3.0 * glowMid)
        glowGainOut = glowGainIn;
    else if (ycIn >= 2.0 * glowMid)
        glowGainOut = 0.0;
    else
        glowGainOut = glowGainIn * (glowMid / ycIn - 1.0 / 2.0);

    return glowGainOut;
}

/*
half cubic_basis_shaper
(
    half x,
    half w   // full base width of the shaper function (in degrees)
)
{
    half M[4][4] = {
        { -1.0 / 6,  3.0 / 6, -3.0 / 6,  1.0 / 6 },
        {  3.0 / 6, -6.0 / 6,  3.0 / 6,  0.0 / 6 },
        { -3.0 / 6,  0.0 / 6,  3.0 / 6,  0.0 / 6 },
        {  1.0 / 6,  4.0 / 6,  1.0 / 6,  0.0 / 6 }
    };

    half knots[5] = {
        -w / 2.0,
        -w / 4.0,
             0.0,
         w / 4.0,
         w / 2.0
    };

    half y = 0.0;
    if ((x > knots[0]) && (x < knots[4]))
    {
        half knot_coord = (x - knots[0]) * 4.0 / w;
        int j = knot_coord;
        half t = knot_coord - j;

        half monomials[4] = { t*t*t, t*t, t, 1.0 };

        // (if/else structure required for compatibility with CTL < v1.5.)
        if (j == 3)
        {
            y = monomials[0] * M[0][0] + monomials[1] * M[1][0] +
                monomials[2] * M[2][0] + monomials[3] * M[3][0];
        }
        else if (j == 2)
        {
            y = monomials[0] * M[0][1] + monomials[1] * M[1][1] +
                monomials[2] * M[2][1] + monomials[3] * M[3][1];
        }
        else if (j == 1)
        {
            y = monomials[0] * M[0][2] + monomials[1] * M[1][2] +
                monomials[2] * M[2][2] + monomials[3] * M[3][2];
        }
        else if (j == 0)
        {
            y = monomials[0] * M[0][3] + monomials[1] * M[1][3] +
                monomials[2] * M[2][3] + monomials[3] * M[3][3];
        }
        else
        {
            y = 0.0;
        }
    }

    return y * 3.0 / 2.0;
}
*/

static const half3x3 M = {
     0.5, -1.0, 0.5,
    -1.0,  1.0, 0.0,
     0.5,  0.5, 0.0
};

half segmented_spline_c5_fwd(half x)
{
    const half coefsLow[6] = { -4.0000000000, -4.0000000000, -3.1573765773, -0.4852499958, 1.8477324706, 1.8477324706 }; // coefs for B-spline between minPoint and midPoint (units of log luminance)
    const half coefsHigh[6] = { -0.7185482425, 2.0810307172, 3.6681241237, 4.0000000000, 4.0000000000, 4.0000000000 }; // coefs for B-spline between midPoint and maxPoint (units of log luminance)
    const half2 minPoint = half2(0.18 * exp2(-15.0), 0.0001); // {luminance, luminance} linear extension below this
    const half2 midPoint = half2(0.18, 0.48); // {luminance, luminance}
    const half2 maxPoint = half2(0.18 * exp2(18.0), 10000.0); // {luminance, luminance} linear extension above this
    const half slopeLow = 0.0; // log-log slope of low linear extension
    const half slopeHigh = 0.0; // log-log slope of high linear extension

    const int N_KNOTS_LOW = 4;
    const int N_KNOTS_HIGH = 4;

    // Check for negatives or zero before taking the log. If negative or zero,
    // set to ACESMIN.1
    float xCheck = x;
    if (xCheck <= 0.0) xCheck = 0.00006103515; // = pow(2.0, -14.0);

    half logx = log10(xCheck);
    half logy;

    if (logx <= log10(minPoint.x))
    {
        logy = logx * slopeLow + (log10(minPoint.y) - slopeLow * log10(minPoint.x));
    }
    else if ((logx > log10(minPoint.x)) && (logx < log10(midPoint.x)))
    {
        half knot_coord = (N_KNOTS_LOW - 1) * (logx - log10(minPoint.x)) / (log10(midPoint.x) - log10(minPoint.x));
        int j = knot_coord;
        half t = knot_coord - j;

        half3 cf = half3(coefsLow[j], coefsLow[j + 1], coefsLow[j + 2]);
        half3 monomials = half3(t * t, t, 1.0);
        logy = dot(monomials, mul(M, cf));
    }
    else if ((logx >= log10(midPoint.x)) && (logx < log10(maxPoint.x)))
    {
        half knot_coord = (N_KNOTS_HIGH - 1) * (logx - log10(midPoint.x)) / (log10(maxPoint.x) - log10(midPoint.x));
        int j = knot_coord;
        half t = knot_coord - j;

        half3 cf = half3(coefsHigh[j], coefsHigh[j + 1], coefsHigh[j + 2]);
        half3 monomials = half3(t * t, t, 1.0);
        logy = dot(monomials, mul(M, cf));
    }
    else
    { //if (logIn >= log10(maxPoint.x)) {
        logy = logx * slopeHigh + (log10(maxPoint.y) - slopeHigh * log10(maxPoint.x));
    }

    return pow(10.0, logy);
}

half segmented_spline_c9_fwd(half x)
{
    const half coefsLow[10] = { -1.6989700043, -1.6989700043, -1.4779000000, -1.2291000000, -0.8648000000, -0.4480000000, 0.0051800000, 0.4511080334, 0.9113744414, 0.9113744414 }; // coefs for B-spline between minPoint and midPoint (units of log luminance)
    const half coefsHigh[10] = { 0.5154386965, 0.8470437783, 1.1358000000, 1.3802000000, 1.5197000000, 1.5985000000, 1.6467000000, 1.6746091357, 1.6878733390, 1.6878733390 }; // coefs for B-spline between midPoint and maxPoint (units of log luminance)
    const half2 minPoint = half2(segmented_spline_c5_fwd(0.18 * exp2(-6.5)), 0.02); // {luminance, luminance} linear extension below this
    const half2 midPoint = half2(segmented_spline_c5_fwd(0.18), 4.8); // {luminance, luminance}
    const half2 maxPoint = half2(segmented_spline_c5_fwd(0.18 * exp2(6.5)), 48.0); // {luminance, luminance} linear extension above this
    const half slopeLow = 0.0; // log-log slope of low linear extension
    const half slopeHigh = 0.04; // log-log slope of high linear extension

    const int N_KNOTS_LOW = 8;
    const int N_KNOTS_HIGH = 8;

    // Check for negatives or zero before taking the log. If negative or zero,
    // set to OCESMIN.
    half xCheck = x;
    if (xCheck <= 0.0) xCheck = 1e-4;

    half logx = log10(xCheck);
    half logy;

    if (logx <= log10(minPoint.x))
    {
        logy = logx * slopeLow + (log10(minPoint.y) - slopeLow * log10(minPoint.x));
    }
    else if ((logx > log10(minPoint.x)) && (logx < log10(midPoint.x)))
    {
        half knot_coord = (N_KNOTS_LOW - 1) * (logx - log10(minPoint.x)) / (log10(midPoint.x) - log10(minPoint.x));
        int j = knot_coord;
        half t = knot_coord - j;

        half3 cf = half3(coefsLow[j], coefsLow[j + 1], coefsLow[j + 2]);
        half3 monomials = half3(t * t, t, 1.0);
        logy = dot(monomials, mul(M, cf));
    }
    else if ((logx >= log10(midPoint.x)) && (logx < log10(maxPoint.x)))
    {
        half knot_coord = (N_KNOTS_HIGH - 1) * (logx - log10(midPoint.x)) / (log10(maxPoint.x) - log10(midPoint.x));
        int j = knot_coord;
        half t = knot_coord - j;

        half3 cf = half3(coefsHigh[j], coefsHigh[j + 1], coefsHigh[j + 2]);
        half3 monomials = half3(t * t, t, 1.0);
        logy = dot(monomials, mul(M, cf));
    }
    else
    { //if (logIn >= log10(maxPoint.x)) {
        logy = logx * slopeHigh + (log10(maxPoint.y) - slopeHigh * log10(maxPoint.x));
    }

    return pow(10.0, logy);
}

static const half RRT_GLOW_GAIN = 0.05;
static const half RRT_GLOW_MID = 0.08;

static const half RRT_RED_SCALE = 0.82;
static const half RRT_RED_PIVOT = 0.03;
static const half RRT_RED_HUE = 0.0;
static const half RRT_RED_WIDTH = 135.0;

static const half RRT_SAT_FACTOR = 0.96;

half3 RRT(half3 aces)
{
    // --- Glow module --- //
    half saturation = rgb_2_saturation(aces);
    half ycIn = rgb_2_yc(aces);
    half s = sigmoid_shaper((saturation - 0.4) / 0.2);
    half addedGlow = 1.0 + glow_fwd(ycIn, RRT_GLOW_GAIN * s, RRT_GLOW_MID);
    aces *= addedGlow;

    // --- Red modifier --- //
    half hue = rgb_2_hue(aces);
    half centeredHue = center_hue(hue, RRT_RED_HUE);
    half hueWeight;
    {
        //hueWeight = cubic_basis_shaper(centeredHue, RRT_RED_WIDTH);
        hueWeight = smoothstep(0.0, 1.0, 1.0 - abs(2.0 * centeredHue / RRT_RED_WIDTH));
        hueWeight *= hueWeight;
    }

    aces.r += hueWeight * saturation * (RRT_RED_PIVOT - aces.r) * (1.0 - RRT_RED_SCALE);

    // --- ACES to RGB rendering space --- //
    aces = clamp(aces, 0.0, HALF_MAX);  // avoids saturated negative colors from becoming positive in the matrix
    half3 rgbPre = mul(AP0_2_AP1_MAT, aces);
    rgbPre = clamp(rgbPre, 0, HALF_MAX);

    // --- Global desaturation --- //
    //rgbPre = mul(RRT_SAT_MAT, rgbPre);
    rgbPre = lerp(dot(rgbPre, AP1_RGB2Y).xxx, rgbPre, RRT_SAT_FACTOR.xxx);

    // --- Apply the tonescale independently in rendering-space RGB --- //
    half3 rgbPost;
    rgbPost.x = segmented_spline_c5_fwd(rgbPre.x);
    rgbPost.y = segmented_spline_c5_fwd(rgbPre.y);
    rgbPost.z = segmented_spline_c5_fwd(rgbPre.z);

    // --- RGB rendering space to OCES --- //
    half3 rgbOces = mul(AP1_2_AP0_MAT, rgbPost);

    return rgbOces;
}

//
// Output Device Transform
//
half3 Y_2_linCV(half3 Y, half Ymax, half Ymin)
{
    return (Y - Ymin) / (Ymax - Ymin);
}

half3 XYZ_2_xyY(half3 XYZ)
{
    half divisor = max(dot(XYZ, (1.0).xxx), 1e-4);
    return half3(XYZ.xy / divisor, XYZ.y);
}

half3 xyY_2_XYZ(half3 xyY)
{
    half m = xyY.z / max(xyY.y, 1e-4);
    half3 XYZ = half3(xyY.xz, (1.0 - xyY.x - xyY.y));
    XYZ.xz *= m;
    return XYZ;
}

static const half DIM_SURROUND_GAMMA = 0.9811;

half3 darkSurround_to_dimSurround(half3 linearCV)
{
    half3 XYZ = mul(AP1_2_XYZ_MAT, linearCV);

    half3 xyY = XYZ_2_xyY(XYZ);
    xyY.z = clamp(xyY.z, 0.0, HALF_MAX);
    xyY.z = pow(xyY.z, DIM_SURROUND_GAMMA);
    XYZ = xyY_2_XYZ(xyY);

    return mul(XYZ_2_AP1_MAT, XYZ);
}

half moncurve_r(half y, half gamma, half offs)
{
    // Reverse monitor curve
    half x;
    const half yb = pow(offs * gamma / ((gamma - 1.0) * (1.0 + offs)), gamma);
    const half rs = pow((gamma - 1.0) / offs, gamma - 1.0) * pow((1.0 + offs) / gamma, gamma);
    if (y >= yb)
        x = (1.0 + offs) * pow(y, 1.0 / gamma) - offs;
    else
        x = y * rs;
    return x;
}

half bt1886_r(half L, half gamma, half Lw, half Lb)
{
    // The reference EOTF specified in Rec. ITU-R BT.1886
    // L = a(max[(V+b),0])^g
    half a = pow(pow(Lw, 1.0 / gamma) - pow(Lb, 1.0 / gamma), gamma);
    half b = pow(Lb, 1.0 / gamma) / (pow(Lw, 1.0 / gamma) - pow(Lb, 1.0 / gamma));
    half V = pow(max(L / a, 0.0), 1.0 / gamma) - b;
    return V;
}

half roll_white_fwd(
    half x,       // color value to adjust (white scaled to around 1.0)
    half new_wht, // white adjustment (e.g. 0.9 for 10% darkening)
    half width    // adjusted width (e.g. 0.25 for top quarter of the tone scale)
    )
{
    const half x0 = -1.0;
    const half x1 = x0 + width;
    const half y0 = -new_wht;
    const half y1 = x1;
    const half m1 = (x1 - x0);
    const half a = y0 - y1 + m1;
    const half b = 2.0 * (y1 - y0) - m1;
    const half c = y0;
    const half t = (-x - x0) / (x1 - x0);
    half o = 0.0;
    if (t < 0.0)
        o = -(t * b + c);
    else if (t > 1.0)
        o = x;
    else
        o = -((t * a + b) * t + c);
    return o;
}

half3 linear_to_sRGB(half3 x)
{
    return (x <= 0.0031308 ? (x * 12.9232102) : 1.055 * pow(x, 1.0 / 2.4) - 0.055);
}

half3 linear_to_bt1886(half3 x, half gamma, half Lw, half Lb)
{
    // Good enough approximation for now, may consider using the exact formula instead
    // TODO: Experiment
    return pow(max(x, 0.0), 1.0 / 2.4);

    // Correct implementation (Reference EOTF specified in Rec. ITU-R BT.1886) :
    // L = a(max[(V+b),0])^g
    half invgamma = 1.0 / gamma;
    half p_Lw = pow(Lw, invgamma);
    half p_Lb = pow(Lb, invgamma);
    half3 a = pow(p_Lw - p_Lb, gamma).xxx;
    half3 b = (p_Lb / p_Lw - p_Lb).xxx;
    half3 V = pow(max(x / a, 0.0), invgamma.xxx) - b;
    return V;
}

#if defined(CUSTOM_WHITE_POINT)
half CINEMA_WHITE;
half CINEMA_BLACK;
#else
static const half CINEMA_WHITE = 48.0;
static const half CINEMA_BLACK = CINEMA_WHITE / 2400.0;
#endif

static const half ODT_SAT_FACTOR = 0.93;

// <ACEStransformID>ODT.Academy.RGBmonitor_100nits_dim.a1.0.3</ACEStransformID>
// <ACESuserName>ACES 1.0 Output - sRGB</ACESuserName>

//
// Output Device Transform - RGB computer monitor
//

//
// Summary :
//  This transform is intended for mapping OCES onto a desktop computer monitor
//  typical of those used in motion picture visual effects production. These
//  monitors may occasionally be referred to as "sRGB" displays, however, the
//  monitor for which this transform is designed does not exactly match the
//  specifications in IEC 61966-2-1:1999.
//
//  The assumed observer adapted white is D65, and the viewing environment is
//  that of a dim surround.
//
//  The monitor specified is intended to be more typical of those found in
//  visual effects production.
//
// Device Primaries :
//  Primaries are those specified in Rec. ITU-R BT.709
//  CIE 1931 chromaticities:  x         y         Y
//              Red:          0.64      0.33
//              Green:        0.3       0.6
//              Blue:         0.15      0.06
//              White:        0.3127    0.329     100 cd/m^2
//
// Display EOTF :
//  The reference electro-optical transfer function specified in
//  IEC 61966-2-1:1999.
//
// Signal Range:
//    This transform outputs full range code values.
//
// Assumed observer adapted white point:
//         CIE 1931 chromaticities:    x            y
//                                     0.3127       0.329
//
// Viewing Environment:
//   This ODT has a compensation for viewing environment variables more typical
//   of those associated with video mastering.
//
half3 ODT_RGBmonitor_100nits_dim(half3 oces)
{
    // OCES to RGB rendering space
    half3 rgbPre = mul(AP0_2_AP1_MAT, oces);

    // Apply the tonescale independently in rendering-space RGB
    half3 rgbPost;
    rgbPost.x = segmented_spline_c9_fwd(rgbPre.x);
    rgbPost.y = segmented_spline_c9_fwd(rgbPre.y);
    rgbPost.z = segmented_spline_c9_fwd(rgbPre.z);

    // Scale luminance to linear code value
    half3 linearCV = Y_2_linCV(rgbPost, CINEMA_WHITE, CINEMA_BLACK);

     // Apply gamma adjustment to compensate for dim surround
    linearCV = darkSurround_to_dimSurround(linearCV);

    // Apply desaturation to compensate for luminance difference
    //linearCV = mul(ODT_SAT_MAT, linearCV);
    linearCV = lerp(dot(linearCV, AP1_RGB2Y).xxx, linearCV, ODT_SAT_FACTOR.xxx);

    // Convert to display primary encoding
    // Rendering space RGB to XYZ
    half3 XYZ = mul(AP1_2_XYZ_MAT, linearCV);

    // Apply CAT from ACES white point to assumed observer adapted white point
    XYZ = mul(D60_2_D65_CAT, XYZ);

    // CIE XYZ to display primaries
    linearCV = mul(XYZ_2_REC709_MAT, XYZ);

    // Handle out-of-gamut values
    // Clip values < 0 or > 1 (i.e. projecting outside the display primaries)
    linearCV = saturate(linearCV);

    // TODO: Revisit when it is possible to deactivate Unity default framebuffer encoding
    // with sRGB opto-electrical transfer function (OETF).
    /*
    // Encode linear code values with transfer function
    half3 outputCV;
    // moncurve_r with gamma of 2.4 and offset of 0.055 matches the EOTF found in IEC 61966-2-1:1999 (sRGB)
    const half DISPGAMMA = 2.4;
    const half OFFSET = 0.055;
    outputCV.x = moncurve_r(linearCV.x, DISPGAMMA, OFFSET);
    outputCV.y = moncurve_r(linearCV.y, DISPGAMMA, OFFSET);
    outputCV.z = moncurve_r(linearCV.z, DISPGAMMA, OFFSET);

    outputCV = linear_to_sRGB(linearCV);
    */

    // Unity already draws to a sRGB target
    return linearCV;
}

// <ACEStransformID>ODT.Academy.RGBmonitor_D60sim_100nits_dim.a1.0.3</ACEStransformID>
// <ACESuserName>ACES 1.0 Output - sRGB (D60 sim.)</ACESuserName>

//
// Output Device Transform - RGB computer monitor (D60 simulation)
//

//
// Summary :
//  This transform is intended for mapping OCES onto a desktop computer monitor
//  typical of those used in motion picture visual effects production. These
//  monitors may occasionally be referred to as "sRGB" displays, however, the
//  monitor for which this transform is designed does not exactly match the
//  specifications in IEC 61966-2-1:1999.
//
//  The assumed observer adapted white is D60, and the viewing environment is
//  that of a dim surround.
//
//  The monitor specified is intended to be more typical of those found in
//  visual effects production.
//
// Device Primaries :
//  Primaries are those specified in Rec. ITU-R BT.709
//  CIE 1931 chromaticities:  x         y         Y
//              Red:          0.64      0.33
//              Green:        0.3       0.6
//              Blue:         0.15      0.06
//              White:        0.3127    0.329     100 cd/m^2
//
// Display EOTF :
//  The reference electro-optical transfer function specified in
//  IEC 61966-2-1:1999.
//
// Signal Range:
//    This transform outputs full range code values.
//
// Assumed observer adapted white point:
//         CIE 1931 chromaticities:    x            y
//                                     0.32168      0.33767
//
// Viewing Environment:
//   This ODT has a compensation for viewing environment variables more typical
//   of those associated with video mastering.
//
half3 ODT_RGBmonitor_D60sim_100nits_dim(half3 oces)
{
    // OCES to RGB rendering space
    half3 rgbPre = mul(AP0_2_AP1_MAT, oces);

    // Apply the tonescale independently in rendering-space RGB
    half3 rgbPost;
    rgbPost.x = segmented_spline_c9_fwd(rgbPre.x);
    rgbPost.y = segmented_spline_c9_fwd(rgbPre.y);
    rgbPost.z = segmented_spline_c9_fwd(rgbPre.z);

    // Scale luminance to linear code value
    half3 linearCV = Y_2_linCV(rgbPost, CINEMA_WHITE, CINEMA_BLACK);

    // --- Compensate for different white point being darker  --- //
    // This adjustment is to correct an issue that exists in ODTs where the device
    // is calibrated to a white chromaticity other than D60. In order to simulate
    // D60 on such devices, unequal code values are sent to the display to achieve
    // neutrals at D60. In order to produce D60 on a device calibrated to the DCI
    // white point (i.e. equal code values yield CIE x,y chromaticities of 0.314,
    // 0.351) the red channel is higher than green and blue to compensate for the
    // "greenish" DCI white. This is the correct behavior but it means that as
    // highlight increase, the red channel will hit the device maximum first and
    // clip, resulting in a chromaticity shift as the green and blue channels
    // continue to increase.
    // To avoid this clipping error, a slight scale factor is applied to allow the
    // ODTs to simulate D60 within the D65 calibration white point.

    // Scale and clamp white to avoid casted highlights due to D60 simulation
    const half SCALE = 0.955;
    linearCV = min(linearCV, 1.0) * SCALE;

    // Apply gamma adjustment to compensate for dim surround
    linearCV = darkSurround_to_dimSurround(linearCV);

    // Apply desaturation to compensate for luminance difference
    //linearCV = mul(ODT_SAT_MAT, linearCV);
    linearCV = lerp(dot(linearCV, AP1_RGB2Y).xxx, linearCV, ODT_SAT_FACTOR.xxx);

    // Convert to display primary encoding
    // Rendering space RGB to XYZ
    half3 XYZ = mul(AP1_2_XYZ_MAT, linearCV);

    // CIE XYZ to display primaries
    linearCV = mul(XYZ_2_REC709_MAT, XYZ);

    // Handle out-of-gamut values
    // Clip values < 0 or > 1 (i.e. projecting outside the display primaries)
    linearCV = saturate(linearCV);

    // TODO: Revisit when it is possible to deactivate Unity default framebuffer encoding
    // with sRGB opto-electrical transfer function (OETF).
    /*
    // Encode linear code values with transfer function
    half3 outputCV;
    // moncurve_r with gamma of 2.4 and offset of 0.055 matches the EOTF found in IEC 61966-2-1:1999 (sRGB)
    const half DISPGAMMA = 2.4;
    const half OFFSET = 0.055;
    outputCV.x = moncurve_r(linearCV.x, DISPGAMMA, OFFSET);
    outputCV.y = moncurve_r(linearCV.y, DISPGAMMA, OFFSET);
    outputCV.z = moncurve_r(linearCV.z, DISPGAMMA, OFFSET);

    outputCV = linear_to_sRGB(linearCV);
    */

    // Unity already draws to a sRGB target
    return linearCV;
}

// <ACEStransformID>ODT.Academy.Rec709_100nits_dim.a1.0.3</ACEStransformID>
// <ACESuserName>ACES 1.0 Output - Rec.709</ACESuserName>

//
// Output Device Transform - Rec709
//

//
// Summary :
//  This transform is intended for mapping OCES onto a Rec.709 broadcast monitor
//  that is calibrated to a D65 white point at 100 cd/m^2. The assumed observer
//  adapted white is D65, and the viewing environment is a dim surround.
//
//  A possible use case for this transform would be HDTV/video mastering.
//
// Device Primaries :
//  Primaries are those specified in Rec. ITU-R BT.709
//  CIE 1931 chromaticities:  x         y         Y
//              Red:          0.64      0.33
//              Green:        0.3       0.6
//              Blue:         0.15      0.06
//              White:        0.3127    0.329     100 cd/m^2
//
// Display EOTF :
//  The reference electro-optical transfer function specified in
//  Rec. ITU-R BT.1886.
//
// Signal Range:
//    By default, this transform outputs full range code values. If instead a
//    SMPTE "legal" signal is desired, there is a runtime flag to output
//    SMPTE legal signal. In ctlrender, this can be achieved by appending
//    '-param1 legalRange 1' after the '-ctl odt.ctl' string.
//
// Assumed observer adapted white point:
//         CIE 1931 chromaticities:    x            y
//                                     0.3127       0.329
//
// Viewing Environment:
//   This ODT has a compensation for viewing environment variables more typical
//   of those associated with video mastering.
//
half3 ODT_Rec709_100nits_dim(half3 oces)
{
    // OCES to RGB rendering space
    half3 rgbPre = mul(AP0_2_AP1_MAT, oces);

    // Apply the tonescale independently in rendering-space RGB
    half3 rgbPost;
    rgbPost.x = segmented_spline_c9_fwd(rgbPre.x);
    rgbPost.y = segmented_spline_c9_fwd(rgbPre.y);
    rgbPost.z = segmented_spline_c9_fwd(rgbPre.z);

    // Scale luminance to linear code value
    half3 linearCV = Y_2_linCV(rgbPost, CINEMA_WHITE, CINEMA_BLACK);

    // Apply gamma adjustment to compensate for dim surround
    linearCV = darkSurround_to_dimSurround(linearCV);

    // Apply desaturation to compensate for luminance difference
    //linearCV = mul(ODT_SAT_MAT, linearCV);
    linearCV = lerp(dot(linearCV, AP1_RGB2Y).xxx, linearCV, ODT_SAT_FACTOR.xxx);

    // Convert to display primary encoding
    // Rendering space RGB to XYZ
    half3 XYZ = mul(AP1_2_XYZ_MAT, linearCV);

    // Apply CAT from ACES white point to assumed observer adapted white point
    XYZ = mul(D60_2_D65_CAT, XYZ);

    // CIE XYZ to display primaries
    linearCV = mul(XYZ_2_REC709_MAT, XYZ);

    // Handle out-of-gamut values
    // Clip values < 0 or > 1 (i.e. projecting outside the display primaries)
    linearCV = saturate(linearCV);

    // Encode linear code values with transfer function
    const half DISPGAMMA = 2.4;
    const half L_W = 1.0;
    const half L_B = 0.0;
    half3 outputCV = linear_to_bt1886(linearCV, DISPGAMMA, L_W, L_B);

    // TODO: Implement support for legal range.

    // NOTE: Unity framebuffer encoding is encoded with sRGB opto-electrical transfer function (OETF)
    // by default which will result in double perceptual encoding, thus for now if one want to use
    // this ODT, he needs to decode its output with sRGB electro-optical transfer function (EOTF) to
    // compensate for Unity default behaviour.

    return outputCV;
}

// <ACEStransformID>ODT.Academy.Rec709_D60sim_100nits_dim.a1.0.3</ACEStransformID>
// <ACESuserName>ACES 1.0 Output - Rec.709 (D60 sim.)</ACESuserName>

//
// Output Device Transform - Rec709 (D60 simulation)
//

//
// Summary :
//  This transform is intended for mapping OCES onto a Rec.709 broadcast monitor
//  that is calibrated to a D65 white point at 100 cd/m^2. The assumed observer
//  adapted white is D60, and the viewing environment is a dim surround.
//
//  A possible use case for this transform would be cinema "soft-proofing".
//
// Device Primaries :
//  Primaries are those specified in Rec. ITU-R BT.709
//  CIE 1931 chromaticities:  x         y         Y
//              Red:          0.64      0.33
//              Green:        0.3       0.6
//              Blue:         0.15      0.06
//              White:        0.3127    0.329     100 cd/m^2
//
// Display EOTF :
//  The reference electro-optical transfer function specified in
//  Rec. ITU-R BT.1886.
//
// Signal Range:
//    By default, this transform outputs full range code values. If instead a
//    SMPTE "legal" signal is desired, there is a runtime flag to output
//    SMPTE legal signal. In ctlrender, this can be achieved by appending
//    '-param1 legalRange 1' after the '-ctl odt.ctl' string.
//
// Assumed observer adapted white point:
//         CIE 1931 chromaticities:    x            y
//                                     0.32168      0.33767
//
// Viewing Environment:
//   This ODT has a compensation for viewing environment variables more typical
//   of those associated with video mastering.
//
half3 ODT_Rec709_D60sim_100nits_dim(half3 oces)
{
    // OCES to RGB rendering space
    half3 rgbPre = mul(AP0_2_AP1_MAT, oces);

    // Apply the tonescale independently in rendering-space RGB
    half3 rgbPost;
    rgbPost.x = segmented_spline_c9_fwd(rgbPre.x);
    rgbPost.y = segmented_spline_c9_fwd(rgbPre.y);
    rgbPost.z = segmented_spline_c9_fwd(rgbPre.z);

    // Scale luminance to linear code value
    half3 linearCV = Y_2_linCV(rgbPost, CINEMA_WHITE, CINEMA_BLACK);

    // --- Compensate for different white point being darker  --- //
    // This adjustment is to correct an issue that exists in ODTs where the device
    // is calibrated to a white chromaticity other than D60. In order to simulate
    // D60 on such devices, unequal code values must be sent to the display to achieve
    // the chromaticities of D60. More specifically, in order to produce D60 on a device
    // calibrated to a D65 white point (i.e. equal code values yield CIE x,y
    // chromaticities of 0.3127, 0.329) the red channel must be slightly higher than
    // that of green and blue in order to compensate for the relatively more "blue-ish"
    // D65 white. This unequalness of color channels is the correct behavior but it
    // means that as neutral highlights increase, the red channel will hit the
    // device maximum first and clip, resulting in a small chromaticity shift as the
    // green and blue channels continue to increase to their maximums.
    // To avoid this clipping error, a slight scale factor is applied to allow the
    // ODTs to simulate D60 within the D65 calibration white point.

    // Scale and clamp white to avoid casted highlights due to D60 simulation
    const half SCALE = 0.955;
    linearCV = min(linearCV, 1.0) * SCALE;

    // Apply gamma adjustment to compensate for dim surround
    linearCV = darkSurround_to_dimSurround(linearCV);

    // Apply desaturation to compensate for luminance difference
    //linearCV = mul(ODT_SAT_MAT, linearCV);
    linearCV = lerp(dot(linearCV, AP1_RGB2Y).xxx, linearCV, ODT_SAT_FACTOR.xxx);

    // Convert to display primary encoding
    // Rendering space RGB to XYZ
    half3 XYZ = mul(AP1_2_XYZ_MAT, linearCV);

    // CIE XYZ to display primaries
    linearCV = mul(XYZ_2_REC709_MAT, XYZ);

    // Handle out-of-gamut values
    // Clip values < 0 or > 1 (i.e. projecting outside the display primaries)
    linearCV = saturate(linearCV);

    // Encode linear code values with transfer function
    const half DISPGAMMA = 2.4;
    const half L_W = 1.0;
    const half L_B = 0.0;
    half3 outputCV = linear_to_bt1886(linearCV, DISPGAMMA, L_W, L_B);

    // TODO: Implement support for legal range.

    // NOTE: Unity framebuffer encoding is encoded with sRGB opto-electrical transfer function (OETF)
    // by default which will result in double perceptual encoding, thus for now if one want to use
    // this ODT, he needs to decode its output with sRGB electro-optical transfer function (EOTF) to
    // compensate for Unity default behaviour.

    return outputCV;
}

// <ACEStransformID>ODT.Academy.Rec2020_100nits_dim.a1.0.3</ACEStransformID>
// <ACESuserName>ACES 1.0 Output - Rec.2020</ACESuserName>

//
// Output Device Transform - Rec2020
//

//
// Summary :
//  This transform is intended for mapping OCES onto a Rec.2020 broadcast
//  monitor that is calibrated to a D65 white point at 100 cd/m^2. The assumed
//  observer adapted white is D65, and the viewing environment is that of a dim
//  surround.
//
//  A possible use case for this transform would be UHDTV/video mastering.
//
// Device Primaries :
//  Primaries are those specified in Rec. ITU-R BT.2020
//  CIE 1931 chromaticities:  x         y         Y
//              Red:          0.708     0.292
//              Green:        0.17      0.797
//              Blue:         0.131     0.046
//              White:        0.3127    0.329     100 cd/m^2
//
// Display EOTF :
//  The reference electro-optical transfer function specified in
//  Rec. ITU-R BT.1886.
//
// Signal Range:
//    By default, this transform outputs full range code values. If instead a
//    SMPTE "legal" signal is desired, there is a runtime flag to output
//    SMPTE legal signal. In ctlrender, this can be achieved by appending
//    '-param1 legalRange 1' after the '-ctl odt.ctl' string.
//
// Assumed observer adapted white point:
//         CIE 1931 chromaticities:    x            y
//                                     0.3127       0.329
//
// Viewing Environment:
//   This ODT has a compensation for viewing environment variables more typical
//   of those associated with video mastering.
//

half3 ODT_Rec2020_100nits_dim(half3 oces)
{
    // OCES to RGB rendering space
    half3 rgbPre = mul(AP0_2_AP1_MAT, oces);

    // Apply the tonescale independently in rendering-space RGB
    half3 rgbPost;
    rgbPost.x = segmented_spline_c9_fwd(rgbPre.x);
    rgbPost.y = segmented_spline_c9_fwd(rgbPre.y);
    rgbPost.z = segmented_spline_c9_fwd(rgbPre.z);

    // Scale luminance to linear code value
    half3 linearCV = Y_2_linCV(rgbPost, CINEMA_WHITE, CINEMA_BLACK);

    // Apply gamma adjustment to compensate for dim surround
    linearCV = darkSurround_to_dimSurround(linearCV);

    // Apply desaturation to compensate for luminance difference
    //linearCV = mul(ODT_SAT_MAT, linearCV);
    linearCV = lerp(dot(linearCV, AP1_RGB2Y).xxx, linearCV, ODT_SAT_FACTOR.xxx);

    // Convert to display primary encoding
    // Rendering space RGB to XYZ
    half3 XYZ = mul(AP1_2_XYZ_MAT, linearCV);

    // Apply CAT from ACES white point to assumed observer adapted white point
    XYZ = mul(D60_2_D65_CAT, XYZ);

    // CIE XYZ to display primaries
    linearCV = mul(XYZ_2_REC2020_MAT, XYZ);

    // Handle out-of-gamut values
    // Clip values < 0 or > 1 (i.e. projecting outside the display primaries)
    linearCV = saturate(linearCV);

    // Encode linear code values with transfer function
    const half DISPGAMMA = 2.4;
    const half L_W = 1.0;
    const half L_B = 0.0;
    half3 outputCV = linear_to_bt1886(linearCV, DISPGAMMA, L_W, L_B);

    // TODO: Implement support for legal range.

    // NOTE: Unity framebuffer encoding is encoded with sRGB opto-electrical transfer function (OETF)
    // by default which will result in double perceptual encoding, thus for now if one want to use
    // this ODT, he needs to decode its output with sRGB electro-optical transfer function (EOTF) to
    // compensate for Unity default behaviour.

    return outputCV;
}

// <ACEStransformID>ODT.Academy.P3DCI_48nits.a1.0.3</ACEStransformID>
// <ACESuserName>ACES 1.0 Output - P3-DCI</ACESuserName>

//
// Output Device Transform - P3DCI (D60 Simulation)
//

//
// Summary :
//  This transform is intended for mapping OCES onto a P3 digital cinema
//  projector that is calibrated to a DCI white point at 48 cd/m^2. The assumed
//  observer adapted white is D60, and the viewing environment is that of a dark
//  theater.
//
// Device Primaries :
//  CIE 1931 chromaticities:  x         y         Y
//              Red:          0.68      0.32
//              Green:        0.265     0.69
//              Blue:         0.15      0.06
//              White:        0.314     0.351     48 cd/m^2
//
// Display EOTF :
//  Gamma: 2.6
//
// Assumed observer adapted white point:
//         CIE 1931 chromaticities:    x            y
//                                     0.32168      0.33767
//
// Viewing Environment:
//  Environment specified in SMPTE RP 431-2-2007
//
half3 ODT_P3DCI_48nits(half3 oces)
{
    // OCES to RGB rendering space
    half3 rgbPre = mul(AP0_2_AP1_MAT, oces);

    // Apply the tonescale independently in rendering-space RGB
    half3 rgbPost;
    rgbPost.x = segmented_spline_c9_fwd(rgbPre.x);
    rgbPost.y = segmented_spline_c9_fwd(rgbPre.y);
    rgbPost.z = segmented_spline_c9_fwd(rgbPre.z);

    // Scale luminance to linear code value
    half3 linearCV = Y_2_linCV(rgbPost, CINEMA_WHITE, CINEMA_BLACK);

    // --- Compensate for different white point being darker  --- //
    // This adjustment is to correct an issue that exists in ODTs where the device
    // is calibrated to a white chromaticity other than D60. In order to simulate
    // D60 on such devices, unequal code values are sent to the display to achieve
    // neutrals at D60. In order to produce D60 on a device calibrated to the DCI
    // white point (i.e. equal code values yield CIE x,y chromaticities of 0.314,
    // 0.351) the red channel is higher than green and blue to compensate for the
    // "greenish" DCI white. This is the correct behavior but it means that as
    // highlight increase, the red channel will hit the device maximum first and
    // clip, resulting in a chromaticity shift as the green and blue channels
    // continue to increase.
    // To avoid this clipping error, a slight scale factor is applied to allow the
    // ODTs to simulate D60 within the D65 calibration white point. However, the
    // magnitude of the scale factor required for the P3DCI ODT was considered too
    // large. Therefore, the scale factor was reduced and the additional required
    // compression was achieved via a reshaping of the highlight rolloff in
    // conjunction with the scale. The shape of this rolloff was determined
    // throught subjective experiments and deemed to best reproduce the
    // "character" of the highlights in the P3D60 ODT.

    // Roll off highlights to avoid need for as much scaling
    const half NEW_WHT = 0.918;
    const half ROLL_WIDTH = 0.5;
    linearCV.x = roll_white_fwd(linearCV.x, NEW_WHT, ROLL_WIDTH);
    linearCV.y = roll_white_fwd(linearCV.y, NEW_WHT, ROLL_WIDTH);
    linearCV.z = roll_white_fwd(linearCV.z, NEW_WHT, ROLL_WIDTH);

    // Scale and clamp white to avoid casted highlights due to D60 simulation
    const half SCALE = 0.96;
    linearCV = min(linearCV, NEW_WHT) * SCALE;

    // Convert to display primary encoding
    // Rendering space RGB to XYZ
    half3 XYZ = mul(AP1_2_XYZ_MAT, linearCV);

    // CIE XYZ to display primaries
    linearCV = mul(XYZ_2_DCIP3_MAT, XYZ);

    // Handle out-of-gamut values
    // Clip values < 0 or > 1 (i.e. projecting outside the display primaries)
    linearCV = saturate(linearCV);

    // Encode linear code values with transfer function
    const half DISPGAMMA = 2.6;
    half3 outputCV = pow(linearCV, 1.0 / DISPGAMMA);

    // NOTE: Unity framebuffer encoding is encoded with sRGB opto-electrical transfer function (OETF)
    // by default which will result in double perceptual encoding, thus for now if one want to use
    // this ODT, he needs to decode its output with sRGB electro-optical transfer function (EOTF) to
    // compensate for Unity default behaviour.

    return outputCV;
}

#endif // __ACES__