mirror of https://github.com/mpv-player/mpv
vo_gpu: improve tone mapping desaturation
Instead of desaturating towards luma, we desaturate towards the per-channel tone mapped version. This essentially proves a smooth roll-off towards the "hollywood"-style (non-chromatic) tone mapping algorithm, which works better for bright content, while continuing to use the "linear" style (chromatic) tone mapping algorithm for primarily in-gamut content. We also split up the desaturation algorithm into strength and exponent, which allows users to use less aggressive desaturation settings without affecting the overall curve.
This commit is contained in:
parent
36600ff163
commit
3fe882d4ae
|
@ -47,6 +47,10 @@ Interface changes
|
|||
- support for `--spirv-compiler=nvidia` has been removed, leaving `shaderc`
|
||||
as the only option. The `--spirv-compiler` option itself has been marked
|
||||
as deprecated, and may be removed in the future.
|
||||
- split up `--tone-mapping-desaturate`` into strength + exponent, instead of
|
||||
only using a single value (which previously just controlled the exponent).
|
||||
The strength now linearly blends between the linear and nonlinear tone
|
||||
mapped versions of a color.
|
||||
--- mpv 0.29.0 ---
|
||||
- drop --opensles-sample-rate, as --audio-samplerate should be used if desired
|
||||
- drop deprecated --videotoolbox-format, --ff-aid, --ff-vid, --ff-sid,
|
||||
|
|
|
@ -5245,17 +5245,26 @@ The following video options are currently all specific to ``--vo=gpu`` and
|
|||
The special value ``auto`` (default) will enable HDR peak computation
|
||||
automatically if compute shaders and SSBOs are supported.
|
||||
|
||||
``--tone-mapping-desaturate=<value>``
|
||||
Apply desaturation for highlights. The parameter essentially controls the
|
||||
steepness of the desaturation curve. The higher the parameter, the more
|
||||
aggressively colors will be desaturated. This setting helps prevent
|
||||
unnaturally blown-out colors for super-highlights, by (smoothly) turning
|
||||
into white instead. This makes images feel more natural, at the cost of
|
||||
reducing information about out-of-range colors.
|
||||
``--tone-mapping-desaturate=<0.0..1.0>``
|
||||
Apply desaturation for highlights (default: 0.75). The parameter controls
|
||||
the strength of the desaturation curve. A value of 0.0 completely disables
|
||||
it, while a value of 1.0 means that overly bright colors will tend towards
|
||||
white. (This is not always the case, especially not for highlights that are
|
||||
near primary colors)
|
||||
|
||||
The default of 0.5 provides a good balance. This value is weaker than the
|
||||
ACES ODT curves' recommendation, but works better for most content in
|
||||
practice. A setting of 0.0 disables this option.
|
||||
Values in between apply progressively more/less aggressive desaturation.
|
||||
This setting helps prevent unnaturally oversaturated colors for
|
||||
super-highlights, by (smoothly) turning them into less saturated (per
|
||||
channel tone mapped) colors instead. This makes images feel more natural,
|
||||
at the cost of chromatic distortions for out-of-range colors. The default
|
||||
value of 0.75 provides a good balance. Setting this to 0.0 preserves the
|
||||
chromatic accuracy of the tone mapping process.
|
||||
|
||||
``--tone-mapping-desaturate-exponent=<0.0..20.0>``
|
||||
This setting controls the exponent of the desaturation curve, which
|
||||
controls how bright a color needs to be in order to start being
|
||||
desaturated. The default of 1.5 provides a reasonable balance. Decreasing
|
||||
this exponent makes the curve more aggressive.
|
||||
|
||||
``--gamut-warning``
|
||||
If enabled, mpv will mark all clipped/out-of-gamut pixels that exceed a
|
||||
|
|
|
@ -313,9 +313,12 @@ static const struct gl_video_opts gl_video_opts_def = {
|
|||
.alpha_mode = ALPHA_BLEND_TILES,
|
||||
.background = {0, 0, 0, 255},
|
||||
.gamma = 1.0f,
|
||||
.tone_mapping = TONE_MAPPING_HABLE,
|
||||
.tone_mapping_param = NAN,
|
||||
.tone_mapping_desat = 0.5,
|
||||
.tone_map = {
|
||||
.curve = TONE_MAPPING_HABLE,
|
||||
.curve_param = NAN,
|
||||
.desat = 0.75,
|
||||
.desat_exp = 1.5,
|
||||
},
|
||||
.early_flush = -1,
|
||||
.hwdec_interop = "auto",
|
||||
};
|
||||
|
@ -353,20 +356,22 @@ const struct m_sub_options gl_video_conf = {
|
|||
OPT_CHOICE_C("target-trc", target_trc, 0, mp_csp_trc_names),
|
||||
OPT_CHOICE_OR_INT("target-peak", target_peak, 0, 10, 10000,
|
||||
({"auto", 0})),
|
||||
OPT_CHOICE("tone-mapping", tone_mapping, 0,
|
||||
OPT_CHOICE("tone-mapping", tone_map.curve, 0,
|
||||
({"clip", TONE_MAPPING_CLIP},
|
||||
{"mobius", TONE_MAPPING_MOBIUS},
|
||||
{"reinhard", TONE_MAPPING_REINHARD},
|
||||
{"hable", TONE_MAPPING_HABLE},
|
||||
{"gamma", TONE_MAPPING_GAMMA},
|
||||
{"linear", TONE_MAPPING_LINEAR})),
|
||||
OPT_CHOICE("hdr-compute-peak", compute_hdr_peak, 0,
|
||||
OPT_CHOICE("hdr-compute-peak", tone_map.compute_peak, 0,
|
||||
({"auto", 0},
|
||||
{"yes", 1},
|
||||
{"no", -1})),
|
||||
OPT_FLOAT("tone-mapping-param", tone_mapping_param, 0),
|
||||
OPT_FLOAT("tone-mapping-desaturate", tone_mapping_desat, 0),
|
||||
OPT_FLAG("gamut-warning", gamut_warning, 0),
|
||||
OPT_FLOAT("tone-mapping-param", tone_map.curve_param, 0),
|
||||
OPT_FLOAT("tone-mapping-desaturate", tone_map.desat, 0),
|
||||
OPT_FLOATRANGE("tone-mapping-desaturate-exponent",
|
||||
tone_map.desat_exp, 0, 0.0, 20.0),
|
||||
OPT_FLAG("gamut-warning", tone_map.gamut_warning, 0),
|
||||
OPT_FLAG("opengl-pbo", pbo, 0),
|
||||
SCALER_OPTS("scale", SCALER_SCALE),
|
||||
SCALER_OPTS("dscale", SCALER_DSCALE),
|
||||
|
@ -2472,7 +2477,8 @@ static void pass_colormanage(struct gl_video *p, struct mp_colorspace src, bool
|
|||
if (!dst.sig_peak)
|
||||
dst.sig_peak = mp_trc_nom_peak(dst.gamma);
|
||||
|
||||
bool detect_peak = p->opts.compute_hdr_peak >= 0 && mp_trc_is_hdr(src.gamma);
|
||||
struct gl_tone_map_opts tone_map = p->opts.tone_map;
|
||||
bool detect_peak = tone_map.compute_peak >= 0 && mp_trc_is_hdr(src.gamma);
|
||||
if (detect_peak && !p->hdr_peak_ssbo) {
|
||||
struct {
|
||||
uint32_t counter;
|
||||
|
@ -2493,8 +2499,8 @@ static void pass_colormanage(struct gl_video *p, struct mp_colorspace src, bool
|
|||
p->hdr_peak_ssbo = ra_buf_create(ra, ¶ms);
|
||||
if (!p->hdr_peak_ssbo) {
|
||||
MP_WARN(p, "Failed to create HDR peak detection SSBO, disabling.\n");
|
||||
tone_map.compute_peak = p->opts.tone_map.compute_peak = -1;
|
||||
detect_peak = false;
|
||||
p->opts.compute_hdr_peak = -1;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2515,9 +2521,7 @@ static void pass_colormanage(struct gl_video *p, struct mp_colorspace src, bool
|
|||
}
|
||||
|
||||
// Adapt from src to dst as necessary
|
||||
pass_color_map(p->sc, src, dst, p->opts.tone_mapping,
|
||||
p->opts.tone_mapping_param, p->opts.tone_mapping_desat,
|
||||
detect_peak, p->opts.gamut_warning, p->use_linear && !osd);
|
||||
pass_color_map(p->sc, p->use_linear && !osd, src, dst, &tone_map);
|
||||
|
||||
if (p->use_lut_3d) {
|
||||
gl_sc_uniform_texture(p->sc, "lut_3d", p->lut_3d_texture);
|
||||
|
@ -3583,12 +3587,12 @@ static void check_gl_features(struct gl_video *p)
|
|||
}
|
||||
|
||||
bool have_compute_peak = have_compute && have_ssbo;
|
||||
if (!have_compute_peak && p->opts.compute_hdr_peak >= 0) {
|
||||
int msgl = p->opts.compute_hdr_peak == 1 ? MSGL_WARN : MSGL_V;
|
||||
if (!have_compute_peak && p->opts.tone_map.compute_peak >= 0) {
|
||||
int msgl = p->opts.tone_map.compute_peak == 1 ? MSGL_WARN : MSGL_V;
|
||||
MP_MSG(p, msgl, "Disabling HDR peak computation (one or more of the "
|
||||
"following is not supported: compute shaders=%d, "
|
||||
"SSBO=%d).\n", have_compute, have_ssbo);
|
||||
p->opts.compute_hdr_peak = -1;
|
||||
p->opts.tone_map.compute_peak = -1;
|
||||
}
|
||||
|
||||
p->forced_dumb_mode = p->opts.dumb_mode > 0 || !have_fbo || !have_texrg;
|
||||
|
@ -3610,7 +3614,6 @@ static void check_gl_features(struct gl_video *p)
|
|||
.alpha_mode = p->opts.alpha_mode,
|
||||
.use_rectangle = p->opts.use_rectangle,
|
||||
.background = p->opts.background,
|
||||
.compute_hdr_peak = p->opts.compute_hdr_peak,
|
||||
.dither_algo = p->opts.dither_algo,
|
||||
.dither_depth = p->opts.dither_depth,
|
||||
.dither_size = p->opts.dither_size,
|
||||
|
@ -3618,9 +3621,7 @@ static void check_gl_features(struct gl_video *p)
|
|||
.temporal_dither_period = p->opts.temporal_dither_period,
|
||||
.tex_pad_x = p->opts.tex_pad_x,
|
||||
.tex_pad_y = p->opts.tex_pad_y,
|
||||
.tone_mapping = p->opts.tone_mapping,
|
||||
.tone_mapping_param = p->opts.tone_mapping_param,
|
||||
.tone_mapping_desat = p->opts.tone_mapping_desat,
|
||||
.tone_map = p->opts.tone_map,
|
||||
.early_flush = p->opts.early_flush,
|
||||
.icc_opts = p->opts.icc_opts,
|
||||
.hwdec_interop = p->opts.hwdec_interop,
|
||||
|
|
|
@ -98,6 +98,15 @@ enum tone_mapping {
|
|||
// How many frames to average over for HDR peak detection
|
||||
#define PEAK_DETECT_FRAMES 63
|
||||
|
||||
struct gl_tone_map_opts {
|
||||
int curve;
|
||||
float curve_param;
|
||||
int compute_peak;
|
||||
float desat;
|
||||
float desat_exp;
|
||||
int gamut_warning; // bool
|
||||
};
|
||||
|
||||
struct gl_video_opts {
|
||||
int dumb_mode;
|
||||
struct scaler_config scaler[4];
|
||||
|
@ -107,11 +116,7 @@ struct gl_video_opts {
|
|||
int target_prim;
|
||||
int target_trc;
|
||||
int target_peak;
|
||||
int tone_mapping;
|
||||
int compute_hdr_peak;
|
||||
float tone_mapping_param;
|
||||
float tone_mapping_desat;
|
||||
int gamut_warning;
|
||||
struct gl_tone_map_opts tone_map;
|
||||
int correct_downscaling;
|
||||
int linear_downscaling;
|
||||
int linear_upscaling;
|
||||
|
|
|
@ -580,7 +580,7 @@ static void hdr_update_peak(struct gl_shader_cache *sc)
|
|||
|
||||
// Have each thread update the work group sum with the local value
|
||||
GLSL(barrier();)
|
||||
GLSLF("atomicAdd(wg_sum, uint(sig * %f));\n", MP_REF_WHITE);
|
||||
GLSLF("atomicAdd(wg_sum, uint(sig_max * %f));\n", MP_REF_WHITE);
|
||||
|
||||
// Have one thread per work group update the global atomics. We use the
|
||||
// work group average even for the global sum, to make the values slightly
|
||||
|
@ -642,48 +642,42 @@ static void hdr_update_peak(struct gl_shader_cache *sc)
|
|||
|
||||
// Tone map from a known peak brightness to the range [0,1]. If ref_peak
|
||||
// is 0, we will use peak detection instead
|
||||
static void pass_tone_map(struct gl_shader_cache *sc, bool detect_peak,
|
||||
static void pass_tone_map(struct gl_shader_cache *sc,
|
||||
float src_peak, float dst_peak,
|
||||
enum tone_mapping algo, float param, float desat)
|
||||
const struct gl_tone_map_opts *opts)
|
||||
{
|
||||
GLSLF("// HDR tone mapping\n");
|
||||
|
||||
// To prevent discoloration due to out-of-bounds clipping, we need to make
|
||||
// sure to reduce the value range as far as necessary to keep the entire
|
||||
// signal in range, so tone map based on the brightest component.
|
||||
GLSL(float sig = max(max(color.r, color.g), color.b);)
|
||||
GLSL(int sig_idx = 0;)
|
||||
GLSL(if (color[1] > color[sig_idx]) sig_idx = 1;)
|
||||
GLSL(if (color[2] > color[sig_idx]) sig_idx = 2;)
|
||||
GLSL(float sig_max = color[sig_idx];)
|
||||
GLSLF("float sig_peak = %f;\n", src_peak);
|
||||
GLSLF("float sig_avg = %f;\n", sdr_avg);
|
||||
|
||||
if (detect_peak)
|
||||
if (opts->compute_peak >= 0)
|
||||
hdr_update_peak(sc);
|
||||
|
||||
GLSLF("vec3 sig = color.rgb;\n");
|
||||
|
||||
// Rescale the variables in order to bring it into a representation where
|
||||
// 1.0 represents the dst_peak. This is because all of the tone mapping
|
||||
// algorithms are defined in such a way that they map to the range [0.0, 1.0].
|
||||
if (dst_peak > 1.0) {
|
||||
GLSLF("sig *= %f;\n", 1.0 / dst_peak);
|
||||
GLSLF("sig_peak *= %f;\n", 1.0 / dst_peak);
|
||||
GLSLF("sig *= 1.0/%f;\n", dst_peak);
|
||||
GLSLF("sig_peak *= 1.0/%f;\n", dst_peak);
|
||||
}
|
||||
|
||||
GLSL(float sig_orig = sig;)
|
||||
GLSL(float sig_orig = sig[sig_idx];)
|
||||
GLSLF("float slope = min(1.0, %f / sig_avg);\n", sdr_avg);
|
||||
GLSL(sig *= slope;)
|
||||
GLSL(sig_peak *= slope;)
|
||||
|
||||
// Desaturate the color using a coefficient dependent on the signal.
|
||||
// Do this after peak detection in order to prevent over-desaturating
|
||||
// overly bright souces
|
||||
if (desat > 0) {
|
||||
float base = 0.18 * dst_peak;
|
||||
GLSL(float luma = dot(dst_luma, color.rgb);)
|
||||
GLSLF("float coeff = max(sig - %f, 1e-6) / max(sig, 1e-6);\n", base);
|
||||
GLSLF("coeff = pow(coeff, %f);\n", 10.0 / desat);
|
||||
GLSL(color.rgb = mix(color.rgb, vec3(luma), coeff);)
|
||||
GLSL(sig = mix(sig, luma * slope, coeff);) // also make sure to update `sig`
|
||||
}
|
||||
|
||||
switch (algo) {
|
||||
float param = opts->curve_param;
|
||||
switch (opts->curve) {
|
||||
case TONE_MAPPING_CLIP:
|
||||
GLSLF("sig = %f * sig;\n", isnan(param) ? 1.0 : param);
|
||||
break;
|
||||
|
@ -697,14 +691,15 @@ static void pass_tone_map(struct gl_shader_cache *sc, bool detect_peak,
|
|||
GLSLF("float b = (j*j - 2.0*j*sig_peak + sig_peak) / "
|
||||
"max(1e-6, sig_peak - 1.0);\n");
|
||||
GLSLF("float scale = (b*b + 2.0*b*j + j*j) / (b-a);\n");
|
||||
GLSL(sig = sig > j ? scale * (sig + a) / (sig + b) : sig;)
|
||||
GLSLF("sig = mix(sig, scale * (sig + vec3(a)) / (sig + vec3(b)),"
|
||||
" greaterThan(sig, vec3(j)));\n");
|
||||
GLSLF("}\n");
|
||||
break;
|
||||
|
||||
case TONE_MAPPING_REINHARD: {
|
||||
float contrast = isnan(param) ? 0.5 : param,
|
||||
offset = (1.0 - contrast) / contrast;
|
||||
GLSLF("sig = sig / (sig + %f);\n", offset);
|
||||
GLSLF("sig = sig / (sig + vec3(%f));\n", offset);
|
||||
GLSLF("float scale = (sig_peak + %f) / sig_peak;\n", offset);
|
||||
GLSL(sig *= scale;)
|
||||
break;
|
||||
|
@ -712,19 +707,25 @@ static void pass_tone_map(struct gl_shader_cache *sc, bool detect_peak,
|
|||
|
||||
case TONE_MAPPING_HABLE: {
|
||||
float A = 0.15, B = 0.50, C = 0.10, D = 0.20, E = 0.02, F = 0.30;
|
||||
GLSLHF("float hable(float x) {\n");
|
||||
GLSLHF("return ((x * (%f*x + %f)+%f)/(x * (%f*x + %f) + %f)) - %f;\n",
|
||||
A, C*B, D*E, A, B, D*F, E/F);
|
||||
GLSLHF("vec3 hable(vec3 x) {\n");
|
||||
GLSLHF("return (x * (%f*x + vec3(%f)) + vec3(%f)) / "
|
||||
" (x * (%f*x + vec3(%f)) + vec3(%f)) "
|
||||
" - vec3(%f);\n",
|
||||
A, C*B, D*E,
|
||||
A, B, D*F,
|
||||
E/F);
|
||||
GLSLHF("}\n");
|
||||
GLSL(sig = hable(sig) / hable(sig_peak);)
|
||||
GLSLF("sig = hable(max(vec3(0.0), sig)) / hable(vec3(sig_peak)).x;\n");
|
||||
break;
|
||||
}
|
||||
|
||||
case TONE_MAPPING_GAMMA: {
|
||||
float gamma = isnan(param) ? 1.8 : param;
|
||||
GLSLF("const float cutoff = 0.05, gamma = %f;\n", 1.0/gamma);
|
||||
GLSL(float scale = pow(cutoff / sig_peak, gamma) / cutoff;)
|
||||
GLSL(sig = sig > cutoff ? pow(sig / sig_peak, gamma) : scale * sig;)
|
||||
GLSLF("const float cutoff = 0.05, gamma = 1.0/%f;\n", gamma);
|
||||
GLSL(float scale = pow(cutoff / sig_peak, gamma.x) / cutoff;)
|
||||
GLSLF("sig = mix(scale * sig,"
|
||||
" pow(sig / sig_peak, vec3(gamma)),"
|
||||
" greaterThan(sig, vec3(cutoff)));\n");
|
||||
break;
|
||||
}
|
||||
|
||||
|
@ -738,24 +739,32 @@ static void pass_tone_map(struct gl_shader_cache *sc, bool detect_peak,
|
|||
abort();
|
||||
}
|
||||
|
||||
// Apply the computed scale factor to the color, linearly to prevent
|
||||
// discoloration
|
||||
GLSL(sig = min(sig, 1.0);)
|
||||
GLSL(color.rgb *= vec3(sig / sig_orig);)
|
||||
GLSL(sig = min(sig, vec3(1.0));)
|
||||
GLSL(vec3 sig_lin = color.rgb * (sig[sig_idx] / sig_orig);)
|
||||
|
||||
// Mix between the per-channel tone mapped and the linear tone mapped
|
||||
// signal based on the desaturation strength
|
||||
if (opts->desat > 0) {
|
||||
float base = 0.18 * dst_peak;
|
||||
GLSLF("float coeff = max(sig[sig_idx] - %f, 1e-6) / "
|
||||
" max(sig[sig_idx], 1.0);\n", base);
|
||||
GLSLF("coeff = %f * pow(coeff, %f);\n", opts->desat, opts->desat_exp);
|
||||
GLSLF("color.rgb = mix(sig_lin, %f * sig, coeff);\n", dst_peak);
|
||||
} else {
|
||||
GLSL(color.rgb = sig_lin;)
|
||||
}
|
||||
}
|
||||
|
||||
// Map colors from one source space to another. These source spaces must be
|
||||
// known (i.e. not MP_CSP_*_AUTO), as this function won't perform any
|
||||
// auto-guessing. If is_linear is true, we assume the input has already been
|
||||
// linearized (e.g. for linear-scaling). If `detect_peak` is true, we will
|
||||
// detect the peak instead of relying on metadata. Note that this requires
|
||||
// the caller to have already bound the appropriate SSBO and set up the
|
||||
// compute shader metadata
|
||||
void pass_color_map(struct gl_shader_cache *sc,
|
||||
// linearized (e.g. for linear-scaling). If `opts->compute_peak` is true, we
|
||||
// will detect the peak instead of relying on metadata. Note that this requires
|
||||
// the caller to have already bound the appropriate SSBO and set up the compute
|
||||
// shader metadata
|
||||
void pass_color_map(struct gl_shader_cache *sc, bool is_linear,
|
||||
struct mp_colorspace src, struct mp_colorspace dst,
|
||||
enum tone_mapping algo, float tone_mapping_param,
|
||||
float tone_mapping_desat, bool detect_peak,
|
||||
bool gamut_warning, bool is_linear)
|
||||
const struct gl_tone_map_opts *opts)
|
||||
{
|
||||
GLSLF("// color mapping\n");
|
||||
|
||||
|
@ -803,10 +812,8 @@ void pass_color_map(struct gl_shader_cache *sc,
|
|||
|
||||
// Tone map to prevent clipping when the source signal peak exceeds the
|
||||
// encodable range or we've reduced the gamut
|
||||
if (src.sig_peak > dst.sig_peak) {
|
||||
pass_tone_map(sc, detect_peak, src.sig_peak, dst.sig_peak, algo,
|
||||
tone_mapping_param, tone_mapping_desat);
|
||||
}
|
||||
if (src.sig_peak > dst.sig_peak)
|
||||
pass_tone_map(sc, src.sig_peak, dst.sig_peak, opts);
|
||||
|
||||
if (need_ootf)
|
||||
pass_inverse_ootf(sc, dst.light, dst.sig_peak);
|
||||
|
@ -821,7 +828,7 @@ void pass_color_map(struct gl_shader_cache *sc,
|
|||
GLSLF("color.rgb *= vec3(%f);\n", 1.0 / dst_range);
|
||||
|
||||
// Warn for remaining out-of-gamut colors is enabled
|
||||
if (gamut_warning) {
|
||||
if (opts->gamut_warning) {
|
||||
GLSL(if (any(greaterThan(color.rgb, vec3(1.01)))))
|
||||
GLSL(color.rgb = vec3(1.0) - color.rgb;) // invert
|
||||
}
|
||||
|
|
|
@ -40,11 +40,9 @@ void pass_sample_oversample(struct gl_shader_cache *sc, struct scaler *scaler,
|
|||
void pass_linearize(struct gl_shader_cache *sc, enum mp_csp_trc trc);
|
||||
void pass_delinearize(struct gl_shader_cache *sc, enum mp_csp_trc trc);
|
||||
|
||||
void pass_color_map(struct gl_shader_cache *sc,
|
||||
void pass_color_map(struct gl_shader_cache *sc, bool is_linear,
|
||||
struct mp_colorspace src, struct mp_colorspace dst,
|
||||
enum tone_mapping algo, float tone_mapping_param,
|
||||
float tone_mapping_desat, bool use_detected_peak,
|
||||
bool gamut_warning, bool is_linear);
|
||||
const struct gl_tone_map_opts *opts);
|
||||
|
||||
void pass_sample_deband(struct gl_shader_cache *sc, struct deband_opts *opts,
|
||||
AVLFG *lfg, enum mp_csp_trc trc);
|
||||
|
|
Loading…
Reference in New Issue