From 761e671439e8838cd08a4c76ae351dd9b0bc29aa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Mon, 23 Feb 2026 16:37:47 +0100 Subject: [PATCH 1/2] Add inverted tonemapping --- source/Renderer/shaders/pbr.frag | 22 +++- source/Renderer/shaders/tonemapping.glsl | 147 +++++++++++++++++++++++ 2 files changed, 165 insertions(+), 4 deletions(-) diff --git a/source/Renderer/shaders/pbr.frag b/source/Renderer/shaders/pbr.frag index a9771938..2777a58a 100644 --- a/source/Renderer/shaders/pbr.frag +++ b/source/Renderer/shaders/pbr.frag @@ -40,6 +40,22 @@ void main() #if ALPHAMODE == ALPHAMODE_OPAQUE baseColor.a = 1.0; #endif +#ifdef MATERIAL_UNLIT +#if ALPHAMODE == ALPHAMODE_MASK + if (baseColor.a < u_AlphaCutoff) + { + discard; + } + baseColor.a = 1.0; +#endif +#ifdef LINEAR_OUTPUT +// If used for transmission, we need to invert exposure and tone mapping, so the original color is computed in the general render pass + g_finalColor = vec4(toneMapInverse(baseColor.rgb), baseColor.a); +#else + g_finalColor = vec4(linearTosRGB(baseColor.rgb), baseColor.a); +#endif +// PBR Flow. This else goes all the way to the end of the file +#else vec3 color = vec3(0); vec3 v = normalize(u_Camera - v_Position); @@ -363,10 +379,7 @@ void main() f_emissive *= texture(u_EmissiveSampler, getEmissiveUV()).rgb; #endif - -#ifdef MATERIAL_UNLIT - color = baseColor.rgb; -#elif defined(NOT_TRIANGLE) && !defined(HAS_NORMAL_VEC3) +#if defined(NOT_TRIANGLE) && !defined(HAS_NORMAL_VEC3) //Points or Lines with no NORMAL attribute SHOULD be rendered without lighting and instead use the sum of the base color value and the emissive value. color = f_emissive + baseColor.rgb; #else @@ -548,5 +561,6 @@ vec3 specularTexture = vec3(1.0); #endif #endif +#endif #endif } diff --git a/source/Renderer/shaders/tonemapping.glsl b/source/Renderer/shaders/tonemapping.glsl index 30d39afa..b20b4d0f 100644 --- a/source/Renderer/shaders/tonemapping.glsl +++ b/source/Renderer/shaders/tonemapping.glsl @@ -133,3 +133,150 @@ vec3 toneMap(vec3 color) return linearTosRGB(color); } + +// ============================================================================= +// INVERSE TONEMAPPING FUNCTIONS +// ============================================================================= + +// Inverse ACES tone map (Narkowicz approximation) +// Solves: y = (x*(A*x + B)) / (x*(C*x + D) + E) for x given y +vec3 toneMapACES_NarkowiczInverse(vec3 toneMapped) +{ + const float A = 2.51; + const float B = 0.03; + const float C = 2.43; + const float D = 0.59; + const float E = 0.14; + + // For each component, solve the quadratic equation + vec3 result; + for(int i = 0; i < 3; i++) + { + float y = toneMapped[i]; + + // Rearrange to: y*(C*x^2 + D*x + E) = x*(A*x + B) + // Which gives: y*C*x^2 + y*D*x + y*E = A*x^2 + B*x + // Rearrange to: (y*C - A)*x^2 + (y*D - B)*x + y*E = 0 + + float a = y * C - A; + float b = y * D - B; + float c = y * E; + + // Solve quadratic equation: ax^2 + bx + c = 0 + float discriminant = b * b - 4.0 * a * c; + + // Take positive root + float x1 = (-b + sqrt(discriminant)) / (2.0 * a); + float x2 = (-b - sqrt(discriminant)) / (2.0 * a); + result[i] = max(0.0, max(x1, x2)); + + } + + return result; +} + +// Inverse RRT and ODT fit +// Solves: y = (x*(x + 0.0245786) - 0.000090537) / (x*(0.983729*x + 0.4329510) + 0.238081) for x +vec3 RRTAndODTFitInverse(vec3 toneMapped) +{ + vec3 result; + for(int i = 0; i < 3; i++) + { + float y = toneMapped[i]; + + // Rearrange: y * (x*(0.983729*x + 0.4329510) + 0.238081) = x*(x + 0.0245786) - 0.000090537 + // y * (0.983729*x^2 + 0.4329510*x + 0.238081) = x^2 + 0.0245786*x - 0.000090537 + // y*0.983729*x^2 + y*0.4329510*x + y*0.238081 = x^2 + 0.0245786*x - 0.000090537 + // (y*0.983729 - 1.0)*x^2 + (y*0.4329510 - 0.0245786)*x + (y*0.238081 + 0.000090537) = 0 + + float a = y * 0.983729 - 1.0; + float b = y * 0.4329510 - 0.0245786; + float c = y * 0.238081 + 0.000090537; + + float discriminant = b * b - 4.0 * a * c; + + float x1 = (-b + sqrt(discriminant)) / (2.0 * a); + float x2 = (-b - sqrt(discriminant)) / (2.0 * a); + result[i] = max(0.0, max(x1, x2)); + + } + + return result; +} + +// Inverse ACES Hill tone mapping +vec3 toneMapACES_HillInverse(vec3 toneMapped) +{ + vec3 color = toneMapped; + + // Undo ACES output matrix + color = inverse(ACESOutputMat) * color; + + // Undo RRT and ODT + color = RRTAndODTFitInverse(color); + + // Undo ACES input matrix + color = inverse(ACESInputMat) * color; + + return color; +} + +// Inverse Khronos PBR neutral tone mapping +// Note: This is a complex inverse that may not be perfectly accurate due to the conditional logic +vec3 toneMap_KhronosPbrNeutralInverse(vec3 toneMapped) +{ + const float startCompression = 0.8 - 0.04; + const float desaturation = 0.15; + + // This is an approximation - the forward function has complex conditional logic + // that makes perfect inversion difficult + vec3 color = toneMapped; + + // Try to undo the desaturation mix + float peak = max(color.r, max(color.g, color.b)); + + // Approximate inverse of the compression + if (peak >= startCompression) { + const float d = 1.0 - startCompression; + // Approximate inverse of: newPeak = 1. - d * d / (peak + d - startCompression) + // This is a rough approximation + float originalPeak = peak / (1.0 - peak + startCompression); + color *= originalPeak / peak; + } + + // Try to undo the offset + float x = min(color.r, min(color.g, color.b)); + float offset = x < 0.08 ? x - 6.25 * x * x : 0.04; + color += offset; + + return color; +} + +// Complete inverse tone mapping function +vec3 toneMapInverse(vec3 toneMapped) +{ + vec3 color = toneMapped; + + // Then undo the specific tonemapping (this would need to match the forward path) +#ifdef TONEMAP_KHR_PBR_NEUTRAL + color = toneMap_KhronosPbrNeutralInverse(color); +#endif + +#ifdef TONEMAP_ACES_HILL_EXPOSURE_BOOST + color = toneMapACES_HillInverse(color); + color *= 0.6; // Undo the exposure boost +#endif + +#ifdef TONEMAP_ACES_HILL + color = toneMapACES_HillInverse(color); +#endif + +#ifdef TONEMAP_ACES_NARKOWICZ + color = toneMapACES_NarkowiczInverse(color); +#endif + + // Finally undo the exposure + color /= u_Exposure; + + return color; +} From 672842893cbc107548021cdc80e3ccabc3857ed2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Patrick=20H=C3=A4rtl?= Date: Tue, 31 Mar 2026 12:06:24 +0200 Subject: [PATCH 2/2] Simplify the functions --- source/Renderer/shaders/tonemapping.glsl | 71 ++++++++++-------------- 1 file changed, 28 insertions(+), 43 deletions(-) diff --git a/source/Renderer/shaders/tonemapping.glsl b/source/Renderer/shaders/tonemapping.glsl index b20b4d0f..a2696dbc 100644 --- a/source/Renderer/shaders/tonemapping.glsl +++ b/source/Renderer/shaders/tonemapping.glsl @@ -147,30 +147,22 @@ vec3 toneMapACES_NarkowiczInverse(vec3 toneMapped) const float C = 2.43; const float D = 0.59; const float E = 0.14; - - // For each component, solve the quadratic equation - vec3 result; - for(int i = 0; i < 3; i++) - { - float y = toneMapped[i]; - - // Rearrange to: y*(C*x^2 + D*x + E) = x*(A*x + B) - // Which gives: y*C*x^2 + y*D*x + y*E = A*x^2 + B*x - // Rearrange to: (y*C - A)*x^2 + (y*D - B)*x + y*E = 0 - - float a = y * C - A; - float b = y * D - B; - float c = y * E; - - // Solve quadratic equation: ax^2 + bx + c = 0 - float discriminant = b * b - 4.0 * a * c; - - // Take positive root - float x1 = (-b + sqrt(discriminant)) / (2.0 * a); - float x2 = (-b - sqrt(discriminant)) / (2.0 * a); - result[i] = max(0.0, max(x1, x2)); + + vec3 y = toneMapped; - } + // Rearrange to: y*(C*x^2 + D*x + E) = x*(A*x + B) + // Which gives: y*C*x^2 + y*D*x + y*E = A*x^2 + B*x + // Rearrange to: (y*C - A)*x^2 + (y*D - B)*x + y*E = 0 + + vec3 a = y * C - A; + vec3 b = y * D - B; + vec3 c = y * E; + + // Solve quadratic equation: ax^2 + bx + c = 0 + vec3 discriminant = b * b - 4.0 * a * c; + + // Take positive root + vec3 result = (-b - sqrt(discriminant)) / (2.0 * a); return result; } @@ -179,27 +171,20 @@ vec3 toneMapACES_NarkowiczInverse(vec3 toneMapped) // Solves: y = (x*(x + 0.0245786) - 0.000090537) / (x*(0.983729*x + 0.4329510) + 0.238081) for x vec3 RRTAndODTFitInverse(vec3 toneMapped) { - vec3 result; - for(int i = 0; i < 3; i++) - { - float y = toneMapped[i]; - - // Rearrange: y * (x*(0.983729*x + 0.4329510) + 0.238081) = x*(x + 0.0245786) - 0.000090537 - // y * (0.983729*x^2 + 0.4329510*x + 0.238081) = x^2 + 0.0245786*x - 0.000090537 - // y*0.983729*x^2 + y*0.4329510*x + y*0.238081 = x^2 + 0.0245786*x - 0.000090537 - // (y*0.983729 - 1.0)*x^2 + (y*0.4329510 - 0.0245786)*x + (y*0.238081 + 0.000090537) = 0 - - float a = y * 0.983729 - 1.0; - float b = y * 0.4329510 - 0.0245786; - float c = y * 0.238081 + 0.000090537; - - float discriminant = b * b - 4.0 * a * c; - - float x1 = (-b + sqrt(discriminant)) / (2.0 * a); - float x2 = (-b - sqrt(discriminant)) / (2.0 * a); - result[i] = max(0.0, max(x1, x2)); + vec3 y = toneMapped; - } + // Rearrange: y * (x*(0.983729*x + 0.4329510) + 0.238081) = x*(x + 0.0245786) - 0.000090537 + // y * (0.983729*x^2 + 0.4329510*x + 0.238081) = x^2 + 0.0245786*x - 0.000090537 + // y*0.983729*x^2 + y*0.4329510*x + y*0.238081 = x^2 + 0.0245786*x - 0.000090537 + // (y*0.983729 - 1.0)*x^2 + (y*0.4329510 - 0.0245786)*x + (y*0.238081 + 0.000090537) = 0 + + vec3 a = y * 0.983729 - 1.0; + vec3 b = y * 0.4329510 - 0.0245786; + vec3 c = y * 0.238081 + 0.000090537; + + vec3 discriminant = b * b - 4.0 * a * c; + + vec3 result = (-b - sqrt(discriminant)) / (2.0 * a); return result; }