Please show off what crt shaders can do!

shrug, dunno what to tell you. If you run just that shader on a white screen, you’ll see that it’s giving full ff00ff magenta and 00fb00 green (not sure why that’s not 00ff00; seems to be ~1.5% difference from theoretical max).

2 Likes

Yep, it’s definitely working as intended! It’s just doing something weird when I try stacking it with CRT-Aperture. Just haven’t found the right settings yet.

1 Like

Sounds like it’s something crt-apeture is doing with the color then, imo.

The shader hunterk wrote is working correctly, but CRT-Aperture interacts with it in a weird way. I’ve tried playing with a lot of different settings and haven’t been able to pinpoint the problem, so I suspect it’s some conflict in the code somewhere.

I’m curious to see your shader with a mask included,very close to this picture. With an afterglow effect like crt-guestvenom it’s maybe possible to make something cool.

4 Likes

I made something you might like :slight_smile:

I used the same 2D array idea to fake beam dynamics (similar to torridgristle’s GritsScanlines, but without a LUT). I think it’s pretty close to what you’ve been looking for, with full-strength masks and scanlines:

#version 450

layout(push_constant) uniform Push
{
	vec4 SourceSize;
	vec4 OriginalSize;
	vec4 OutputSize;
	uint FrameCount;
   float phosphor_selector, mask_strength, scanmix;
} params;

#pragma parameter phosphor_selector "Phosphor Layout" 1.0 1.0 3.0 1.0
#pragma parameter mask_strength "Mask Strength" 0.3 0.0 1.0 0.01
#pragma parameter scanmix "Scanline Strength" 0.3 0.0 1.0 0.01

int phosphor_layout = int(params.phosphor_selector);
#define mask_strength params.mask_strength

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;

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 = 2) uniform sampler2D Source;

vec3 beam(vec3 px, vec2 coord){
   vec3 pixel = pow(px, vec3(2.2));
   vec3 black = vec3(0.,0.,0.);
   vec3 beam_profile[9][4] = {
      {black,       black,       black,        pixel * 0.12},
      {black,       pixel * 0.12,pixel * 0.25, pixel * 0.5},
      {pixel * 0.12,pixel * 0.5, pixel * 0.5,  pixel      },
      {pixel * 0.5, pixel,       pixel,        pixel      },
      {pixel,       pixel,       pixel,        pixel      },
      {pixel * 0.5, pixel,       pixel,        pixel      },
      {pixel * 0.12,pixel * 0.5, pixel * 0.5,  pixel      },
      {black,       pixel * 0.12,pixel * 0.25, pixel * 0.5},
      {black,       black,       black,        pixel * 0.12}
   };
   
   int j = int(floor(mod(coord.y * 9.0, 8.9999)));
   int k = int(floor(max(max(px.r, px.g), px.b) * 3.9999));
   
   return pow(beam_profile[j][k], vec3(1./2.2));
}

vec3 mask_weights(vec2 coord, float mask_intensity){
   vec3 weights = vec3(0.,0.,0.);
   vec3 green   = vec3(1.-mask_intensity, 1.0, 1.-mask_intensity);
   vec3 magenta = vec3(1.0,1.-mask_intensity,1.0);
   vec3 black   = vec3(1.-mask_intensity,1.-mask_intensity,1.-mask_intensity);
   
   vec3 aperture_weights = mix(magenta, green, floor(mod(coord.x, 2.0)));

   if(phosphor_layout == 1){
      // classic aperture
      weights  = aperture_weights;
   }

   if(phosphor_layout == 2){
      // 2x2 shadow mask
      vec3 inverse_aperture = mix(green, magenta, floor(mod(coord.x, 2.0)));
      weights               = mix(aperture_weights, inverse_aperture, floor(mod(coord.y, 2.0)));
   }

   if(phosphor_layout == 3){
      // slot mask
      vec3 slotmask[4][6] = {
         {magenta, green, black, magenta, green, black},
         {magenta, green, black, black,   black, black},
         {magenta, green, black, magenta, green, black},
         {black,   black, black, magenta, green, black}
      };

      // find the vertical index
      int j = int(floor(mod(coord.y, 4.0)));

      // find the horizontal index
      int k = int(floor(mod(coord.x, 6.0)));

      // use the indexes to find which color to apply to the current pixel
      weights = slotmask[j][k];
   }

   return weights;
}

vec2 quilez(vec2 coord){
	vec2 p = coord.xy;

	p = p * params.SourceSize.xy + vec2(0.5, 0.5);

	vec2 i = floor(p);
	vec2 f = p - i;
	f = f * f * f * (f * (f * 6.0 - vec2(15.0, 15.0)) + vec2(10.0, 10.0));
	p = i + f;

   p = (p - vec2(0.5, 0.5)) * params.SourceSize.zw;
   
   return p;
}


void main()
{
   vec3 mask_weights = mask_weights(vTexCoord.xy * params.OutputSize.xy, mask_strength);
	vec3 screen = texture(Source, quilez(vTexCoord) * 1.0001).rgb;
   vec3 scanlines = beam(screen.rgb, vTexCoord.xy * params.SourceSize.xy);
   FragColor = vec4(mix(screen, scanlines, params.scanmix) * mask_weights, 1.0);
}

Looks like this (fake 4K; check out the coat to see the dynamics at work):

the slotmask needs at least 10x multiplier to look decent, but the others look fine at 4K.

4 Likes

Did you ever get around to posting this new version??

Awesome, I’m excited to try this! I can’t get the shader to load though. Do I need to add an “endif” somewhere?

Shouldn’t have to, no, but it is in slang format (should have mentioned that), and it’s currently not compatible with GLSL at all since it uses newer features than are available there. I just re-pasted the code, though, in case something was wrong with the first paste.

2 Likes

okay, looks like I need to play around with my drivers. Any suggestions for getting the best performance with slang shaders?

edit: can you use hard gpu sync with whatever drivers slang is compatible with?

Yeah, if you switch to glcore, it should be exactly the same as gl, just with slang shaders.

EDIT: before you go too far, though, here it is in regular GLSL format (I already had an idea of what I needed to do to make it work there):

#version 130
#pragma parameter phosphor_layout "Phosphor Layout" 1.0 1.0 3.0 1.0
#pragma parameter mask_strength "Mask Strength" 1.0 0.0 1.0 0.01
#pragma parameter scanmix "Scanline Strength" 1.0 0.0 1.0 0.01

#if defined(VERTEX)

#if __VERSION__ >= 130
#define COMPAT_VARYING out
#define COMPAT_ATTRIBUTE in
#define COMPAT_TEXTURE texture
#else
#define COMPAT_VARYING varying 
#define COMPAT_ATTRIBUTE attribute 
#define COMPAT_TEXTURE texture2D
#endif

#ifdef GL_ES
#define COMPAT_PRECISION mediump
#else
#define COMPAT_PRECISION
#endif

COMPAT_ATTRIBUTE vec4 VertexCoord;
COMPAT_ATTRIBUTE vec4 TexCoord;
COMPAT_VARYING vec4 TEX0;

uniform mat4 MVPMatrix;
uniform COMPAT_PRECISION int FrameDirection;
uniform COMPAT_PRECISION int FrameCount;
uniform COMPAT_PRECISION vec2 OutputSize;
uniform COMPAT_PRECISION vec2 TextureSize;
uniform COMPAT_PRECISION vec2 InputSize;

// vertex compatibility #defines
#define vTexCoord TEX0.xy
#define SourceSize vec4(TextureSize, 1.0 / TextureSize) //either TextureSize or InputSize
#define outsize vec4(OutputSize, 1.0 / OutputSize)

void main()
{
    gl_Position = MVPMatrix * VertexCoord;
    TEX0.xy = TexCoord.xy;
}

#elif defined(FRAGMENT)

#ifdef GL_ES
#ifdef GL_FRAGMENT_PRECISION_HIGH
precision highp float;
#else
precision mediump float;
#endif
#define COMPAT_PRECISION mediump
#else
#define COMPAT_PRECISION
#endif

#if __VERSION__ >= 130
#define COMPAT_VARYING in
#define COMPAT_TEXTURE texture
out COMPAT_PRECISION vec4 FragColor;
#else
#define COMPAT_VARYING varying
#define FragColor gl_FragColor
#define COMPAT_TEXTURE texture2D
#endif

uniform COMPAT_PRECISION int FrameDirection;
uniform COMPAT_PRECISION int FrameCount;
uniform COMPAT_PRECISION vec2 OutputSize;
uniform COMPAT_PRECISION vec2 TextureSize;
uniform COMPAT_PRECISION vec2 InputSize;
uniform sampler2D Texture;
COMPAT_VARYING vec4 TEX0;

// fragment compatibility #defines
#define Source Texture
#define vTexCoord TEX0.xy

#define SourceSize vec4(TextureSize, 1.0 / TextureSize) //either TextureSize or InputSize
#define outsize vec4(OutputSize, 1.0 / OutputSize)

#ifdef PARAMETER_UNIFORM
uniform COMPAT_PRECISION float phosphor_layout, mask_strength, scanmix;
#else
#define phosphor_layout 1.0
#define mask_strength 0.3
#define scanmix 0.3
#endif

vec3 beam(vec3 px, vec2 coord){
   vec3 pixel = pow(px, vec3(2.2));
   vec3 black = vec3(0.,0.,0.);
  
   vec3 beam_profile_1[9] = vec3[](black, black, pixel * 0.12, pixel * 0.5, pixel, pixel * 0.5, pixel * 0.12, black, black);
   vec3 beam_profile_2[9] = vec3[](black, pixel * 0.12, pixel * 0.5, pixel, pixel, pixel, pixel * 0.5, pixel * 0.12, black);
   vec3 beam_profile_3[9] = vec3[](black, pixel * 0.25, pixel * 0.5, pixel, pixel, pixel, pixel * 0.5, pixel * 0.25, black);
   vec3 beam_profile_4[9] = vec3[](pixel * 0.12, pixel * 0.5, pixel, pixel, pixel, pixel, pixel, pixel * 0.5, pixel * 0.12);
   
   int j = int(floor(mod(coord.y * 9.0, 8.9999)));
   int k = int(floor(max(max(px.r, px.g), px.b) * 3.9999));
   
   vec3 array_bool = (k == 3) ? beam_profile_4[j] : (k == 2) ? beam_profile_3[j] : (k == 1) ? beam_profile_2[j] : beam_profile_1[j];
   
   return pow(array_bool, vec3(1./2.2));
}

vec3 mask_weights(vec2 coord, float mask_strength){
   vec3 weights = vec3(0.,0.,0.);
   vec3 green = vec3(1.-mask_strength, 1.0, 1.-mask_strength);
   vec3 magenta = vec3(1.0,1.-mask_strength,1.0);
   vec3 black = vec3(1.-mask_strength,1.-mask_strength,1.-mask_strength);

   // classic aperture
   vec3 aperture_weights = mix(magenta, green, floor(mod(coord.x, 2.0)));

   // 2x2 shadow mask
   vec3 inverse_aperture = mix(green, magenta, floor(mod(coord.x, 2.0)));
   vec3 shadow_weights   = mix(aperture_weights, inverse_aperture, floor(mod(coord.y, 2.0)));

   // slot mask
   // Can't do 2D arrays until version 430, so do this stupid thing instead
   // first lay out the horizontal pixels in arrays
   vec3 slotmask_x1[6] = vec3[](magenta,green,black,magenta,green,black);
   vec3 slotmask_x2[6] = vec3[](magenta,green,black,black,black,black);
   vec3 slotmask_x3[6] = vec3[](magenta,green,black,magenta,green,black);
   vec3 slotmask_x4[6] = vec3[](black,black,black,magenta,green,black);

   // find the horizontal index
   int slot_index_x = int(floor(mod(coord.x, 6.0)));
   int j = slot_index_x; // use a single letter for variable to make comparison easier to read later

   // find the vertical index
   int slot_index_y = int(floor(mod(coord.y, 4.0)));

   // do a big, dumb comparison in place of a 2D array
   vec3 slot_weights = (slot_index_y == 1) ? slotmask_x1[j] : (slot_index_y == 2) ? slotmask_x2[j] : (slot_index_y == 3) ? slotmask_x3[j] : slotmask_x4[j];
   
   if(phosphor_layout == 1.) weights = aperture_weights;
   else if(phosphor_layout == 2.) weights = shadow_weights;
   else weights = slot_weights;
   
   return weights;
}

vec2 quilez(vec2 coord){
	vec2 p = coord.xy;

	p = p * SourceSize.xy + vec2(0.5, 0.5);

	vec2 i = floor(p);
	vec2 f = p - i;
	f = f * f * f * (f * (f * 6.0 - vec2(15.0, 15.0)) + vec2(10.0, 10.0));
	p = i + f;

   p = (p - vec2(0.5, 0.5)) * SourceSize.zw;
   
   return p;
}

void main()
{
   vec3 mask_weights = mask_weights(gl_FragCoord.xy, mask_strength);
	vec3 screen = COMPAT_TEXTURE(Source, quilez(vTexCoord) * 1.0001).rgb;
   vec3 scanlines = beam(screen.rgb, vTexCoord.xy * SourceSize.xy);
   FragColor = vec4(mix(screen, scanlines, scanmix) * mask_weights, 1.0);
} 
#endif
5 Likes

lol :joy: You really are the best. This is fantastic!

1 Like

@hunterk

This is a great proof of concept! I think the aperture grille and dotmask look good even at 4x scale, and I think you can get away with 8x for the slotmask if you’re going for a low TVL look.

A few additions would make this the best shader ever (I know, I’m a greedy bastard lol):

  1. whatever automatic color/brightness/gamma correction CRT-aperture is doing. Looks like mask strength is tied up in a single function with a lot of the other variables…? How is CRT Aperture able to preserve brightness and color so well when using mask/scanlines…?
  2. adjustable sharpness, needs just the slightest blur. Again, I think CRT-aperture is doing a good job here. The lowest setting is still sharper than bilinear filter.
  3. gamma options
  4. adjustable beam width (looks like it’s possible to edit the shader pretty easily for this, though)
  5. additional mask types, as per cgwg’s recommendations. Both of these, like the magenta/green patterns, result in even spacing of the LCD subpixels, but they both need 4K to look good.

-aperture grille #2 Red, Yellow, Cyan, Green

-aperture grille #3 Red, Green, Blue, Black

  1. “switch mask colors” option for those with RBG subpixels. This would swap magenta/green in the magenta/green patterns with yellow/blue
  2. adjustment for mask size, so you can raise/lower the dot pitch/TVL as desired.
1 Like

Guys i’m so proud of this preset :grinning: colors are so vivid with good white intensity and a mask, it’s in slang but can work in glsl by changing all extensions in preset (slang -> glsl)

shaders = "2"
shader0 = "shaders_slang/crt/shaders/crt-aperture.slang"
wrap_mode0 = "clamp_to_border"
mipmap_input0 = "false"
alias0 = ""
float_framebuffer0 = "false"
srgb_framebuffer0 = "false"
scale_type_x0 = "source"
scale_x0 = "9.000000"
scale_type_y0 = "source"
scale_y0 = "9.000000"
shader1 = "shaders_slang/reshade/shaders/blendoverlay/blendoverlay.slang"
filter_linear1 = "true"
wrap_mode1 = "clamp_to_border"
mipmap_input1 = "false"
alias1 = ""
float_framebuffer1 = "false"
srgb_framebuffer1 = "false"
parameters = "SHARPNESS_IMAGE;SHARPNESS_EDGES;GLOW_WIDTH;GLOW_HEIGHT;GLOW_HALATION;GLOW_DIFFUSION;MASK_COLORS;MASK_STRENGTH;MASK_SIZE;SCANLINE_SIZE_MIN;SCANLINE_SIZE_MAX;SCANLINE_SHAPE;SCANLINE_OFFSET;GAMMA_INPUT;GAMMA_OUTPUT;BRIGHTNESS;OverlayMix;LUTWidth;LUTHeight"
SHARPNESS_IMAGE = "1.000000"
SHARPNESS_EDGES = "3.000000"
GLOW_WIDTH = "0.650000"
GLOW_HEIGHT = "0.650000"
GLOW_HALATION = "0.350000"
GLOW_DIFFUSION = "0.000000"
MASK_COLORS = "2.000000"
MASK_STRENGTH = "0.000000"
MASK_SIZE = "1.000000"
SCANLINE_SIZE_MIN = "0.500000"
SCANLINE_SIZE_MAX = "1.500000"
SCANLINE_SHAPE = "1.000000"
SCANLINE_OFFSET = "0.000000"
GAMMA_INPUT = "2.500000"
GAMMA_OUTPUT = "2.200000"
BRIGHTNESS = "2.000000"
OverlayMix = "1.000000"
LUTWidth = "4.000000"
LUTHeight = "1.000000"
textures = "overlay"
overlay = "shaders_slang/reshade/shaders/blendoverlay/delta_1_4x1_rgb.png"
overlay_wrap_mode = "clamp_to_border"
overlay_mipmap = "false"

Edit: High quality screenshots

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

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

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

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

5.https://pasteboard.co/IKPi0AC.png

6.https://pasteboard.co/IKPiaSt.png

7.https://pasteboard.co/IKPipfq.png

8.https://pasteboard.co/IKPiAQB.png

9.https://pasteboard.co/IKPiNRa.png

10.https://pasteboard.co/IKPj4ri.png

11.https://pasteboard.co/IKPjiGH.png

6 Likes

looks awesome, but the scanlines appear to be inconsistent; are you using integer scale? There’s only so much you can do if you’re not using integer scale. Looks great aside from that, very BVM / Royale-like.

1 Like

Thanks , yes i’m using integer scaling but everything is compressed ,mask missing ,uneven scanlines bad scaling haha ,i’m gonna reupload everything on pasteboard with no loss. It’s like day and night haha

Edit: screenshots added

2 Likes

That’s what the issue with mask was, lol. I was looking at the shader chain and saw the blendoverlay but didn’t see the mask and was confused :joy:.

1 Like

Yeah that’s why i always test shader when someone post a preset,it never looks like the screenshot :sweat_smile:

Edit: even worst when seen on lower resolution to the native screenshot,libretro forums compress when file size is huge.

1 Like

Yeahhhh… I’m going to just start posting my images on an image hosting site instead of using the forums method of uploading images (seems to destroy the quality :joy:).

1 Like

I’m curious can you try my preset ,then post a screenshot on libretro,then on pasteboard. i want to see how it looks on my screen with lower resolution,i assume you have 1080p screen. :man_mechanic:

1 Like