/*------------------.
| :: Description :: |
'-------------------/

Contrast Limited Sharpening PS (version 1.1.8)

Copyright:
This code © 2023 Jakub Maksymilian Fober

License:
This work is licensed under the Creative Commons Attribution-NonCommercial-
NoDerivs 3.0 Unported License. To view a copy of this license, visit
http://creativecommons.org/licenses/by-nc-nd/3.0/

Additional permissions under Creative Commons Plus (CC+):

§ 1. The copyright owner further grants permission for commercial reuse of image
recordings based on the work (e.g., Let's Play videos, gameplay streams, and
screenshots featuring ReShade filters). Any such use must include credit to the
creator and the name of the used shader.
 Intent §: To facilitate non-corporate, common use of the shader at no cost.
Outcome §: That recognition of the work in any derivative images is ensured.

§ 2. Additionally, permission is granted for the translation of the front-end UI
text within this shader.
 Intent §: To increase accessibility and understanding across different
languages.
Outcome §: That usability across users from diverse linguistic backgrounds is
promoted, allowing them to fully engage with the shader.

Contact:
If you want additional licensing for your commercial product, please contact me:
jakub.m.fober@protonmail.com
*/

/*-------------.
| :: Macros :: |
'-------------*/

// Fixed sharpen radius for performance
#ifndef CONTRAST_SHARPEN_RADIUS
	#define CONTRAST_SHARPEN_RADIUS 0
#endif

/*--------------.
| :: Commons :: |
'--------------*/

#include "ReShade.fxh"
#include "ReShadeUI.fxh"
#include "ColorConversion.fxh"
#include "BlueNoiseDither.fxh"

/*-----------.
| :: Menu :: |
'-----------*/

#if !CONTRAST_SHARPEN_RADIUS
uniform uint SharpenRadius
<	__UNIFORM_SLIDER_INT1
	ui_label = "sharpening radius";
	ui_tooltip =
		"Sharpening sampling radius in pixels,\n"
		"with Gaussian falloff.\n"
		"This setting directly affects performance.";
	ui_min = 2u; ui_max = 32u;
	ui_category = "sharpening settings";
> = 16u;
#endif

uniform float SharpenAmount
<	__UNIFORM_SLIDER_FLOAT1
	ui_label = "sharpening amount";
	ui_tooltip =
		"High-pass layer multiplier.\n"
		"Values higher than 1.0 may increase noise.";
	ui_min = 0f; ui_max = 2f;
	ui_step = 0.01;
	ui_category = "sharpening settings";
> = 1f;

uniform uint BlendingMode
<	__UNIFORM_RADIO_INT1
	ui_label = "sharpening mode";
	ui_tooltip = "Blending mode for the high-pass layer.";
	ui_items =
		"hard light\0"
		"overlay\0";
	ui_category = "sharpening settings";
> = 0u;

uniform float ContrastAmount
<	__UNIFORM_SLIDER_FLOAT1
	ui_label = "contrast amount";
	ui_tooltip =
		"Contrast limiting threshold.\n"
		"Lower values remove 'halos'.";
	ui_min = 0.01; ui_max = 1f;
	ui_step = 0.01;
	ui_category = "additional settings";
	ui_category_closed = true;
> = 0.16;

uniform bool DitheringEnabled
<	__UNIFORM_INPUT_BOOL1
	ui_label = "remove banding";
	ui_tooltip =
		"Applies invisible dithering effect, to\n"
		"increase perceivable image bit-depth.";
	ui_category = "additional settings";
> = true;

/*---------------.
| :: Textures :: |
'---------------*/

// Render target for two-pass blur
texture ContrastSharpenTarget
{
	Width  = BUFFER_WIDTH;
	Height = BUFFER_HEIGHT;

	Format = R8;
};
sampler ContrastSharpenSampler
{ Texture = ContrastSharpenTarget; };

/*----------------.
| :: Functions :: |
'----------------*/

/* Exponential bell weight falloff by JMF
   Generates smooth bell falloff for blur, with perfect weights
   distribution for a given number of samples.
   Input: position ∈ [-1, 1] */
float bellWeight(float position)
{
	// Get deviation for minimum value for a given step size
	const float deviation = log(rcp(256u)); // Logarithm of base e for an 8-bit final weight
	// Get smooth bell falloff without aliasing or zero value at the last sample
	return exp(position*position*deviation); // Gaussian bell falloff
}

/* Overlay blending function.
   Base and blend layers are in [0,1] range. */
float overlay(float baseLayer, float blendLayer)
{
	baseLayer *= 2f;
	return mad(mad(
		-min(baseLayer, 1f), blendLayer, 1f), // Multiply filter
		 max(baseLayer, 1f)-2f,               // Screen filter
		1f);
}

/*--------------.
| :: Shaders :: |
'--------------*/

// Vertex shader generating a triangle covering the entire screen
void ContrastSharpenVS(
	in  uint   vertexId  : SV_VertexID,
	out float4 vertexPos : SV_Position
)
{
	// Define vertex position
	const float2 vertexPosList[3] =
	{
		float2(-1f, 1f), // Top left
		float2(-1f,-3f), // Bottom left
		float2( 3f, 1f)  // Top right
	};
	// Export  vertex position,
	vertexPos.xy = vertexPosList[vertexId];
	vertexPos.zw = float2(0f, 1f); // Export vertex position
}

// Horizontal luminosity blur pass
void ContrastSharpenPassHorizontalPS(
	in  float4  pixCoord : SV_Position,
	out float luminosity : SV_Target
)
{
	// Get current pixel coordinates
	uint2 texelPos = uint2(pixCoord.xy);
	// Get current pixel luminosity value
	luminosity = ColorConvert::RGB_to_Luma(tex2Dfetch(ReShade::BackBuffer, texelPos).rgb);
	// Prepare cumulative variables
	float cumilativeLuminosity = 0f, cumulativeWeight = 0f;
#if CONTRAST_SHARPEN_RADIUS // Fixed contrast sharpen radius
	static float sampleWeight[CONTRAST_SHARPEN_RADIUS*2u+1u];
	const uint SharpenRadius = CONTRAST_SHARPEN_RADIUS;
#endif
	// Sample blur kernel
	for (uint yPos=0u; yPos<=SharpenRadius*2u; yPos++)
	{
		// Sample back-buffer luminosity
		float sampleLuminosity = ColorConvert::RGB_to_Luma(
			tex2Dfetch(ReShade::BackBuffer, uint2(
				texelPos.x,
				// Offset coordinates
				clamp(int(texelPos.y+yPos)-SharpenRadius, 0, int(BUFFER_HEIGHT)-1)
			)).rgb
		);
		// Get step size
		const float stepSize = rcp(SharpenRadius);
		// Get current sample weight
#if CONTRAST_SHARPEN_RADIUS // for fixed contrast sharpen radius
		sampleWeight[yPos] =
#else // for dynamic contrast sharpen radius
		float sampleWeight =
#endif
			bellWeight(mad(yPos, stepSize, -1f));// Y radius
		float sampleContrastWeight = saturate(abs(sampleLuminosity-luminosity)/ContrastAmount); // Contrast
		sampleContrastWeight = bellWeight(sampleContrastWeight); // Natural falloff
		// Apply weight and add to blurred luminosity
		sampleContrastWeight *=
#if CONTRAST_SHARPEN_RADIUS // for fixed contrast sharpen radius
			sampleWeight[yPos];
#else // for dynamic contrast sharpen radius
			sampleWeight;
#endif
		cumilativeLuminosity += sampleLuminosity*sampleContrastWeight;
		cumulativeWeight += sampleContrastWeight;
	}
	// Save output and restore brightness
	luminosity = cumilativeLuminosity/cumulativeWeight;

	// Dither output to increase perceivable picture bit-depth
	if (DitheringEnabled)
		luminosity = BlueNoise::dither(luminosity, uint2(pixCoord.xy));
}

// Horizontal luminance blur and contrast sharpening pass
void ContrastSharpenPassVerticalPS(
	in  float4 pixCoord : SV_Position,
	out float3    color : SV_Target
)
{
	// Get current pixel coordinates
	uint2 texelPos = uint2(pixCoord.xy);
	// Get current pixel YCbCr color value
	color = ColorConvert::RGB_to_YCbCr(tex2Dfetch(ReShade::BackBuffer, texelPos).rgb);
	// Prepare cumulative variables
	float cumilativeLuminosity = 0f, cumulativeWeight = 0f;
#if CONTRAST_SHARPEN_RADIUS // Fixed contrast sharpen radius
	static float sampleWeight[CONTRAST_SHARPEN_RADIUS*2u+1u];
	const uint SharpenRadius = CONTRAST_SHARPEN_RADIUS;
#endif
	// Sample blur kernel
	for (uint xPos=0u; xPos<=SharpenRadius*2u; xPos++)
	{
		// Sample back-buffer luminosity
		float sampleLuminosity = tex2Dfetch(ContrastSharpenSampler, uint2(
				// Offset coordinates
				clamp(int(texelPos.x+xPos)-SharpenRadius, 0, int(BUFFER_WIDTH)-1),
				texelPos.y
			)).r;
		// Get step size
		const float stepSize = rcp(SharpenRadius);
		// Get current sample weight
#if CONTRAST_SHARPEN_RADIUS // for fixed contrast sharpen radius
		sampleWeight[xPos] =
#else // for dynamic contrast sharpen radius
		float sampleWeight =
#endif
			bellWeight(mad(xPos, stepSize, -1f)); // X position
		float sampleContrastWeight = saturate(abs(sampleLuminosity-color.x)/ContrastAmount); // Contrast
		sampleContrastWeight = bellWeight(sampleContrastWeight); // Natural falloff
		// Apply weight and add to blurred luminosity
		sampleContrastWeight *=
#if CONTRAST_SHARPEN_RADIUS // for fixed contrast sharpen radius
			sampleWeight[xPos];
#else // for dynamic contrast sharpen radius
			sampleWeight;
#endif
		cumilativeLuminosity += sampleLuminosity*sampleContrastWeight;
		cumulativeWeight += sampleContrastWeight;
	}
	// Restore brightness
	cumilativeLuminosity /= cumulativeWeight;
	// Generate high-pass filter
	float highPass = mad(color.x-cumilativeLuminosity, SharpenAmount*0.5, 0.5);
	// Blend high-pass with the base image luminosity
	switch (BlendingMode)
	{
		case 1: // blend using overlay method
			color.x = overlay(color.x, highPass);
			break;
		default: // blend using hard-light method
			color.x = overlay(highPass, color.x);
			break;
	}
	// Convert to RGB color space
	color = saturate(ColorConvert::YCbCr_to_RGB(color)); // and clamp result

	// Dither output to increase perceivable picture bit-depth
	if (DitheringEnabled)
		color = BlueNoise::dither(color, uint2(pixCoord.xy));
}

/*-------------.
| :: Output :: |
'-------------*/

technique ContrastSharpen
<
	ui_label = "Contrast Limited Sharpening";
	ui_tooltip =
		"Contrast Limited Sharpening effect.\n"
		"\n"
		"Increases local contrast without enhancing\n"
		"already sharp edges.\n"
		"\n"
		"	· dynamic or fixed per-pixel sampling.\n"
		"	· removes 'halo' effect.\n"
		"	· removes 'banding' effect.\n"
		"\n"
		"This effect © 2023 Jakub Maksymilian Fober\n"
		"Licensed under CC BY-NC-ND 3.0 +\n"
		"for additional permissions see the source code.";
>
{
	pass GaussianContrastBlurHorizontal
	{
		RenderTarget = ContrastSharpenTarget;

		VertexShader = ContrastSharpenVS;
		PixelShader  = ContrastSharpenPassHorizontalPS;
	}
	pass GaussianContrastBlurVerticalAndSharpening
	{
		VertexShader = ContrastSharpenVS;
		PixelShader  = ContrastSharpenPassVerticalPS;
	}
}
