feat(draw): apply sharpening effect during the color correction pass
This effect frags two deuces with one grenade. Firstly, it enhances the image's clarify for aesthetic reasons. Secondly, it offsets OpenSpades' fog density, which is often claimed to be denser than the original 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 tendency to strengthen the effect because the human light perception is logarithmic. The effect amount can be adjusted by `r_sharpen` config variable.
This commit is contained in:
parent
ae12df59cc
commit
a26a9c7a26
@ -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.)));
|
||||
|
@ -19,6 +19,7 @@
|
||||
*/
|
||||
|
||||
#include <vector>
|
||||
#include <cmath>
|
||||
|
||||
#include <Core/Debug.h>
|
||||
#include <Core/Math.h>
|
||||
@ -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();
|
||||
|
||||
|
@ -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);
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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");
|
||||
|
@ -65,6 +65,7 @@ namespace spades {
|
||||
TypedItemHandle<int> r_radiosity { *this, "r_radiosity", ItemFlags::Latch };
|
||||
TypedItemHandle<float> r_saturation { *this, "r_saturation" };
|
||||
TypedItemHandle<int> r_shadowMapSize { *this, "r_shadowMapSize", ItemFlags::Latch };
|
||||
TypedItemHandle<float> r_sharpen { *this, "r_sharpen" };
|
||||
TypedItemHandle<int> r_softParticles { *this, "r_softParticles", ItemFlags::Latch };
|
||||
TypedItemHandle<bool> r_sparseShadowMaps { *this, "r_sparseShadowMaps", ItemFlags::Latch };
|
||||
TypedItemHandle<bool> r_srgb { *this, "r_srgb", ItemFlags::Latch };
|
||||
|
Loading…
x
Reference in New Issue
Block a user