core/colorpipeline: refactor tone mapping to be about dimming instead of addnig headroom

No functional change, just so that the logic is a bit easier to follow
This commit is contained in:
Xaver Hugl 2024-08-30 19:04:05 +02:00
parent 587afb3076
commit eae1f51304
3 changed files with 21 additions and 16 deletions

View file

@ -37,7 +37,7 @@ ColorPipeline ColorPipeline::create(const ColorDescription &from, const ColorDes
// that's not necessarily true, and figuring out the actual range could be complicated.. // that's not necessarily true, and figuring out the actual range could be complicated..
ret.addMatrix(from.toOther(to, intent), ret.currentOutputRange() * (to.referenceLuminance() / from.referenceLuminance())); ret.addMatrix(from.toOther(to, intent), ret.currentOutputRange() * (to.referenceLuminance() / from.referenceLuminance()));
if (!s_disableTonemapping && ret.currentOutputRange().max > maxOutputLuminance * 1.01 && intent == RenderingIntent::Perceptual) { if (!s_disableTonemapping && ret.currentOutputRange().max > maxOutputLuminance * 1.01 && intent == RenderingIntent::Perceptual) {
ret.addTonemapper(to.containerColorimetry(), to.referenceLuminance(), ret.currentOutputRange().max, maxOutputLuminance, 1.5); ret.addTonemapper(to.containerColorimetry(), to.referenceLuminance(), ret.currentOutputRange().max, maxOutputLuminance);
} }
ret.addInverseTransferFunction(to.transferFunction()); ret.addInverseTransferFunction(to.transferFunction());
@ -240,7 +240,7 @@ static const QMatrix4x4 s_toICtCp = QMatrix4x4(
0.0, 0.0, 0.0, 1.0).transposed(); 0.0, 0.0, 0.0, 1.0).transposed();
static const QMatrix4x4 s_fromICtCp = s_toICtCp.inverted(); static const QMatrix4x4 s_fromICtCp = s_toICtCp.inverted();
void ColorPipeline::addTonemapper(const Colorimetry &containerColorimetry, double referenceLuminance, double maxInputLuminance, double maxOutputLuminance, double maxAddedHeadroom) void ColorPipeline::addTonemapper(const Colorimetry &containerColorimetry, double referenceLuminance, double maxInputLuminance, double maxOutputLuminance)
{ {
// convert from rgb to ICtCp // convert from rgb to ICtCp
addMatrix(containerColorimetry.toLMS(), currentOutputRange()); addMatrix(containerColorimetry.toLMS(), currentOutputRange());
@ -249,8 +249,8 @@ void ColorPipeline::addTonemapper(const Colorimetry &containerColorimetry, doubl
// apply the tone mapping to the intensity component // apply the tone mapping to the intensity component
ops.push_back(ColorOp{ ops.push_back(ColorOp{
.input = currentOutputRange(), .input = currentOutputRange(),
.operation = ColorTonemapper(referenceLuminance, maxInputLuminance, maxOutputLuminance, maxAddedHeadroom), .operation = ColorTonemapper(referenceLuminance, maxInputLuminance, maxOutputLuminance),
.output = ValueRange { .output = ValueRange{
.min = currentOutputRange().min, .min = currentOutputRange().min,
.max = maxOutputLuminance, .max = maxOutputLuminance,
}, },
@ -335,23 +335,26 @@ ColorMultiplier::ColorMultiplier(double factor)
{ {
} }
ColorTonemapper::ColorTonemapper(double referenceLuminance, double maxInputLuminance, double maxOutputLuminance, double maxAddedHeadroom) ColorTonemapper::ColorTonemapper(double referenceLuminance, double maxInputLuminance, double maxOutputLuminance)
: m_inputReferenceLuminance(referenceLuminance) : m_inputReferenceLuminance(referenceLuminance)
, m_maxInputLuminance(maxInputLuminance) , m_maxInputLuminance(maxInputLuminance)
, m_maxOutputLuminance(maxOutputLuminance) , m_maxOutputLuminance(maxOutputLuminance)
{ {
m_inputRange = maxInputLuminance / referenceLuminance; m_inputRange = maxInputLuminance / referenceLuminance;
const double outputRange = maxOutputLuminance / referenceLuminance; const double outputRange = maxOutputLuminance / referenceLuminance;
// = how much dynamic range this algorithm adds, by reducing the reference luminance // how much range we need to at least decently present the content
m_addedRange = std::min(m_inputRange, std::clamp(maxAddedHeadroom / outputRange, 1.0, maxAddedHeadroom)); // 50% HDR headroom should be enough for the tone mapper to do a good enough job, without dimming the image too much
m_outputReferenceLuminance = referenceLuminance / m_addedRange; const double minDecentRange = std::min(m_inputRange, 1.5);
// if the output doesn't provide enough HDR headroom for the tone mapper to do a good job, dim the image to create some
m_referenceDimming = 1.0 / std::clamp(outputRange / minDecentRange, 1.0, minDecentRange);
m_outputReferenceLuminance = referenceLuminance * m_referenceDimming;
} }
double ColorTonemapper::map(double pqEncodedLuminance) const double ColorTonemapper::map(double pqEncodedLuminance) const
{ {
const double luminance = TransferFunction(TransferFunction::PerceptualQuantizer).encodedToNits(pqEncodedLuminance); const double luminance = TransferFunction(TransferFunction::PerceptualQuantizer).encodedToNits(pqEncodedLuminance);
// keep things linear up to the reference luminance // keep things linear up to the reference luminance
const double low = std::min(luminance / m_addedRange, m_outputReferenceLuminance); const double low = std::min(luminance * m_referenceDimming, m_outputReferenceLuminance);
// and apply a nonlinear curve above, to reduce the luminance without completely removing differences // and apply a nonlinear curve above, to reduce the luminance without completely removing differences
const double relativeHighlight = std::clamp((luminance / m_inputReferenceLuminance - 1.0) / (m_inputRange - 1.0), 0.0, 1.0); const double relativeHighlight = std::clamp((luminance / m_inputReferenceLuminance - 1.0) / (m_inputRange - 1.0), 0.0, 1.0);
const double high = std::log(relativeHighlight * (std::numbers::e - 1) + 1) * (m_maxOutputLuminance - m_outputReferenceLuminance); const double high = std::log(relativeHighlight * (std::numbers::e - 1) + 1) * (m_maxOutputLuminance - m_outputReferenceLuminance);

View file

@ -67,7 +67,7 @@ public:
class KWIN_EXPORT ColorTonemapper class KWIN_EXPORT ColorTonemapper
{ {
public: public:
explicit ColorTonemapper(double referenceLuminance, double maxInputLuminance, double maxOutputLuminance, double maxAddedHeadroom); explicit ColorTonemapper(double referenceLuminance, double maxInputLuminance, double maxOutputLuminance);
double map(double pqEncodedLuminance) const; double map(double pqEncodedLuminance) const;
bool operator==(const ColorTonemapper &) const = default; bool operator==(const ColorTonemapper &) const = default;
@ -77,7 +77,7 @@ public:
double m_maxOutputLuminance; double m_maxOutputLuminance;
private: private:
double m_inputRange; double m_inputRange;
double m_addedRange; double m_referenceDimming;
double m_outputReferenceLuminance; double m_outputReferenceLuminance;
}; };
@ -118,7 +118,7 @@ public:
void addTransferFunction(TransferFunction tf); void addTransferFunction(TransferFunction tf);
void addInverseTransferFunction(TransferFunction tf); void addInverseTransferFunction(TransferFunction tf);
void addMatrix(const QMatrix4x4 &mat, const ValueRange &output); void addMatrix(const QMatrix4x4 &mat, const ValueRange &output);
void addTonemapper(const Colorimetry &containerColorimetry, double referenceLuminance, double maxInputLuminance, double maxOutputLuminance, double maxAddedHeadroom); void addTonemapper(const Colorimetry &containerColorimetry, double referenceLuminance, double maxInputLuminance, double maxOutputLuminance);
void add(const ColorOp &op); void add(const ColorOp &op);
ValueRange inputRange; ValueRange inputRange;

View file

@ -114,14 +114,16 @@ vec3 doTonemapping(vec3 color) {
vec3 ICtCp = toICtCp * lms_PQ; vec3 ICtCp = toICtCp * lms_PQ;
float luminance = singlePqToLinear(ICtCp.r) * 10000.0; float luminance = singlePqToLinear(ICtCp.r) * 10000.0;
// if the reference is too close to the maximum luminance, reduce it to get up to 50% headroom
float inputRange = maxTonemappingLuminance / destinationReferenceLuminance; float inputRange = maxTonemappingLuminance / destinationReferenceLuminance;
float outputRange = maxDestinationLuminance / destinationReferenceLuminance; float outputRange = maxDestinationLuminance / destinationReferenceLuminance;
float addedRange = min(inputRange, clamp(1.5 / outputRange, 1.0, 1.5)); // how much dynamic range we need to decently present the content
float outputReferenceLuminance = destinationReferenceLuminance / addedRange; float minDecentRange = min(inputRange, 1.5);
// if the output doesn't provide enough HDR headroom for the tone mapper to do a good job, dim the image to create some
float referenceDimming = 1.0 / clamp(outputRange / minDecentRange, 1.0, minDecentRange);
float outputReferenceLuminance = destinationReferenceLuminance * referenceDimming;
// keep it linear up to the reference luminance // keep it linear up to the reference luminance
float low = min(luminance / addedRange, outputReferenceLuminance); float low = min(luminance * referenceDimming, outputReferenceLuminance);
// and apply a nonlinear curve above, to reduce the luminance without completely removing differences // and apply a nonlinear curve above, to reduce the luminance without completely removing differences
float relativeHighlight = clamp((luminance / destinationReferenceLuminance - 1.0) / (inputRange - 1.0), 0.0, 1.0); float relativeHighlight = clamp((luminance / destinationReferenceLuminance - 1.0) / (inputRange - 1.0), 0.0, 1.0);
const float e = 2.718281828459045; const float e = 2.718281828459045;