Please show off what crt shaders can do!

Some experiments with “slot mask” and minimal diffusion bloom. :man_mechanic:

1 https://pasteboard.co/Ij8nHld.png

2 https://pasteboard.co/Ij8oePX.png

3 https://pasteboard.co/Ij8mKX2.png

4 https://pasteboard.co/Ij8oHz5.png

6 Likes

@torridgristle , @hunterk

That new curvature is looking great, not the biggest fan of EWA though (it makes makes me feel as I’ve rubbed Vaseline on my screen), lol. Is it possible to get version of this in Lottes, Hyllian, Apeture, or something (doesn’t necessarily need to be part of the repo)? I’d just really like to see in nearest neighbor.

Regardless, thanks for all your hard work guys!

I put it into ewa because I try not to change people’s code too much without their permission/support. The curvature block from ewa can drop right into lottes, though, as I purposely used the same function names, etc.

1 Like

That’s understandable, thank you for the reply.

I’ll try to port it over later, if I can’t get it to work I may message you with whatever I have done to try and trouble-shoot. If that’s alright with you?

EDIT: Figured it out, you’re definitely right about it just dropping in. The only change I had to do besides just dropping the code in, was move vec2 Distortion into the Warp code (this was for GLSL though, probably could’ve left it be for Slang).

1 Like

After all these years, CRT-Geom still looks fantastic. In fact, only CRT-Aperture is better in terms of ease of use and objective picture quality, IMO. I prefer a slightly sharper image than what CRT-Geom provides on the middle sharpness setting, but I don’t like how sharp it is on the sharpest setting, either. Still, CRT-Geom is a good alternative to CRT-Aperture if you prefer a slightly softer/lower-res look.

Disclaimer: brightness and gamma are different in person because I’m using black frame insertion and maxing out my backlight. A backlight/brightness setting of 50% on most LED-lit LCDs should give an approximate idea of what I’m seeing on my display.

CRT-Geom with my preferred settings:

CRT-Aperture with my preferred settings:

Settings, CRT-Geom:

alias0 = ""
cornersize = "0.001000"
cornersmooth = "1000.000000"
CRTgamma = "2.500000"
CURVATURE = "0.000000"
d = "1.600000"
DOTMASK = "0.300000"
filter_linear0 = "false"
float_framebuffer0 = "false"
interlace_detect = "1.000000"
lum = "0.650000"
mipmap_input0 = "false"
monitorgamma = "2.200000"
overscan_x = "100.000000"
overscan_y = "100.000000"
parameters = "CRTgamma;monitorgamma;d;CURVATURE;R;cornersize;cornersmooth;x_tilt;y_tilt;overscan_x;overscan_y;DOTMASK;SHARPER;scanline_weight;lum;interlace_detect"
R = "6.599996"
scanline_weight = "0.300000"
shader0 = "C:\Program Files\RetroArch\shaders\shaders_glsl\crt\shaders\crt-geom.glsl"
shaders = "1"
SHARPER = "2.000000"
srgb_framebuffer0 = "false"
wrap_mode0 = "clamp_to_border"
x_tilt = "0.000000"
y_tilt = "0.000000"

Settings, CRT-Aperture:

See my previous post.

4 Likes

Any programmers in this topic that could take a look at this issue?

Thought I would bring it up here since the main thing that bothers me about it is that CRT shaders end up looking pretty bad when you use them on rotated vertical games in FB Neo. This comparison shows it well: http://screenshotcomparison.com/comparison/701

With the mouse off the image it shows MAME using crt-guest-dr-venom with the shader’s TATE mode on. Mousing over the image shows FB Neo with TATE off. The first one looks good because the rotation is done using the MAME core’s software rotation. That doesn’t also rotate shaders, but if the shader has a rotation option, it works pretty well. FB Neo uses RetroArch’s rotation API. It rotates shaders, but for some reason it looks bad. Something with the scaling or alignment maybe?

Hello, hope someone can help me.

Crt-royale is said to be 4k friendly, however there is noticeable difference between 1080 and 4k resolutions and not in 4k favor. With 4k everything looks more blocky with less color bleed, overall shader intensity appears to be reduced. At 1080 image looks infinitely better and I can achieve similar results with 4k display if I force 1080 res in retroarch settings. So perhaps there are some settings in shader itself I need to change to make 4k comparable?

This pic from this thread on retrogameboards caught my atention.

Can you guys elaborate on why it looks so green or suggest a preset that comes closer to what colors on the CRT look like? I just booted it on retroarch to confirm it looks like the capture (it looks even greener on blastem core).

This is the “ntsc-320px-gauss-scanline.slangp” shader preset (in the ntsc directory). I does really look like that old garbage TV my cousin had many years ago to play N64.

(I’m using a 1440p display, so it might look wrong when viewing on 1080p screens.)

3 Likes

First post here, forgive me if my nomenclature isn’t entirely accurate.

I’ve noticed that CRT shader presets that integrate resolve.slang cut off some of the top portion of the screen as seen below. I noticed it a few days ago, and used the process of elimination to discover it was resolve.slang that was causing it. I simply loaded up crtglow_gauss_ntsc_3phase and disabled the 8 passes one by one to find the culprit.

Would anyone happen to know if this cutting off of a small portion of the top of the screen is by design? Is it intended, or have I done something wrong on my end? I played around with some or the parameters that are exposed, and none had an effect.

It’s not a gamebreaker, and I’ve played on enough CRTs back in the day to know that the curvature and bezel crop things off anyway, but I’m just wondering if this cropping of the top when using CRT presets that include resolve.slang is intended or not.

Also, I didn’t want to make a separate topic for this as I’ve been lurking this thread for a few days and have noticed one of the developers who is credited in resolve.slang is active in this thread, plus a few others whose names I recognize. So I figured this may be an appropriate topic to ask in.

1 Like

That has to do with the moire-mitigation code, which only matters when there’s curvature applied, so you can try replacing that pass with this modified version that won’t bother with it when there’s no curvature:

#version 450

// Original bits by Themaister
// Moire mitigation bits by Timothy Lottes, added by hunterk
 
layout(push_constant) uniform Push
{
   float BLOOM_STRENGTH;
   float OUTPUT_GAMMA;
   float CURVATURE;
   float moire_mitigation;
   float warpX;
   float warpY;
   float shadowMask;
   float maskDark;
   float maskLight;
} params;
 
#pragma parameter BLOOM_STRENGTH "Glow Strength" 0.45 0.0 0.8 0.05
#pragma parameter OUTPUT_GAMMA "Monitor Gamma" 2.2 1.8 2.6 0.02
#pragma parameter CURVATURE "Curvature" 0.0 0.0 1.0 1.0
#pragma parameter moire_mitigation "Moire:Noise Tradeoff" 4.0 1.0 10.0 1.0
#pragma parameter warpX "Curvature X-Axis" 0.031 0.0 0.125 0.01
#pragma parameter warpY "Curvature Y-Axis" 0.041 0.0 0.125 0.01
#pragma parameter shadowMask "Mask Effect" 0.0 0.0 4.0 1.0
#pragma parameter maskDark "maskDark" 0.5 0.0 2.0 0.1
#pragma parameter maskLight "maskLight" 1.5 0.0 2.0 0.1
 
#define iTime mod(float(global.FrameCount) / 60.0, 600.0)
#define fragCoord (vTexCoord.xy * global.OutputSize.xy)
 
layout(std140, set = 0, binding = 0) uniform UBO
{
    mat4 MVP;
    vec4 OutputSize;
    vec4 OriginalSize;
    vec4 SourceSize;
    vec4 CRTPassSize;
   uint FrameCount;
} global;
 
#pragma stage vertex
layout(location = 0) in  vec4  Position;
layout(location = 1) in  vec2  TexCoord;
layout(location = 0) out vec2  vTexCoord;
 
void main()
{
    gl_Position = global.MVP * Position;
    vTexCoord   = TexCoord;
}
 
#pragma stage fragment
layout(location = 0) in  vec2  vTexCoord;
layout(location = 0) out vec4  FragColor;
layout(set = 0, binding = 1) uniform sampler2D Source;
layout(set = 0, binding = 2) uniform sampler2D CRTPass;
 
// For debugging
#define BLOOM_ONLY 0
 
#define CRT_PASS CRTPass
 
// Convert from linear to sRGB.
//float Srgb(float c){return(c<0.0031308?c*12.92:1.055*pow(c,0.41666)-0.055);}
vec4 Srgb(vec4 c){return pow(c, vec4(1.0 / 2.2));}
 
// Convert from sRGB to linear.
//float Linear(float c){return(c<=0.04045)?c/12.92:pow((c+0.055)/1.055,2.4);}
float Linear(float c){return pow(c, 2.2);}
 
//
// Semi-Poor Quality Temporal Noise
//
 
// Base.
// Ripped ad modified from: https://www.shadertoy.com/view/4djSRW
float Noise(vec2 p,float x){p+=x;
 vec3 p3=fract(vec3(p.xyx)*10.1031);
 p3+=dot(p3,p3.yzx+19.19);
 return (fract((p3.x+p3.y)*p3.z)*2.0-1.0) / pow(2.0, 11.0 - params.moire_mitigation);}
 
// Step 1 in generation of the dither source texture.
float Noise1(vec2 uv,float n){
 float a=1.0,b=2.0,c=-12.0,t=1.0;  
 return (1.0/max(a*4.0+b*4.0,-c))*(
  Noise(uv+vec2(-1.0,-1.0)*t,n)*a+
  Noise(uv+vec2( 0.0,-1.0)*t,n)*b+
  Noise(uv+vec2( 1.0,-1.0)*t,n)*a+
  Noise(uv+vec2(-1.0, 0.0)*t,n)*b+
  Noise(uv+vec2( 0.0, 0.0)*t,n)*c+
  Noise(uv+vec2( 1.0, 0.0)*t,n)*b+
  Noise(uv+vec2(-1.0, 1.0)*t,n)*a+
  Noise(uv+vec2( 0.0, 1.0)*t,n)*b+
  Noise(uv+vec2( 1.0, 1.0)*t,n)*a+
 0.0);}
   
// Step 2 in generation of the dither source texture.
float Noise2(vec2 uv,float n){
 float a=1.0,b=2.0,c=-2.0,t=1.0;  
 return (1.0/(a*4.0+b*4.0))*(
  Noise1(uv+vec2(-1.0,-1.0)*t,n)*a+
  Noise1(uv+vec2( 0.0,-1.0)*t,n)*b+
  Noise1(uv+vec2( 1.0,-1.0)*t,n)*a+
  Noise1(uv+vec2(-1.0, 0.0)*t,n)*b+
  Noise1(uv+vec2( 0.0, 0.0)*t,n)*c+
  Noise1(uv+vec2( 1.0, 0.0)*t,n)*b+
  Noise1(uv+vec2(-1.0, 1.0)*t,n)*a+
  Noise1(uv+vec2( 0.0, 1.0)*t,n)*b+
  Noise1(uv+vec2( 1.0, 1.0)*t,n)*a+
 0.0);}
 
// Compute temporal dither from integer pixel position uv.
float Noise3(vec2 uv){return Noise2(uv,fract(iTime));}    
 
// Energy preserving dither, for {int pixel pos,color,amount}.
vec2 Noise4(vec2 uv,vec2 c,float a){
 // Grain value {-1 to 1}.
 vec2 g=vec2(Noise3(uv)*2.0);
 // Step size for black in non-linear space.
 float rcpStep=1.0/(256.0-1.0);
 // Estimate amount negative which still quantizes to zero.
 vec2 black=vec2(0.5*Linear(rcpStep));
 // Estimate amount above 1.0 which still quantizes to 1.0.
 vec2 white=vec2(2.0-Linear(1.0-rcpStep));
 // Add grain.
 return vec2(clamp(c+g*min(c+black,min(white-c,a)),0.0,1.0));}
 
//
// Pattern
//
 
// 4xMSAA pattern for quad given integer coordinates.
//
//  . x . . | < pixel
//  . . . x |
//  x . . .
//  . . x .
//
//  01
//  23
//
vec2 Quad4(vec2 pp){
 int q=(int(pp.x)&1)+((int(pp.y)&1)<<1);
 if(q==0)return pp+vec2( 0.25,-0.25);
 if(q==1)return pp+vec2( 0.25, 0.25);
 if(q==2)return pp+vec2(-0.25,-0.25);
         return pp+vec2(-0.25, 0.25);}
 
// Rotate {0.0,r} by a {-1.0 to 1.0}.
vec2 Rot(float r,float a){return vec2(r*cos(a*3.14159),r*sin(a*3.14159));}
 
//
// POOR QUALITY JITTERED
//
 
// Jittered position.
vec2 Jit(vec2 pp){
 // Start with better baseline pattern.
 pp=Quad4(pp);
 // Very poor quality (clumping) move in disc around pixel.
 float n=Noise(pp,fract(iTime));    
 float m=Noise(pp,fract(iTime*0.333))*0.5+0.5;
 m = sqrt(m) / 4.0;
 return pp+Rot(0.707*0.5*m,n);}
 
//
// POOR QUALITY JITTERED 4x
//
 
// Gaussian filtered jittered tap.
void JitGaus4(inout vec2 sumC,inout vec2 sumW,vec2 pp,vec2 mm){
 vec2 jj=Jit(pp);
 vec2 c=jj;
 vec2 vv=mm-jj;
 float w=exp2(-1.0*dot(vv,vv));    
 sumC+=c*vec2(w); sumW+=vec2(w);}  
 
// Many tap gaussian from poor quality jittered 4/sample per pixel
//
//  . x x x .
//  x x x x x
//  x x x x x
//  x x x x x
//  . x x x .
//
vec2 ResolveJitGaus4(vec2 pp){
 vec2 ppp=(pp);
 vec2 sumC=vec2(0.0);
 vec2 sumW=vec2(0.0);
 JitGaus4(sumC,sumW,ppp+vec2(-1.0,-2.0),pp);
 JitGaus4(sumC,sumW,ppp+vec2( 0.0,-2.0),pp);
 JitGaus4(sumC,sumW,ppp+vec2( 1.0,-2.0),pp);
 JitGaus4(sumC,sumW,ppp+vec2(-2.0,-1.0),pp);
 JitGaus4(sumC,sumW,ppp+vec2(-1.0,-1.0),pp);
 JitGaus4(sumC,sumW,ppp+vec2( 0.0,-1.0),pp);
 JitGaus4(sumC,sumW,ppp+vec2( 1.0,-1.0),pp);
 JitGaus4(sumC,sumW,ppp+vec2( 2.0,-1.0),pp);
 JitGaus4(sumC,sumW,ppp+vec2(-2.0, 0.0),pp);
 JitGaus4(sumC,sumW,ppp+vec2(-1.0, 0.0),pp);
 JitGaus4(sumC,sumW,ppp+vec2( 0.0, 0.0),pp);
 JitGaus4(sumC,sumW,ppp+vec2( 1.0, 0.0),pp);
 JitGaus4(sumC,sumW,ppp+vec2( 2.0, 0.0),pp);
 JitGaus4(sumC,sumW,ppp+vec2(-2.0, 1.0),pp);
 JitGaus4(sumC,sumW,ppp+vec2(-1.0, 1.0),pp);
 JitGaus4(sumC,sumW,ppp+vec2( 0.0, 1.0),pp);
 JitGaus4(sumC,sumW,ppp+vec2( 1.0, 1.0),pp);
 JitGaus4(sumC,sumW,ppp+vec2( 2.0, 1.0),pp);
 JitGaus4(sumC,sumW,ppp+vec2(-1.0, 2.0),pp);
 JitGaus4(sumC,sumW,ppp+vec2( 0.0, 2.0),pp);
 JitGaus4(sumC,sumW,ppp+vec2( 1.0, 2.0),pp);
 return sumC/sumW;}
 
vec2 moire_resolve(vec2 coord){
   vec2 pp = coord;
   vec2 cc = vec2(0.0, 0.0);
   
   cc = ResolveJitGaus4(pp);
   cc = Noise4(pp, cc, 1.0 / 32.0);
   cc = (params.CURVATURE < 0.5) ? pp : cc + vec2(0.0105, 0.015);
   
   return cc;
}
 
// Distortion of scanlines, and end of screen alpha.
vec2 Warp(vec2 pos)
{
    pos  = pos*2.0-1.0;    
    pos *= vec2(1.0 + (pos.y*pos.y)*params.warpX, 1.0 + (pos.x*pos.x)*params.warpY);
   
    return pos*0.5 + 0.5;
}
 
// Shadow mask.
vec3 Mask(vec2 pos)
{
    vec3 mask = vec3(params.maskDark, params.maskDark, params.maskDark);
 
    // Very compressed TV style shadow mask.
    if (params.shadowMask == 1.0)
    {
        float line = params.maskLight;
        float odd = 0.0;
       
        if (fract(pos.x*0.166666666) < 0.5) odd = 1.0;
        if (fract((pos.y + odd) * 0.5) < 0.5) line = params.maskDark;  
       
        pos.x = fract(pos.x*0.333333333);
 
        if      (pos.x < 0.333) mask.r = params.maskLight;
        else if (pos.x < 0.666) mask.g = params.maskLight;
        else                    mask.b = params.maskLight;
        mask*=line;  
    }
 
    // Aperture-grille.
    else if (params.shadowMask == 2.0)
    {
        pos.x = fract(pos.x*0.333333333);
 
        if      (pos.x < 0.333) mask.r = params.maskLight;
        else if (pos.x < 0.666) mask.g = params.maskLight;
        else                    mask.b = params.maskLight;
    }
 
    // Stretched VGA style shadow mask (same as prior shaders).
    else if (params.shadowMask == 3.0)
    {
        pos.x += pos.y*3.0;
        pos.x  = fract(pos.x*0.166666666);
 
        if      (pos.x < 0.333) mask.r = params.maskLight;
        else if (pos.x < 0.666) mask.g = params.maskLight;
        else                    mask.b = params.maskLight;
    }
 
    // VGA style shadow mask.
    else if (params.shadowMask == 4.0)
    {
        pos.xy  = floor(pos.xy*vec2(1.0, 0.5));
        pos.x  += pos.y*3.0;
        pos.x   = fract(pos.x*0.166666666);
 
        if      (pos.x < 0.333) mask.r = params.maskLight;
        else if (pos.x < 0.666) mask.g = params.maskLight;
        else                    mask.b = params.maskLight;
    }
 
    return mask;
}
 
void main()
{
   vec2 pp = moire_resolve(vTexCoord.xy);
   pp = (params.CURVATURE > 0.5) ? Warp(pp) : pp;
   
#if BLOOM_ONLY
    vec3 source = BLOOM_STRENGTH * texture(Source, pp).rgb;
#else
 
    vec3 source = 1.15 * texture(CRT_PASS, pp).rgb;
    vec3 bloom  = texture(Source, pp).rgb;
    source     += params.BLOOM_STRENGTH * bloom;
#endif
    FragColor = vec4(pow(clamp(source, 0.0, 1.0), vec3(1.0 / params.OUTPUT_GAMMA)), 1.0);
    if (params.shadowMask > 0.0)
        FragColor.rgb = pow(pow(FragColor.rgb, vec3(2.2)) * Mask(vTexCoord.xy * global.OutputSize.xy * 1.000001), vec3(1.0 / 2.2));
    if (params.CURVATURE < 0.5) return;
     /* TODO/FIXME - hacky clamp fix */
    if ( pp.x > 0.0106 && pp.x < 0.9999 && pp.y > 0.016 && pp.y < 0.9999)
        FragColor.rgb = FragColor.rgb;
    else
        FragColor.rgb = vec3(0.0);
}
1 Like

That shader preset may be my favorite one in the entire RetroArch shader package. It may not be intended to provide the sharpest picture, but it looks exactly like the cheap TV I had in my bedroom as a child.

I kid you not, when I first stumbled across that NTSC gaussian scanline shader, I said “Wow” out loud. Not because of image clarity or anything, but rather because of its perfect representation of my childhood video game experience. Beauty is in the eye of the beholder, and recreating the blurry garbage of 1995 on a modern LCD is completely stunning to me.

1 Like

Works perfectly.

Thank you so much hunterk, your helpful forum presence and your contributions to RetroArch’s shaders are immensely appreciated.

1 Like

Do you happen to know what the “320px” means? Obviously it says something about 320 pixels, but what does that mean? I see shader presets with 240 vs 320 variants.

1 Like

I’ve always wondered this as well. How the 320 v 240 affects this shader pack.

NTSC emulation is closely tied to the timing of the signal, which equates to horizontal resolution in digital terms. While there are many resolutions that consoles can output, most either do 256 or 320 px wide, or some permutation thereof (640 or 512, etc.). The presets correspond to that.

1 Like

So when using this shader-set, is there going to be a noticeable difference if you use the width that doesn’t correspond to the games native horizontal res? All my widths are being stretched to 1920 for super-res, for example. Or would the visual distinctions between the two versions only be significant if you were doing true native resolutions on 15Khz?

I’m not an expert, but here’s my experience. Water in Super Mario World has diagonal lines in 240, which is how I remember it as a kid, And the Sonic waterfall effect only happens for 320. From my understanding that’s because these two systems put out video in these respective parameters, with the exception of a few games. Apparently most systems are 240, but some people seem to prefer the 320 shader on just about everything.

Are there any shaders which emulate color banding/dithering? I’ve tried the dither shaders but it’s not quite what I’m looking for. I’m trying to recreate the blockiness/dithering that happens in the VI filter in Parallel N64.

Here’s a slang shader for dithering, based on a nice shadertoy:

#version 450

// based on Jodie's "analytical bayer matrix dither" shadertoy
// https://www.shadertoy.com/view/4ssfWM

layout(push_constant) uniform Push
{
	vec4 SourceSize;
	vec4 OriginalSize;
	vec4 OutputSize;
	uint FrameCount;
   float color_depth, dither_factor;
} params;

#pragma parameter color_depth "Color Depth (2/4/8/16/32/64/128)" 4.0 1.0 7.0 1.0
#pragma parameter dither_factor "Dither Strength" 0.05 0.0 1.0 0.01

float bayer2(vec2 a){
    a = floor(a);
    return fract(dot(a,vec2(.5, a.y*.75)));
}
float bayer4(vec2 a)   {return bayer2( .5*a)   * .25     + bayer2(a); }
float bayer8(vec2 a)   {return bayer4( .5*a)   * .25     + bayer2(a); }
float bayer16(vec2 a)  {return bayer4( .25*a)  * .0625   + bayer4(a); }
float bayer32(vec2 a)  {return bayer8( .25*a)  * .0625   + bayer4(a); }
float bayer64(vec2 a)  {return bayer8( .125*a) * .015625 + bayer8(a); }
float bayer128(vec2 a) {return bayer16(.125*a) * .015625 + bayer8(a); }

#define dither2(p)   (bayer2(  p)-.375      )
#define dither4(p)   (bayer4(  p)-.46875    )
#define dither8(p)   (bayer8(  p)-.4921875  )
#define dither16(p)  (bayer16( p)-.498046875)
#define dither32(p)  (bayer32( p)-.499511719)
#define dither64(p)  (bayer64( p)-.49987793 )
#define dither128(p) (bayer128(p)-.499969482)

layout(std140, set = 0, binding = 0) uniform UBO
{
	mat4 MVP;
} global;

#pragma stage vertex
layout(location = 0) in vec4 Position;
layout(location = 1) in vec2 TexCoord;
layout(location = 0) out vec2 vTexCoord;
layout(location = 1) out vec2 coord;

void main()
{
   gl_Position = global.MVP * Position;
   vTexCoord = TexCoord;
   coord = vTexCoord * params.SourceSize.xy;
}

#pragma stage fragment
layout(location = 0) in vec2 vTexCoord;
layout(location = 1) in vec2 coord;
layout(location = 0) out vec4 FragColor;
layout(set = 0, binding = 2) uniform sampler2D Source;

void main()
{
   int depth = int(params.color_depth);
   vec4 dithered;
   vec4 res = texture(Source, vTexCoord);
   if(depth == 1) dithered.rgb = floor((res.rgb + dither2(coord)) + 0.5);
   else if(depth == 2) dithered.rgb = floor((res.rgb + dither4(coord)) + 0.5);
   else if(depth == 3) dithered.rgb = floor((res.rgb + dither8(coord)) + 0.5);
   else if(depth == 4) dithered.rgb = floor((res.rgb + dither16(coord)) + 0.5);
   else if(depth == 5) dithered.rgb = floor((res.rgb + dither32(coord)) + 0.5);
   else if(depth == 6) dithered.rgb = floor((res.rgb + dither64(coord)) + 0.5);
   else dithered.rgb = floor((res.rgb + dither128(coord)) + 0.5);
   FragColor = mix(res, dithered, params.dither_factor);
}

Dunno how accurate it is to N64/PS1 dithering, but it seems fairly close to me. It looks good with NTSC effects on top, IMO.

2 Likes