// SPDX-License-Identifier: Apache-2.0 // Copyright Contributors to the ACES Project. // 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 // // Chromaticities & Conversion matrices // Academy Primaries 0 (i.e. "ACES" Primaries from SMPTE ST2065-1) const Chromaticities AP0 = { {0.73470, 0.26530}, {0.00000, 1.00000}, {0.00010, -0.07700}, {0.32168, 0.33767}}; const float AP0_XYZ_TO_RGB[3][3] = XYZtoRGB_f33(AP0, 1.0); const float AP0_RGB_TO_XYZ[3][3] = RGBtoXYZ_f33(AP0, 1.0); // Academy Primaries 1 const Chromaticities AP1 = { {0.713, 0.293}, {0.165, 0.830}, {0.128, 0.044}, {0.32168, 0.33767}}; const float AP1_XYZ_TO_RGB[3][3] = XYZtoRGB_f33(AP1, 1.0); const float AP1_RGB_TO_XYZ[3][3] = RGBtoXYZ_f33(AP1, 1.0); const float AP0_TO_AP1[3][3] = mult_f33_f33(AP0_RGB_TO_XYZ, AP1_XYZ_TO_RGB); const float AP1_TO_AP0[3][3] = mult_f33_f33(AP1_RGB_TO_XYZ, AP0_XYZ_TO_RGB); // "Reach" Primaries - equal to ACES "AP1" primaries const Chromaticities REACH_PRI = AP1; // Table generation const int tableSize = 360; const int additionalTableEntries = 2; // allots for extra entries to wrap the hues const int totalTableSize = tableSize + additionalTableEntries; const int baseIndex = 1; // array index for smallest filled entry of padded table const float hue_limit = 360.; const int cuspCornerCount = 6; const int totalCornerCount = cuspCornerCount + 2; const int max_sorted_corners = 2 * cuspCornerCount; const float reach_cusp_tolerance = 1e-3; const float display_cusp_tolerance = 1e-7; const float gamma_minimum = 0.0; const float gamma_maximum = 5.0; const float gamma_search_step = 0.4; const float gamma_accuracy = 1e-5; // CAM Parameters const float ref_luminance = 100.; const float L_A = 100.; const float Y_b = 20.; const float surround[3] = {0.9, 0.59, 0.9}; // Dim surround const float J_scale = 100.0; const float cam_nl_Y_reference = 100.0; const float cam_nl_offset = 0.2713 * cam_nl_Y_reference; const float cam_nl_scale = 4.0 * cam_nl_Y_reference; const float model_gamma = surround[1] * (1.48 + sqrt(Y_b / ref_luminance)); // Chroma compression const float chroma_compress = 2.4; const float chroma_compress_fact = 3.3; const float chroma_expand = 1.3; const float chroma_expand_fact = 0.69; const float chroma_expand_thr = 0.5; // Gamut compression const float smooth_cusps = 0.12; const float smooth_m = 0.27; const float cusp_mid_blend = 1.3; const float focus_gain_blend = 0.3; const float focus_adjust_gain = 0.55; const float focus_distance = 1.35; const float focus_distance_scaling = 1.75; const float compression_threshold = 0.75; const float MATRIX_IDENTITY[3][3] = { {1., 0, 0}, {0, 1., 0}, {0, 0, 1.}}; struct JMhParams { // Pre-computed conversion matrices and constants for conversions to/from JMh float MATRIX_RGB_to_CAM16_c[3][3]; float MATRIX_CAM16_c_to_RGB[3][3]; float MATRIX_cone_response_to_Aab[3][3]; float MATRIX_Aab_to_cone_response[3][3]; float F_L_n; // F_L normalised float cz; float inv_cz; // 1/cz float A_w_J; float inv_A_w_J; // 1/A_w_J }; struct HueDependentGamutParams { // Hue-dependent gamut parameters float JMcusp[2]; float gamma_bottom_inv; float gamma_top_inv; float focus_J; float analytical_threshold; }; struct ODTParams { float peakLuminance; // JMh parameters JMhParams input_params; JMhParams reach_params; JMhParams limit_params; // Tonescale parameters TSParams ts; // Shared compression parameters float limit_J_max; float model_gamma_inv; float TABLE_reach_M[totalTableSize]; // Chroma compression parameters float sat; float sat_thr; float compr; float chroma_compress_scale; // Gamut compression parameters float mid_J; float focus_dist; float lower_hull_gamma_inv; float TABLE_hues[totalTableSize]; float TABLE_gamut_cusps[totalTableSize][3]; float TABLE_upper_hull_gamma[totalTableSize]; int hue_linearity_search_range[2]; }; 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 / hue_limit * 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 * hue_limit / table_size; return result; } // CAM Functions float _post_adaptation_cone_response_compression_fwd(float Rc) { const float F_L_Y = pow(Rc, 0.42); const float Ra = (F_L_Y) / (cam_nl_offset + F_L_Y); return Ra; } float _post_adaptation_cone_response_compression_inv(float Ra) { const float Ra_lim = min(Ra, 0.99); const float F_L_Y = (cam_nl_offset * Ra_lim) / (1. - Ra_lim); const float Rc = pow(F_L_Y, 1. / 0.42); return Rc; } float post_adaptation_cone_response_compression_fwd(float v) { const float abs_v = fabs(v); const float Ra = _post_adaptation_cone_response_compression_fwd(abs_v); return copysign(Ra, v); } float post_adaptation_cone_response_compression_inv(float v) { const float abs_v = fabs(v); const float Rc = _post_adaptation_cone_response_compression_inv(abs_v); return copysign(Rc, v); } float Achromatic_n_to_J(float A, float cz) { return J_scale * pow(A, cz); } float J_to_Achromatic_n(float J, float inv_cz) { return pow(J * (1. / J_scale), inv_cz); } // Optimization for achromatic values float _A_to_Y(float A, JMhParams p) { float Ra = p.A_w_J * A; float Y = _post_adaptation_cone_response_compression_inv(Ra) / p.F_L_n; return Y; } float J_to_Y(float J, JMhParams p) { float abs_J = fabs(J); return _A_to_Y(J_to_Achromatic_n(abs_J, p.inv_cz), p); } float Y_to_J(float Y, JMhParams p) { float abs_Y = fabs(Y); float Ra = _post_adaptation_cone_response_compression_fwd(abs_Y * p.F_L_n); float J = Achromatic_n_to_J(Ra * p.inv_A_w_J, p.cz); return copysign(J, Y); } float[3] RGB_to_Aab(float RGB[3], JMhParams p) { float rgb_m[3] = mult_f3_f33(RGB, p.MATRIX_RGB_to_CAM16_c); float rgb_a[3] = { post_adaptation_cone_response_compression_fwd(rgb_m[0]), post_adaptation_cone_response_compression_fwd(rgb_m[1]), post_adaptation_cone_response_compression_fwd(rgb_m[2])}; float Aab[3] = mult_f3_f33(rgb_a, p.MATRIX_cone_response_to_Aab); return Aab; } float[3] Aab_to_JMh(float Aab[3], JMhParams p) { float JMh[3] = {0., 0., 0.}; if (Aab[0] <= 0.) { return JMh; } float J = Achromatic_n_to_J(Aab[0], p.cz); float M = sqrt(Aab[1] * Aab[1] + Aab[2] * Aab[2]); float h_rad = atan2(Aab[2], Aab[1]); float h = wrap_to_360(radians_to_degrees(h_rad)); JMh[0] = J; JMh[1] = M; JMh[2] = h; return JMh; } float[3] RGB_to_JMh(float RGB[3], JMhParams p) { float Aab[3] = RGB_to_Aab(RGB, p); float JMh[3] = Aab_to_JMh(Aab, p); return JMh; } float[3] JMh_to_Aab(float JMh[3], JMhParams p) { float J = JMh[0]; float M = JMh[1]; float h = JMh[2]; float h_rad = degrees_to_radians(h); float cos_hr = cos(h_rad); float sin_hr = sin(h_rad); float A = J_to_Achromatic_n(J, p.inv_cz); float a = M * cos_hr; float b = M * sin_hr; float Aab[3] = {A, a, b}; return Aab; } float[3] Aab_to_RGB(float Aab[3], JMhParams p) { float rgb_a[3] = mult_f3_f33(Aab, p.MATRIX_Aab_to_cone_response); float rgb_m[3] = { post_adaptation_cone_response_compression_inv(rgb_a[0]), post_adaptation_cone_response_compression_inv(rgb_a[1]), post_adaptation_cone_response_compression_inv(rgb_a[2])}; float rgb[3] = mult_f3_f33(rgb_m, p.MATRIX_CAM16_c_to_RGB); return rgb; } float[3] JMh_to_RGB(float JMh[3], JMhParams p) { float Aab[3] = JMh_to_Aab(JMh, p); float rgb[3] = Aab_to_RGB(Aab, p); return rgb; } float[3] clamp_AP0_to_AP1(float aces[3], float clamp_lower_limit, float clamp_upper_limit) { float AP1[3] = mult_f3_f33(aces, AP0_TO_AP1); float AP1_clamped[3] = clamp_f3(AP1, clamp_lower_limit, clamp_upper_limit); float AP0_clamped[3] = mult_f3_f33(AP1_clamped, AP1_TO_AP0); return AP0_clamped; } float reach_M_from_table(float h, float table[]) { int base = hue_position_in_uniform_table(h, tableSize); float t = h - base; int i_lo = base + baseIndex; int i_hi = i_lo + 1; return lerp(table[i_lo], table[i_hi], t); } float reinhard_remap(float scale, float nd, bool invert = false) { if (invert) { if (nd >= 1.0) { return scale; } else { return scale * -(nd / (nd - 1.)); } } return scale * nd / (1. + nd); } float midpoint(float low, float high) { return (low + high) / 2.; } // 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(float x, float limit, float k1_in, float k2_in, bool invert = false) { 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); if (invert) { return (x * x + k1 * x) / (k3 * (x + k2)); } else { 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 chroma_compress_norm(float h, float chroma_compress_scale) { 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 * chroma_compress_scale; } // 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[3] chroma_compress_fwd(float JMh[3], float tonemapped_J, ODTParams p, bool invert = false) { float J = JMh[0]; float M = JMh[1]; float h = JMh[2]; float M_compr = M; if (M != 0.0) { const float nJ = tonemapped_J / p.limit_J_max; const float snJ = max(0., 1. - nJ); float Mnorm = chroma_compress_norm(h, p.chroma_compress_scale); float limit = pow(nJ, p.model_gamma_inv) * reach_M_from_table(h, p.TABLE_reach_M) / Mnorm; float toe_limit = limit - 0.001; float toe_snJ_sat = snJ * p.sat; float toe_sqrt_nJ_sat_thr = sqrt(nJ * nJ + p.sat_thr); float toe_nJ_compr = nJ * p.compr; // 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_compr = M * pow(tonemapped_J / J, p.model_gamma_inv); // Normalize M with the rendering space cusp M M_compr = M_compr / 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_compr = limit - toe(limit - M_compr, toe_limit, toe_snJ_sat, toe_sqrt_nJ_sat_thr, false); // 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_compr = toe(M_compr, limit, toe_nJ_compr, snJ, false); // Denormalize M_compr = M_compr * Mnorm; } float out[3] = {tonemapped_J, M_compr, h}; return out; } float[3] chroma_compress_inv(float JMh[3], float J, ODTParams p, bool invert = false) { float tonemapped_J = JMh[0]; float M_compr = JMh[1]; float h = JMh[2]; float M = M_compr; if (M_compr != 0.0) { const float nJ = tonemapped_J / p.limit_J_max; const float snJ = max(0., 1. - nJ); float Mnorm = chroma_compress_norm(h, p.chroma_compress_scale); float limit = pow(nJ, p.model_gamma_inv) * reach_M_from_table(h, p.TABLE_reach_M) / Mnorm; float toe_limit = limit - 0.001; float toe_snJ_sat = snJ * p.sat; float toe_sqrt_nJ_sat_thr = sqrt(nJ * nJ + p.sat_thr); float toe_nJ_compr = nJ * p.compr; M = M_compr / Mnorm; M = toe(M, limit, toe_nJ_compr, snJ, true); M = limit - toe(limit - M, toe_limit, toe_snJ_sat, toe_sqrt_nJ_sat_thr, true); M = M * Mnorm; M = M * pow(tonemapped_J / J, -p.model_gamma_inv); } float out[3] = {J, M, h}; return out; } float[3] tonemap_and_compress_fwd(float JMh[3], ODTParams p) { // Applies the forward tonescale, then compresses M based on J and tonemapped J // Tonemap float linear = J_to_Y(JMh[0], p.input_params) / ref_luminance; float tonemapped_Y = tonescale_fwd(linear, p.ts); float J_ts = Y_to_J(tonemapped_Y, p.input_params); // Compress M; function returns { tonemapped J, compressed M, h } float JMh_tc[3] = chroma_compress_fwd(JMh, J_ts, p, false); return JMh_tc; } float[3] tonemap_and_compress_inv(float JMh_tc[3], ODTParams p) { // Applies the inverse tonescale, then uncompresses M based on tonemapped J and J // Un-tonemap float luminance = J_to_Y(JMh_tc[0], p.input_params); float linear = tonescale_inv(luminance / ref_luminance, p.ts); float J = Y_to_J(linear * ref_luminance, p.input_params); // Un-compress M; function returns { J, M, h } float JMh[3] = chroma_compress_inv(JMh_tc, J, p, true); return JMh; } float compute_compression_vector_slope(float intersect_J, float focus_J, float limit_J_max, float slope_gain) { float direction_scalar; if (intersect_J < focus_J) { direction_scalar = intersect_J; } else { direction_scalar = limit_J_max - intersect_J; } return direction_scalar * (intersect_J - focus_J) / (focus_J * slope_gain); } float solve_J_intersect(float J, float M, float focusJ, float maxJ, float slope_gain) { const float M_scaled = M / slope_gain; const float a = M_scaled / focusJ; if (J < focusJ) { const float b = 1. - M_scaled; const float c = -J; const float det = b * b - 4. * a * c; const float root = sqrt(det); return -2. * c / (b + root); } else { const float b = -(1. + M_scaled + maxJ * a); const float c = maxJ * M_scaled + J; const float det = b * b - 4. * a * c; const float root = sqrt(det); return -2. * c / (b - root); } } // Smooth minimum about the scaled reference, based upon a cubic polynomial float smin_scaled(float a, float b, float scale_reference) { const float s_scaled = smooth_cusps * scale_reference; const float h = max(s_scaled - fabs(a - b), 0.0) / s_scaled; return min(a, b) - h * h * h * s_scaled * (1. / 6.); } float estimate_line_and_boundary_intersection_M(float J_axis_intersect, float slope, float inv_gamma, float J_max, float M_max, float J_intersection_reference) { // Line defined by J = slope * x + J_axis_intersect // Boundary defined by J = J_max * (x / M_max) ^ (1/inv_gamma) // Approximate as we do not want to iteratively solve intersection of a // straight line and an exponential // We calculate a shifted intersection from the original intersection using // the inverse of the exponential and the provided reference const float normalised_J = J_axis_intersect / J_intersection_reference; const float shifted_intersection = J_intersection_reference * pow(normalised_J, inv_gamma); // Now we find the M intersection of two lines // line from origin to J,M Max l1(x) = J/M * x // line from J Intersect' with slope l2(x) = slope * x + Intersect' // return shifted_intersection / ((J_max / M_max) - slope); return shifted_intersection * M_max / (J_max - slope * M_max); } float find_gamut_boundary_intersection(float JM_cusp[2], float J_max, float gamma_top_inv, float gamma_bottom_inv, float J_intersect_source, float slope, float J_intersect_cusp) { const float M_boundary_lower = estimate_line_and_boundary_intersection_M(J_intersect_source, slope, gamma_bottom_inv, JM_cusp[0], JM_cusp[1], J_intersect_cusp); // The upper hull is flipped and thus 'zeroed' at J_max // Also note we negate the slope const float f_J_intersect_cusp = J_max - J_intersect_cusp; const float f_J_intersect_source = J_max - J_intersect_source; const float f_JM_cusp_J = J_max - JM_cusp[0]; const float M_boundary_upper = estimate_line_and_boundary_intersection_M(f_J_intersect_source, -slope, gamma_top_inv, f_JM_cusp_J, JM_cusp[1], f_J_intersect_cusp); // Smooth minimum between the two calculated values for the M component float M_boundary = smin_scaled(M_boundary_lower, M_boundary_upper, JM_cusp[1]); return M_boundary; } float hueDependentUpperHullGamma(float h, float gamma_table[]) { const int i_lo = hue_position_in_uniform_table(h, tableSize) + baseIndex; const int i_hi = next_position_in_table(i_lo, gamma_table.size); 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[i_lo], gamma_table[i_hi], t); } float get_focus_gain(float J, float analytical_threshold, float limit_J_max, float focus_dist) { float gain = limit_J_max * focus_dist; if (J > analytical_threshold) { // Approximate inverse required above threshold due to the introduction of J in the calculation float gain_adjustment = log10((limit_J_max - analytical_threshold) / max(0.0001, limit_J_max - J)); gain_adjustment = gain_adjustment * gain_adjustment + 1.; gain = gain * gain_adjustment; } return gain; } float remap_M(float M, float gamut_boundary_M, float reach_boundary_M, bool invert = false) { const float boundary_ratio = gamut_boundary_M / reach_boundary_M; const float proportion = max(boundary_ratio, compression_threshold); const float threshold = proportion * gamut_boundary_M; if (M <= threshold || proportion >= 1.) return M; // Translate to place threshold at zero const float m_offset = M - threshold; const float gamut_offset = gamut_boundary_M - threshold; const float reach_offset = reach_boundary_M - threshold; const float scale = reach_offset / ((reach_offset / gamut_offset) - 1.); const float nd = m_offset / scale; // Shift result back to absolute return threshold + reinhard_remap(scale, nd, invert); } float[3] compress_gamut(float JMh[3], float Jx, ODTParams p, HueDependentGamutParams hdp, bool invert = false) { const float J = JMh[0]; const float M = JMh[1]; const float h = JMh[2]; const float slope_gain = get_focus_gain(Jx, hdp.analytical_threshold, p.limit_J_max, p.focus_dist); const float J_intersect_source = solve_J_intersect(J, M, hdp.focus_J, p.limit_J_max, slope_gain); const float gamut_slope = compute_compression_vector_slope(J_intersect_source, hdp.focus_J, p.limit_J_max, slope_gain); const float J_intersect_cusp = solve_J_intersect(hdp.JMcusp[0], hdp.JMcusp[1], hdp.focus_J, p.limit_J_max, slope_gain); const float gamut_boundary_M = find_gamut_boundary_intersection(hdp.JMcusp, p.limit_J_max, hdp.gamma_top_inv, hdp.gamma_bottom_inv, J_intersect_source, gamut_slope, J_intersect_cusp); if (gamut_boundary_M <= 0.) { float returnJMh[3] = {J, 0., h}; return returnJMh; } float reach_max_M = reach_M_from_table(h, p.TABLE_reach_M); const float reach_boundary_M = estimate_line_and_boundary_intersection_M(J_intersect_source, gamut_slope, p.model_gamma_inv, p.limit_J_max, reach_max_M, p.limit_J_max); const float remapped_M = remap_M(M, gamut_boundary_M, reach_boundary_M, invert); float JMhcompressed[3] = {J_intersect_source + remapped_M * gamut_slope, remapped_M, h}; return JMhcompressed; } float[2] cusp_from_table(float h, float table[][3]) { float lo[3]; float hi[3]; int low_i = 0; int high_i = baseIndex + tableSize; int i = hue_position_in_uniform_table(h, tableSize) + baseIndex; while (low_i + 1 < high_i) { if (h > table[i][2]) { low_i = i; } else { high_i = i; } i = midpoint(low_i, high_i); } lo = table[high_i - 1]; hi = table[high_i]; float t = (h - lo[2]) / (hi[2] - lo[2]); float cusp_J = lerp(lo[0], hi[0], t); float cusp_M = lerp(lo[1], hi[1], t); float cusp_JM[2] = {cusp_J, cusp_M}; return cusp_JM; } int lookup_hue_interval(float h, float hue_table[totalTableSize], int hue_linearity_search_range[2]) { // Search the given table for the interval containing the desired hue // Returns the upper index of the interval // We can narrow the search range based on the hues being almost uniform unsigned int i = baseIndex + hue_position_in_uniform_table(h, totalTableSize); unsigned int i_lo = max(baseIndex, i + hue_linearity_search_range[0]); unsigned int i_hi = min(baseIndex + tableSize, i + hue_linearity_search_range[1]); while (i_lo + 1 < i_hi) { if (h > hue_table[i]) { i_lo = i; } else { i_hi = i; } i = midpoint(i_lo, i_hi); } i_hi = max(1, i_hi); return i_hi; } float interpolation_weight(float h, float h_lo, float h_hi) { return (h - h_lo); } float compute_focus_J(float cusp_J, float mid_J, float limit_J_max) { return lerp(cusp_J, mid_J, min(1, cusp_mid_blend - (cusp_J / limit_J_max))); } HueDependentGamutParams init_HueDependentGamutParams(float hue, ODTParams p) { HueDependentGamutParams hdp; hdp.gamma_bottom_inv = p.lower_hull_gamma_inv; const int i_hi = lookup_hue_interval(hue, p.TABLE_hues, p.hue_linearity_search_range); const float t = interpolation_weight(hue, p.TABLE_hues[i_hi - 1], p.TABLE_hues[i_hi]); hdp.JMcusp = cusp_from_table(hue, p.TABLE_gamut_cusps); hdp.gamma_top_inv = lerp(p.TABLE_upper_hull_gamma[i_hi - 1], p.TABLE_upper_hull_gamma[i_hi], t); hdp.focus_J = compute_focus_J(hdp.JMcusp[0], p.mid_J, p.limit_J_max); hdp.analytical_threshold = lerp(hdp.JMcusp[0], p.limit_J_max, focus_gain_blend); return hdp; } float[3] gamut_compress_fwd(float JMh[3], ODTParams p) { const float J = JMh[0]; const float M = JMh[1]; const float h = JMh[2]; if (J <= 0.0) { float JMh[3] = {0., 0., h}; return JMh; } if (M < 0. || J > p.limit_J_max) { float JMh[3] = {J, 0., h}; return JMh; } HueDependentGamutParams hdp = init_HueDependentGamutParams(h, p); return compress_gamut(JMh, J, p, hdp, false); } float[3] gamut_compress_inv(float JMh[3], ODTParams p) { const float J = JMh[0]; const float M = JMh[1]; const float h = JMh[2]; if (J <= 0.0) { float JMh[3] = {0., 0., h}; return JMh; } if (M < 0. || J > p.limit_J_max) { float JMh[3] = {J, 0., h}; return JMh; } HueDependentGamutParams hdp = init_HueDependentGamutParams(h, p); float Jx = J; if (Jx > hdp.analytical_threshold) { Jx = compress_gamut(JMh, Jx, p, hdp, true)[0]; } return compress_gamut(JMh, Jx, p, hdp, true); } // Table building functions bool any_below_zero(float newLimitRGB[3]) { return (newLimitRGB[0] < 0. || newLimitRGB[1] < 0. || newLimitRGB[2] < 0.); } JMhParams init_JMhParams(Chromaticities prims) { const Chromaticities CAM16_PRI = { {0.8336, 0.1735}, {2.3854, -1.4659}, {0.087, -0.125}, {0.333, 0.333}}; const float MATRIX_16[3][3] = XYZtoRGB_f33(CAM16_PRI, 1.0); const float base_cone_response_to_Aab[3][3] = { {2., 1., 1. / 9.}, {1., -12. / 11., 1. / 9.}, {1. / 20., 1. / 11., -2. / 9.}}; const float RGB_TO_XYZ[3][3] = RGBtoXYZ_f33(prims, 1.0); const float XYZ_w[3] = mult_f3_f33(f3_from_f(ref_luminance), RGB_TO_XYZ); float Y_w = XYZ_w[1]; // Step 0 - Converting CIE XYZ tristimulus values to sharpened RGB values float RGB_w[3] = mult_f3_f33(XYZ_w, MATRIX_16); // Viewing condition dependent parameters const float k = 1. / (5. * L_A + 1.); const float k4 = k * k * k * k; const float F_L = 0.2 * k4 * (5. * L_A) + 0.1 * pow((1. - k4), 2.) * pow(5. * L_A, 1. / 3.); const float F_L_n = F_L / ref_luminance; const float cz = model_gamma; const float D_RGB[3] = { F_L_n * Y_w / RGB_w[0], F_L_n * Y_w / RGB_w[1], F_L_n * Y_w / RGB_w[2]}; const float RGB_wc[3] = { D_RGB[0] * RGB_w[0], D_RGB[1] * RGB_w[1], D_RGB[2] * RGB_w[2]}; const float RGB_Aw[3] = { post_adaptation_cone_response_compression_fwd(RGB_wc[0]), post_adaptation_cone_response_compression_fwd(RGB_wc[1]), post_adaptation_cone_response_compression_fwd(RGB_wc[2])}; float cone_response_to_Aab[3][3] = mult_f33_f33(mult_f_f33(cam_nl_scale, MATRIX_IDENTITY), base_cone_response_to_Aab); float A_w = cone_response_to_Aab[0][0] * RGB_Aw[0] + cone_response_to_Aab[1][0] * RGB_Aw[1] + cone_response_to_Aab[2][0] * RGB_Aw[2]; float A_w_J = _post_adaptation_cone_response_compression_fwd(F_L); // Prescale the CAM16 LMS responses to directly provide for chromatic adaptation float M1[3][3] = mult_f33_f33(RGB_TO_XYZ, MATRIX_16); float M2[3][3] = mult_f_f33(ref_luminance, MATRIX_IDENTITY); float MATRIX_RGB_to_CAM16[3][3] = mult_f33_f33(M1, M2); float MATRIX_RGB_to_CAM16_c[3][3] = mult_f33_f33(MATRIX_RGB_to_CAM16, scale_matrix_diagonal_f33_f3(MATRIX_IDENTITY, D_RGB)); float MATRIX_cone_response_to_Aab[3][3] = { {cone_response_to_Aab[0][0] / A_w, cone_response_to_Aab[0][1] * 43. * surround[2], cone_response_to_Aab[0][2] * 43. * surround[2]}, {cone_response_to_Aab[1][0] / A_w, cone_response_to_Aab[1][1] * 43. * surround[2], cone_response_to_Aab[1][2] * 43. * surround[2]}, {cone_response_to_Aab[2][0] / A_w, cone_response_to_Aab[2][1] * 43. * surround[2], cone_response_to_Aab[2][2] * 43. * surround[2]}}; JMhParams p; p.MATRIX_RGB_to_CAM16_c = MATRIX_RGB_to_CAM16_c; p.MATRIX_CAM16_c_to_RGB = invert_f33(MATRIX_RGB_to_CAM16_c); p.MATRIX_cone_response_to_Aab = MATRIX_cone_response_to_Aab; p.MATRIX_Aab_to_cone_response = invert_f33(MATRIX_cone_response_to_Aab); p.F_L_n = F_L_n; p.cz = cz; p.inv_cz = 1. / cz; p.A_w_J = A_w_J; p.inv_A_w_J = 1. / A_w_J; return p; } float[3] generate_unit_cube_cusp_corners(int corner) { float result[3]; // Generation order R, Y, G, C, B, M to ensure hues rotate in correct order if (((corner + 1) % cuspCornerCount) < 3) result[0] = 1; else result[0] = 0; if (((corner + 5) % cuspCornerCount) < 3) result[1] = 1; else result[1] = 0; if (((corner + 3) % cuspCornerCount) < 3) result[2] = 1; else result[2] = 0; return result; } void build_limiting_cusp_corners_tables(output float RGB_corners[totalCornerCount][3], output float JMh_corners[totalCornerCount][3], input JMhParams params, input float peakLuminance) { // We calculate the RGB and JMh values for the limiting gamut cusp corners // They are then arranged into a cycle with the lowest JMh value at [1] to // allow for hue wrapping float temp_RGB_corners[cuspCornerCount][3]; float temp_JMh_corners[cuspCornerCount][3]; int min_index = 0; for (int i = 0; i != cuspCornerCount; i = i + 1) { temp_RGB_corners[i] = mult_f_f3(peakLuminance / ref_luminance, generate_unit_cube_cusp_corners(i)); temp_JMh_corners[i] = RGB_to_JMh(temp_RGB_corners[i], params); if (temp_JMh_corners[i][2] < temp_JMh_corners[min_index][2]) min_index = i; } // Rotate entries placing lowest at [1] (not [0]) for (int i = 0; i != cuspCornerCount; i = i + 1) { RGB_corners[i + 1] = temp_RGB_corners[(i + min_index) % cuspCornerCount]; JMh_corners[i + 1] = temp_JMh_corners[(i + min_index) % cuspCornerCount]; } // Copy end elements to create a cycle RGB_corners[0] = RGB_corners[cuspCornerCount]; RGB_corners[cuspCornerCount + 1] = RGB_corners[1]; JMh_corners[0] = JMh_corners[cuspCornerCount]; JMh_corners[cuspCornerCount + 1] = JMh_corners[1]; // Wrap the hues, to maintain monotonicity these entries will fall outside [0.0, hue_limit) JMh_corners[0][2] = JMh_corners[0][2] - hue_limit; JMh_corners[cuspCornerCount + 1][2] = JMh_corners[cuspCornerCount + 1][2] + hue_limit; // return JMh_corners; } float[totalCornerCount][3] find_reach_corners_table(JMhParams params_reach, ODTParams p) { // We need to find the value of JMh that corresponds to limitJ for each // corner This is done by scaling the unit corners converting to JMh until // the J value is near the limitJ // As an optimisation we use the equivalent Achromatic value to search for // the J value and avoid the non-linear transform during the search. // Strictly speaking we should only need to find the R, G and B "corners" // as the reach is unbounded and as such does not form a cube, but is formed // by the transformed 3 lower planes of the cube and the plane at J = limitJ float temp_JMh_corners[cuspCornerCount][3]; float JMh_corners[totalCornerCount][3]; float limitA = J_to_Achromatic_n(p.limit_J_max, params_reach.inv_cz); int min_index = 0; for (int i = 0; i != cuspCornerCount; i = i + 1) { const float rgb_vector[3] = generate_unit_cube_cusp_corners(i); float lower = 0.0; float upper = p.ts.forward_limit; while ((upper - lower) > reach_cusp_tolerance) { float test = (lower + upper) / 2.; float test_corner[3] = mult_f_f3(test, rgb_vector); float A = RGB_to_Aab(test_corner, params_reach)[0]; if (A < limitA) { lower = test; } else { upper = test; } } temp_JMh_corners[i] = RGB_to_JMh(mult_f_f3(upper, rgb_vector), params_reach); if (temp_JMh_corners[i][2] < temp_JMh_corners[min_index][2]) min_index = i; } // Rotate entries placing lowest at [1] (not [0]) for (int i = 0; i != cuspCornerCount; i = i + 1) { JMh_corners[i + 1] = temp_JMh_corners[(i + min_index) % cuspCornerCount]; } // Copy end elements to create a cycle JMh_corners[0] = JMh_corners[cuspCornerCount]; JMh_corners[cuspCornerCount + 1] = JMh_corners[1]; // Wrap the hues, to maintain monotonicity these entries will fall outside [0.0, hue_limit) JMh_corners[0][2] = JMh_corners[0][2] - hue_limit; JMh_corners[cuspCornerCount + 1][2] = JMh_corners[cuspCornerCount + 1][2] + hue_limit; return JMh_corners; } float[max_sorted_corners] extract_sorted_cube_hues(float reach_JMh[totalCornerCount][3], float limit_JMh[totalCornerCount][3]) { float sorted_hues[max_sorted_corners]; // Basic merge of 2 sorted arrays, extracting the unique hues. // Return the count of the unique hues int idx = 0; int reach_idx = 1; int limit_idx = 1; while ((reach_idx < (cuspCornerCount + 1)) || (limit_idx < (cuspCornerCount + 1))) { float reach_hue = reach_JMh[reach_idx][2]; float limit_hue = limit_JMh[limit_idx][2]; if (reach_hue == limit_hue) { sorted_hues[idx] = reach_hue; reach_idx = reach_idx + 1; limit_idx = limit_idx + 1; // When equal consume both } else { if (reach_hue < limit_hue) { sorted_hues[idx] = reach_hue; reach_idx = reach_idx + 1; } else { sorted_hues[idx] = limit_hue; limit_idx = limit_idx + 1; } } idx = idx + 1; } return sorted_hues; } float[totalTableSize] build_hue_sample_interval(int samples, float lower, float upper, float hue_table[totalTableSize], int base) { float mod_hue_table[totalTableSize] = hue_table; float delta = (upper - lower) / samples; int i; for (i = 0; i != samples; i = i + 1) { mod_hue_table[base + i] = lower + i * delta; } return mod_hue_table; } float[totalTableSize] build_hue_table(float sorted_hues[max_sorted_corners]) { float hue_table[totalTableSize]; float ideal_spacing = tableSize / hue_limit; int samples_count[2 * cuspCornerCount + 2]; int last_idx; int min_index; if (sorted_hues[0] == 0.0) { min_index = 0; } else { min_index = 1; } int hue_idx; for (hue_idx = 0; hue_idx != max_sorted_corners; hue_idx = hue_idx + 1) { float raw_idx = round(sorted_hues[hue_idx] * ideal_spacing); int nominal_idx = min(max(round(sorted_hues[hue_idx] * ideal_spacing), min_index), tableSize - 1); if (last_idx == nominal_idx) { // Last two hues should sample at same index, need to adjust them // Adjust previous sample down if we can if (hue_idx > 1 && samples_count[hue_idx - 2] != (samples_count[hue_idx - 1] - 1)) { samples_count[hue_idx - 1] = samples_count[hue_idx - 1] - 1; } else { nominal_idx = nominal_idx + 1; } } samples_count[hue_idx] = min(nominal_idx, tableSize - 1); min_index = nominal_idx; last_idx = min_index; } int total_samples = 0; // Special cases for ends int i = 0; hue_table = build_hue_sample_interval(samples_count[i], 0.0, sorted_hues[i], hue_table, total_samples + 1); total_samples = total_samples + samples_count[i]; for (i = i + 1; i != max_sorted_corners; i = i + 1) { int samples = samples_count[i] - samples_count[i - 1]; hue_table = build_hue_sample_interval(samples, sorted_hues[i - 1], sorted_hues[i], hue_table, total_samples + 1); total_samples = total_samples + samples; } hue_table = build_hue_sample_interval(tableSize - total_samples, sorted_hues[i - 1], hue_limit, hue_table, total_samples + 1); hue_table[0] = hue_table[baseIndex + tableSize - 1] - hue_limit; hue_table[baseIndex + tableSize] = hue_table[baseIndex] + hue_limit; return hue_table; } float[2] find_display_cusp_for_hue(float hue, float RGB_corners[totalCornerCount][3], float JMh_corners[totalCornerCount][3], JMhParams params, float previous[2]) { // This works by finding the required line segment between two of the XYZ // cusp corners, then binary searching along the line calculating the JMh of // points along the line till we find the required value. All values on the // line segments are valid cusp locations. float return_JM[2]; int upper_corner = 1; int found = 0; for (int i = upper_corner; i != totalCornerCount && !found; i = i + 1) { if (JMh_corners[i][2] > hue) { upper_corner = i; found = 1; } } int lower_corner = upper_corner - 1; // hue should now be within [lower_corner, upper_corner), handle exact match if (JMh_corners[lower_corner][2] == hue) { return_JM[0] = JMh_corners[lower_corner][0]; return_JM[1] = JMh_corners[lower_corner][1]; return return_JM; } // search by lerping between RGB corners for the hue float cusp_lower[3] = RGB_corners[lower_corner]; float cusp_upper[3] = RGB_corners[upper_corner]; float sample[3]; float sample_t; float lower_t = 0.0; if (upper_corner == previous[0]) lower_t = previous[1]; float upper_t = 1.0; float JMh[3]; // There is an edge case where we need to search towards the range when // across the [0.0, hue_limit] boundary each edge needs the directions // swapped. This is handled by comparing against the appropriate corner to // make sure we are still in the expected range between the lower and upper // corner hue limits while ((upper_t - lower_t) > display_cusp_tolerance) { sample_t = midpoint(lower_t, upper_t); sample = lerp_f3(cusp_lower, cusp_upper, sample_t); JMh = RGB_to_JMh(sample, params); if (JMh[2] < JMh_corners[lower_corner][2]) { upper_t = sample_t; } else if (JMh[2] >= JMh_corners[upper_corner][2]) { lower_t = sample_t; } else if (JMh[2] > hue) { upper_t = sample_t; } else { lower_t = sample_t; } } // Use the midpoint of the final interval for the actual samples sample_t = midpoint(lower_t, upper_t); sample = lerp_f3(cusp_lower, cusp_upper, sample_t); JMh = RGB_to_JMh(sample, params); // previous[0] = upper_corner; // previous[1] = sample_t; return_JM[0] = JMh[0]; return_JM[1] = JMh[1]; return return_JM; } float[totalTableSize][3] build_cusp_table(float hue_table[totalTableSize], float RGB_corners[totalCornerCount][3], float JMh_corners[totalCornerCount][3], JMhParams params) { float previous[2] = {0.0, 0.0}; float output_table[totalTableSize][3]; for (int i = baseIndex; i != totalTableSize; i = i + 1) { float hue = hue_table[i]; float JM[2] = find_display_cusp_for_hue(hue, RGB_corners, JMh_corners, params, previous); output_table[i][0] = JM[0]; output_table[i][1] = JM[1] * (1. + smooth_m * smooth_cusps); output_table[i][2] = hue; } // Copy last nominal entry to start output_table[0][0] = output_table[tableSize][0]; output_table[0][1] = output_table[tableSize][1]; output_table[0][2] = hue_table[0]; // Copy first nominal entry to end output_table[baseIndex + tableSize][0] = output_table[baseIndex][0]; output_table[baseIndex + tableSize][1] = output_table[baseIndex][1]; output_table[baseIndex + tableSize][2] = hue_table[baseIndex + tableSize]; return output_table; } float[totalTableSize][3] make_uniform_hue_gamut_table(JMhParams reach_params, JMhParams limit_params, ODTParams p) { // The principal here is to sample the hues as uniformly as possible, whilst // ensuring we sample the corners of the limiting gamut and the reach // primaries at limit J Max // // The corners are calculated then the hues are extracted and merged to form // a unique sorted hue list We then build the hue table from the list, those // hues are then used to compute the JMh of the limiting gamut cusp. float reach_JMh_corners[totalCornerCount][3]; float limiting_RGB_corners[totalCornerCount][3]; float limiting_JMh_corners[totalCornerCount][3]; reach_JMh_corners = find_reach_corners_table(reach_params, p); build_limiting_cusp_corners_tables(limiting_RGB_corners, limiting_JMh_corners, limit_params, p.peakLuminance); float sorted_hues[max_sorted_corners] = extract_sorted_cube_hues(reach_JMh_corners, limiting_JMh_corners); float hue_table[totalTableSize] = build_hue_table(sorted_hues); float cusp_JMh_table[totalTableSize][3] = build_cusp_table(hue_table, limiting_RGB_corners, limiting_JMh_corners, limit_params); return cusp_JMh_table; } // Finds reach gamut M value at limitJmax float[totalTableSize] make_reach_m_table(JMhParams params, float limitJmax) { float reachTable[totalTableSize]; for (int i = 0; i < tableSize; i = i + 1) { float i_float = i; float hue = base_hue_for_position(i, tableSize); const float search_range = 50.; const float search_maximum = 1300.; float low = 0.; float high = low + search_range; bool outside = false; while ((outside != true) & (high < search_maximum)) { float searchJMh[3] = {limitJmax, high, hue}; float newLimitRGB[3] = JMh_to_RGB(searchJMh, params); outside = any_below_zero(newLimitRGB); if (outside == false) { low = high; high = high + search_range; } } while (high - low > 1e-2) { float sampleM = (high + low) / 2.; float searchJMh[3] = {limitJmax, sampleM, hue}; float newLimitRGB[3] = JMh_to_RGB(searchJMh, params); outside = any_below_zero(newLimitRGB); if (outside) { high = sampleM; } else { low = sampleM; } } reachTable[i + baseIndex] = high; } // Copy last populated entry to first empty spot reachTable[0] = reachTable[tableSize]; // Copy first populated entry to last empty spot reachTable[baseIndex + tableSize] = reachTable[baseIndex]; return reachTable; } bool outside_hull(float rgb[3], float maxRGBtestVal) { return rgb[0] > maxRGBtestVal || rgb[1] > maxRGBtestVal || rgb[2] > maxRGBtestVal; } const int test_count = 5; const float testPositions[test_count] = {0.01, 0.1, 0.5, 0.8, 0.99}; struct TestData { float test_JMh[3]; float J_intersect_source; float slope; float J_intersect_cusp; }; void generate_gamma_test_data(input float JMcusp[2], input float hue, input float limit_J_max, input float mid_J, input float focus_dist, output float test_JMh[test_count][3], output float J_intersect_source[test_count], output float slopes[test_count], output float J_intersect_cusp[test_count]) { float analytical_threshold = lerp(JMcusp[0], limit_J_max, focus_gain_blend); float focus_J = compute_focus_J(JMcusp[0], mid_J, limit_J_max); for (int testIndex = 0; testIndex != test_count; testIndex = testIndex + 1) { float test_J = lerp(JMcusp[0], limit_J_max, testPositions[testIndex]); float slope_gain = get_focus_gain(test_J, analytical_threshold, limit_J_max, focus_dist); float J_intersect = solve_J_intersect(test_J, JMcusp[1], focus_J, limit_J_max, slope_gain); float slope = compute_compression_vector_slope(J_intersect, focus_J, limit_J_max, slope_gain); float J_cusp = solve_J_intersect(JMcusp[0], JMcusp[1], focus_J, limit_J_max, slope_gain); // Store values in parallel arrays test_JMh[testIndex][0] = test_J; test_JMh[testIndex][1] = JMcusp[1]; test_JMh[testIndex][2] = hue; J_intersect_source[testIndex] = J_intersect; slopes[testIndex] = slope; J_intersect_cusp[testIndex] = J_cusp; } } bool evaluate_gamma_fit(float JMcusp[2], float test_JMh[test_count][3], float J_intersect_source[test_count], float slopes[test_count], float J_intersect_cusp[test_count], float top_gamma_inv, float peakLuminance, float limit_J_max, float lower_hull_gamma_inv, JMhParams limit_params) { float luminance_limit = peakLuminance / ref_luminance; for (int testIndex = 0; testIndex < test_count; testIndex = testIndex + 1) { // Compute gamut boundary intersection float approxLimit_M = find_gamut_boundary_intersection(JMcusp, limit_J_max, top_gamma_inv, lower_hull_gamma_inv, J_intersect_source[testIndex], slopes[testIndex], J_intersect_cusp[testIndex]); float approxLimit_J = J_intersect_source[testIndex] + slopes[testIndex] * approxLimit_M; // Store JMh values float approximate_JMh[3] = {approxLimit_J, approxLimit_M, test_JMh[testIndex][2]}; // Convert to RGB float newLimitRGB[3] = JMh_to_RGB(approximate_JMh, limit_params); // Check if any values exceed the luminance limit. If so, we are outside of the top gamut shell. if (!outside_hull(newLimitRGB, luminance_limit)) return false; } return true; } float[totalTableSize] make_upper_hull_gamma_table(float gamutCuspTable[totalTableSize][3], ODTParams p) { // Find upper hull gamma values for the gamut mapper. // Start by taking a h angle // Get the cusp J value for that angle // Find a J value halfway to the Jmax // Iterate through gamma values until the approximate max M is // negative through the actual boundary // positions between the cusp and Jmax we will check variables that get // set as we iterate through, once all are set to true we break the loop float upper_hull_gamma[totalTableSize]; for (int i = baseIndex; i != baseIndex + tableSize; i = i + 1) { // Get cusp from cusp table at hue position float hue = gamutCuspTable[i][2]; float JMcusp[2] = {gamutCuspTable[i][0], gamutCuspTable[i][1]}; float test_JMh[test_count][3]; float J_intersect_source[test_count]; float slopes[test_count]; float J_intersect_cusp[test_count]; generate_gamma_test_data(JMcusp, hue, p.limit_J_max, p.mid_J, p.focus_dist, test_JMh, J_intersect_source, slopes, J_intersect_cusp); float search_range = gamma_search_step; float low = gamma_minimum; float high = low + search_range; bool outside = false; while (!(outside) && (high < gamma_maximum)) { bool gammaFound = evaluate_gamma_fit(JMcusp, test_JMh, J_intersect_source, slopes, J_intersect_cusp, 1. / high, p.peakLuminance, p.limit_J_max, p.lower_hull_gamma_inv, p.limit_params); if (!gammaFound) { low = high; high = high + search_range; } else { outside = true; } } float testGamma = -1.0; while ((high - low) > gamma_accuracy) { testGamma = midpoint(high, low); bool gammaFound = evaluate_gamma_fit(JMcusp, test_JMh, J_intersect_source, slopes, J_intersect_cusp, 1. / testGamma, p.peakLuminance, p.limit_J_max, p.lower_hull_gamma_inv, p.limit_params); if (gammaFound) { high = testGamma; } else { low = testGamma; } } upper_hull_gamma[i] = 1. / high; } // Copy last populated entry to first empty spot upper_hull_gamma[0] = upper_hull_gamma[tableSize]; // Copy first populated entry to last empty spot upper_hull_gamma[tableSize + baseIndex] = upper_hull_gamma[baseIndex]; return upper_hull_gamma; } int[2] determine_hue_linearity_search_range(float hue_table[]) { // This function searches through the hues looking for the largest // deviations from a linear distribution. We can then use this to initialise // the binary search range to something smaller than the full one to reduce // the number of lookups per hue lookup from ~ceil(log2(table size)) to // ~ceil(log2(range)) during image rendering. const int lower_padding = 0; const int upper_padding = 1; int hue_linearity_search_range[2] = {lower_padding, upper_padding}; for (int i = baseIndex; i != baseIndex + tableSize; i = i + 1) { const int pos = hue_position_in_uniform_table(hue_table[i], totalTableSize); const int delta = i - pos; hue_linearity_search_range[0] = min(hue_linearity_search_range[0], delta + lower_padding); hue_linearity_search_range[1] = max(hue_linearity_search_range[1], delta + upper_padding); } return hue_linearity_search_range; } ODTParams init_ODTParams(float peakLuminance, Chromaticities limitingPrimaries) { ODTParams p; p.peakLuminance = peakLuminance; // JMh parameters p.input_params = init_JMhParams(AP0); p.reach_params = init_JMhParams(REACH_PRI); p.limit_params = init_JMhParams(limitingPrimaries); // Tonescale parameters p.ts = init_TSParams(peakLuminance); // Shared compression paramters p.limit_J_max = Y_to_J(peakLuminance, p.input_params); p.model_gamma_inv = 1. / model_gamma; p.TABLE_reach_M = make_reach_m_table(p.reach_params, p.limit_J_max); // Chroma compression parameters p.sat = max(0.2, chroma_expand - (chroma_expand * chroma_expand_fact) * p.ts.log_peak); p.sat_thr = chroma_expand_thr / peakLuminance; p.compr = chroma_compress + (chroma_compress * chroma_compress_fact) * p.ts.log_peak; p.chroma_compress_scale = pow(0.03379 * peakLuminance, 0.30596) - 0.45135; // Gamut compression parameters p.mid_J = Y_to_J(p.ts.c_t * ref_luminance, p.input_params); p.focus_dist = focus_distance + focus_distance * focus_distance_scaling * p.ts.log_peak; const float lower_hull_gamma = 1.14 + 0.07 * p.ts.log_peak; p.lower_hull_gamma_inv = 1. / lower_hull_gamma; p.TABLE_gamut_cusps = make_uniform_hue_gamut_table(p.reach_params, p.limit_params, p); for (int i = 0; i != totalTableSize; i = i + 1) { p.TABLE_hues[i] = p.TABLE_gamut_cusps[i][2]; } p.TABLE_upper_hull_gamma = make_upper_hull_gamma_table(p.TABLE_gamut_cusps, p); p.hue_linearity_search_range = determine_hue_linearity_search_range(p.TABLE_hues); return p; } float[3] outputTransform_fwd(float aces[3], ODTParams p) { float AP0_clamped[3] = clamp_AP0_to_AP1(aces, 0., p.ts.forward_limit); float JMh[3] = RGB_to_JMh(AP0_clamped, p.input_params); float tonemappedJMh[3] = tonemap_and_compress_fwd(JMh, p); float compressedJMh[3] = gamut_compress_fwd(tonemappedJMh, p); float RGBout[3] = JMh_to_RGB(compressedJMh, p.limit_params); return RGBout; } float[3] outputTransform_inv(float RGBout[3], ODTParams p) { float compressedJMh[3] = RGB_to_JMh(RGBout, p.limit_params); float tonemappedJMh[3] = gamut_compress_inv(compressedJMh, p); float JMh[3] = tonemap_and_compress_inv(tonemappedJMh, p); float aces[3] = JMh_to_RGB(JMh, p.input_params); return aces; }