// Copyright Epic Games, Inc. All Rights Reserved. // urn:ampas:aces:transformId:v2.0:Lib.Academy.OutputTransform.a2.v1 // Output Transform Functions // // Library File with functions and presets used for the forward and inverse output // transform // #pragma once #include "ACESUtilities.ush" #include "ACESTonescale.ush" // Gamut compression constants static const float smoothCusps = 0.12; static const float smoothM = 0.27; static const float cuspMidBlend = 1.3; static const float focusGainBlend = 0.3; static const float focusAdjustGain = 0.55; static const float focusDistance = 1.35; static const float focusDistanceScaling = 1.75; // Constant used in gamut mapping static const float compressionThreshold = 0.75; static const int tableSize = 360; // add 1 extra entry at end that is to duplicate first entry for wrapped hue static const int additionalTableEntries = 2; // allots for extra entries to wrap the hues without special cases static const int totalTableSize = tableSize + additionalTableEntries; static const int baseIndex = 1; // array index for smallest hue, which is not necessarily a 0.0 hue angle static const float gammaMinimum = 0.0; static const float gammaMaximum = 5.0; static const float gammaSearchStep = 0.4; static const float gammaAccuracy = 1e-5; static const float referenceLuminance = 100.; // CAM Parameters static const float L_A = 100.; static const float Y_b = 20.; static const float ac_resp = 1.0; static const float ra = 2. * ac_resp; static const float ba = 0.05 + (2. - ra); static const float3 surround = float3(0.9, 0.59, 0.9); // Matrix for Hellwig inverse static const float3x3 panlrcm = { 460.0, 451.0, 288.0 , 460.0, -891.0, -261.0 , 460.0, -220.0, -6300.0 }; struct ODTParams { float peakLuminance; // Tonescale // Set via TSParams structure float n_r; // normalized white in nits (what 1.0 should be) float g; // surround / contrast float t_1; // shadow toe or flare/glare compensation float c_t; float s_2; float u_2; float m_2; // Chroma Compression float limitJmax; float midJ; float model_gamma; float sat; float sat_thr; float compr; float chromaCompressScale; float focusDist; // Limit float3x3 LIMIT_RGB_TO_XYZ; float3x3 LIMIT_XYZ_TO_RGB; float3 XYZ_w_limit; // Output float3x3 OUTPUT_RGB_TO_XYZ; float3x3 OUTPUT_XYZ_TO_RGB; float3 XYZ_w_output; float lowerHullGamma; Texture2D reach_table; Texture2D gamut_table; Texture2D gamma_table; }; float wrap_to_360(float hue) { float y = fmod(hue, 360.); if (y < 0.) { y = y + 360.; } return y; } int hue_position_in_uniform_table(float hue, int table_size) { const float wrapped_hue = wrap_to_360(hue); int result = (wrapped_hue / 360. * table_size); return result; } int next_position_in_table(int entry, int table_size) { int result = (entry + 1) % table_size; return result; } float base_hue_for_position(int i_lo, int table_size) { float result = i_lo * 360. / table_size; return result; } // CAM Functions float3 post_adaptation_non_linear_response_compression_forward(float3 RGB, float F_L) { float3 F_L_RGB = spow(F_L * abs(RGB) / 100., 0.42); float3 RGB_c = (400. * copysign(1., RGB) * F_L_RGB) / (27.13 + F_L_RGB); return RGB_c; } float3 post_adaptation_non_linear_response_compression_inverse(float3 RGB, float F_L) { float3 RGB_p = sign(RGB) * 100. / F_L * spow((27.13 * abs(RGB)) / (400. - abs(RGB)), 1.0 / 0.42); return RGB_p; } float3 XYZ_to_Hellwig2022_JMh( float3 XYZ, float3 XYZ_w) { float Y_w = XYZ_w[1]; // Step 0 - Converting CIE XYZ tristimulus values to sharpened RGB values float3 RGB_w = mul(XYZ_2_CAM16_MAT, XYZ_w); // Ignore degree of adaptation. float D = 1.0; // Viewing condition dependent parameters float k = 1. / (5. * L_A + 1.); float k4 = pow(k, 4.); float F_L = 0.2 * k4 * (5. * L_A) + 0.1 * pow((1. - k4), 2.) * spow(5. * L_A, 1. / 3.); float n = Y_b / Y_w; float z = 1.48 + sqrt(n); float3 D_RGB = D * Y_w / RGB_w + 1. - D; float3 RGB_wc = D_RGB * RGB_w; float3 RGB_aw = post_adaptation_non_linear_response_compression_forward(RGB_wc, F_L); float A_w = ra * RGB_aw[0] + RGB_aw[1] + ba * RGB_aw[2]; // Step 1 - Converting CIE XYZ tristimulus values to sharpened RGB values float3 RGB = mul(XYZ_2_CAM16_MAT, XYZ); // Step 2 float3 RGB_c = D_RGB * RGB; // Step 3 - apply forward post-adaptation non-linear response compression float3 RGB_a = post_adaptation_non_linear_response_compression_forward(RGB_c, F_L); // Step 4 - Converting to preliminary cartesian coordinates float a = RGB_a[0] - 12. * RGB_a[1] / 11. + RGB_a[2] / 11.; float b = (RGB_a[0] + RGB_a[1] - 2. * RGB_a[2]) / 9.; // Computing the hue angle math h // Unclear why this isn't matching the python version float hr = atan2(b, a); float h = wrap_to_360(radians_to_degrees(hr)); // Step 6 - Computing achromatic responses for the stimulus float A = ra * RGB_a[0] + RGB_a[1] + ba * RGB_a[2]; // Step 7 - Computing the correlate of lightness, J float J = 100. * pow(A / A_w, surround[1] * z); // Step 9 - Computing the correlate of colourfulness, M float M = (J == 0.0) ? 0.0 : 43. * surround[2] * sqrt(a * a + b * b); float3 return_JMh = float3(J, M, h); return return_JMh; } float3 Hellwig2022_JMh_to_XYZ( float3 JMh, float3 XYZ_w) { float J = JMh[0]; float M = JMh[1]; float h = JMh[2]; float Y_w = XYZ_w[1]; // Step 0 - Converting CIE XYZ tristimulus values to sharpened RGB values. float3 RGB_w = mul(XYZ_2_CAM16_MAT, XYZ_w); // Ignore degree of adaptation. float D = 1.0; // Viewing condition dependent parameters float k = 1.0 / (5.0 * L_A + 1.0); float k4 = pow(k, 4.0); float F_L = 0.2 * k4 * (5.0 * L_A) + 0.1 * pow((1.0 - k4), 2.0) * spow(5.0 * L_A, 1.0 / 3.0); float n = sdiv(Y_b, Y_w); float z = 1.48 + sqrt(n); float3 D_RGB = D * Y_w / RGB_w + 1.0 - D; float3 RGB_wc = D_RGB * RGB_w; float3 RGB_aw = post_adaptation_non_linear_response_compression_forward(RGB_wc, F_L); float A_w = ra * RGB_aw[0] + RGB_aw[1] + ba * RGB_aw[2]; float hr = degrees_to_radians(h); // No Helmholtz-Kohlrausch effect // Computing achromatic respons A for the stimulus float A = A_w * spow(J / 100.0, 1.0 / (surround[1] * z)); // Computing P_p_1 to P_p_2 float P_p_1 = 43.0 * surround[2]; float P_p_2 = A; // Step 3 - Computing opponent colour dimensions a and b float gamma = M / P_p_1; float a = gamma * cos(hr); float b = gamma * sin(hr); // Step 4 - Applying post-adaptation non-linear response compression matrix float3 vec = {P_p_2, a, b}; float3 RGB_a = (1.0 / 1403.0) * mul(panlrcm, vec); // Step 5 - Applying the inverse post-adaptation non-linear respnose compression float3 RGB_c = post_adaptation_non_linear_response_compression_inverse(RGB_a, F_L); // Step 6 float3 RGB = RGB_c / D_RGB; // Step 7 float3 XYZ = mul(CAM16_2_XYZ_MAT, RGB); return XYZ; } float Hellwig_J_to_Y(float J, float surround = 0.59, float L_A = 100., float Y_b = 20.) { // Viewing conditions dependent parameters (could be pre-calculated) float k = 1.0 / (5. * L_A + 1.); float k4 = k * k * k * k; float F_L = 0.2 * k4 * (5. * L_A) + 0.1 * pow((1. - k4), 2.) * pow(5. * L_A, 1. / 3.); float n = Y_b / 100.; float z = 1.48 + sqrt(n); float F_L_W = pow(F_L, 0.42); float A_w = (400. * F_L_W) / (27.13 + F_L_W); float A = A_w * pow(abs(J) / 100., 1. / (surround * z)); return sign(J) * 100. / F_L * pow((27.13 * A) / (400.0 - A), 1. / 0.42); } float Y_to_Hellwig_J(float Y, float surround = 0.59, float L_A = 100., float Y_b = 20.) { // Viewing conditions dependent parameters (could be pre-calculated) float k = 1.0 / (5. * L_A + 1.); float k4 = k * k * k * k; float F_L = 0.2 * k4 * (5. * L_A) + 0.1 * pow((1. - k4), 2.) * pow(5. * L_A, 1. / 3.); float n = Y_b / 100.; float z = 1.48 + sqrt(n); float F_L_W = pow(F_L, 0.42); float A_w = (400. * F_L_W) / (27.13 + F_L_W); float F_L_Y = pow(F_L * abs(Y) / 100., 0.42); return sign(Y) * 100. * pow(((400. * F_L_Y) / (27.13 + F_L_Y)) / A_w, surround * z); } float3 clamp_AP1(float3 AP1, float peakLuminance) { const float upper_clamp_limit = 8. * (128 + 768 * (log(peakLuminance/100.)/log(10000./100.))); // limit to nice power of 2 (3 stops) above that needed to max out // note the quantity (128 + ...) is the definition of r_hit from the tonescale constants float3 ap1_clamped = clamp(AP1, 0., upper_clamp_limit); float3 XYZ_clamped = mul(AP1_2_XYZ_MAT, ap1_clamped); return XYZ_clamped; } float3 aces_to_JMh(float3 aces, float peakLuminance) { // AP0 to AP1 float3 AP1 = mul(AP0_2_AP1_MAT, aces); // Clamp to half float range float3 XYZ = clamp_AP1(AP1, peakLuminance); // XYZ to JMh const float3 RGB_w = referenceLuminance; float3 XYZ_w = mul(AP0_2_XYZ_MAT, RGB_w); float3 XYZluminance = referenceLuminance * XYZ; float3 JMh = XYZ_to_Hellwig2022_JMh(XYZluminance, XYZ_w); return JMh; } float3 JMh_to_aces(float3 JMh, float peakLuminance) { const float3 RGB_w = referenceLuminance; float3 XYZ_w_aces = mul(AP0_2_XYZ_MAT, RGB_w); // JMh to XYZ float3 XYZluminance = Hellwig2022_JMh_to_XYZ(JMh, XYZ_w_aces); float3 XYZ = (1. / referenceLuminance) * XYZluminance; // XYZ to ACES float3 ACES = mul(XYZ_2_AP0_MAT, XYZ); return ACES; } ODTParams init_ODTParams( float peakLuminance, float3x3 LIMIT_RGB_TO_XYZ, float3x3 LIMIT_XYZ_TO_RGB, float3x3 OUTPUT_RGB_TO_XYZ, float3x3 OUTPUT_XYZ_TO_RGB, Texture2D reach_table, Texture2D gamut_table, Texture2D gamma_table ) { TSParams TSPARAMS = init_TSParams(peakLuminance); float limitJmax = Y_to_Hellwig_J(peakLuminance); float midJ = Y_to_Hellwig_J(TSPARAMS.c_t * 100.); // Chroma compress presets static const float chroma_compress = 2.4; static const float chroma_compress_fact = 3.3; static const float chroma_expand = 1.3; static const float chroma_expand_fact = 0.69; static const float chroma_expand_thr = 0.5; // Calculated chroma compress variables const float log_peak = log10(TSPARAMS.n / TSPARAMS.n_r); const float compr = chroma_compress + (chroma_compress * chroma_compress_fact) * log_peak; const float sat = max(0.2, chroma_expand - (chroma_expand * chroma_expand_fact) * log_peak); const float sat_thr = chroma_expand_thr / TSPARAMS.n; const float chromaCompressScale = pow(0.03379 * TSPARAMS.n, 0.30596) - 0.45135; const float model_gamma = 1. / (surround[1] * (1.48 + sqrt(Y_b / L_A))); const float focusDist = focusDistance + focusDistance * focusDistanceScaling * log_peak; static const float3 RGB_w = float3(100., 100., 100.); const float lowerHullGamma = 1.14 + 0.07 * log_peak; // Limiting Primaries float3 XYZ_w_limit = mul(LIMIT_RGB_TO_XYZ, RGB_w); // Output / Encoding Primaries float3 XYZ_w_output = mul(OUTPUT_RGB_TO_XYZ, RGB_w); const ODTParams ODTPARAMS = { peakLuminance, // Tonescale TSPARAMS.n_r, // normalized white in nits (what 1.0 should be) TSPARAMS.g, // surround / contrast TSPARAMS.t_1, TSPARAMS.c_t, TSPARAMS.s_2, TSPARAMS.u_2, TSPARAMS.m_2, // Chroma Compression limitJmax, midJ, model_gamma, sat, sat_thr, compr, chromaCompressScale, focusDist, // Limit LIMIT_RGB_TO_XYZ, LIMIT_XYZ_TO_RGB, XYZ_w_limit, // Output LIMIT_RGB_TO_XYZ, OUTPUT_XYZ_TO_RGB, XYZ_w_output, lowerHullGamma, // lowerHullGamma reach_table, gamut_table, gamma_table }; return ODTPARAMS; } // Chroma compression // // Compresses colors inside the gamut with the aim for colorfulness to have an // appropriate rate of change from display black to display white, and from // achromatic outward to purer colors. // float chromaCompressionNorm(float h, ODTParams PARAMS) { float hr = degrees_to_radians(h); float a = cos(hr); float b = sin(hr); float cos_hr2 = a * a - b * b; float sin_hr2 = 2.0 * a * b; float cos_hr3 = 4.0 * a * a * a - 3.0 * a; float sin_hr3 = 3.0 * b - 4.0 * b * b * b; float M = 11.34072 * a + 16.46899 * cos_hr2 + 7.88380 * cos_hr3 + 14.66441 * b + -6.37224 * sin_hr2 + 9.19364 * sin_hr3 + 77.12896; return M * PARAMS.chromaCompressScale; } // A "toe" function that remaps the given value x between 0 and limit. // The k1 and k2 parameters change the size and shape of the toe. // https://www.desmos.com/calculator/6vplvw14ti float toe_fwd(float x, float limit, float k1_in, float k2_in) { if (x > limit) return x; const float k2 = max(k2_in, 0.001); const float k1 = sqrt(k1_in * k1_in + k2 * k2); const float k3 = (limit + k1) / (limit + k2); const float minus_b = k3 * x - k1; const float minus_c = k2 * k3 * x; return 0.5 * (minus_b + sqrt(minus_b * minus_b + 4. * minus_c)); } float toe_inv(float x, float limit, float k1_in, float k2_in) { if (x > limit) return x; float k2 = max(k2_in, 0.001); float k1 = sqrt(k1_in * k1_in + k2 * k2); float k3 = (limit + k1) / (limit + k2); return (x * x + k1 * x) / (k3 * (x + k2)); } float reachMFromTable(float h, Texture2D reach_table) { int i_lo = hue_position_in_uniform_table(h, tableSize); int i_hi = next_position_in_table(i_lo, tableSize); float t = (h - i_lo) / (i_hi - i_lo); return lerp(reach_table.Load(int3(i_lo, 0, 0)), reach_table.Load(int3(i_hi, 0, 0)), t); } // In-gamut chroma compression // // Compresses colors inside the gamut with the aim for colorfulness to have an // appropriate rate of change from display black to display white, and from // achromatic outward to purer colors. float chromaCompression(float3 JMh, float origJ, ODTParams PARAMS, bool _invert = false) { float J = JMh[0]; float M = JMh[1]; float h = JMh[2]; if (M == 0.0) { return M; } float nJ = J / PARAMS.limitJmax; float snJ = max(0., 1. - nJ); float Mnorm = chromaCompressionNorm(h, PARAMS); float limit = pow(nJ, PARAMS.model_gamma) * reachMFromTable(h, PARAMS.reach_table) / Mnorm; float toe_limit = limit - 0.001; float toe_snJ_sat = snJ * PARAMS.sat; float toe_sqrt_nJ_sat_thr = sqrt(nJ * nJ + PARAMS.sat_thr); float toe_nJ_compr = nJ * PARAMS.compr; if (!_invert) { // Forward chroma compression // Rescaling of M with the tonescaled J to get the M to the same range as // J after the tonescale. The rescaling uses the Hellwig2022 model gamma to // keep the M/J ratio correct (keeping the chromaticities constant). M = M * pow(J / origJ, PARAMS.model_gamma); // Normalize M with the rendering space cusp M M = M / Mnorm; // Expand the colorfulness by running the toe function in reverse. The goal is to // expand less saturated colors less and more saturated colors more. The expansion // increases saturation in the shadows and mid-tones but not in the highlights. // The 0.001 offset starts the expansions slightly above zero. The sat_thr makes // the toe less aggressive near black to reduce the expansion of noise. M = limit - toe_fwd(limit - M, toe_limit, toe_snJ_sat, toe_sqrt_nJ_sat_thr); // Compress the colorfulness. The goal is to compress less saturated colors more and // more saturated colors less, especially in the highlights. This step creates the // saturation roll-off in the highlights, but attemps to preserve pure colors. This // mostly affects highlights and mid-tones, and does not compress shadows. M = toe_fwd(M, limit, toe_nJ_compr, snJ); // Denormalize M = M * Mnorm; } else { M = M / Mnorm; M = toe_inv(M, limit, toe_nJ_compr, snJ); M = limit - toe_inv(limit - M, toe_limit, toe_snJ_sat, toe_sqrt_nJ_sat_thr); M = M * Mnorm; M = M * pow(J / origJ, -PARAMS.model_gamma); } return M; } float compressionFunction(float v, float thr, float lim, bool _invert = false) { float s = (lim - thr) * (1.0 - thr) / (lim - 1.0); float nd = (v - thr) / s; float vCompressed; if (_invert) { if (v < thr || lim <= 1.0001 || v > thr + s) { vCompressed = v; } else { vCompressed = thr + s * (-nd / (nd - 1)); } } else { if (v < thr || lim <= 1.0001) { vCompressed = v; } else { vCompressed = thr + s * nd / (1.0 + nd); } } return vCompressed; } int midpoint(int low, int high) { return (low + high) / 2; } float2 cuspFromTable(float h, Texture2D gamut_table) { float3 lo; float3 hi; int low_i = 0; int high_i = baseIndex + tableSize; // allowed as we have an extra entry in the table int i = hue_position_in_uniform_table(h, tableSize) + baseIndex; while (low_i + 1 < high_i) { if (h > gamut_table.Load(int3(i, 0, 0))[2]) { low_i = i; } else { high_i = i; } i = midpoint(low_i, high_i); } lo = gamut_table.Load(int3(high_i - 1, 0, 0)); hi = gamut_table.Load(int3(high_i, 0, 0)); float t = (h - lo[2]) / (hi[2] - lo[2]); float cuspJ = lerp(lo[0], hi[0], t); float cuspM = lerp(lo[1], hi[1], t); float2 cuspJM = float2(cuspJ, cuspM); return cuspJM; } float3 tonemapAndCompress_fwd(float3 inputJMh, ODTParams PARAMS) { float3 outputJMh; float _linear = Hellwig_J_to_Y(inputJMh[0]) / referenceLuminance; TSParams TSPARAMS = init_TSParams(PARAMS.peakLuminance); float luminanceTS = tonescale_fwd(_linear, TSPARAMS); float tonemappedJ = Y_to_Hellwig_J(luminanceTS); float3 tonemappedJMh = float3(tonemappedJ, inputJMh[1], inputJMh[2]); outputJMh = tonemappedJMh; outputJMh[1] = chromaCompression(outputJMh, inputJMh[0], PARAMS, false); return outputJMh; } float3 tonemapAndCompress_inv(float3 JMh, ODTParams PARAMS) { float3 tonemappedJMh = JMh; float luminance = Hellwig_J_to_Y(JMh[0]); TSParams TSPARAMS = init_TSParams(PARAMS.peakLuminance); float linear_c = tonescale_inv(luminance / referenceLuminance, TSPARAMS); float untonemappedJ = Y_to_Hellwig_J(linear_c * referenceLuminance); float3 untonemappedColorJMh = float3(untonemappedJ, tonemappedJMh[1], tonemappedJMh[2]); // Chroma compression untonemappedColorJMh[1] = chromaCompression(tonemappedJMh, untonemappedColorJMh[0], PARAMS, true); return untonemappedColorJMh; } float3 JMh_to_output_XYZ(float3 JMh, ODTParams PARAMS) { float3 XYZluminance = Hellwig2022_JMh_to_XYZ(JMh, PARAMS.XYZ_w_limit); float3 XYZ = (1. / referenceLuminance) * XYZluminance; return XYZ; } float3 XYZ_output_to_JMh(float3 XYZ, ODTParams PARAMS) { float3 XYZluminance = referenceLuminance * XYZ; float3 JMh = XYZ_to_Hellwig2022_JMh(XYZluminance, PARAMS.XYZ_w_limit); return JMh; } float hueDependentUpperHullGamma(float h, Texture2D gamma_table) { uint3 gammaSize;// = uint3(totalTableSize, 1, 1); gamma_table.GetDimensions(0, gammaSize.x, gammaSize.y, gammaSize.z); const int i_lo = hue_position_in_uniform_table(h, tableSize) + baseIndex; const int i_hi = next_position_in_table(i_lo, (int)gammaSize.x); const float base_hue = base_hue_for_position(i_lo - baseIndex, tableSize); const float t = wrap_to_360(h) - base_hue; return lerp(gamma_table.Load(int3(i_lo, 0, 0)), gamma_table.Load(int3(i_hi, 0, 0)), t); } float getFocusGain(float J, float cuspJ, float limitJmax) { float thr = lerp(cuspJ, limitJmax, focusGainBlend); if (J > thr) { // Approximate inverse required above threshold float gain = (limitJmax - thr) / max(0.0001, limitJmax - J); return pow(log10(gain), 1. / focusAdjustGain) + 1.; } else { // Analytic inverse possible below cusp return 1.; } } float solve_J_intersect(float J, float M, float focusJ, float maxJ, float slope_gain) { float a = M / (focusJ * slope_gain); float b = 0.0; float c = 0.0; float intersectJ = 0.0; if (J < focusJ) { b = 1.0 - M / slope_gain; } else { b = -(1.0 + M / slope_gain + maxJ * M / (focusJ * slope_gain)); } if (J < focusJ) { c = -J; } else { c = maxJ * M / slope_gain + J; } float root = sqrt(b * b - 4.0 * a * c); if (J < focusJ) { intersectJ = 2.0 * c / (-b - root); } else { intersectJ = 2.0 * c / (-b + root); } return intersectJ; } float3 findGamutBoundaryIntersection(float3 JMh_s, float2 JM_cusp_in, float J_focus, float J_max, float slope_gain, float gamma_top, float gamma_bottom) { float slope = 0.0; float s = max(0.000001, smoothCusps); float2 JM_cusp = JM_cusp_in; JM_cusp[1] = JM_cusp_in[1] * (1.0 + smoothM * s); // M float J_intersect_source = solve_J_intersect(JMh_s[0], JMh_s[1], J_focus, J_max, slope_gain); float J_intersect_cusp = solve_J_intersect(JM_cusp[0], JM_cusp[1], J_focus, J_max, slope_gain); if (J_intersect_source < J_focus) { slope = J_intersect_source * (J_intersect_source - J_focus) / (J_focus * slope_gain); } else { slope = (J_max - J_intersect_source) * (J_intersect_source - J_focus) / (J_focus * slope_gain); } float M_boundary_lower = J_intersect_cusp * pow(J_intersect_source / J_intersect_cusp, 1. / gamma_bottom) / (JM_cusp[0] / JM_cusp[1] - slope); float M_boundary_upper = JM_cusp[1] * (J_max - J_intersect_cusp) * pow((J_max - J_intersect_source) / (J_max - J_intersect_cusp), 1. / gamma_top) / (slope * JM_cusp[1] + J_max - JM_cusp[0]); float M_boundary = JM_cusp[1] * smin(M_boundary_lower / JM_cusp[1], M_boundary_upper / JM_cusp[1], s); float J_boundary = J_intersect_source + slope * M_boundary; float3 return_JMh = {J_boundary, M_boundary, J_intersect_source}; return return_JMh; } float3 getReachBoundary(float J, float M, float h, ODTParams PARAMS, float2 JMcusp, float focusJ) { float limitJmax = PARAMS.limitJmax; float midJ = PARAMS.midJ; float model_gamma = PARAMS.model_gamma; float focusDist = PARAMS.focusDist; const float reachMaxM = reachMFromTable(h, PARAMS.reach_table); float slope_gain = limitJmax * focusDist * getFocusGain(J, JMcusp[0], limitJmax); float intersectJ = solve_J_intersect(J, M, focusJ, limitJmax, slope_gain); float slope; if (intersectJ < focusJ) { slope = intersectJ * (intersectJ - focusJ) / (focusJ * slope_gain); } else { slope = (limitJmax - intersectJ) * (intersectJ - focusJ) / (focusJ * slope_gain); } float boundary = limitJmax * pow(intersectJ / limitJmax, model_gamma) * reachMaxM / (limitJmax - slope * reachMaxM); float3 result = {J, boundary, h}; return result; } float3 compressGamut(float3 JMh, ODTParams PARAMS, float Jx, bool _invert = false) { float limitJmax = PARAMS.limitJmax; float midJ = PARAMS.midJ; float focusDist = PARAMS.focusDist; float model_gamma = PARAMS.model_gamma; float2 project_from = float2(JMh[0], JMh[1]); float2 JMcusp = cuspFromTable(JMh[2], PARAMS.gamut_table); if (JMh[1] < 0.0001 || JMh[0] > limitJmax) { float3 JMh_return = float3(JMh[0], 0.0, JMh[2]); return JMh_return; } // Calculate where the out of gamut color is projected to float focusJ = lerp(JMcusp[0], midJ, min(1., cuspMidBlend - (JMcusp[0] / limitJmax))); float slope_gain = limitJmax * focusDist * getFocusGain(Jx, JMcusp[0], limitJmax); // Find gamut intersection float gamma_top = hueDependentUpperHullGamma(JMh[2], PARAMS.gamma_table); float gamma_bottom = PARAMS.lowerHullGamma; float3 boundaryReturn = findGamutBoundaryIntersection(JMh, JMcusp, focusJ, limitJmax, slope_gain, gamma_top, gamma_bottom); float2 JMboundary = float2(boundaryReturn[0], boundaryReturn[1]); float2 project_to = float2(boundaryReturn[2], 0.0); float projectJ = boundaryReturn[2]; // Calculate AP1 reach boundary float3 reachBoundary = getReachBoundary(JMboundary[0], JMboundary[1], JMh[2], PARAMS, JMcusp, focusJ); float difference = max(1.0001, reachBoundary[1] / JMboundary[1]); float threshold = max(compressionThreshold, 1. / difference); // Compress the out of gamut color along the projection line float v = project_from[1] / JMboundary[1]; v = compressionFunction(v, threshold, difference, _invert); float2 JMcompressed = project_to + v * (JMboundary - project_to); float3 return_JMh = float3(JMcompressed[0], JMcompressed[1], JMh[2]); return return_JMh; } float3 gamutMap_fwd(float3 JMh, ODTParams PARAMS ) { return compressGamut(JMh, PARAMS, JMh[0], false); } float3 gamutMap_inv(float3 JMh, ODTParams PARAMS) { float2 JMcusp = cuspFromTable(JMh[2], PARAMS.gamut_table); float Jx = JMh[0]; // Analytic inverse below threshold if (Jx <= lerp(JMcusp[0], PARAMS.limitJmax, focusGainBlend)) return compressGamut(JMh, PARAMS, Jx, true); // Approximation above threshold Jx = compressGamut(JMh, PARAMS, Jx, true)[0]; return compressGamut(JMh, PARAMS, Jx, true); } float3 outputTransform_fwd( float3 aces, float peakLuminance, ODTParams PARAMS) { float3 JMh = aces_to_JMh(aces, peakLuminance); float3 tonemappedJMh = tonemapAndCompress_fwd(JMh, PARAMS); float3 compressedJMh = gamutMap_fwd(tonemappedJMh, PARAMS); float3 XYZ = JMh_to_output_XYZ(compressedJMh, PARAMS); return XYZ; } float3 outputTransform_inv( float3 XYZ, float peakLuminance, ODTParams PARAMS) { float3 compressedJMh = XYZ_output_to_JMh(XYZ, PARAMS); float3 tonemappedJMh = gamutMap_inv(compressedJMh, PARAMS); float3 JMh = tonemapAndCompress_inv(tonemappedJMh, PARAMS); float3 aces = JMh_to_aces(JMh, peakLuminance); return aces; }