From 6f06bf19893bcf70b7dfa4b85c8529a2d2abdf1a Mon Sep 17 00:00:00 2001 From: Xaver Hugl Date: Mon, 11 Dec 2023 14:44:04 +0100 Subject: [PATCH] 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 --- src/opengl/colormanagement.glsl | 108 +++++++++++ src/opengl/glshadermanager.cpp | 194 ++++++++------------ src/opengl/glshadermanager.h | 1 + src/opengl/saturation.glsl | 9 + src/plugins/invert/shaders/invert.frag | 18 +- src/plugins/invert/shaders/invert_core.frag | 19 +- src/resources.qrc | 4 +- 7 files changed, 222 insertions(+), 131 deletions(-) create mode 100644 src/opengl/colormanagement.glsl create mode 100644 src/opengl/saturation.glsl diff --git a/src/opengl/colormanagement.glsl b/src/opengl/colormanagement.glsl new file mode 100644 index 0000000000..2e3c8bcba9 --- /dev/null +++ b/src/opengl/colormanagement.glsl @@ -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); +} diff --git a/src/opengl/glshadermanager.cpp b/src/opengl/glshadermanager.cpp index c26a196280..1263c64943 100644 --- a/src/opengl/glshadermanager.cpp +++ b/src/opengl/glshadermanager.cpp @@ -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 ShaderManager::generateShader(ShaderTraits traits) return generateCustomShader(traits); } +std::optional 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 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 shader{new GLShader(GLShader::ExplicitLinking)}; - shader->load(vertex, fragment); + shader->load(*vertex, *fragment); shader->bindAttributeLocation("position", VA_Position); shader->bindAttributeLocation("texcoord", VA_TexCoord); diff --git a/src/opengl/glshadermanager.h b/src/opengl/glshadermanager.h index 5d629788b3..6de55580cc 100644 --- a/src/opengl/glshadermanager.h +++ b/src/opengl/glshadermanager.h @@ -152,6 +152,7 @@ private: void bindFragDataLocations(GLShader *shader); void bindAttributeLocations(GLShader *shader) const; + std::optional preprocess(const QByteArray &src, int recursionDepth = 0) const; QByteArray generateVertexSource(ShaderTraits traits) const; QByteArray generateFragmentSource(ShaderTraits traits) const; std::unique_ptr generateShader(ShaderTraits traits); diff --git a/src/opengl/saturation.glsl b/src/opengl/saturation.glsl new file mode 100644 index 0000000000..0eef6a7e2e --- /dev/null +++ b/src/opengl/saturation.glsl @@ -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); +} diff --git a/src/plugins/invert/shaders/invert.frag b/src/plugins/invert/shaders/invert.frag index 49d7861e15..e08f449592 100644 --- a/src/plugins/invert/shaders/invert.frag +++ b/src/plugins/invert/shaders/invert.frag @@ -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); } diff --git a/src/plugins/invert/shaders/invert_core.frag b/src/plugins/invert/shaders/invert_core.frag index 632f0a3d88..ec2801b91e 100644 --- a/src/plugins/invert/shaders/invert_core.frag +++ b/src/plugins/invert/shaders/invert_core.frag @@ -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); } diff --git a/src/resources.qrc b/src/resources.qrc index 4214120ba9..b8f9c0ac0c 100644 --- a/src/resources.qrc +++ b/src/resources.qrc @@ -1,8 +1,10 @@ + opengl/colormanagement.glsl + opengl/saturation.glsl scene/shaders/debug_fractional.frag - scene/shaders/debug_fractional_core.frag scene/shaders/debug_fractional.vert + scene/shaders/debug_fractional_core.frag scene/shaders/debug_fractional_core.vert