diff --git a/Resources/Shaders/PostFilters/ColorCorrection.fs b/Resources/Shaders/PostFilters/ColorCorrection.fs index 6e707b51..6674d8ae 100644 --- a/Resources/Shaders/PostFilters/ColorCorrection.fs +++ b/Resources/Shaders/PostFilters/ColorCorrection.fs @@ -26,16 +26,74 @@ varying vec2 texCoord; uniform float enhancement; uniform float saturation; uniform vec3 tint; +uniform float sharpening; +uniform float sharpeningFinalGain; vec3 acesToneMapping(vec3 x) { return clamp((x * (2.51 * x + 0.03)) / (x * (2.43 * x + 0.59) + 0.14), 0.0, 1.0); } +// 1/(dacesToneMapping(x)/dx) +float acesToneMappingDiffRcp(float color) { + float denom = 0.0576132 + color * (0.242798 + color); + return (denom * denom) / (0.0007112 + color * (0.11902 + color * 0.238446)); +} + void main() { // Input is in the device color space gl_FragColor = texture2D(mainTexture, texCoord); + // Blur kernel: 1/4 2/4 1/4 + // 2/4 4/4 2/4 + // 1/4 2/4 1/4 + float dx = dFdx(texCoord.x) * 0.5, dy = dFdy(texCoord.y) * 0.5; + vec4 blurred = 0.25 * ( + texture2D(mainTexture, texCoord + vec2(dx, dy)) + + texture2D(mainTexture, texCoord + vec2(dx, -dy)) + + texture2D(mainTexture, texCoord + vec2(-dx, dy)) + + texture2D(mainTexture, texCoord + vec2(-dx, -dy))); + + // `sharpening` tells to what extent we must enhance the edges based on + // global factors. + float enhancingFactor = sharpening; +#if USE_HDR + // Now we take the derivative of `acesToneMapping` into consideration. + // Specifially, when `acesToneMapping` reduces the color contrast + // around the current pixel by N times, we compensate by scaling + // `enhancingFactor` by N. + float localLuminance = dot(blurred.xyz, vec3(1. / 3.)); + float localLuminanceLinear = clamp(localLuminance * localLuminance, 0.0, 1.0); + enhancingFactor *= acesToneMappingDiffRcp(localLuminanceLinear * 0.8); + + // We don't want specular highlights to cause black edges, so weaken the + // effect if the local luminance is high. + localLuminance = max(localLuminance, dot(gl_FragColor.xyz, vec3(1. / 3.))); + if (localLuminance > 0.8) { + localLuminance -= 0.8; + enhancingFactor *= 1.0 - (localLuminance + localLuminance * localLuminance) * 100.0; + } +#endif + + // Clamp the sharpening effect's intensity. + enhancingFactor = clamp(enhancingFactor, 1.0, 4.0); + + // Derive the value of `localSharpening` that achieves the desired + // contrast enhancement. When `sharpeningFinalGain = 2`, the sharpening + // effect multiplies the color contrast exactly by `enhancingFactor`. + float localSharpening = (enhancingFactor - 1.0) * sharpeningFinalGain; + + // Given a parameter value `localSharpening`, the sharpening kernel defined + // in here enhances the color difference across a horizontal or vertical + // edge by the following factor: + // + // r_sharp = 1 + localSharpening / 2 + + // Sharpening is done by reversing the effect of the blur kernel. + gl_FragColor.xyz += (gl_FragColor.xyz - blurred.xyz) * localSharpening; + gl_FragColor.xyz = max(gl_FragColor.xyz, vec3(0.0)); + + // Apply tinting and manual exposure gl_FragColor.xyz *= tint; vec3 gray = vec3(dot(gl_FragColor.xyz, vec3(1. / 3.))); diff --git a/Sources/Draw/GLColorCorrectionFilter.cpp b/Sources/Draw/GLColorCorrectionFilter.cpp index 8999c1a0..68b86008 100644 --- a/Sources/Draw/GLColorCorrectionFilter.cpp +++ b/Sources/Draw/GLColorCorrectionFilter.cpp @@ -19,6 +19,7 @@ */ #include +#include #include #include @@ -37,7 +38,8 @@ namespace spades { : renderer(renderer), settings(renderer->GetSettings()) { lens = renderer->RegisterProgram("Shaders/PostFilters/ColorCorrection.program"); } - GLColorBuffer GLColorCorrectionFilter::Filter(GLColorBuffer input, Vector3 tintVal) { + GLColorBuffer GLColorCorrectionFilter::Filter(GLColorBuffer input, Vector3 tintVal, + float fogLuminance) { SPADES_MARK_FUNCTION(); IGLDevice *dev = renderer->GetGLDevice(); @@ -49,10 +51,14 @@ namespace spades { static GLProgramUniform saturation("saturation"); static GLProgramUniform enhancement("enhancement"); static GLProgramUniform tint("tint"); + static GLProgramUniform sharpening("sharpening"); + static GLProgramUniform sharpeningFinalGain("sharpeningFinalGain"); saturation(lens); enhancement(lens); tint(lens); + sharpening(lens); + sharpeningFinalGain(lens); dev->Enable(IGLDevice::Blend, false); @@ -88,6 +94,67 @@ namespace spades { lensTexture.SetValue(0); + // Calculate the sharpening factor + // + // One reason to do this is for aesthetic reasons. Another reason is to offset + // OpenSpades' denser fog compared to the vanilla client. Technically, the fog density + // function is mostly identical between these two clients. However, OpenSpades applies + // the fog color in the linear color space, which is physically accurate but has an + // unexpected consequence of somewhat strengthening the effect. + // + // (`r_volumetricFog` completely changes the density function, which we leave out from + // this discussion.) + // + // Given an object color o (only one color channel is discussed here), fog color f, and + // fog density d, the output color c_voxlap and c_os for the vanilla client and + // OpenSpades, respectively, is calculated by: + // + // c_voxlap = o^(1/2)(1-d) + f^(1/2)d + // c_os = (o(1-d) + fd)^(1/2) + // + // Here the sRGB transfer function is approximated by an exact gamma = 2 power law. + // o and f are in the linear color space, whereas c_voxlap and c_os are in the sRGB + // color space (because that's how `ColorCorrection.fs` is implemented). + // + // The contrast reduction by the fog can be calculated by differentiating each of them + // by o: + // + // c_voxlap' = (1-d) / sqrt(o) / 2 + // c_os' = (1-d) / sqrt(o(1-d) + fd) / 2 + // + // Now we find out the amount of color contrast we must recover by dividing c_voxlap' by + // c_os'. Since it's objects around the fog end distance that concern the users, let + // d = 1: + // + // c_voxlap' / c_os' = sqrt(o(1-d) + fd) / sqrt(o) + // = sqrt(f) / sqrt(o) + // + // (Turns out, the result so far does not change whichever color space c_voxlap and c_os + // are represented in.) + // + // This is a function over an object color o and fog color f. Let us calculate the + // average of this function assuming a uniform distribution of o over the interval + // [o_min, o_max]: + // + // ∫[c_voxlap' / c_os', {o, o_min, o_max}] + // = 2sqrt(f)(sqrt(o_max) - sqrt(o_min)) / (o_max - o_min) + // + // Since the pixels aren't usually fully lit nor completely dark, let us arbitrarily + // assume o_min = 0.001 and o_max = 0.5 (I think this is reasonable for a deuce hiding + // in a shady corridor) (and let it be `r_offset`): + // + // r_offset + // = 2sqrt(f)(sqrt(o_max) - sqrt(o_min)) / (o_max - o_min) + // ≈ 2.70 sqrt(f) + // + // So if this value is higher than 1, we need enhance the rendered image. Otherwise, + // we will maintain the status quo for now. (In most servers I have encountered, the fog + // color was a bright color, so this status quo won't be a problem, I think. No one has + // complained about it so far.) + sharpening.SetValue(std::sqrt(fogLuminance) * 2.7f); + sharpeningFinalGain.SetValue( + std::max(std::min(settings.r_sharpen.operator float(), 1.0f), 0.0f) * 2.0f); + // composite to the final image GLColorBuffer output = input.GetManager()->CreateBufferHandle(); diff --git a/Sources/Draw/GLColorCorrectionFilter.h b/Sources/Draw/GLColorCorrectionFilter.h index 2a7aaad4..f535f8b9 100644 --- a/Sources/Draw/GLColorCorrectionFilter.h +++ b/Sources/Draw/GLColorCorrectionFilter.h @@ -34,7 +34,10 @@ namespace spades { public: GLColorCorrectionFilter(GLRenderer *); - GLColorBuffer Filter(GLColorBuffer, Vector3 tint); + /** + * @param fogLuminance The luminance of the fog color. Must be in the sRGB color space. + */ + GLColorBuffer Filter(GLColorBuffer, Vector3 tint, float fogLuminance); }; } } diff --git a/Sources/Draw/GLRenderer.cpp b/Sources/Draw/GLRenderer.cpp index 059a818d..2d6fa4d6 100644 --- a/Sources/Draw/GLRenderer.cpp +++ b/Sources/Draw/GLRenderer.cpp @@ -1043,8 +1043,11 @@ namespace spades { tint = Mix(tint, MakeVector3(1.f, 1.f, 1.f), 0.2f); tint *= 1.f / std::min(std::min(tint.x, tint.y), tint.z); + float fogLuminance = (fogColor.x + fogColor.y + fogColor.z) * (1.0f / 3.0f); + float exposure = powf(2.f, (float)settings.r_exposureValue * 0.5f); - handle = GLColorCorrectionFilter(this).Filter(handle, tint * exposure); + handle = + GLColorCorrectionFilter(this).Filter(handle, tint * exposure, fogLuminance); // update smoothed fog color smoothedFogColor = Mix(smoothedFogColor, fogColor, 0.002f); diff --git a/Sources/Draw/GLSettings.cpp b/Sources/Draw/GLSettings.cpp index a06fd8f5..e38a6282 100644 --- a/Sources/Draw/GLSettings.cpp +++ b/Sources/Draw/GLSettings.cpp @@ -58,6 +58,7 @@ DEFINE_SPADES_SETTING(r_physicalLighting, "0"); DEFINE_SPADES_SETTING(r_radiosity, "0"); DEFINE_SPADES_SETTING(r_saturation, "1"); DEFINE_SPADES_SETTING(r_shadowMapSize, "2048"); +DEFINE_SPADES_SETTING(r_sharpen, "1"); DEFINE_SPADES_SETTING(r_softParticles, "1"); DEFINE_SPADES_SETTING(r_sparseShadowMaps, "1"); DEFINE_SPADES_SETTING(r_srgb, "0"); diff --git a/Sources/Draw/GLSettings.h b/Sources/Draw/GLSettings.h index 605b738b..edb0b738 100644 --- a/Sources/Draw/GLSettings.h +++ b/Sources/Draw/GLSettings.h @@ -65,6 +65,7 @@ namespace spades { TypedItemHandle r_radiosity { *this, "r_radiosity", ItemFlags::Latch }; TypedItemHandle r_saturation { *this, "r_saturation" }; TypedItemHandle r_shadowMapSize { *this, "r_shadowMapSize", ItemFlags::Latch }; + TypedItemHandle r_sharpen { *this, "r_sharpen" }; TypedItemHandle r_softParticles { *this, "r_softParticles", ItemFlags::Latch }; TypedItemHandle r_sparseShadowMaps { *this, "r_sparseShadowMaps", ItemFlags::Latch }; TypedItemHandle r_srgb { *this, "r_srgb", ItemFlags::Latch };