output.frag 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. /* output.frag -- Scene output fragment shader
  2. *
  3. * Performs tone mapping, debanding, color adjustments, and
  4. * converts from linear color space to sRGB.
  5. *
  6. * Copyright (c) 2025 Victor Le Juez
  7. *
  8. * This software is distributed under the terms of the accompanying LICENSE file.
  9. * It is provided "as-is", without any express or implied warranty.
  10. */
  11. #version 330 core
  12. /* === Includes === */
  13. #include "../include/math.glsl"
  14. /* === Definitions === */
  15. #define TONEMAP_LINEAR 0
  16. #define TONEMAP_REINHARD 1
  17. #define TONEMAP_FILMIC 2
  18. #define TONEMAP_ACES 3
  19. #define TONEMAP_AGX 4
  20. /* === Varyings === */
  21. noperspective in vec2 vTexCoord;
  22. /* === Uniforms === */
  23. uniform sampler2D uSceneTex; //< Scene color texture
  24. uniform float uTonemapExposure; //< Tonemap exposure
  25. uniform float uTonemapWhite; //< Tonemap white point, not used with AGX
  26. uniform int uTonemapMode; //< Tonemap mode used (e.g. TONEMAP_LINEAR)
  27. uniform float uBrightness; //< Brightness adjustment
  28. uniform float uContrast; //< Contrast adjustment
  29. uniform float uSaturation; //< Saturation adjustment
  30. /* === Fragments === */
  31. out vec4 FragColor;
  32. /* === Tonemap Functions === */
  33. // Based on Reinhard's extended formula, see equation 4 in https://doi.org/cjbgrt
  34. vec3 TonemapReinhard(vec3 color, float pWhite)
  35. {
  36. float whiteSquared = pWhite * pWhite;
  37. vec3 whiteSquaredColor = whiteSquared * color;
  38. // Equivalent to color * (1 + color / whiteSquared) / (1 + color)
  39. return (whiteSquaredColor + color * color) / (whiteSquaredColor + whiteSquared);
  40. }
  41. vec3 TonemapFilmic(vec3 color, float pWhite)
  42. {
  43. // exposure bias: input scale (color *= bias, white *= bias) to make the brightness consistent with other tonemappers
  44. // also useful to scale the input to the range that the tonemapper is designed for (some require very high input values)
  45. // has no effect on the curve's general shape or visual properties
  46. const float exposureBias = 2.0;
  47. const float A = 0.22 * exposureBias * exposureBias; // bias baked into constants for performance
  48. const float B = 0.30 * exposureBias;
  49. const float C = 0.10;
  50. const float D = 0.20;
  51. const float E = 0.01;
  52. const float F = 0.30;
  53. vec3 colorTonemapped = ((color * (A * color + C * B) + D * E) / (color * (A * color + B) + D * F)) - E / F;
  54. float pWhiteTonemapped = ((pWhite * (A * pWhite + C * B) + D * E) / (pWhite * (A * pWhite + B) + D * F)) - E / F;
  55. return colorTonemapped / pWhiteTonemapped;
  56. }
  57. // Adapted from https://github.com/TheRealMJP/BakingLab/blob/master/BakingLab/ACES.hlsl
  58. // (MIT License).
  59. vec3 TonemapACES(vec3 color, float pWhite)
  60. {
  61. const float exposureBias = 1.8;
  62. const float A = 0.0245786;
  63. const float B = 0.000090537;
  64. const float C = 0.983729;
  65. const float D = 0.432951;
  66. const float E = 0.238081;
  67. // Exposure bias baked into transform to save shader instructions. Equivalent to `color *= exposureBias`
  68. const mat3 rgb_to_rrt = mat3(
  69. vec3(0.59719 * exposureBias, 0.35458 * exposureBias, 0.04823 * exposureBias),
  70. vec3(0.07600 * exposureBias, 0.90834 * exposureBias, 0.01566 * exposureBias),
  71. vec3(0.02840 * exposureBias, 0.13383 * exposureBias, 0.83777 * exposureBias)
  72. );
  73. const mat3 odt_to_rgb = mat3(
  74. vec3(1.60475, -0.53108, -0.07367),
  75. vec3(-0.10208, 1.10813, -0.00605),
  76. vec3(-0.00327, -0.07276, 1.07602)
  77. );
  78. color *= rgb_to_rrt;
  79. vec3 colorTonemapped = (color * (color + A) - B) / (color * (C * color + D) + E);
  80. colorTonemapped *= odt_to_rgb;
  81. pWhite *= exposureBias;
  82. float pWhiteTonemapped = (pWhite * (pWhite + A) - B) / (pWhite * (C * pWhite + D) + E);
  83. return colorTonemapped / pWhiteTonemapped;
  84. }
  85. // Polynomial approximation of EaryChow's AgX sigmoid curve.
  86. // x must be within range [0.0, 1.0]
  87. vec3 AgXContrastApprox(vec3 x)
  88. {
  89. // 6th order polynomial generated from sigmoid curve with 57 sample points
  90. // Intercept set to 0.0 for performance and correct intersection
  91. vec3 x2 = x * x;
  92. vec3 x4 = x2 * x2;
  93. return 0.021 * x + 4.0111 * x2 - 25.682 * x2 * x + 70.359 * x4 - 74.778 * x4 * x + 27.069 * x4 * x2;
  94. }
  95. // AgX tonemap implementation based on EaryChow's algorithm used by Blender
  96. // Source: https://github.com/EaryChow/AgX_LUT_Gen/blob/main/AgXBasesRGB.py
  97. vec3 TonemapAgX(vec3 color)
  98. {
  99. // Combined sRGB to Rec2020 + AgX inset transform
  100. const mat3 srgbToRec2020AgxInsetMat = mat3(
  101. 0.54490813676363087053, 0.14044005884001287035, 0.088827411851915368603,
  102. 0.37377945959812267119, 0.75410959864013760045, 0.17887712465043811023,
  103. 0.081384976686407536266, 0.10543358536857773485, 0.73224999956948382528
  104. );
  105. // Combined inverse AgX outset + Rec2020 to sRGB transform
  106. const mat3 agxOutsetRec2020ToSrgbMatrix = mat3(
  107. 1.9645509602733325934, -0.29932243390911083839, -0.16436833806080403409,
  108. -0.85585845117807513559, 1.3264510741502356555, -0.23822464068860595117,
  109. -0.10886710826831608324, -0.027084020983874825605, 1.402665347143271889
  110. );
  111. // EV range constants (LOG2_MIN = -10.0, LOG2_MAX = +6.5, MIDDLE_GRAY = 0.18)
  112. const float minEV = -12.4739311883324; // log2(pow(2, LOG2_MIN) * MIDDLE_GRAY)
  113. const float maxEV = 4.02606881166759; // log2(pow(2, LOG2_MAX) * MIDDLE_GRAY)
  114. // Prevent negative values to avoid darker/oversaturated results after matrix transform
  115. // Small epsilon (2e-10) prevents log2(0.0) while maintaining minimal error
  116. color = max(color, 2e-10);
  117. // Transform to Rec2020 and apply inset matrix
  118. color = srgbToRec2020AgxInsetMat * color;
  119. // Log2 encoding and normalization to [0,1] range
  120. color = clamp(log2(color), minEV, maxEV);
  121. color = (color - minEV) / (maxEV - minEV);
  122. // Apply sigmoid contrast curve
  123. color = AgXContrastApprox(color);
  124. // Convert back to linear (gamma 2.4)
  125. color = pow(color, vec3(2.4));
  126. // Apply outset matrix and return to sRGB
  127. color = agxOutsetRec2020ToSrgbMatrix * color;
  128. // Return color (may contain negative components useful for further adjustments)
  129. return color;
  130. }
  131. /* === Main Functions === */
  132. vec3 Tonemapping(vec3 color, float exposure, float pWhite) // inputs are LINEAR
  133. {
  134. // Ensure color values passed to tonemappers are positive.
  135. // They can be negative in the case of negative lights, which leads to undesired behavior.
  136. color *= exposure;
  137. switch (uTonemapMode) {
  138. case TONEMAP_REINHARD:
  139. color = TonemapReinhard(max(vec3(0.0), color), pWhite);
  140. break;
  141. case TONEMAP_FILMIC:
  142. color = TonemapFilmic(max(vec3(0.0), color), pWhite);
  143. break;
  144. case TONEMAP_ACES:
  145. color = TonemapACES(max(vec3(0.0), color), pWhite);
  146. break;
  147. case TONEMAP_AGX:
  148. color = TonemapAgX(color);
  149. break;
  150. default:
  151. break;
  152. }
  153. return color;
  154. }
  155. vec3 Adjustments(vec3 color, float brightness, float contrast, float saturation)
  156. {
  157. color = mix(vec3(0.0), color, brightness);
  158. color = mix(vec3(0.5), color, contrast);
  159. color = mix(vec3(dot(vec3(1.0), color) * 0.33333), color, saturation);
  160. return color;
  161. }
  162. vec3 Debanding(vec3 color)
  163. {
  164. const float ditherStrength = 1.0 / 255.0;
  165. float n = M_HashIGN(gl_FragCoord.xy);
  166. float d = (n - 0.5) * ditherStrength;
  167. return color + d;
  168. }
  169. vec3 LinearToSRGB(vec3 color)
  170. {
  171. // color = clamp(color, vec3(0.0), vec3(1.0));
  172. // const vec3 a = vec3(0.055f);
  173. // return mix((vec3(1.0f) + a) * pow(color.rgb, vec3(1.0f / 2.4f)) - a, 12.92f * color.rgb, lessThan(color.rgb, vec3(0.0031308f)));
  174. // Approximation from http://chilliant.blogspot.com/2012/08/srgb-approximations-for-hlsl.html
  175. return max(vec3(1.055) * pow(color, vec3(0.416666667)) - vec3(0.055), vec3(0.0));
  176. }
  177. /* === Main program === */
  178. void main()
  179. {
  180. vec3 color = texelFetch(uSceneTex, ivec2(gl_FragCoord.xy), 0).rgb;
  181. color = Tonemapping(color, uTonemapExposure, uTonemapWhite);
  182. color = Adjustments(color, uBrightness, uContrast, uSaturation);
  183. color = LinearToSRGB(color);
  184. FragColor = vec4(Debanding(color), 1.0);
  185. }