diff --git a/meson.build b/meson.build index c700aedf7bb98d33f40a4c9944fe1acf052763db..dcfd58311f53f5e94509e1194167b2af8e567454 100644 --- a/meson.build +++ b/meson.build @@ -2,7 +2,7 @@ project('libplacebo', ['c', 'cpp'], license: 'LGPL2.1+', default_options: ['c_std=c99', 'cpp_std=c++11', 'warning_level=2'], meson_version: '>=0.49', - version: '2.79.0', + version: '2.80.0', ) # Version number diff --git a/src/colorspace.c b/src/colorspace.c index e6bcce55df6357a6a1f593d978128e00f09c0236..25bf3027885bc74a837bb428f3ea3daa98aa59bc 100644 --- a/src/colorspace.c +++ b/src/colorspace.c @@ -782,6 +782,36 @@ struct pl_matrix3x3 pl_get_color_mapping_matrix(const struct pl_raw_primaries *s return xyz2rgb_d; } +// Test the sign of 'p' relative to the line 'ab' (barycentric coordinates) +static float test_point_line(const struct pl_cie_xy p, + const struct pl_cie_xy a, + const struct pl_cie_xy b) +{ + return (p.x - b.x) * (a.y - b.y) - (a.x - b.x) * (p.y - b.y); +} + +// Test if a point is entirely inside a gamut +static float test_point_gamut(struct pl_cie_xy point, + const struct pl_raw_primaries *prim) +{ + float d1 = test_point_line(point, prim->red, prim->green), + d2 = test_point_line(point, prim->green, prim->blue), + d3 = test_point_line(point, prim->blue, prim->red); + + bool has_neg = d1 < 0 || d2 < 0 || d3 < 0, + has_pos = d1 > 0 || d2 > 0 || d3 > 0; + + return !(has_neg && has_pos); +} + +bool pl_primaries_superset(const struct pl_raw_primaries *a, + const struct pl_raw_primaries *b) +{ + return test_point_gamut(b->red, a) && + test_point_gamut(b->green, a) && + test_point_gamut(b->blue, a); +} + /* Fill in the Y, U, V vectors of a yuv-to-rgb conversion matrix * based on the given luma weights of the R, G and B components (lr, lg, lb). * lr+lg+lb is assumed to equal 1. diff --git a/src/include/libplacebo/colorspace.h b/src/include/libplacebo/colorspace.h index 49671f577eb34862e9f10561296fde498cecc383..cf8e5a1f2ff22327ea5e961f38596f3687507c1a 100644 --- a/src/include/libplacebo/colorspace.h +++ b/src/include/libplacebo/colorspace.h @@ -385,6 +385,11 @@ struct pl_matrix3x3 pl_get_color_mapping_matrix(const struct pl_raw_primaries *s const struct pl_raw_primaries *dst, enum pl_rendering_intent intent); +// Returns true if 'b' is entirely contained in 'a'. Useful for figuring out if +// colorimetric clipping will occur or not. +bool pl_primaries_superset(const struct pl_raw_primaries *a, + const struct pl_raw_primaries *b); + // Cone types involved in human vision enum pl_cone { PL_CONE_L = 1 << 0, diff --git a/src/include/libplacebo/shaders/colorspace.h b/src/include/libplacebo/shaders/colorspace.h index 82dd29c251c9cd0494346c6d9718d924df29a32f..bc43134fbc4d3ccba0411d3206a2b5b71315a0cb 100644 --- a/src/include/libplacebo/shaders/colorspace.h +++ b/src/include/libplacebo/shaders/colorspace.h @@ -250,6 +250,13 @@ struct pl_color_map_params { // all out-of-gamut colors (by inverting them), if they would have been // clipped as a result of gamut or tone mapping. bool gamut_warning; + + // If true, enables colorimetric clipping. This will colorimetrically clip + // out-of-gamut colors by desaturating them until they hit the boundary of + // the permissible color volume, rather than by hard-clipping. This mode of + // clipping preserves luminance between the source and the destination, at + // the cost of introducing some color distortion in the opposite direction. + bool gamut_clipping; }; extern const struct pl_color_map_params pl_color_map_default_params; diff --git a/src/shaders/colorspace.c b/src/shaders/colorspace.c index 4b8f67ad862e8f82ee530d4aabce86504a949a11..d592533b71114241ee54e356f06926b6126a6ee4 100644 --- a/src/shaders/colorspace.c +++ b/src/shaders/colorspace.c @@ -813,6 +813,7 @@ const struct pl_color_map_params pl_color_map_default_params = { .desaturation_strength = 0.75, .desaturation_exponent = 1.50, .desaturation_base = 0.18, + .gamut_clipping = true, }; static void pl_shader_tone_map(struct pl_shader *sh, struct pl_color_space src, @@ -1061,7 +1062,7 @@ void pl_shader_color_map(struct pl_shader *sh, src.sig_avg != dst.sig_avg || src.sig_scale != dst.sig_scale || src.light != dst.light; - + bool need_gamut_warn = false; bool is_linear = prelinearized; if (need_linear && !is_linear) { pl_shader_linearize(sh, src.transfer); @@ -1072,8 +1073,10 @@ void pl_shader_color_map(struct pl_shader *sh, pl_shader_ootf(sh, src); // Tone map to rescale the signal average/peak if needed - if (src.sig_peak * src.sig_scale > dst.sig_peak * dst.sig_scale + 1e-6) + if (src.sig_peak * src.sig_scale > dst.sig_peak * dst.sig_scale + 1e-6) { pl_shader_tone_map(sh, src, dst, peak_detect_state, params); + need_gamut_warn = true; + } // Adapt to the right colorspace (primaries) if necessary if (src.primaries != dst.primaries) { @@ -1082,14 +1085,34 @@ void pl_shader_color_map(struct pl_shader *sh, csp_dst = pl_raw_primaries_get(dst.primaries); struct pl_matrix3x3 cms_mat; cms_mat = pl_get_color_mapping_matrix(csp_src, csp_dst, params->intent); + GLSL("color.rgb = %s * color.rgb;\n", sh_var(sh, (struct pl_shader_var) { .var = pl_var_mat3("cms_matrix"), .data = PL_TRANSPOSE_3X3(cms_mat.m), })); + + if (!pl_primaries_superset(csp_dst, csp_src)) { + if (params->gamut_clipping) { + GLSL("float cmin = min(min(color.r, color.g), color.b); \n" + "if (cmin < 0.0) { \n" + " float luma = dot(%s, color.rgb); \n" + " float coeff = cmin / (cmin - luma); \n" + " color.rgb = mix(color.rgb, vec3(luma), coeff); \n" + "} \n" + "float cmax = max(max(color.r, color.g), color.b); \n" + "if (cmax > 1.0) \n" + " color.rgb /= cmax; \n" + , + sh_luma_coeffs(sh, dst.primaries)); + + } else { + need_gamut_warn = true; + } + } } // Warn for remaining out-of-gamut colors if enabled - if (params->gamut_warning) { + if (params->gamut_warning && need_gamut_warn) { GLSL("if (any(greaterThan(color.rgb, vec3(%f + 0.005))) ||\n" " any(lessThan(color.rgb, vec3(-0.005))))\n" " color.rgb = vec3(%f) - color.rgb; // invert\n",