duckstation

duckstation, but archived from the revision just before upstream changed it to a proprietary software project, this version is the libre one
git clone https://git.neptards.moe/u3shit/duckstation.git
Log | Files | Refs | README | LICENSE

CRTLottes2.fx (11628B)


      1 #include "ReShade.fxh"
      2 
      3 //
      4 // PUBLIC DOMAIN CRT STYLED SCAN-LINE SHADER
      5 //
      6 //   by Timothy Lottes
      7 //
      8 // This is more along the style of a really good CGA arcade monitor.
      9 // With RGB inputs instead of NTSC.
     10 // The shadow mask example has the mask rotated 90 degrees for less chromatic aberration.
     11 //
     12 // Left it unoptimized to show the theory behind the algorithm.
     13 //
     14 // It is an example what I personally would want as a display option for pixel art games.
     15 // Please take and use, change, or whatever.
     16 //
     17 
     18 #ifndef CRTS_DEBUG
     19 	#define CRTS_DEBUG	0
     20 #endif
     21 
     22 #ifndef CRTS_2_TAP
     23 	#define CRTS_2_TAP	0
     24 #endif
     25 
     26 uniform bool CRTS_WARP <
     27 	ui_type = "boolean";
     28 	ui_label = "Enable Warping [CRT Lottes 2.0]";
     29 > = true;
     30 
     31 uniform float CRTS_WARP_X <
     32 	ui_type = "drag";
     33 	ui_min = 0.0;
     34 	ui_max = 512.0;
     35 	ui_label = "CRT Warping X [CRT Lottes 2.0]";
     36 > = 64.0;
     37 
     38 uniform float CRTS_WARP_Y <
     39 	ui_type = "drag";
     40 	ui_min = 0.0;
     41 	ui_max = 512.0;
     42 	ui_label = "CRT Warping Y [CRT Lottes 2.0]";
     43 > = 48.0;
     44 
     45 uniform bool CRTS_TONE <
     46 	ui_type = "boolean";
     47 	ui_label = "Enable CRT Tonemapping [CRT Lottes 2.0]";
     48 > = true;
     49 
     50 uniform bool CRTS_CONTRAST <
     51 	ui_type = "boolean";
     52 	ui_label = "Enable CRT Contrast [CRT Lottes 2.0]";
     53 > = false;
     54 
     55 uniform bool CRTS_SATURATION <
     56 	ui_type = "boolean";
     57 	ui_label = "Enable CRT Saturation [CRT Lottes 2.0]";
     58 > = false;
     59 
     60 uniform int CRTS_MASK_TYPE <
     61 	ui_type = "combo";
     62 	ui_items = "None\0Aperture Grille\0Aperture Grille (Lite)\0Shadow Mask\0";
     63 	ui_label = "Mask Type [CRT Lottes 2.0]";
     64 > = 2;
     65 
     66 //--------------------------------------------------------------
     67 // Scanline thinness
     68 //  0.50 = fused scanlines
     69 //  0.70 = recommended default
     70 //  1.00 = thinner scanlines (too thin)
     71 
     72 uniform float INPUT_THIN <
     73 	ui_type = "drag";
     74 	ui_min = 0.5;
     75 	ui_max = 1.0;
     76 	ui_label = "Scanlines Thinnes [CRT Lottes 2.0]";
     77 > = 0.70;
     78 
     79 //--------------------------------------------------------------
     80 // Horizonal scan blur
     81 //  -3.0 = pixely
     82 //  -2.5 = default
     83 //  -2.0 = smooth
     84 //  -1.0 = too blurry
     85 
     86 uniform float INPUT_BLUR <
     87 	ui_type = "drag";
     88 	ui_min = -3.0;
     89 	ui_max = 0.0;
     90 	ui_label = "Horizontal Scan Blur [CRT Lottes 2.0]";
     91 > = -2.5;
     92 
     93 //--------------------------------------------------------------
     94 // Shadow mask effect, ranges from,
     95 //  0.25 = large amount of mask (not recommended, too dark)
     96 //  0.50 = recommended default
     97 //  1.00 = no shadow mask
     98 
     99 uniform float INPUT_MASK <
    100 	ui_type = "drag";
    101 	ui_min = 0.0;
    102 	ui_max = 1.0;
    103 	ui_label = "Shadow Mask Intensity [CRT Lottes 2.0]";
    104 > = 0.5;
    105 
    106 //--------------------------------------------------------------
    107 
    108 uniform int INPUT_X <
    109 	ui_type = "drag";
    110 	ui_min = 1;
    111 	ui_max = BUFFER_WIDTH;
    112 	ui_label = "Resolution Width [CRT Lottes 2.0]";
    113 > = 640;
    114 
    115 uniform int INPUT_Y <
    116 	ui_type = "drag";
    117 	ui_min = 1;
    118 	ui_max = BUFFER_HEIGHT;
    119 	ui_label = "Resolution Height [CRT Lottes 2.0]";
    120 > = 480;
    121 
    122 //--------------------------------------------------------------
    123 // Setup the function which returns input image color
    124 
    125 void ToLinear(inout float3 color)
    126 {
    127 	float3 c1 = color.rgb / 12.92;
    128 	float3 c2 = pow((color.rgb + 0.055)/1.055, 2.4);
    129 
    130 	color.r = (color.r <= 0.04045) ? c1.r : c2.r;
    131 	color.g = (color.g <= 0.04045) ? c1.g : c2.g;
    132 	color.b = (color.b <= 0.04045) ? c1.b : c2.b;
    133 }
    134 
    135 void ToSRGB(inout float3 color)
    136 {
    137 	float3 c1 = color.rgb * 12.92;
    138 	float3 c2 = 1.055 * pow(color.rgb, 0.4166) - 0.055;
    139 
    140 	color.r = (color.r < 0.0031308) ? c1.r : c2.r;
    141 	color.g = (color.g < 0.0031308) ? c1.g : c2.g;
    142 	color.b = (color.b < 0.0031308) ? c1.b : c2.b;
    143 }
    144 
    145 float3 CrtsFetch(float2 uv)
    146 {
    147 	float3 color = tex2D(ReShade::BackBuffer, uv).rgb;
    148 	ToLinear(color);
    149 	return color;
    150 }
    151 
    152 float4 CrtsTone(
    153 float contrast,
    154 float saturation,
    155 float thin,
    156 float mask)
    157 {
    158 	
    159 	mask = INPUT_MASK;
    160 
    161   	if (CRTS_MASK_TYPE <= 0){
    162 		mask=1.0;
    163 	}
    164 
    165 
    166   	if(CRTS_MASK_TYPE == 2){
    167 		mask=0.5+INPUT_MASK*0.5;	
    168 	}
    169 
    170   	float4 ret;
    171   	float midOut=0.18/((1.5-thin)*(0.5*mask+0.5));
    172   	float pMidIn=pow(0.18,contrast);
    173   	ret.x=contrast;
    174   	ret.y=((-pMidIn)+midOut)/((1.0-pMidIn)*midOut);
    175   	ret.z=((-pMidIn)*midOut+pMidIn)/(midOut*(-pMidIn)+midOut);
    176   	ret.w=contrast+saturation;
    177   	return ret;
    178 }
    179 
    180 float3 CrtsMask(float2 pos,float dark)
    181 {
    182 	
    183 	if (CRTS_MASK_TYPE == 1){   
    184 		float3 m=dark;
    185 		float x=frac(pos.x*(1.0/3.0));
    186 		if(x<(1.0/3.0))m.r=1.0;
    187 		else if(x<(2.0/3.0))m.g=1.0;
    188 		else m.b=1.0;
    189 		return m;
    190 	} else if (CRTS_MASK_TYPE == 2){
    191 		float3 m=1.0;
    192 		float x=frac(pos.x*(1.0/3.0));
    193 		if(x<(1.0/3.0))m.r=dark;
    194 		else if(x<(2.0/3.0))m.g=dark;
    195 		else m.b=dark;
    196 		return m;
    197 	} else if(CRTS_MASK_TYPE <= 0){
    198 	   return 1.0;	
    199 	} else if(CRTS_MASK_TYPE >= 3){
    200 		pos.x+=pos.y*3.0;
    201 		float3 m=dark;
    202 		float x=frac(pos.x*(1.0/6.0));
    203 		if(x<(1.0/3.0))m.r=1.0;
    204 		else if(x<(2.0/3.0))m.g=1.0;
    205 		else m.b=1.0;
    206 		return m;
    207 	} else {
    208 		return 0.0;
    209 	}
    210  }
    211 
    212  float3 CrtsFilter(
    213 //--------------------------------------------------------------
    214   // SV_POSITION, fragCoord.xy
    215   float2 ipos,
    216 //--------------------------------------------------------------
    217   // inputSize / outputSize (in pixels)
    218   float2 inputSizeDivOutputSize,     
    219 //--------------------------------------------------------------
    220   // 0.5 * inputSize (in pixels)
    221   float2 halfInputSize,
    222 //--------------------------------------------------------------
    223   // 1.0 / inputSize (in pixels)
    224   float2 rcpInputSize,
    225 //--------------------------------------------------------------
    226   // 1.0 / outputSize (in pixels)
    227   float2 rcpOutputSize,
    228 //--------------------------------------------------------------
    229   // 2.0 / outputSize (in pixels)
    230   float2 twoDivOutputSize,   
    231 //--------------------------------------------------------------
    232   // inputSize.y
    233   float inputHeight,
    234 //--------------------------------------------------------------
    235   // Warp scanlines but not phosphor mask
    236   //  0.0 = no warp
    237   //  1.0/64.0 = light warping
    238   //  1.0/32.0 = more warping
    239   // Want x and y warping to be different (based on aspect)
    240   float2 warp,
    241 //--------------------------------------------------------------
    242   // Scanline thinness
    243   //  0.50 = fused scanlines
    244   //  0.70 = recommended default
    245   //  1.00 = thinner scanlines (too thin)
    246   // Shared with CrtsTone() function
    247   float thin,
    248 //--------------------------------------------------------------
    249   // Horizonal scan blur
    250   //  -3.0 = pixely
    251   //  -2.5 = default
    252   //  -2.0 = smooth
    253   //  -1.0 = too blurry
    254   float blur,
    255 //--------------------------------------------------------------
    256   // Shadow mask effect, ranges from,
    257   //  0.25 = large amount of mask (not recommended, too dark)
    258   //  0.50 = recommended default
    259   //  1.00 = no shadow mask
    260   // Shared with CrtsTone() function
    261   float mask,
    262 //--------------------------------------------------------------
    263   // Tonal curve parameters generated by CrtsTone()
    264   float4 tone
    265 //--------------------------------------------------------------
    266  ){
    267 //--------------------------------------------------------------
    268 	#if (CRTS_DEBUG == 1)
    269 		float2 uv=ipos*rcpOutputSize;
    270 		// Show second half processed, and first half un-processed
    271 		if(uv.x<0.5)
    272 		{
    273 				// Force nearest to get squares
    274 				uv*=1.0/rcpInputSize;
    275 				uv=floor(uv)+float2(0.5,0.5);
    276 				uv*=rcpInputSize;
    277 				float3 color=CrtsFetch(uv);
    278 				return color;
    279 		}
    280 	#endif
    281 
    282   	float2 pos;
    283 	float vin;
    284 
    285 	if (CRTS_WARP){
    286 		// Convert to {-1 to 1} range
    287 		pos=ipos*twoDivOutputSize-float2(1.0,1.0);
    288 		// Distort pushes image outside {-1 to 1} range
    289 		pos*=float2(1.0+(pos.y*pos.y)*warp.x,1.0+(pos.x*pos.x)*warp.y);
    290 		// TODO: Vignette needs optimization
    291 		vin=1.0-((1.0-saturate(pos.x*pos.x))*(1.0-saturate(pos.y*pos.y)));
    292 		vin=saturate((-vin)*inputHeight+inputHeight);
    293 		// Leave in {0 to inputSize}
    294 		pos=pos*halfInputSize+halfInputSize;     
    295 	} else {
    296 		pos=ipos*inputSizeDivOutputSize;
    297 	}
    298 	
    299   	// Snap to center of first scanline
    300   	float y0=floor(pos.y-0.5)+0.5;
    301 
    302 	#if (CRTS_2_TAP == 1)
    303 		// Using Inigo's "Improved Texture Interpolation"
    304 		// http://iquilezles.org/www/articles/texture/texture.htm
    305 		pos.x+=0.5;
    306 		float xi=floor(pos.x);
    307 		float xf=pos.x-xi;
    308 		xf=xf*xf*xf*(xf*(xf*6.0-15.0)+10.0);  
    309 		float x0=xi+xf-0.5;
    310 		float2 p=float2(x0*rcpInputSize.x,y0*rcpInputSize.y);     
    311 		// Coordinate adjusted bilinear fetch from 2 nearest scanlines
    312 		float3 colA=CrtsFetch(p);
    313 		p.y+=rcpInputSize.y;
    314 		float3 colB=CrtsFetch(p);
    315 	#else
    316 		// Snap to center of one of four pixels
    317 		float x0=floor(pos.x-1.5)+0.5;
    318 		// Inital UV position
    319 		float2 p=float2(x0*rcpInputSize.x,y0*rcpInputSize.y);     
    320 		// Fetch 4 nearest texels from 2 nearest scanlines
    321 		float3 colA0=CrtsFetch(p);
    322 		p.x+=rcpInputSize.x;
    323 		float3 colA1=CrtsFetch(p);
    324 		p.x+=rcpInputSize.x;
    325 		float3 colA2=CrtsFetch(p);
    326 		p.x+=rcpInputSize.x;
    327 		float3 colA3=CrtsFetch(p);
    328 		p.y+=rcpInputSize.y;
    329 		float3 colB3=CrtsFetch(p);
    330 		p.x-=rcpInputSize.x;
    331 		float3 colB2=CrtsFetch(p);
    332 		p.x-=rcpInputSize.x;
    333 		float3 colB1=CrtsFetch(p);
    334 		p.x-=rcpInputSize.x;
    335 		float3 colB0=CrtsFetch(p);
    336 	#endif
    337 
    338   	// Vertical filter
    339   	// Scanline intensity is using sine wave
    340   	// Easy filter window and integral used later in exposure
    341   	float off=pos.y-y0;
    342   	float pi2=6.28318530717958;
    343   	float hlf=0.5;
    344   	float scanA=cos(min(0.5,  off *thin     )*pi2)*hlf+hlf;
    345   	float scanB=cos(min(0.5,(-off)*thin+thin)*pi2)*hlf+hlf;
    346 
    347 	#if (CRTS_2_TAP == 1)
    348 	if (CRTS_WARP){
    349 			// Get rid of wrong pixels on edge
    350 			scanA*=vin;
    351 			scanB*=vin;
    352 	}
    353 		// Apply vertical filter
    354 		float3 color=(colA*scanA)+(colB*scanB);
    355 	#else
    356 		 // Horizontal kernel is simple gaussian filter
    357 		float off0=pos.x-x0;
    358 		float off1=off0-1.0;
    359 		float off2=off0-2.0;
    360 		float off3=off0-3.0;
    361 		float pix0=exp2(blur*off0*off0);
    362 		float pix1=exp2(blur*off1*off1);
    363 		float pix2=exp2(blur*off2*off2);
    364 		float pix3=exp2(blur*off3*off3);
    365 		float pixT=rcp(pix0+pix1+pix2+pix3);
    366 
    367 	if (CRTS_WARP){
    368 		// Get rid of wrong pixels on edge
    369 		pixT*=vin;
    370 		}
    371 		scanA*=pixT;
    372 		scanB*=pixT;
    373 		// Apply horizontal and vertical filters
    374 		float3 color=
    375 			(colA0*pix0+colA1*pix1+colA2*pix2+colA3*pix3)*scanA +
    376 			(colB0*pix0+colB1*pix1+colB2*pix2+colB3*pix3)*scanB;
    377 	#endif
    378 	
    379 		// Apply phosphor mask          
    380 		color*=CrtsMask(ipos,mask);
    381 		// Optional color processing
    382 	if (CRTS_TONE){
    383 		// Tonal control, start by protecting from /0
    384 		float peak=max(1.0/(256.0*65536.0),max(color.r,max(color.g,color.b)));
    385 		// Compute the ratios of {R,G,B}
    386 		float3 ratio=color*rcp(peak);
    387 		// Apply tonal curve to peak value
    388 	if (CRTS_CONTRAST){
    389 			peak=pow(peak,tone.x);
    390 	}
    391 		peak=peak*rcp(peak*tone.y+tone.z);
    392 		// Apply saturation
    393 	if (CRTS_SATURATION){
    394 			ratio=pow(ratio,float3(tone.w,tone.w,tone.w));
    395 	}
    396 		// Reconstruct color
    397 		return ratio*peak;
    398 	} else {
    399 		return color;
    400 	}
    401 }
    402 
    403 //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    404 //
    405 //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    406 
    407 
    408 
    409 //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    410 //
    411 //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    412 
    413 void PS_CRTLottes2018(float4 vpos : SV_Position, float2 texcoord : TEXCOORD, out float4 color : SV_Target0)
    414 {
    415 	color = tex2D(ReShade::BackBuffer, texcoord.xy);
    416 
    417  	color.rgb=CrtsFilter(
    418   	vpos.xy,
    419   	float2(INPUT_X,INPUT_Y)/ReShade::ScreenSize.xy,
    420   	float2(INPUT_X,INPUT_Y)*0.5,
    421   	1.0/float2(INPUT_X,INPUT_Y),
    422   	1.0/ReShade::ScreenSize.xy,
    423   	2.0/ReShade::ScreenSize.xy,
    424   	INPUT_Y,
    425   	float2(1.0/CRTS_WARP_X,1.0/CRTS_WARP_Y),
    426   	INPUT_THIN,
    427   	INPUT_BLUR,
    428   	INPUT_MASK,
    429   	CrtsTone(1.0,0.0,INPUT_THIN,INPUT_MASK));
    430  	
    431 	 ToSRGB(color.rgb);
    432 }
    433 
    434 //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    435 //
    436 //++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
    437 
    438 
    439 technique CRTLottes2018
    440 {
    441 	pass
    442 	{
    443 		VertexShader = PostProcessVS;
    444 		PixelShader = PS_CRTLottes2018;
    445 	}
    446 }