Grey dither and crush (Only use pre-downscale)

  • Posts: 59
11 months 2 days ago - 7 months 3 weeks ago #1 by crabshank

This one uses a new dithering function I've developed that outputs a uniformly distributed (almost, actually has a mode of zero) value between 0 and 1 and uses that to change the image i.e. greyDitherSdv changes the standard deviation of the max RGB value, on average by a specified amount; greyDitherAmnt changes the mean of the max RGB value; and greyDitherScurve applies an S-curve with a specified power to the max RGB value, on average. The non-maximum are adjusted by a fraction of themselves over the max RGB.

Note that this works much better on material that will be downscaled after it is applied, so that detail is retained. This is why I was in two minds about porting this one.

Crushing remaps the RGB values to a smaller range and then back. Due to floating point rounding errors, this loses colour information and thus simplifies the colour information. Crushing to 0.5 has a stronger de-dithering effect but is more lossy than the default crushing to the RGB average.
#include "ReShadeUI.fxh"

uniform float greyDitherAmnt < __UNIFORM_DRAG_FLOAT1
	ui_min = -255.0; ui_max = 255.0;
	ui_tooltip = "Change average value by specified amount.";
> = 0;

uniform float greyDitherSdv < __UNIFORM_DRAG_FLOAT1
	ui_min = 0.0; ui_max = 255.0;
	ui_tooltip = "Change standard deviation value by specified amount.";
> = 0;

uniform float greyDitherScurve < __UNIFORM_DRAG_FLOAT1
	ui_min = -1.0; ui_max = 5.0;
> = 1;

uniform int Crushing_type < __UNIFORM_COMBO_INT1
    ui_items = "Crush to RGB average\0Crush to 0.5\0Crush to max RGB\0";
    ui_tooltip = "Crushing to 0.5 has a stronger de-dither effect but loses more information.";
> = 0;

uniform float Crushing_amnt < __UNIFORM_DRAG_FLOAT1
	ui_min = 0.0; ui_max = 1.0;  ui_tooltip = "Disabled if =0";
> = 0;

uniform bool Dark_dither <> = false;

uniform float Dark_dither_pwr < __UNIFORM_DRAG_FLOAT1
	ui_min = 0.0; ui_max = 1.0;
> = 0;

uniform bool Crushing_debug <> = false;

uniform bool Split <> = false;

uniform bool Flip_split <> = false;

uniform float Split_position < __UNIFORM_SLIDER_FLOAT1
	ui_min = 0; ui_max =1;
	ui_tooltip = "0 is on the far left, 1 on the far right.";
> = 0.5;

#include "ReShade.fxh"

float3 sRGB2Linear (float3 rgb){

rgb=(rgb > 0.0404482362771082)?pow(abs((rgb+0.055)/1.055),2.4):rgb/12.92;

return rgb;

float3 Linear2sRGB (float3 rgb){

rgb=(rgb > 0.00313066844250063)?1.055*pow(abs(rgb),1/2.4)-0.055:12.92*rgb;

return rgb;


	float3 rgb2hsv(float3 c)
    float4 K = float4(0.0, -1.0 / 3.0, 2.0 / 3.0, -1.0);
    float4 p = lerp(float4(, K.wz), float4(, K.xy), step(c.b, c.g));
    float4 q = lerp(float4(p.xyw, c.r), float4(c.r, p.yzx), step(p.x, c.r));
    float d = q.x - min(q.w, q.y);
    float e = 1.0e-10;
    return float3(abs(q.z + (q.w - q.y) / (6.0 * d + e)), d / (q.x + e), q.x);

float3 hsv2rgb(float3 c)
    float4 K = float4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
    float3 p = abs(frac( + * 6.0 - K.www);
    return c.z * lerp(, clamp(p -, 0.0, 1.0), c.y);

float random( float2 p )
// We need irrationals for pseudo randomness.
// Most (all?) known transcendental numbers will (generally) work.
const float2 r = float2(
23.1406926327792690,  // e^pi (Gelfond's constant)
 2.6651441426902251); // 2^sqrt(2) (Gelfond-Schneider constant)

float t=frac(acos(p.x/p.y)+sin(p.x)*r.y+cos(p.y)*r.x+p.x*p.y*r.y);

t= frac((800*cos(t/20)+1400)*t);  
t= frac(pow( frac((0.01*t+sin(500*t*t))+tan(t*500)*500),2));

float rMap =3.98;
float tOld=t;
int k=0;

for (k=0;k<100;k++){
 float w = frac(10000*tOld+0.597*tOld);

#define dither_points 7
float2 d[dither_points] = {

float2 d_x_b=float2(0,1);float2 d_y_b=float2(0,1);
float dither=w;
int i=0; int exact=0; 
[branch]if(d[i].x/255==dither) {dither=d[i].y/255;exact=1;i=dither_points-1;}else{if(d[i].x/255<dither&&d[i].x/255>=d_x_b.x){d_x_b.x=d[i].x/255;d_y_b.x=d[i].y/255;} if(d[i].x/255<=d_x_b.y&&dither<d[i].x/255){d_x_b.y=d[i].x/255;d_y_b.y=d[i].y/255;}}} if(exact==0){dither=d_y_b.x+(dither-d_x_b.x)*((d_y_b.y-d_y_b.x)/(d_x_b.y-d_x_b.x));};i=0;exact=0;

return dither;


float grey_dither(float color,float2 tex,float rnd,float sdv, float gamma){

float rand=random(float2((tex.x+BUFFER_WIDTH*BUFFER_RCP_HEIGHT)*color,(tex.y+BUFFER_HEIGHT*BUFFER_RCP_WIDTH)*color));
float randm=rnd*-1*((rand*-4)+1); // averages to color + rnd

color =(rnd!=0)?color+(randm/255):color;

float sAB=sdv*sqrt(12)*0.5;

color =(sdv!=0)?color+(randm/255):color;

float colorSc=color*2;
color =(gamma!=1)?color+randm:color;

return color;

float4 crusher(float4 color){
float4 c0=color;
float3 colorHSV=rgb2hsv(c0.rgb);

float rgbAvg=dot(c0.rgb,pow(3,-1));

float rgbMx=max(color.r,max(color.g,color.b));

float distGrey=sqrt(pow(rgbAvg-c0.r,2)+pow(rgbAvg-c0.g,2)+pow(rgbAvg-c0.b,2));
float normDistGrey=distGrey*pow(0.5*sqrt(3),-1);
float crsh=1-Crushing_amnt;
float lerper=pow(normDistGrey,Crushing_amnt);

float3 crshAvg=lerp(c0.rgb,rgbAvg,lerper);

float3 crshMx=lerp(c0.rgb,rgbMx,lerper);

float mxOld=max(c0.r,max(c0.g,c0.b));
//float mnOld=min(c0.r,min(c0.g,c0.b));
float mxNew=max(crshAvg.r,max(crshAvg.g,crshAvg.b));
float MAX_New=max(crshMx.r,max(crshMx.g,crshMx.b));
//float mnNew=min(crshAvg.r,min(crshAvg.g,crshAvg.b));

float3 avgRevert=(mxNew==0)?0:c0.rgb*(crshAvg.rgb/mxNew);

float3 mxRevert=(MAX_New==0)?0:c0.rgb*(crshMx.rgb/mxNew);

float3 crshHalf=(0.5-0.5*crsh) + ((0.5+0.5*crsh) - (0.5-0.5*crsh)) * c0.rgb;


float fromMin=0.5-0.5*crsh;
float fromMax=0.5+0.5*crsh;

float3 halfRevert=	 ((c0.rgb - fromMin) / (fromMax - fromMin));

float3 c1=(Crushing_debug==1)?crshAvg:avgRevert;
float3 c2=(Crushing_debug==1)?hsv2rgb(colorHSV):halfRevert;
float3 c3=(Crushing_debug==1)?crshMx:mxRevert;

float3 c4=(Crushing_type==0)?c1:c2;
float3 c5=(Crushing_type==2)?c3:c4;

return float4(,color.w);

float4 PS_GreyDither(float4 pos : SV_Position, float2 texcoord : TEXCOORD0) : SV_Target

	float4 c0 = tex2D(ReShade::BackBuffer, texcoord);
	float4 c0OG = c0;
	float c0Max=max(c0.r,max(c0.g,c0.b));
	float4 c1 = c0;

c1.rgb =saturate(grey_dither(c0Max,texcoord,greyDitherAmnt,greyDitherSdv,greyDitherScurve)*(c1.rgb/c0Max));
	float c1Max=max(c1.r,max(c1.g,c1.b));




float4 c2=(texcoord.x>=Split_position*Split)?c1:c0OG;
float4 c3=(texcoord.x<=Split_position*Split)?c1:c0OG;

float4 c4=(Flip_split==1 && Split==1)?c3:c2;

float divLine = abs(texcoord.x - Split_position) < BUFFER_RCP_WIDTH;
c4 =(Split==0)?c4: c4*(1.0 - divLine); //invert divline

return c4;


technique GreyDither {
	pass GreyDither {

The video shader versions are here , I've only ported the grey dither from it so far as I find this the most useful, and crushing here .

Please Log in or Create an account to join the conversation.

  • Posts: 137
11 months 2 days ago #2 by TreyM
I don't understand why you're calling this dither. It's just acting like a noise overlay... That's not really the purpose of dither. Dither should be (in ideal cases) nearly imperceptible and is designed to fix banding before truncation to lower bit depth.

Please Log in or Create an account to join the conversation.

  • Posts: 59
11 months 2 days ago - 10 months 2 weeks ago #3 by crabshank
It actually destroys macroblocks in video, which is what I made it for (mine on bottom):

EDIT: Also, re. truncation to a lower bit depth I have a crushing filter that reduces the effect of the dither afterwards to clean up and boost the image (mine on bottom again):

I just put it way up high for demo purposes lol.

EDIT: Games don't suffer from macroblocking AFAIK, so that's another reason I wasn't sure whether to port this and still unsure whether or not to port the crushing part (cos they go together really).

Please Log in or Create an account to join the conversation.

  • Posts: 137
11 months 1 day ago #4 by TreyM
Games do not suffer from macro blocking... this has no real purpose for gaming...

Please Log in or Create an account to join the conversation.

  • Posts: 59
11 months 1 day ago - 11 months 1 day ago #5 by crabshank
Games do suffer from banding though, and also if someone managed to hook Reshade into a video player it would provide a version with a GUI instead of text editing the .hlsl version.

Please Log in or Create an account to join the conversation.

  • Posts: 137
11 months 1 day ago #6 by TreyM
If a game output is already banding, dither will not fix it. Dither is not debanding. Dither is meant to be used to PREVENT banding before it happens. If you dither over something that is already banded, you're just adding noise to the image and solving nothing.
The following user(s) said Thank You: turgor128

Please Log in or Create an account to join the conversation.

  • Posts: 59
10 months 3 weeks ago #7 by crabshank
I've optimised all the code apart from the dither rempping loop, I may come up with a solution when I re-write my remapping shader but this code is expensive anyway and IDK how much in the way of gains I can get.

Please Log in or Create an account to join the conversation.