From bf1602d9f902410500dcfa8f5a38f716574c1f39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20M=C3=BCller?= Date: Sun, 20 Jul 2025 18:20:27 +0200 Subject: [PATCH] renderer: implement wp-color-management-v1 transfer functions (#11084) --- src/protocols/ColorManagement.cpp | 24 +-- src/protocols/XXColorManagement.cpp | 18 +- src/render/shaders/glsl/CM.glsl | 256 +++++++++++++++++++--------- 3 files changed, 203 insertions(+), 95 deletions(-) diff --git a/src/protocols/ColorManagement.cpp b/src/protocols/ColorManagement.cpp index aa01a365..7cfcbbbc 100644 --- a/src/protocols/ColorManagement.cpp +++ b/src/protocols/ColorManagement.cpp @@ -44,16 +44,13 @@ CColorManager::CColorManager(SP resource) : m_resource(resour m_resource->sendSupportedTfNamed(WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_LINEAR); m_resource->sendSupportedTfNamed(WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ST2084_PQ); m_resource->sendSupportedTfNamed(WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_HLG); - - if (PROTO::colorManagement->m_debug) { - m_resource->sendSupportedTfNamed(WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_BT1886); - m_resource->sendSupportedTfNamed(WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ST240); - m_resource->sendSupportedTfNamed(WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_LOG_100); - m_resource->sendSupportedTfNamed(WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_LOG_316); - m_resource->sendSupportedTfNamed(WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_XVYCC); - m_resource->sendSupportedTfNamed(WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_SRGB); - m_resource->sendSupportedTfNamed(WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ST428); - } + m_resource->sendSupportedTfNamed(WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_BT1886); + m_resource->sendSupportedTfNamed(WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ST240); + m_resource->sendSupportedTfNamed(WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_LOG_100); + m_resource->sendSupportedTfNamed(WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_LOG_316); + m_resource->sendSupportedTfNamed(WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_XVYCC); + m_resource->sendSupportedTfNamed(WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_EXT_SRGB); + m_resource->sendSupportedTfNamed(WP_COLOR_MANAGER_V1_TRANSFER_FUNCTION_ST428); m_resource->sendSupportedIntent(WP_COLOR_MANAGER_V1_RENDER_INTENT_PERCEPTUAL); if (PROTO::colorManagement->m_debug) { @@ -549,6 +546,13 @@ CColorManagementParametricCreator::CColorManagementParametricCreator(SPerror(WP_IMAGE_DESCRIPTION_CREATOR_PARAMS_V1_ERROR_INVALID_TF, "Unsupported transfer function"); return; } diff --git a/src/protocols/XXColorManagement.cpp b/src/protocols/XXColorManagement.cpp index 23b033a0..d01e367a 100644 --- a/src/protocols/XXColorManagement.cpp +++ b/src/protocols/XXColorManagement.cpp @@ -58,6 +58,14 @@ CXXColorManager::CXXColorManager(SP resource_) : m_resource(r m_resource->sendSupportedTfNamed(XX_COLOR_MANAGER_V4_TRANSFER_FUNCTION_SRGB); m_resource->sendSupportedTfNamed(XX_COLOR_MANAGER_V4_TRANSFER_FUNCTION_ST2084_PQ); m_resource->sendSupportedTfNamed(XX_COLOR_MANAGER_V4_TRANSFER_FUNCTION_LINEAR); + m_resource->sendSupportedTfNamed(XX_COLOR_MANAGER_V4_TRANSFER_FUNCTION_BT709); + m_resource->sendSupportedTfNamed(XX_COLOR_MANAGER_V4_TRANSFER_FUNCTION_BT1361); + m_resource->sendSupportedTfNamed(XX_COLOR_MANAGER_V4_TRANSFER_FUNCTION_ST240); + m_resource->sendSupportedTfNamed(XX_COLOR_MANAGER_V4_TRANSFER_FUNCTION_LOG_100); + m_resource->sendSupportedTfNamed(XX_COLOR_MANAGER_V4_TRANSFER_FUNCTION_LOG_316); + m_resource->sendSupportedTfNamed(XX_COLOR_MANAGER_V4_TRANSFER_FUNCTION_XVYCC); + m_resource->sendSupportedTfNamed(XX_COLOR_MANAGER_V4_TRANSFER_FUNCTION_EXT_SRGB); + m_resource->sendSupportedTfNamed(XX_COLOR_MANAGER_V4_TRANSFER_FUNCTION_ST428); m_resource->sendSupportedIntent(XX_COLOR_MANAGER_V4_RENDER_INTENT_PERCEPTUAL); // resource->sendSupportedIntent(XX_COLOR_MANAGER_V4_RENDER_INTENT_RELATIVE); @@ -405,7 +413,15 @@ CXXColorManagementParametricCreator::CXXColorManagementParametricCreator(SPerror(XX_IMAGE_DESCRIPTION_CREATOR_PARAMS_V4_ERROR_INVALID_TF, "Unsupported transfer function"); return; } diff --git a/src/render/shaders/glsl/CM.glsl b/src/render/shaders/glsl/CM.glsl index 8c505ee5..9074fa4f 100644 --- a/src/render/shaders/glsl/CM.glsl +++ b/src/render/shaders/glsl/CM.glsl @@ -25,12 +25,23 @@ uniform mat3 convertMatrix; // sRGB constants #define SRGB_POW 2.4 -#define SRGB_INV_POW (1.0 / SRGB_POW) -#define SRGB_D_CUT 0.04045 -#define SRGB_E_CUT 0.0031308 -#define SRGB_LO 12.92 -#define SRGB_HI 1.055 -#define SRGB_HI_ADD 0.055 +#define SRGB_CUT 0.0031308 +#define SRGB_SCALE 12.92 +#define SRGB_ALPHA 1.055 + +#define BT1886_POW (1.0 / 0.45) +#define BT1886_CUT 0.018053968510807 +#define BT1886_SCALE 4.5 +#define BT1886_ALPHA (1.0 + 5.5 * BT1886_CUT) + +// See http://car.france3.mars.free.fr/HD/INA-%2026%20jan%2006/SMPTE%20normes%20et%20confs/s240m.pdf +#define ST240_POW (1.0 / 0.45) +#define ST240_CUT 0.0228 +#define ST240_SCALE 4.0 +#define ST240_ALPHA 1.1115 + +#define ST428_POW 2.6 +#define ST428_SCALE (52.37 / 48.0) // PQ constants #define PQ_M1 0.1593017578125 @@ -43,7 +54,7 @@ uniform mat3 convertMatrix; // HLG constants #define HLG_D_CUT (1.0 / 12.0) -#define HLG_E_CUT (sqrt(3.0) * pow(HLG_D_CUT, 0.5)) +#define HLG_E_CUT 0.5 #define HLG_A 0.17883277 #define HLG_B 0.28466892 #define HLG_C 0.55991073 @@ -59,7 +70,7 @@ uniform mat3 convertMatrix; vec3 xy2xyz(vec2 xy) { if (xy.y == 0.0) return vec3(0.0, 0.0, 0.0); - + return vec3(xy.x / xy.y, 1.0, (1.0 - xy.x - xy.y) / xy.y); } @@ -71,43 +82,131 @@ vec4 saturate(vec4 color, mat3 primaries, float saturation) { return vec4(mix(vec3(Y), color.rgb, saturation), color[3]); } -vec3 toLinearRGB(vec3 color, int tf) { - if (tf == CM_TRANSFER_FUNCTION_EXT_LINEAR) - return color; - - bvec3 isLow; - vec3 lo; - vec3 hi; - switch (tf) { - case CM_TRANSFER_FUNCTION_ST2084_PQ: - vec3 E = pow(clamp(color.rgb, vec3(0.0), vec3(1.0)), vec3(PQ_INV_M2)); - return pow( - (max(E - PQ_C1, vec3(0.0))) / (PQ_C2 - PQ_C3 * E), - vec3(PQ_INV_M1) - ); - case CM_TRANSFER_FUNCTION_GAMMA22: - return pow(max(color.rgb, vec3(0.0)), vec3(2.2)); - case CM_TRANSFER_FUNCTION_GAMMA28: - return pow(max(color.rgb, vec3(0.0)), vec3(2.8)); - case CM_TRANSFER_FUNCTION_HLG: - isLow = lessThanEqual(color.rgb, vec3(HLG_D_CUT)); - lo = sqrt(3.0) * pow(color.rgb, vec3(0.5)); - hi = HLG_A * log(12.0 * color.rgb - HLG_B) + HLG_C; - return mix(hi, lo, isLow); - case CM_TRANSFER_FUNCTION_BT1886: - case CM_TRANSFER_FUNCTION_ST240: - case CM_TRANSFER_FUNCTION_LOG_100: - case CM_TRANSFER_FUNCTION_LOG_316: - case CM_TRANSFER_FUNCTION_XVYCC: - case CM_TRANSFER_FUNCTION_EXT_SRGB: - case CM_TRANSFER_FUNCTION_ST428: +// The primary source for these transfer functions is https://www.itu.int/dms_pubrec/itu-r/rec/bt/R-REC-BT.1361-0-199802-W!!PDF-E.pdf +vec3 tfInvPQ(vec3 color) { + vec3 E = pow(clamp(color.rgb, vec3(0.0), vec3(1.0)), vec3(PQ_INV_M2)); + return pow( + (max(E - PQ_C1, vec3(0.0))) / (PQ_C2 - PQ_C3 * E), + vec3(PQ_INV_M1) + ); +} +vec3 tfInvHLG(vec3 color) { + bvec3 isLow = lessThanEqual(color.rgb, vec3(HLG_E_CUT)); + vec3 lo = color.rgb * color.rgb / 3.0; + vec3 hi = (exp((color.rgb - HLG_C) / HLG_A) + HLG_B) / 12.0; + return mix(hi, lo, isLow); +} + +// Many transfer functions (including sRGB) follow the same pattern: a linear +// segment for small values and a power function for larger values. The +// following function implements this pattern from which sRGB, BT.1886, and +// others can be derived by plugging in the right constants. +vec3 tfInvLinPow(vec3 color, float gamma, float thres, float scale, float alpha) { + bvec3 isLow = lessThanEqual(color.rgb, vec3(thres * scale)); + vec3 lo = color.rgb / scale; + vec3 hi = pow((color.rgb + alpha - 1.0) / alpha, vec3(gamma)); + return mix(hi, lo, isLow); +} + +vec3 tfInvSRGB(vec3 color) { + return tfInvLinPow(color, SRGB_POW, SRGB_CUT, SRGB_SCALE, SRGB_ALPHA); +} + +vec3 tfInvExtSRGB(vec3 color) { + // EXT sRGB is the sRGB transfer function mirrored around 0. + return sign(color) * tfInvSRGB(abs(color)); +} + +vec3 tfInvBT1886(vec3 color) { + return tfInvLinPow(color, BT1886_POW, BT1886_CUT, BT1886_SCALE, BT1886_ALPHA); +} + +vec3 tfInvXVYCC(vec3 color) { + // The inverse transfer function for XVYCC is the BT1886 transfer function mirrored around 0, + // same as what EXT sRGB is to sRGB. + return sign(color) * tfInvBT1886(abs(color)); +} + +vec3 tfInvST240(vec3 color) { + return tfInvLinPow(color, ST240_POW, ST240_CUT, ST240_SCALE, ST240_ALPHA); +} + +// Forward transfer functions corresponding to the inverse functions above. +vec3 tfPQ(vec3 color) { + vec3 E = pow(clamp(color.rgb, vec3(0.0), vec3(1.0)), vec3(PQ_M1)); + return pow( + (vec3(PQ_C1) + PQ_C2 * E) / (vec3(1.0) + PQ_C3 * E), + vec3(PQ_M2) + ); +} + +vec3 tfHLG(vec3 color) { + bvec3 isLow = lessThanEqual(color.rgb, vec3(HLG_D_CUT)); + vec3 lo = sqrt(max(color.rgb, vec3(0.0)) * 3.0); + vec3 hi = HLG_A * log(max(12.0 * color.rgb - HLG_B, vec3(0.0001))) + HLG_C; + return mix(hi, lo, isLow); +} + +vec3 tfLinPow(vec3 color, float gamma, float thres, float scale, float alpha) { + bvec3 isLow = lessThanEqual(color.rgb, vec3(thres)); + vec3 lo = color.rgb * scale; + vec3 hi = pow(color.rgb, vec3(1.0 / gamma)) * alpha - (alpha - 1.0); + return mix(hi, lo, isLow); +} + +vec3 tfSRGB(vec3 color) { + return tfLinPow(color, SRGB_POW, SRGB_CUT, SRGB_SCALE, SRGB_ALPHA); +} + +vec3 tfExtSRGB(vec3 color) { + // EXT sRGB is the sRGB transfer function mirrored around 0. + return sign(color) * tfSRGB(abs(color)); +} + +vec3 tfBT1886(vec3 color) { + return tfLinPow(color, BT1886_POW, BT1886_CUT, BT1886_SCALE, BT1886_ALPHA); +} + +vec3 tfXVYCC(vec3 color) { + // The transfer function for XVYCC is the BT1886 transfer function mirrored around 0, + // same as what EXT sRGB is to sRGB. + return sign(color) * tfBT1886(abs(color)); +} + +vec3 tfST240(vec3 color) { + return tfLinPow(color, ST240_POW, ST240_CUT, ST240_SCALE, ST240_ALPHA); +} + +vec3 toLinearRGB(vec3 color, int tf) { + switch (tf) { + case CM_TRANSFER_FUNCTION_EXT_LINEAR: + return color; + case CM_TRANSFER_FUNCTION_ST2084_PQ: + return tfInvPQ(color); + case CM_TRANSFER_FUNCTION_GAMMA22: + return pow(max(color, vec3(0.0)), vec3(2.2)); + case CM_TRANSFER_FUNCTION_GAMMA28: + return pow(max(color, vec3(0.0)), vec3(2.8)); + case CM_TRANSFER_FUNCTION_HLG: + return tfInvHLG(color); + case CM_TRANSFER_FUNCTION_EXT_SRGB: + return tfInvExtSRGB(color); + case CM_TRANSFER_FUNCTION_BT1886: + return tfInvBT1886(color); + case CM_TRANSFER_FUNCTION_ST240: + return tfInvST240(color); + case CM_TRANSFER_FUNCTION_LOG_100: + return mix(exp((color - 1.0) * 2.0 * log(10.0)), vec3(0.0), lessThanEqual(color, vec3(0.0))); + case CM_TRANSFER_FUNCTION_LOG_316: + return mix(exp((color - 1.0) * 2.5 * log(10.0)), vec3(0.0), lessThanEqual(color, vec3(0.0))); + case CM_TRANSFER_FUNCTION_XVYCC: + return tfInvXVYCC(color); + case CM_TRANSFER_FUNCTION_ST428: + return pow(max(color, vec3(0.0)), vec3(ST428_POW)) * ST428_SCALE; case CM_TRANSFER_FUNCTION_SRGB: default: - isLow = lessThanEqual(color.rgb, vec3(SRGB_D_CUT)); - lo = color.rgb / SRGB_LO; - hi = pow((color.rgb + SRGB_HI_ADD) / SRGB_HI, vec3(SRGB_POW)); - return mix(hi, lo, isLow); + return tfInvSRGB(color); } } @@ -127,50 +226,41 @@ vec4 toNit(vec4 color, vec2 range) { } vec3 fromLinearRGB(vec3 color, int tf) { - bvec3 isLow; - vec3 lo; - vec3 hi; - switch (tf) { case CM_TRANSFER_FUNCTION_EXT_LINEAR: return color; case CM_TRANSFER_FUNCTION_ST2084_PQ: - vec3 E = pow(clamp(color.rgb, vec3(0.0), vec3(1.0)), vec3(PQ_M1)); - return pow( - (vec3(PQ_C1) + PQ_C2 * E) / (vec3(1.0) + PQ_C3 * E), - vec3(PQ_M2) - ); - break; + return tfPQ(color); case CM_TRANSFER_FUNCTION_GAMMA22: - return pow(max(color.rgb, vec3(0.0)), vec3(1.0 / 2.2)); + return pow(max(color, vec3(0.0)), vec3(1.0 / 2.2)); case CM_TRANSFER_FUNCTION_GAMMA28: - return pow(max(color.rgb, vec3(0.0)), vec3(1.0 / 2.8)); + return pow(max(color, vec3(0.0)), vec3(1.0 / 2.8)); case CM_TRANSFER_FUNCTION_HLG: - isLow = lessThanEqual(color.rgb, vec3(HLG_E_CUT)); - lo = pow(color.rgb / sqrt(3.0), vec3(2.0)); - hi = (pow(vec3(M_E), (color.rgb - HLG_C) / HLG_A) + HLG_B) / 12.0; - return mix(hi, lo, isLow); - case CM_TRANSFER_FUNCTION_BT1886: - case CM_TRANSFER_FUNCTION_ST240: - case CM_TRANSFER_FUNCTION_LOG_100: - case CM_TRANSFER_FUNCTION_LOG_316: - case CM_TRANSFER_FUNCTION_XVYCC: + return tfHLG(color); case CM_TRANSFER_FUNCTION_EXT_SRGB: + return tfExtSRGB(color); + case CM_TRANSFER_FUNCTION_BT1886: + return tfBT1886(color); + case CM_TRANSFER_FUNCTION_ST240: + return tfST240(color); + case CM_TRANSFER_FUNCTION_LOG_100: + return mix(1.0 + log(color) / log(10.0) / 2.0, vec3(0.0), lessThanEqual(color, vec3(0.01))); + case CM_TRANSFER_FUNCTION_LOG_316: + return mix(1.0 + log(color) / log(10.0) / 2.5, vec3(0.0), lessThanEqual(color, vec3(sqrt(10.0) / 1000.0))); + case CM_TRANSFER_FUNCTION_XVYCC: + return tfXVYCC(color); case CM_TRANSFER_FUNCTION_ST428: - + return pow(max(color, vec3(0.0)) / ST428_SCALE, vec3(1.0 / ST428_POW)); case CM_TRANSFER_FUNCTION_SRGB: default: - isLow = lessThanEqual(color.rgb, vec3(SRGB_E_CUT)); - lo = color.rgb * SRGB_LO; - hi = pow(color.rgb, vec3(SRGB_INV_POW)) * SRGB_HI - SRGB_HI_ADD; - return mix(hi, lo, isLow); + return tfSRGB(color); } } vec4 fromLinear(vec4 color, int tf) { if (tf == CM_TRANSFER_FUNCTION_EXT_LINEAR) return color; - + color.rgb /= max(color.a, 0.001); color.rgb = fromLinearRGB(color.rgb, tf); color.rgb *= color.a; @@ -194,7 +284,7 @@ mat3 primaries2xyz(mat4x2 primaries) { vec3 g = xy2xyz(primaries[1]); vec3 b = xy2xyz(primaries[2]); vec3 w = xy2xyz(primaries[3]); - + mat3 invMat = inverse( mat3( r.x, r.y, r.z, @@ -268,7 +358,7 @@ const mat3 ICtCpPQ = mat3( ); //const mat3 ICtCpPQInv = inverse(ICtCpPQ); const mat3 ICtCpPQInv = mat3( - 1.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 0.0086090370379327566, -0.0086090370379327566, 0.560031335710679118, 0.11102962500302595656, -0.11102962500302595656, -0.32062717498731885185 ); @@ -318,19 +408,17 @@ vec4 tonemap(vec4 color, mat3 dstXYZ) { } vec4 doColorManagement(vec4 pixColor, int srcTF, int dstTF, mat4x2 dstPrimaries) { - pixColor.rgb /= max(pixColor.a, 0.001); - pixColor.rgb = toLinearRGB(pixColor.rgb, srcTF); + pixColor.rgb /= max(pixColor.a, 0.001); + pixColor.rgb = toLinearRGB(pixColor.rgb, srcTF); pixColor.rgb = convertMatrix * pixColor.rgb; - pixColor = toNit(pixColor, srcTFRange); - pixColor.rgb *= pixColor.a; - mat3 dstxyz = primaries2xyz(dstPrimaries); - pixColor = tonemap(pixColor, dstxyz); - pixColor = fromLinearNit(pixColor, dstTF, dstTFRange); - if (srcTF == CM_TRANSFER_FUNCTION_SRGB && dstTF == CM_TRANSFER_FUNCTION_ST2084_PQ) { - pixColor = saturate(pixColor, dstxyz, sdrSaturation); - pixColor.rgb /= pixColor.a; + pixColor = toNit(pixColor, srcTFRange); + pixColor.rgb *= pixColor.a; + mat3 dstxyz = primaries2xyz(dstPrimaries); + pixColor = tonemap(pixColor, dstxyz); + pixColor = fromLinearNit(pixColor, dstTF, dstTFRange); + if (srcTF == CM_TRANSFER_FUNCTION_SRGB && dstTF == CM_TRANSFER_FUNCTION_ST2084_PQ) { + pixColor = saturate(pixColor, dstxyz, sdrSaturation); pixColor.rgb *= sdrBrightnessMultiplier; - pixColor.rgb *= pixColor.a; - } - return pixColor; + } + return pixColor; }