plugins/invert: support color management

To do this, this commit adds infrastructure to include glsl files,
extracts all the color management functions and uniforms into such a
file, and makes use of it in the invert effect

BUG: 443148
This commit is contained in:
Xaver Hugl 2023-12-11 14:44:04 +01:00
parent 8f7772da2e
commit 6f06bf1989
7 changed files with 222 additions and 131 deletions

View file

@ -0,0 +1,108 @@
const int sRGB_EOTF = 0;
const int linear_EOTF = 1;
const int PQ_EOTF = 2;
const int scRGB_EOTF = 3;
const int gamma22_EOTF = 4;
uniform mat3 colorimetryTransform;
uniform int sourceNamedTransferFunction;
uniform int destinationNamedTransferFunction;
uniform float sdrBrightness;// in nits
uniform float maxHdrBrightness; // in nits
vec3 nitsToPq(vec3 nits) {
vec3 normalized = clamp(nits / 10000.0, vec3(0), vec3(1));
const float c1 = 0.8359375;
const float c2 = 18.8515625;
const float c3 = 18.6875;
const float m1 = 0.1593017578125;
const float m2 = 78.84375;
vec3 powed = pow(normalized, vec3(m1));
vec3 num = vec3(c1) + c2 * powed;
vec3 denum = vec3(1.0) + c3 * powed;
return pow(num / denum, vec3(m2));
}
vec3 pqToNits(vec3 pq) {
const float c1 = 0.8359375;
const float c2 = 18.8515625;
const float c3 = 18.6875;
const float m1_inv = 1.0 / 0.1593017578125;
const float m2_inv = 1.0 / 78.84375;
vec3 powed = pow(pq, vec3(m2_inv));
vec3 num = max(powed - c1, vec3(0.0));
vec3 den = c2 - c3 * powed;
return 10000.0 * pow(num / den, vec3(m1_inv));
}
vec3 srgbToLinear(vec3 color) {
bvec3 isLow = lessThanEqual(color, vec3(0.04045f));
vec3 loPart = color / 12.92f;
vec3 hiPart = pow((color + 0.055f) / 1.055f, vec3(12.0f / 5.0f));
#if glslVersion >= 1.30
return mix(hiPart, loPart, isLow);
#else
return mix(hiPart, loPart, vec3(isLow.r ? 1.0 : 0.0, isLow.g ? 1.0 : 0.0, isLow.b ? 1.0 : 0.0));
#endif
}
vec3 linearToSrgb(vec3 color) {
bvec3 isLow = lessThanEqual(color, vec3(0.0031308f));
vec3 loPart = color * 12.92f;
vec3 hiPart = pow(color, vec3(5.0f / 12.0f)) * 1.055f - 0.055f;
#if glslVersion >= 1.30
return mix(hiPart, loPart, isLow);
#else
return mix(hiPart, loPart, vec3(isLow.r ? 1.0 : 0.0, isLow.g ? 1.0 : 0.0, isLow.b ? 1.0 : 0.0));
#endif
}
vec3 doTonemapping(vec3 color, float maxBrightness) {
// TODO do something better here
return clamp(color, vec3(0.0), vec3(maxBrightness));
}
vec4 encodingToNits(vec4 color, int sourceTransferFunction) {
if (sourceTransferFunction == sRGB_EOTF) {
color.rgb /= max(color.a, 0.001);
color.rgb = sdrBrightness * srgbToLinear(color.rgb);
color.rgb *= color.a;
} else if (sourceTransferFunction == PQ_EOTF) {
color.rgb /= max(color.a, 0.001);
color.rgb = pqToNits(color.rgb);
color.rgb *= color.a;
} else if (sourceTransferFunction == scRGB_EOTF) {
color.rgb *= 80.0;
} else if (sourceTransferFunction == gamma22_EOTF) {
color.rgb /= max(color.a, 0.001);
color.rgb = sdrBrightness * pow(color.rgb, vec3(2.2));
color.rgb *= color.a;
}
return color;
}
vec4 sourceEncodingToNitsInDestinationColorspace(vec4 color) {
color = encodingToNits(color, sourceNamedTransferFunction);
return vec4(doTonemapping(colorimetryTransform * color.rgb, maxHdrBrightness), color.a);
}
vec4 nitsToEncoding(vec4 color, int destinationTransferFunction) {
if (destinationTransferFunction == sRGB_EOTF) {
color.rgb /= max(color.a, 0.001);
color.rgb = linearToSrgb(doTonemapping(color.rgb, sdrBrightness) / sdrBrightness);
color.rgb *= color.a;
} else if (destinationTransferFunction == PQ_EOTF) {
color.rgb /= max(color.a, 0.001);
color.rgb = nitsToPq(color.rgb);
color.rgb *= color.a;
} else if (destinationTransferFunction == scRGB_EOTF) {
color.rgb /= 80.0;
} else if (destinationTransferFunction == gamma22_EOTF) {
color.rgb /= max(color.a, 0.001);
color.rgb = pow(color.rgb / sdrBrightness, vec3(1.0 / 2.2));
color.rgb *= color.a;
}
return color;
}
vec4 nitsToDestinationEncoding(vec4 color) {
return nitsToEncoding(color, destinationNamedTransferFunction);
}

View file

@ -130,18 +130,6 @@ QByteArray ShaderManager::generateFragmentSource(ShaderTraits traits) const
textureLookup = glsl_es_300 ? QByteArrayLiteral("texture") : QByteArrayLiteral("texture2D");
output = glsl_es_300 ? QByteArrayLiteral("fragColor") : QByteArrayLiteral("gl_FragColor");
}
if (gl->glslVersion() >= Version(1, 30)) {
// mix with bvec3 is only supported with glsl 1.30 and greater
stream << "\n";
stream << "vec3 doMix(vec3 left, vec3 right, bvec3 rightFactor) {\n";
stream << " return mix(left, right, rightFactor);\n";
stream << "}\n";
} else {
stream << "\n";
stream << "vec3 doMix(vec3 left, vec3 right, bvec3 rightFactor) {\n";
stream << " return mix(left, right, vec3(rightFactor.r ? 1.0 : 0.0, rightFactor.g ? 1.0 : 0.0, rightFactor.b ? 1.0 : 0.0));\n";
stream << "}\n";
}
if (traits & ShaderTrait::MapTexture) {
stream << "uniform sampler2D sampler;\n";
@ -159,64 +147,10 @@ QByteArray ShaderManager::generateFragmentSource(ShaderTraits traits) const
stream << "uniform vec4 modulation;\n";
}
if (traits & ShaderTrait::AdjustSaturation) {
stream << "uniform float saturation;\n";
stream << "uniform vec3 primaryBrightness;\n";
stream << "#include \"saturation.glsl\"\n";
}
if (traits & ShaderTrait::TransformColorspace) {
stream << "const int sRGB_EOTF = 0;\n";
stream << "const int linear_EOTF = 1;\n";
stream << "const int PQ_EOTF = 2;\n";
stream << "const int scRGB_EOTF = 3;\n";
stream << "const int gamma22_EOTF = 4;\n";
stream << "\n";
stream << "uniform mat3 colorimetryTransform;\n";
stream << "uniform int sourceNamedTransferFunction;\n";
stream << "uniform int destinationNamedTransferFunction;\n";
stream << "uniform float sdrBrightness;// in nits\n";
stream << "uniform float maxHdrBrightness; // in nits\n";
stream << "\n";
stream << "vec3 nitsToPq(vec3 nits) {\n";
stream << " vec3 normalized = clamp(nits / 10000.0, vec3(0), vec3(1));\n";
stream << " const float c1 = 0.8359375;\n";
stream << " const float c2 = 18.8515625;\n";
stream << " const float c3 = 18.6875;\n";
stream << " const float m1 = 0.1593017578125;\n";
stream << " const float m2 = 78.84375;\n";
stream << " vec3 powed = pow(normalized, vec3(m1));\n";
stream << " vec3 num = vec3(c1) + c2 * powed;\n";
stream << " vec3 denum = vec3(1.0) + c3 * powed;\n";
stream << " return pow(num / denum, vec3(m2));\n";
stream << "}\n";
stream << "vec3 pqToNits(vec3 pq) {\n";
stream << " const float c1 = 0.8359375;\n";
stream << " const float c2 = 18.8515625;\n";
stream << " const float c3 = 18.6875;\n";
stream << " const float m1_inv = 1.0 / 0.1593017578125;\n";
stream << " const float m2_inv = 1.0 / 78.84375;\n";
stream << " vec3 powed = pow(pq, vec3(m2_inv));\n";
stream << " vec3 num = max(powed - c1, vec3(0.0));\n";
stream << " vec3 den = c2 - c3 * powed;\n";
stream << " return 10000.0 * pow(num / den, vec3(m1_inv));\n";
stream << "}\n";
stream << "vec3 srgbToLinear(vec3 color) {\n";
stream << " bvec3 isLow = lessThanEqual(color, vec3(0.04045f));\n";
stream << " vec3 loPart = color / 12.92f;\n";
stream << " vec3 hiPart = pow((color + 0.055f) / 1.055f, vec3(12.0f / 5.0f));\n";
stream << " return doMix(hiPart, loPart, isLow);\n";
stream << "}\n";
stream << "\n";
stream << "vec3 linearToSrgb(vec3 color) {\n";
stream << " bvec3 isLow = lessThanEqual(color, vec3(0.0031308f));\n";
stream << " vec3 loPart = color * 12.92f;\n";
stream << " vec3 hiPart = pow(color, vec3(5.0f / 12.0f)) * 1.055f - 0.055f;\n";
stream << " return doMix(hiPart, loPart, isLow);\n";
stream << "}\n";
stream << "\n";
stream << "vec3 doTonemapping(vec3 color, float maxBrightness) {\n";
stream << " // colorimetric 'tonemapping': just clip to the output color space\n";
stream << " return clamp(color, vec3(0.0), vec3(maxBrightness));\n";
stream << "}\n";
stream << "\n";
stream << "#include \"colormanagement.glsl\"";
}
if (output != QByteArrayLiteral("gl_FragColor")) {
@ -252,48 +186,16 @@ QByteArray ShaderManager::generateFragmentSource(ShaderTraits traits) const
stream << " result = geometryColor;\n";
}
if (traits & ShaderTrait::TransformColorspace) {
stream << " if (sourceNamedTransferFunction == sRGB_EOTF) {\n";
stream << " result.rgb /= max(result.a, 0.001);\n";
stream << " result.rgb = sdrBrightness * srgbToLinear(result.rgb);\n";
stream << " result.rgb *= result.a;\n";
stream << " } else if (sourceNamedTransferFunction == PQ_EOTF) {\n";
stream << " result.rgb /= max(result.a, 0.001);\n";
stream << " result.rgb = pqToNits(result.rgb);\n";
stream << " result.rgb *= result.a;\n";
stream << " } else if (sourceNamedTransferFunction == scRGB_EOTF) {\n";
stream << " result.rgb *= 80.0;\n";
stream << " } else if (sourceNamedTransferFunction == gamma22_EOTF) {\n";
stream << " result.rgb /= max(result.a, 0.001);\n";
stream << " result.rgb = sdrBrightness * pow(result.rgb, vec3(2.2));\n";
stream << " result.rgb *= result.a;\n";
stream << " }\n";
stream << " result.rgb = doTonemapping(colorimetryTransform * result.rgb, maxHdrBrightness);\n";
stream << " result = sourceEncodingToNitsInDestinationColorspace(result);\n";
}
if (traits & ShaderTrait::AdjustSaturation) {
// this calculates the Y component of the XYZ color representation for the color,
// which roughly corresponds to the brightness of the RGB tuple
stream << " float Y = dot(result.rgb, primaryBrightness);\n";
stream << " result.rgb = mix(vec3(Y), result.rgb, saturation);\n";
stream << " result = adjustSaturation(result);\n";
}
if (traits & ShaderTrait::Modulate) {
stream << " result *= modulation;\n";
}
if (traits & ShaderTrait::TransformColorspace) {
stream << " if (destinationNamedTransferFunction == sRGB_EOTF) {\n";
stream << " result.rgb /= max(result.a, 0.001);\n";
stream << " result.rgb = linearToSrgb(doTonemapping(result.rgb, sdrBrightness) / sdrBrightness);\n";
stream << " result.rgb *= result.a;\n";
stream << " } else if (destinationNamedTransferFunction == PQ_EOTF) {\n";
stream << " result.rgb /= max(result.a, 0.001);\n";
stream << " result.rgb = nitsToPq(result.rgb);\n";
stream << " result.rgb *= result.a;\n";
stream << " } else if (destinationNamedTransferFunction == scRGB_EOTF) {\n";
stream << " result.rgb /= 80.0;\n";
stream << " } else if (destinationNamedTransferFunction == gamma22_EOTF) {\n";
stream << " result.rgb /= max(result.a, 0.001);\n";
stream << " result.rgb = pow(result.rgb / sdrBrightness, vec3(1.0 / 2.2));\n";
stream << " result.rgb *= result.a;\n";
stream << " }\n";
stream << " result = nitsToDestinationEncoding(result);\n";
}
stream << " " << output << " = result;\n";
@ -307,21 +209,85 @@ std::unique_ptr<GLShader> ShaderManager::generateShader(ShaderTraits traits)
return generateCustomShader(traits);
}
std::optional<QByteArray> ShaderManager::preprocess(const QByteArray &src, int recursionDepth) const
{
recursionDepth++;
if (recursionDepth > 10) {
qCWarning(KWIN_OPENGL, "shader has too many recursive includes!");
return std::nullopt;
}
QByteArray ret;
ret.reserve(src.size());
const auto split = src.split('\n');
for (auto it = split.begin(); it != split.end(); it++) {
const auto &line = *it;
if (line.startsWith("#include \"") && line.endsWith("\"")) {
static constexpr ssize_t includeLength = QByteArrayView("#include \"").size();
const QByteArray path = ":/opengl/" + line.mid(includeLength, line.size() - includeLength - 1);
QFile file(path);
if (!file.open(QIODevice::ReadOnly)) {
qCWarning(KWIN_OPENGL, "failed to read include line %s", qPrintable(line));
return std::nullopt;
}
const auto processed = preprocess(file.readAll(), recursionDepth);
if (!processed) {
return std::nullopt;
}
ret.append(*processed);
} else if (line.startsWith("#if glslVersion ")) {
static constexpr ssize_t ifLength = QByteArrayView("#if glslVersion ").size();
if (line.size() < ifLength + 3) {
qCWarning(KWIN_OPENGL, "failed parsing #if condition in line %s", qPrintable(line));
return std::nullopt;
}
const QByteArray condition = line.mid(ifLength);
const QByteArray versionString = line.mid(ifLength + 2);
if (!condition.startsWith(">=")) {
qCWarning(KWIN_OPENGL, "unsupported comparison operator in line %s", qPrintable(line));
return std::nullopt;
}
const Version version = Version::parseString(versionString);
if (!version.isValid()) {
qCWarning(KWIN_OPENGL, "invalid version in line %s", qPrintable(line));
return std::nullopt;
}
const bool keep = GLPlatform::instance()->glslVersion() >= version;
for (it = it + 1; it != split.end(); it++) {
if (it->startsWith("#endif")) {
break;
} else if (it->startsWith("#else")) {
for (; it != split.end(); it++) {
if (it->startsWith("#endif")) {
break;
} else if (!keep) {
ret.append(*it);
ret.append('\n');
}
}
break;
} else if (keep) {
ret.append(*it);
ret.append('\n');
}
}
} else {
ret.append(line);
ret.append('\n');
}
}
return ret;
}
std::unique_ptr<GLShader> ShaderManager::generateCustomShader(ShaderTraits traits, const QByteArray &vertexSource, const QByteArray &fragmentSource)
{
const QByteArray vertex = vertexSource.isEmpty() ? generateVertexSource(traits) : vertexSource;
const QByteArray fragment = fragmentSource.isEmpty() ? generateFragmentSource(traits) : fragmentSource;
#if 0
qCDebug(KWIN_OPENGL) << "**************";
qCDebug(KWIN_OPENGL) << vertex;
qCDebug(KWIN_OPENGL) << "**************";
qCDebug(KWIN_OPENGL) << fragment;
qCDebug(KWIN_OPENGL) << "**************";
#endif
const auto vertex = preprocess(vertexSource.isEmpty() ? generateVertexSource(traits) : vertexSource);
const auto fragment = preprocess(fragmentSource.isEmpty() ? generateFragmentSource(traits) : fragmentSource);
if (!vertex || !fragment) {
return nullptr;
}
std::unique_ptr<GLShader> shader{new GLShader(GLShader::ExplicitLinking)};
shader->load(vertex, fragment);
shader->load(*vertex, *fragment);
shader->bindAttributeLocation("position", VA_Position);
shader->bindAttributeLocation("texcoord", VA_TexCoord);

View file

@ -152,6 +152,7 @@ private:
void bindFragDataLocations(GLShader *shader);
void bindAttributeLocations(GLShader *shader) const;
std::optional<QByteArray> preprocess(const QByteArray &src, int recursionDepth = 0) const;
QByteArray generateVertexSource(ShaderTraits traits) const;
QByteArray generateFragmentSource(ShaderTraits traits) const;
std::unique_ptr<GLShader> generateShader(ShaderTraits traits);

View file

@ -0,0 +1,9 @@
uniform float saturation;
uniform vec3 primaryBrightness;
vec4 adjustSaturation(vec4 color) {
// this calculates the Y component of the XYZ color representation for the color,
// which roughly corresponds to the brightness of the RGB tuple
float Y = dot(color.rgb, primaryBrightness);
return vec4(mix(vec3(Y), color.rgb, saturation), color.a);
}

View file

@ -1,22 +1,24 @@
#include "colormanagement.glsl"
#include "saturation.glsl"
uniform sampler2D sampler;
uniform vec4 modulation;
uniform float saturation;
varying vec2 texcoord0;
void main()
{
vec4 tex = texture2D(sampler, texcoord0);
tex = sourceEncodingToNitsInDestinationColorspace(tex);
tex = adjustSaturation(tex);
if (saturation != 1.0) {
vec3 desaturated = tex.rgb * vec3( 0.30, 0.59, 0.11 );
desaturated = vec3( dot( desaturated, tex.rgb ));
tex.rgb = tex.rgb * vec3( saturation ) + desaturated * vec3( 1.0 - saturation );
}
// to preserve perceptual contrast, apply the inversion in gamma 2.2 space
tex = nitsToEncoding(tex, gamma22_EOTF);
tex.rgb /= max(0.001, tex.a);
tex.rgb = vec3(1.0) - tex.rgb;
tex *= modulation;
tex.rgb *= tex.a;
tex = encodingToNits(tex, gamma22_EOTF);
gl_FragColor = tex;
gl_FragColor = nitsToDestinationEncoding(tex);
}

View file

@ -1,7 +1,10 @@
#version 140
#include "colormanagement.glsl"
#include "saturation.glsl"
uniform sampler2D sampler;
uniform vec4 modulation;
uniform float saturation;
in vec2 texcoord0;
@ -10,16 +13,16 @@ out vec4 fragColor;
void main()
{
vec4 tex = texture(sampler, texcoord0);
tex = sourceEncodingToNitsInDestinationColorspace(tex);
tex = adjustSaturation(tex);
if (saturation != 1.0) {
vec3 desaturated = tex.rgb * vec3( 0.30, 0.59, 0.11 );
desaturated = vec3( dot( desaturated, tex.rgb ));
tex.rgb = tex.rgb * vec3( saturation ) + desaturated * vec3( 1.0 - saturation );
}
// to preserve perceptual contrast, apply the inversion in gamma 2.2 space
tex = nitsToEncoding(tex, gamma22_EOTF);
tex.rgb /= max(0.001, tex.a);
tex.rgb = vec3(1.0) - tex.rgb;
tex *= modulation;
tex.rgb *= tex.a;
tex = encodingToNits(tex, gamma22_EOTF);
fragColor = tex;
fragColor = nitsToDestinationEncoding(tex);
}

View file

@ -1,8 +1,10 @@
<!DOCTYPE RCC><RCC version="1.0">
<qresource prefix="/">
<file>opengl/colormanagement.glsl</file>
<file>opengl/saturation.glsl</file>
<file>scene/shaders/debug_fractional.frag</file>
<file>scene/shaders/debug_fractional_core.frag</file>
<file>scene/shaders/debug_fractional.vert</file>
<file>scene/shaders/debug_fractional_core.frag</file>
<file>scene/shaders/debug_fractional_core.vert</file>
</qresource>
</RCC>