AMD FSR CAS (Contrast Adaptive Shapenening)
AMD FSR is a well known upscaling method. It works in two passes, the upscale pass and the sharpening pass. This is the sharpening pass only.
There’s not really a good way to implement multiple passes in gdshaders, but it’s still a decent sharpening filter. (Apply it to a viewport texture.)
Screenshots are closeups of a viewport with FXAA enabled and different levels of CAS. Lower numbers are sharper.
Adapted from https://www.shadertoy.com/view/stXSWB
Shader code
/*
* SOURCE: https://www.shadertoy.com/view/stXSWB
*
* FidelityFX Super Resolution scales up a low resolution
* image, while adding fine detail.
*
* It works in two passes. This is ONLY the second, sharpening pass
*
* MIT Open License
*
* https://gpuopen.com/fsr
*
* For sharpening with a full resolution render buffer,
* FidelityFX CAS is a better option.
* https://www.shadertoy.com/view/ftsXzM
*
* For readability and compatibility, these optimisations have been removed:
* * Fast approximate inverse and inversesqrt
* * textureGather fetches (not WebGL compatible)
* * Multiplying by reciprocal instead of division
*
* Apologies to AMD for the numerous slowdowns and errors I have introduced.
*
*/
shader_type canvas_item;
/***** RCAS *****/
const float FSR_RCAS_LIMIT = (0.25-(1.0/16.0));
const bool FSR_RCAS_DENOISE = false;
void fragment()
{
// Set up constants
float con;
float sharpness = 1.0;
con = exp2(-sharpness);
// Perform RCAS pass
// Constant generated by RcasSetup().
// Algorithm uses minimal 3x3 pixel neighborhood.
// b
// d e f
// h
vec2 off_mult = TEXTURE_PIXEL_SIZE * 1.0;
vec2 sp = UV;//vec2(ip);
vec3 b = texture(TEXTURE, sp + vec2( 0,-1)*off_mult).rgb;
vec3 d = texture(TEXTURE, + vec2(-1, 0)*off_mult).rgb;
vec3 e = texture(TEXTURE, sp).rgb;
vec3 f = texture(TEXTURE, sp+vec2( 1, 0)*off_mult).rgb;
vec3 h = texture(TEXTURE, sp+vec2( 0, 1)*off_mult).rgb;
// Luma times 2.
float bL = b.g + .5 * (b.b + b.r);
float dL = d.g + .5 * (d.b + d.r);
float eL = e.g + .5 * (e.b + e.r);
float fL = f.g + .5 * (f.b + f.r);
float hL = h.g + .5 * (h.b + h.r);
// Noise detection.
float nz = .25 * (bL + dL + fL + hL) - eL;
nz=clamp(
abs(nz)
/(
max(max(bL,dL),max(eL,max(fL,hL)))
-min(min(bL,dL),min(eL,min(fL,hL)))
),
0., 1.
);
nz=1.-.5*nz;
// Min and max of ring.
vec3 mn4 = min(b, min(f, h));
vec3 mx4 = max(b, max(f, h));
// Immediate constants for peak range.
vec2 peakC = vec2(1., -4.);
// Limiters, these need to be high precision RCPs.
vec3 hitMin = mn4 / (4. * mx4);
vec3 hitMax = (peakC.x - mx4) / (4.* mn4 + peakC.y);
vec3 lobeRGB = max(-hitMin, hitMax);
float lobe = max(
-FSR_RCAS_LIMIT,
min(max(lobeRGB.r, max(lobeRGB.g, lobeRGB.b)), 0.)
)*con;
// Apply noise removal.
if (FSR_RCAS_DENOISE) {
lobe *= nz;
}
// Resolve, which needs the medium precision rcp approximation to avoid visible tonality changes.
vec3 col = (lobe * (b + d + h + f) + e) / (4. * lobe + 1.);
COLOR.rgb = col;
}
Note that in Godot 3.4 and later, there’s a built-in CAS implementation you can enable on a per-Viewport basis (or on the root Viewport in the Project Settings). It only works in GLES3 as CAS itself can’t be implemented on GLES2. Also, it only affects 3D rendering unless you set the environment’s background mode to Canvas (as the sharpening is performed in the tonemapping pass).
As of writing, Godot 4 doesn’t have a built-in sharpening shader yet.