/* Shader from Godot Shaders - the free shader library. godotshaders.com/shader/VHS-and-CRT-monitor-effect This shader is under CC0 license. Feel free to use, improve and change this shader according to your needs and consider sharing the modified result to godotshaders.com. */ #define scanlines_opacity 0.4 #define scanlines_width 0.25 #define grille_opacity 0.3 #define pixel_size 120.0 // Set the number of rows the texture will be divided in. Scanlines and grille will make a square of a size based on this value #define pixelate true // Fill each square ("pixel") with a sampled color, creating a pixel look and a more accurate representation of how a CRT monitor would work. #define roll true #define roll_speed 8.0 // Positive values are down, negative are up #define roll_size 15.0 #define roll_variation 1.8 // This valie is not an exact science. You have to play around with the value to find a look you like. How this works is explained in the code below. #define distort_intensity 0.05 // The distortion created by the rolling effect. #define noise_opacity 0.4 #define noise_speed 5.0 // There is a movement in the noise pattern that can be hard to see first. This sets the speed of that movement. #define static_noise_intensity 0.06 #define aberration 0.03 // Chromatic aberration, a distortion on each color channel. #define brightness 1.4 // When adding scanline gaps and grille the image can get very dark. Brightness tries to compensate for that. #define discolor true // Add a discolor effect simulating a VHS #define warp_amount 1.0 // Warp the texture edges simulating the curved glass of a CRT monitor or old TV. #define clip_warp false #define vignette_intensity 0.4 // Size of the vignette, how far towards the middle it should go. #define vignette_opacity 0.5 // Used by the noise functin to generate a pseudo random value between 0.0 and 1.0 vec2 random(vec2 uv){ uv = vec2( dot(uv, vec2(127.1,311.7) ), dot(uv, vec2(269.5,183.3) ) ); return -1.0 + 2.0 * fract(sin(uv) * 43758.5453123); } // Generate a Perlin noise used by the distortion effects float noise(vec2 uv) { vec2 uv_index = floor(uv); vec2 uv_fract = fract(uv); vec2 blur = smoothstep(0.0, 1.0, uv_fract); return mix( mix( dot( random(uv_index + vec2(0.0,0.0) ), uv_fract - vec2(0.0,0.0) ), dot( random(uv_index + vec2(1.0,0.0) ), uv_fract - vec2(1.0,0.0) ), blur.x), mix( dot( random(uv_index + vec2(0.0,1.0) ), uv_fract - vec2(0.0,1.0) ), dot( random(uv_index + vec2(1.0,1.0) ), uv_fract - vec2(1.0,1.0) ), blur.x), blur.y) * 0.5 + 0.5; } // Takes in the UV and warps the edges, creating the spherized effect vec2 warp(vec2 uv){ vec2 delta = uv - 0.5; float delta2 = dot(delta.xy, delta.xy); float delta4 = delta2 * delta2; float delta_offset = delta4 * warp_amount; return uv + delta * delta_offset; } // Adds a black border to hide stretched pixel created by the warp effect float border(vec2 uv){ float radius = min(warp_amount, 0.08); radius = max(min(min(abs(radius * 2.0), abs(1.0)), abs(1.0)), 1e-5); vec2 v_uv = abs(uv * 2. - 1.) - vec2(1.0, 1.0) + radius; float d = length(max(vec2(0.), v_uv)) / radius; return clamp(0.0, 1.0, ((1. - d) / fwidth(d))); } // Adds a vignette shadow to the edges of the image float vignette(vec2 uv){ uv *= 1.0 - uv.xy; float vignette = uv.x * uv.y * 15.0; return pow(vignette, vignette_intensity * vignette_opacity); } void mainImage( out vec4 fragColor, in vec2 fragCoord ) { vec2 UV = fragCoord/iResolution.xy; vec4 text; vec2 uv = warp(UV); // Warp the uv. uv will be used in most cases instead of UV to keep the warping vec2 roll_uv = vec2(0.0); float time = roll ? iTime : 0.0; // Pixelate the texture based on the given pixel size. if (pixelate) { uv = ceil(uv * float(pixel_size)) / float(pixel_size); } // Create the rolling effect. We need roll_line a bit later to make the noise effect. // That is why this runs if roll is true OR noise_opacity is over 0. float roll_line = 0.0; if (roll || noise_opacity > 0.0) { // Create the areas/lines where the texture will be distorted. roll_line = smoothstep(0.3, 0.9, sin(uv.y * roll_size - (time * roll_speed) ) ); // Create more lines of a different size and apply to the first set of lines. This creates a bit of variation. roll_line *= roll_line * smoothstep(0.3, 0.9, sin(uv.y * roll_size * roll_variation - (time * roll_speed * roll_variation) ) ); // Distort the UV where where the lines are roll_uv = vec2(( roll_line * distort_intensity * (1.-UV.x)), 0.0); } if (roll) { // If roll is true distort the texture with roll_uv. The texture is split up into RGB to // make some chromatic aberration. We apply the aberration to the red and green channels accorging to the aberration parameter // and intensify it a bit in the roll distortion. text.r = texture(iChannel0, uv + roll_uv * 0.8 - vec2(aberration, 0.0) * .1).r; text.g = texture(iChannel0, uv + roll_uv * 1.2 + vec2(aberration, 0.0) * .1 ).g; text.b = texture(iChannel0, uv + roll_uv).b; text.a = 1.0; } else { // If roll is false only apply the aberration without any distorion. The aberration values are very small so the .1 is only // to make the slider in the Inspector less sensitive. text.r = texture(iChannel0, uv - vec2(aberration, 0.0) * .1).r; text.g = texture(iChannel0, uv + vec2(aberration, 0.0) * .1).g; text.b = texture(iChannel0, uv).b; text.a = 1.0; } float r = text.r; float g = text.g; float b = text.b; // CRT monitors don't have pixels but groups of red, green and blue dots or lines, called grille. We isolate the texture's color channels // and divide it up in 3 offsetted lines to show the red, green and blue colors next to each other, with a small black gap between. // I'll use the word pixel here not because it is accurate but because it is easy to understand if (grille_opacity > 0.0){ // Create lines that are the size of each pixel and divide it by 3 to get a line that is 1 color channel wide. // The 3.1416 just happened to be a very perfect value. I'm too bad at math to understand if there is an actual relation with PI here. float g_r = smoothstep(0.1, 0.9, abs(sin(warp(UV).x * (pixel_size * 3.1416) * 3.))); // Make a new wider line and offset it to cover upp the other channels g_r *= step(0.9, abs(sin(1.1 + warp(UV).x * (pixel_size * 3.1416) * 1.))); // You can choose how transparent the grille effect should be. 100% make the image very dark because of the black lines. r = mix(r, r * g_r, grille_opacity); // Same as above for green float g_g = smoothstep(0.1, 0.9, abs(sin(warp(UV).x * (pixel_size * 3.1416) * 3.))); g_g *= step(0.9, abs(sin(3.1 + warp(UV).x * (pixel_size * 3.1416) * 1.))); g = mix(g, g * g_g, grille_opacity); // Same as above for blue float g_b = smoothstep(0.1, 0.9, abs(sin(warp(UV).x * (pixel_size * 3.1416) * 3.))); g_b *= step(0.9, abs(sin(2.1 + warp(UV).x * (pixel_size * 3.1416) * 1.))); b = mix(b, b * g_b, grille_opacity); } // Apply the grille to the texture's color channels and apply Brightness. Since the grille and the scanlines (below) make the image very dark you // can compensate by increasing the brightness. text.r = clamp(0.0, 1.0, r * brightness); text.g = clamp(0.0, 1.0, g * brightness); text.b = clamp(0.0, 1.0, b * brightness); // Scanlines are the horizontal lines that make up the image on a CRT monitor. // Here we are actual setting the black gap between each line, which I guess is not the right definition of the word, but you get the idea float scanlines = 0.5; if (scanlines_opacity > 0.0) { // Same technique as above, create lines with sine and applying it to the texture. Smoothstep to allow setting the line size. scanlines = smoothstep(scanlines_width, scanlines_width + 0.5, abs(sin(warp(UV).y * (pixel_size * 3.1416)))); text.rgb = mix(text.rgb, text.rgb * vec3(scanlines) , scanlines_opacity); } // Apply the banded noise. if (noise_opacity > 0.0) { // Generate a noise pattern that is very stretched horizontally, and animate it with noise_speed float noise = smoothstep(0.4, 0.5, noise(uv * vec2(2.0, 200.) + vec2(10.0, (iTime * (noise_speed))) ) ); // We use roll_line (set above) to define how big the noise should be vertically (multiplying cuts off all black parts). // We also add in some basic noise with random() to break up the noise pattern above. The noise is sized according to the pixel_size // value set in the inspector, since in real world it can't be in half grilles. If you don't like this look you can // change "ceil(warp(UV) * float(pixel_size)) / float(pixel_size)" to only "uv". Or multiply pixel_size with som value // greater than 1.0 to make them smaller. roll_line *= noise * scanlines * (clamp(0.0, 1.0, random((ceil(warp(UV) * float(pixel_size)) / float(pixel_size)) + vec2(iTime * 0.8, 0.0)).x + 0.8)); // Add it to the texture based on noise_opacity text.rgb = clamp( vec3(0.0), vec3(1.0), mix(text.rgb, text.rgb + roll_line, noise_opacity)); } // Apply static noise by generating it over the whole screen in the same way as above if (static_noise_intensity > 0.0) { text.rgb += clamp(0.0, 1.0, random((ceil(warp(UV) * float(pixel_size)) / float(pixel_size)) + fract(iTime)).x) * static_noise_intensity; } // Apply a black border to hide imperfections caused by the warping. // Also apply the vignette text.rgb *= border(warp(UV)); text.rgb *= vignette(warp(UV)); // Hides the black border and make that area transparent. Good if you want to add the the texture on top an image of a TV or monitor. if (clip_warp) { text.a = border(warp(UV)); } // Apply discoloration to get a VHS look (lower saturation and higher contrast) // You can play with the values below or expose them in the Inspector. float saturation = 0.5; float contrast = 1.2; if (discolor) { // Saturation vec3 greyscale = vec3(text.r + text.g + text.b) / 3.; text.rgb = mix(text.rgb, greyscale, saturation); // Contrast float midpoint = pow(0.5, 2.2); text.rgb = (text.rgb - vec3(midpoint)) * contrast + midpoint; } fragColor = text; }