Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion shaders/config.glsl
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@
| colortex5 | rgba16f | 256, 256 | Sky environment map
| colortex6 | rgba8 | Full res | Solid albedo, rain alpha
| colortex7 | rgba16ui | Full res | Material data
| colortex8 | rgba16 | Full res | Normal data -> LDR output
| colortex8 | rgba16 | Full res | Normal data -> LDR output / PQ Intermediate
| colortex9 | rgba16f | Full res | Cloud history
| colortex10 | | | Unused
| colortex11 | rgba32ui | Half res | Volumetric fog, linear depth
Expand Down
37 changes: 25 additions & 12 deletions shaders/lib/post/ACES.glsl
Original file line number Diff line number Diff line change
Expand Up @@ -394,22 +394,35 @@ vec3 RRTAndODTFit(in vec3 rgb) {

return a / b;
}
#ifndef HDR_ENABLED
vec3 AcademyFit(in vec3 rgb) {
rgb *= sRGB_2_AP0;

vec3 AcademyFit(in vec3 rgb) {
rgb *= sRGB_2_AP0;

// Apply RRT sweeteners
rgb = RRTSweeteners(rgb);

// Apply RRT and ODT
rgb = RRTAndODTFit(rgb);
// Apply RRT sweeteners
rgb = RRTSweeteners(rgb);

// Global desaturation
rgb = mix(vec3(dot(rgb, AP1_RGB2Y)), rgb, odtSatFactor);
// Apply RRT and ODT
rgb = RRTAndODTFit(rgb);

return rgb * AP1_2_sRGB;
}
// Global desaturation
rgb = mix(vec3(dot(rgb, AP1_RGB2Y)), rgb, odtSatFactor);

return rgb * AP1_2_sRGB;
}
#else
// Use this simpler fit for HDR as of now.
// https://knarkowicz.wordpress.com/2016/08/31/hdr-display-first-steps/
vec3 AcademyFit(vec3 x){
x *= 1.2;
x *= sRGB_2_Rec2020;
float a = 15.8f;
float b = 2.12f;
float c = 1.2f;
float d = 5.92f;
float e = 1.9f;
return ( x * ( a * x + b ) ) / ( x * ( c * x + d ) + e ) * Rec2020_2_sRGB;
}
#endif
//======// ACES Full //===========================================================================//

vec3 RRT(in vec3 aces) {
Expand Down
98 changes: 98 additions & 0 deletions shaders/lib/post/AgX.glsl
Original file line number Diff line number Diff line change
Expand Up @@ -290,3 +290,101 @@ vec3 AgXConfigurable(in vec3 rgb) {
vec3 AgX_Full(in vec3 rgb) {
return sRGBToLinear(AgXConfigurable(rgb));
}

//======// AgX AllenWp, for HDR/SDR use //============================================================================//
// allenwp tonemapping curve; developed for use in the Godot game engine.
// Source and details: https://allenwp.com/blog/2025/05/29/allenwp-tonemapping-curve/
// Input must be a non-negative linear scene value.
vec3 allenwp_curve(vec3 x) {
#ifdef HDR_ENABLED
float output_max_value = HdrGamePeakBrightness / HdrGamePaperWhiteBrightness;
#else
float output_max_value = 1.0;
#endif
// These constants must match the those in the C++ code that calculates the parameters.
// 18% "middle gray" is perceptually 50% of the brightness of reference white.
const float awp_crossover_point = 0.1841865;
const float awp_shoulder_max = output_max_value - awp_crossover_point;
float awp_high_clip = 12.0;
awp_high_clip = max(awp_high_clip, output_max_value);
float awp_contrast = 1.5;
float awp_toe_a = ((1.0 / awp_crossover_point) - 1.0) * pow(awp_crossover_point, awp_contrast);
float awp_slope_denom = pow(awp_crossover_point, awp_contrast) + awp_toe_a;
float awp_slope = (awp_contrast * pow(awp_crossover_point, awp_contrast - 1.0) * awp_toe_a) / (awp_slope_denom * awp_slope_denom);
float awp_w = awp_high_clip - awp_crossover_point;
awp_w = awp_w * awp_w;
awp_w = awp_w / awp_shoulder_max;
awp_w = awp_w * awp_slope;

// Reinhard-like shoulder:
vec3 s = x - awp_crossover_point;
vec3 slope_s = awp_slope * s;
s = slope_s * (1.0 + s / awp_w) / (1.0 + (slope_s / awp_shoulder_max));
s += awp_crossover_point;

// Sigmoid power function toe:
vec3 t = pow(x, vec3(awp_contrast));
t = t / (t + awp_toe_a);

return mix(s, t, lessThan(x, vec3(awp_crossover_point)));
}

// This is an approximation and simplification of EaryChow's AgX implementation that is used by Blender.
// This code is based off of the script that generates the AgX_Base_sRGB.cube LUT that Blender uses.
// Source: https://github.com/EaryChow/AgX_LUT_Gen/blob/main/AgXBasesRGB.py
// Colorspace transformation source: https://www.colour-science.org:8010/apps/rgb_colourspace_transformation_matrix
vec3 AgX_AllenWp(vec3 color) {
// Input color should be non-negative!
// Large negative values in one channel and large positive values in other
// channels can result in a colour that appears darker and more saturated than
// desired after passing it through the inset matrix. For this reason, it is
// best to prevent negative input values.
// This is done before the Rec. 2020 transform to allow the Rec. 2020
// transform to be combined with the AgX inset matrix. This results in a loss
// of color information that could be correctly interpreted within the
// Rec. 2020 color space as positive RGB values, but is often not worth
// the performance cost of an additional matrix multiplication.
//
// Additionally, this AgX configuration was created subjectively based on
// output appearance in the Rec. 709 color gamut, so it is possible that these
// matrices will not perform well with non-Rec. 709 output (more testing with
// future wide-gamut displays is be needed).
// See this comment from the author on the decisions made to create the matrices:
// https://github.com/godotengine/godot-proposals/issues/12317#issuecomment-2835824250

// Combined Rec. 709 to Rec. 2020 and Blender AgX inset matrices:
const mat3 rec709_to_rec2020_agx_inset_matrix = mat3(
0.544814746488245, 0.140416948464053, 0.0888104196149096,
0.373787398372697, 0.754137554567394, 0.178871756420858,
0.0813978551390581, 0.105445496968552, 0.732317823964232);

// Combined inverse AgX outset matrix and Rec. 2020 to Rec. 709 matrices.
const mat3 agx_outset_rec2020_to_rec709_matrix = mat3(
1.96488741169489, -0.299313364904742, -0.164352742528393,
-0.855988495690215, 1.32639796461980, -0.238183969428088,
-0.108898916004672, -0.0270845997150571, 1.40253671195648);
#ifdef HDR_ENABLED
float output_max_value = HdrGamePeakBrightness / HdrGamePaperWhiteBrightness;
#else
float output_max_value = 1.0;
#endif

// Apply inset matrix.
color = rec709_to_rec2020_agx_inset_matrix * color * 2.0;

// Use the allenwp tonemapping curve to match the Blender AgX curve while
// providing stability across all variable dyanimc range (SDR, HDR, EDR).
color = allenwp_curve(color);

// Clipping to output_max_value is required to address a cyan colour that occurs
// with very bright inputs.
color = min(vec3(output_max_value), color);

// Apply outset to make the result more chroma-laden and then go back to Rec. 709.
color = agx_outset_rec2020_to_rec709_matrix * color;

// Blender's lusRGB.compensate_low_side is too complex for this shader, so
// simply return the color, even if it has negative components. These negative
// components may be useful for subsequent color adjustments.
return color;
}
14 changes: 11 additions & 3 deletions shaders/lib/post/GT.glsl
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@
// Math: https://www.desmos.com/calculator/gslcdxvipg
// Source: https://www.slideshare.net/nikuque/hdr-theory-and-practicce-jp
vec3 GT(in vec3 x) {
const float maxDisplayBrightness = 1.0;
#ifdef HDR_ENABLED
float maxDisplayBrightness = HdrGamePeakBrightness / HdrGamePaperWhiteBrightness;
#else
const float maxDisplayBrightness = 1.0;
#endif
const float contrast = 1.0;
const float linearStart = 0.2;
const float linearLength = 0.1;
Expand Down Expand Up @@ -432,9 +436,13 @@ vec3 GT7(in vec3 color) {
color *= sRGB_2_Rec2020;

GT7ToneMapping tm;
initializeAsSDR(tm);
#ifdef HDR_ENABLED
initializeAsHDR(HdrGamePeakBrightness,tm);
#else
initializeAsSDR(tm);
#endif

applyToneMapping(color, tm);

return color * Rec2020_2_sRGB;
}
}
6 changes: 6 additions & 0 deletions shaders/lib/universal/Uniform.glsl
Original file line number Diff line number Diff line change
Expand Up @@ -206,3 +206,9 @@ uniform vec3 viewLightVector;
#define lodProjectionInv gbufferProjectionInverse
#define lodPrevProjection gbufferPreviousProjection
#endif

#ifdef HDR_ENABLED
uniform float HdrGamePeakBrightness;
uniform float HdrGamePaperWhiteBrightness;
uniform float HdrUIBrightness;
#endif
35 changes: 35 additions & 0 deletions shaders/lib/utility/Color.glsl
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,26 @@ const mat3 Rec2020_2_XYZ = mat3(
0.0000000000, 0.0280726930, 1.0609850577
);

// https://en.wikipedia.org/wiki/Perceptual_quantizer
const float PQ_M1 = 2610.0/4096 * 1.0/4;
const float PQ_M2 = 2523.0/4096 * 128;
const float PQ_C1 = 3424.0/4096;
const float PQ_C2 = 2413.0/4096 * 32;
const float PQ_C3 = 2392.0/4096 * 32;

vec3 linearToPq(vec3 c, float scaling) {
c *= scaling / 10000.0;
c = pow(c, vec3(PQ_M1));
c = (vec3(PQ_C1) + vec3(PQ_C2) * c) / (vec3(1.0) + vec3(PQ_C3) * c);
return pow(c, vec3(PQ_M2));
}

vec3 PqToLinear(vec3 color, float scaling) {
vec3 e_m12 = pow(color, vec3(1.0 / PQ_M2));
vec3 out_color = pow(max(vec3(0), e_m12 - PQ_C1) / (PQ_C2 - PQ_C3 * e_m12), vec3(1.0 / PQ_M1));
return out_color * (10000.0 / scaling);
}

// https://en.wikipedia.org/wiki/SRGB
// https://github.com/tobspr/GLSL-Color-Spaces/blob/master/ColorSpaces.inc.glsl
vec3 linearToSRGB(in vec3 color) {
Expand All @@ -45,6 +65,21 @@ vec3 sRGBToLinear(in vec3 color) {
return mix(color * 0.07739938, pow((color + 0.055) * 0.94786729, vec3(2.4)), step(vec3(0.04045), color));
}

// https://en.wikipedia.org/wiki/SRGB
// https://en.wikipedia.org/wiki/ScRGB
// -f(-x) for negative values.
vec3 sRGBToLinearSafe(in vec3 color) {
vec3 color_sign = sign(color);
vec3 color_abs = abs(color);
return mix(color_abs * 0.07739938, pow((color_abs + 0.055) * 0.94786729, vec3(2.4)), step(vec3(0.04045), color_abs)) * color_sign;
}

vec3 linearToSRGBSafe(in vec3 color) {
vec3 color_sign = sign(color);
vec3 color_abs = abs(color);
return mix(color_abs * 12.92, 1.055 * pow(color_abs, vec3(0.41666666)) - 0.055, step(vec3(0.0031308), color_abs)) * color_sign;
}

// https://chilliant.blogspot.com/2012/08/srgb-approximations-for-hlsl.html
vec3 linearToSRGBApprox(in vec3 color) {
vec3 S1 = color * inversesqrt(color);
Expand Down
30 changes: 25 additions & 5 deletions shaders/program/post/Final.frag
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,21 @@ out vec3 finalOut;
// Reference: Lou Kramer, FidelityFX CAS, AMD Developer Day 2019,
// https://gpuopen.com/wp-content/uploads/2019/07/FidelityFX-CAS.pptx
// https://github.com/GPUOpen-Effects/FidelityFX-CAS
vec3 FFXCasFilter(in ivec2 texel, in float sharpness) {
#define CasLoad(offset) texelFetchOffset(colortex8, texel, 0, offset).rgb
vec3 AFromPq(in vec3 color) {
vec3 p = pow(color, vec3(0.0126833));
return pow(saturate(p - vec3(0.835938))/(vec3(18.8516) - vec3(18.6875)*p), vec3(6.27739));
}

vec3 AToPq(in vec3 color) {
vec3 p = pow(color, vec3(0.159302));
return pow((vec3(0.835938) + vec3(18.8516) * p)/(vec3(1.0)+vec3(18.6875)*p),vec3(78.8438));
}
vec3 FFXCasFilter(in ivec2 texel, in float sharpness) {
#ifdef HDR_ENABLED
#define CasLoad(offset) AFromPq(texelFetchOffset(colortex8, texel, 0, offset).rgb)
#else
#define CasLoad(offset) texelFetchOffset(colortex8, texel, 0, offset).rgb
#endif
#ifndef CAS_ENABLED
return CasLoad(ivec2(0, 0));
#endif
Expand Down Expand Up @@ -93,6 +105,7 @@ void HistogramDisplay(inout vec3 color, in ivec2 texel) {
}
}


//======// Main //================================================================================//
void main() {
ivec2 texelPos = ivec2(gl_FragCoord.xy);
Expand All @@ -103,7 +116,12 @@ void main() {
#ifdef DEBUG_BLOOM_TILES
finalOut = texelFetch(colortex4, texelPos, 0).rgb;
#else
finalOut = FFXCasFilter(texelPos, CAS_STRENGTH);
#ifdef HDR_ENABLED
// PQ decode back up to game brightness after CAS
finalOut = linearToSRGBSafe(PqToLinear(AToPq(FFXCasFilter(texelPos, CAS_STRENGTH)), HdrUIBrightness) * Rec2020_2_sRGB);
#else
finalOut = FFXCasFilter(texelPos, CAS_STRENGTH);
#endif
#endif

// Text display
Expand Down Expand Up @@ -143,6 +161,8 @@ void main() {
HistogramDisplay(finalOut, texelPos);
#endif

// Apply bayer dithering to reduce banding artifacts
finalOut += (bayer16(gl_FragCoord.xy) - 0.5) * rcp255;
#ifndef HDR_ENABLED
// Apply bayer dithering to reduce banding artifacts
finalOut += (bayer16(gl_FragCoord.xy) - 0.5) * rcp255;
#endif
}
13 changes: 10 additions & 3 deletions shaders/program/post/Grade.comp
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const vec2 workGroupsRender = vec2(1.0, 1.0);

#include "/lib/Utility.glsl"

#define TONE_MAPPER AgX_Minimal // [None AcademyFit AcademyFull AgX_Minimal AgX_Full Lottes GT GT7]
#define TONE_MAPPER GT7 // [None AcademyFit AcademyFull AgX_AllenWp AgX_Minimal AgX_Full Lottes GT GT7]

#define BLOOM_BLENDING_MODE 1 // [0 1 2]
#define BLOOM_INTENSITY 1.0 // [0.0 0.01 0.02 0.05 0.07 0.1 0.15 0.2 0.3 0.4 0.5 0.6 0.7 0.8 0.9 1.0 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9 2.0 3.0 4.0 5.0 7.0 10.0 15.0 20.0]
Expand All @@ -39,7 +39,7 @@ const vec2 workGroupsRender = vec2(1.0, 1.0);

//======// Uniform //=============================================================================//

writeonly uniform image2D colorimg8; // LDR output
writeonly uniform image2D colorimg8; // LDR / Tonemapped HDR output

#include "/lib/universal/Uniform.glsl"

Expand Down Expand Up @@ -206,7 +206,14 @@ void main() {
// color *= Rec2020_2_sRGB;

// Working to display space
color = saturate(linearToSRGB(color));
#ifdef HDR_ENABLED
// Normalize for FFX CAS using PQ encoding
color *= sRGB_2_Rec2020;
color = max(vec3(0.0), color); // Avoid negative values causing issues with PQ encoding
color = saturate(linearToPq(color, HdrGamePaperWhiteBrightness));
#else
color = saturate(linearToSRGB(color));
#endif
}

// Debug tone mapping plot
Expand Down