CRT Shader with realistic blurring
port of this shadertoy shader for the Godot Engine
About: Easy to use & read CRT Shader with an gorgeous retro feel.
Shader code
//SHADER ORIGINALY CREADED BY "TimothyLottes" FROM SHADERTOY
//PORTED AND MODIFYED TO GODOT BY AHOPNESS (@ahopness)
//LICENSE : CC0
//COMATIBLE WITH : GLES2, GLES3, WEBGL
//SHADERTOY LINK : https://www.shadertoy.com/view/MsjXzh
// PUBLIC DOMAIN CRT STYLED SCAN-LINE SHADER
//
// by Timothy Lottes
//
// This is more along the style of a really good CGA arcade monitor.
// With RGB inputs instead of NTSC.
// The shadow mask example has the mask rotated 90 degrees for less chromatic aberration.
//
// Left it unoptimized to show the theory behind the algorithm.
//
// It is an example what I personally would want as a display option for pixel art games.
// Please take and use, change, or whatever.
shader_type canvas_item;
// Emulated input resolution.
uniform vec2 res;
// Mask type
// 0 = Very compressed TV style shadow mask
// 1 = Stretched VGA style shadow mask (same as prior shaders)
// 2 = VGA style shadow mask
uniform int mask_type :hint_range(0, 2) = 0;
// Bloom Type
// 0 = Normalized exposure
// 1 = Aditive bloom
// 2 = No Bloom
uniform int bloom_type :hint_range(0, 2) = 0;
// Hardness of scanline.
// -8.0 = soft
// -16.0 = medium
uniform float hardScan :hint_range(-12.0, -1.0) = -8.0;
// Hardness of pixels in scanline.
// -2.0 = soft
// -4.0 = hard
uniform float hardPix :hint_range(-4.0, 0.0) = -2.0;
// Hardness of short vertical bloom.
// -1.0 = wide to the point of clipping (bad)
// -1.5 = wide
// -4.0 = not very wide at all
uniform float hardBloomScan :hint_range(-4.0, 0.0) = -2.0;
// Hardness of short horizontal bloom.
// -0.5 = wide to the point of clipping (bad)
// -1.0 = wide
// -2.0 = not very wide at all
uniform float hardBloomPix :hint_range(-2.0, 0.0) = -1.5;
// Amount of small bloom effect.
// 1.0/1.0 = only bloom
// 1.0/16.0 = what I think is a good amount of small bloom
// 0.0 = no bloom
uniform float bloomAmount :hint_range(1.0, 16.0) = 16.0;
// Display warp.
// 0.0 = none
// 1.0/8.0 = extreme
uniform vec2 warp = vec2(64.0, 24.0);
// Amount of shadow mask.
uniform float maskDark :hint_range(0.0, 1.0) = 0.5;
uniform float maskLight :hint_range(1.0, 2.0) = 1.5;
//------------------------------------------------------------------------
// sRGB to Linear.
// Assuing using sRGB typed textures this should not be needed.
float ToLinear1(float c){ return(c <= 0.04045) ? c / 12.92 : pow((c + 0.055) / 1.055, 2.4); }
vec3 ToLinear(vec3 c){ return vec3(ToLinear1(c.r), ToLinear1(c.g), ToLinear1(c.b)); }
// Linear to sRGB.
// Assuing using sRGB typed textures this should not be needed.
float ToSrgb1(float c){ return(c < 0.0031308?c * 12.92 : 1.055 * pow(c, 0.41666) - 0.055); }
vec3 ToSrgb(vec3 c){ return vec3(ToSrgb1(c.r), ToSrgb1(c.g), ToSrgb1(c.b)); }
// Nearest emulated sample given floating point position and texel offset.
// Also zero's off screen.
vec3 Fetch(vec2 pos, vec2 off, sampler2D iChannel0){
pos = floor(pos * res + off) / res;
if(max(abs(pos.x - 0.5), abs(pos.y - 0.5)) > 0.5){
return vec3(0.0, 0.0, 0.0);
}
return ToLinear(texture(iChannel0 , pos.xy , -16.0).rgb);
}
// Distance in emulated pixels to nearest texel.
vec2 Dist(vec2 pos){
pos = pos * res;
return - ((pos - floor(pos)) - vec2(0.5));
}
// 1D Gaussian.
float Gaus(float pos, float scale){ return exp2(scale * pos * pos); }
// 3-tap Gaussian filter along horz line.
vec3 Horz3(vec2 pos, float off, sampler2D iChannel0){
vec3 b = Fetch(pos, vec2(-1.0, off), iChannel0);
vec3 c = Fetch(pos, vec2( 0.0, off), iChannel0);
vec3 d = Fetch(pos, vec2( 1.0, off), iChannel0);
float dst = Dist(pos).x;
// Convert distance to weight.
float scale = hardPix;
float wb = Gaus(dst - 1.0, scale);
float wc = Gaus(dst + 0.0, scale);
float wd = Gaus(dst + 1.0, scale);
// Return filtered sample.
return (b * wb + c * wc + d * wd) / (wb + wc + wd);
}
// 5-tap Gaussian filter along horz line.
vec3 Horz5(vec2 pos, float off, sampler2D iChannel0){
vec3 a = Fetch(pos, vec2(-2.0, off), iChannel0);
vec3 b = Fetch(pos, vec2(-1.0, off), iChannel0);
vec3 c = Fetch(pos, vec2( 0.0, off), iChannel0);
vec3 d = Fetch(pos, vec2( 1.0, off), iChannel0);
vec3 e = Fetch(pos, vec2( 2.0, off), iChannel0);
float dst = Dist(pos).x;
// Convert distance to weight.
float scale = hardPix;
float wa = Gaus(dst - 2.0, scale);
float wb = Gaus(dst - 1.0, scale);
float wc = Gaus(dst + 0.0, scale);
float wd = Gaus(dst + 1.0, scale);
float we = Gaus(dst + 2.0, scale);
// Return filtered sample.
return (a * wa + b * wb + c * wc + d * wd + e * we) / (wa + wb + wc + wd + we);
}
// 7-tap Gaussian filter along horz line.
vec3 Horz7(vec2 pos, float off, sampler2D iChannel0){
vec3 a = Fetch(pos, vec2(-3.0, off), iChannel0);
vec3 b = Fetch(pos, vec2(-2.0, off), iChannel0);
vec3 c = Fetch(pos, vec2( 1.0, off), iChannel0);
vec3 d = Fetch(pos, vec2( 0.0, off), iChannel0);
vec3 e = Fetch(pos, vec2( 1.0, off), iChannel0);
vec3 f = Fetch(pos, vec2( 2.0, off), iChannel0);
vec3 g = Fetch(pos, vec2( 3.0, off), iChannel0);
float dst = Dist(pos).x;
// Convert distance to weight.
float scale = hardBloomPix;
float wa = Gaus(dst - 3.0, scale);
float wb = Gaus(dst - 2.0, scale);
float wc = Gaus(dst - 1.0, scale);
float wd = Gaus(dst + 0.0, scale);
float we = Gaus(dst + 1.0, scale);
float wf = Gaus(dst + 2.0, scale);
float wg = Gaus(dst + 3.0, scale);
// Return filtered sample.
return (a * wa + b * wb + c * wc + d * wd + e * we + f * wf + g * wg) / (wa + wb + wc + wd + we + wf + wg);
}
// Return scanline weight.
float Scan(vec2 pos, float off){
float dst = Dist(pos).y;
return Gaus(dst + off, hardScan);
}
// Return scanline weight for bloom.
float BloomScan(vec2 pos, float off){
float dst = Dist(pos).y;
return Gaus(dst + off, hardBloomScan);
}
// Allow nearest three lines to effect pixel.
vec3 Tri(vec2 pos, sampler2D iChannel0){
vec3 a = Horz3(pos,-1.0, iChannel0);
vec3 b = Horz5(pos, 0.0, iChannel0);
vec3 c = Horz3(pos, 1.0, iChannel0);
float wa = Scan(pos,-1.0);
float wb = Scan(pos, 0.0);
float wc = Scan(pos, 1.0);
return a * wa + b * wb + c * wc;
}
// Small bloom.
vec3 Bloom(vec2 pos, sampler2D iChannel0){
vec3 a = Horz5(pos,-2.0, iChannel0);
vec3 b = Horz7(pos,-1.0, iChannel0);
vec3 c = Horz7(pos, 0.0, iChannel0);
vec3 d = Horz7(pos, 1.0, iChannel0);
vec3 e = Horz5(pos, 2.0, iChannel0);
float wa = BloomScan(pos,-2.0);
float wb = BloomScan(pos,-1.0);
float wc = BloomScan(pos, 0.0);
float wd = BloomScan(pos, 1.0);
float we = BloomScan(pos, 2.0);
return a * wa + b * wb + c * wc + d * wd + e * we;
}
// 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) * 1.0 / warp.x, 1.0 + (pos.x * pos.x) * 1.0/ warp.y);
return pos * 0.5+0.5;
}
vec3 Mask(vec2 pos){
if (mask_type == 0){
float line = maskLight;
float odd = 0.0;
if(fract(pos.x / 6.0) < 0.5) odd = 1.0;
if(fract((pos.y + odd) / 2.0) < 0.5) line = maskDark;
pos.x = fract(pos.x / 3.0);
vec3 mask = vec3(maskDark, maskDark, maskDark);
if(pos.x < 0.333)mask.r = maskLight;
else if(pos.x < 0.666)mask.g = maskLight;
else mask.b = maskLight;
mask *= line;
return mask;
}else if (mask_type == 1){
pos.x += pos.y * 3.0;
vec3 mask = vec3(maskDark, maskDark, maskDark);
pos.x = fract(pos.x / 6.0);
if(pos.x < 0.333)mask.r = maskLight;
else if(pos.x < 0.666)mask.g = maskLight;
else mask.b = maskLight;
return mask;
}else if (mask_type == 2){
pos.xy = floor(pos.xy * vec2(1.0, 0.5));
pos.x += pos.y * 3.0;
vec3 mask = vec3(maskDark, maskDark, maskDark);
pos.x = fract(pos.x / 6.0);
if(pos.x < 0.333)mask.r = maskLight;
else if(pos.x < 0.666)mask.g = maskLight;
else mask.b = maskLight;
return mask;
}
}
// Draw dividing bars.
float Bar(float pos, float bar){ pos -= bar; return pos * pos < 4.0 ? 0.0 : 1.0; }
// Entry.
void fragment(){
vec2 pos = Warp(FRAGCOORD.xy / (1.0 / SCREEN_PIXEL_SIZE).xy);
COLOR.rgb = Tri(pos, SCREEN_TEXTURE) * Mask(FRAGCOORD.xy);
if (bloom_type == 0){
COLOR.rgb = mix(COLOR.rgb,Bloom(pos, SCREEN_TEXTURE), 1.0 / bloomAmount);
}else if (bloom_type == 1){
COLOR.rgb += Bloom(pos, SCREEN_TEXTURE) * 1.0 / bloomAmount;
}
COLOR.a = 1.0;
COLOR.rgb = ToSrgb(COLOR.rgb);
}
The effect is very good.
Could you make a version with black corner edges like an old TV?
You can copy the ‘border()’ function from https://godotshaders.com/shader/vhs-and-crt-monitor-effect/
and then replace the final line with
You’ll get an error saying ‘warp_amount’ isn’t defined. Just replace it with ‘1.0’.
Is there any way of making this work on Godot 4? It’s completely black besides the dots?
I changed the alpha value.
in void fragment() its the COLOR.a
What value do I set it to in order for it to work?
You need to set the rez
How do I add the bar effect? It says that a function called ‘Bar’ exists but is never used.
A perfect retro arcade screen effect. I love it, thank you!
how do you even put this in? because every time i try to code it, it says half the code is a error
This is an absolute masterpiece. Thanks a lot!