// In-game CRT shader // Author: sarphiv // License: CC BY-NC-SA 4.0 // Description: // Shader for ghostty that is focussed on being usable while looking like a stylized CRT terminal in a modern video game. // I know a tiny bit about shaders, and nothing about GLSL, // so this is a Frakenstein's monster combination of other shaders together with a lot of surgery. // On the bright side, i've cleaned up the body parts and surgery a lot. // Based on: // 1. https://gist.github.com/mitchellh/39d62186910dcc27cad097fed16eb882 (forces the choice of license) // 2. https://gist.github.com/qwerasd205/c3da6c610c8ffe17d6d2d3cc7068f17f // 3. https://gist.github.com/seanwcom/0fbe6b270aaa5f28823e053d3dbb14ca // Settings: // How straight the terminal is in each axis // (x, y) \in R^2 : x, y > 0 #define CURVE 13.0, 11.0 // How far apart the different colors are from each other // x \in R #define COLOR_FRINGING_SPREAD 1.0 // How much the ghost images are spread out // x \in R : x >= 0 #define GHOSTING_SPREAD 0.75 // How visible ghost images are // x \in R : x >= 0 #define GHOSTING_STRENGTH 1.0 // How much of the non-linearly darkened colors are mixed in // [0, 1] #define DARKEN_MIX 0.4 // How far in the vignette spreads // x \in R : x >= 0 #define VIGNETTE_SPREAD 0.3 // How bright the vignette is // x \in R : x >= 0 #define VIGNETTE_BRIGHTNESS 6.4 // Tint all colors // [0, 1]^3 #define TINT 0.93, 1.00, 0.96 // How visible the scan line effect is // NOTE: Technically these are not scan lines, but rather the lack of them // [0, 1] #define SCAN_LINES_STRENGTH 0.15 // How bright the spaces between the lines are // [0, 1] #define SCAN_LINES_VARIANCE 0.35 // Pixels per scan line effect // x \in R : x > 0 #define SCAN_LINES_PERIOD 4.0 // How visible the aperture grille is // x \in R : x >= 0 #define APERTURE_GRILLE_STRENGTH 0.2 // Pixels per aperture grille // x \in R : x > 0 #define APERTURE_GRILLE_PERIOD 2.0 // How much the screen flickers // x \in R : x >= 0 #define FLICKER_STRENGTH 0.05 // How fast the screen flickers // x \in R : x > 0 #define FLICKER_FREQUENCY 15.0 // How much noise is added to filled areas // [0, 1] #define NOISE_CONTENT_STRENGTH 0.15 // How much noise is added everywhere // [0, 1] #define NOISE_UNIFORM_STRENGTH 0.03 // How big the bloom is // x \in R : x >= 0 #define BLOOM_SPREAD 8.0 // How visible the bloom is // [0, 1] #define BLOOM_STRENGTH 0.04 // How fast colors fade in and out // [0, 1] #define FADE_FACTOR 0.55 // Disabled values for when the settings are not defined #ifndef COLOR_FRINGING_SPREAD #define COLOR_FRINGING_SPREAD 0.0 #endif #if !defined(GHOSTING_SPREAD) || !defined(GHOSTING_STRENGTH) #undef GHOSTING_SPREAD #undef GHOSTING_STRENGTH #define GHOSTING_SPREAD 0.0 #define GHOSTING_STRENGTH 0.0 #endif #ifndef DARKEN_MIX #define DARKEN_MIX 0.0 #endif #if !defined(VIGNETTE_SPREAD) || !defined(VIGNETTE_BRIGHTNESS) #undef VIGNETTE_SPREAD #undef VIGNETTE_BRIGHTNESS #define VIGNETTE_SPREAD 0.0 #define VIGNETTE_BRIGHTNESS 1.0 #endif #ifndef TINT #define TINT 1.00, 1.00, 1.00 #endif #if !defined(SCAN_LINES_STRENGTH) || !defined(SCAN_LINES_VARIANCE) || !defined(SCAN_LINES_PERIOD) #undef SCAN_LINES_STRENGTH #undef SCAN_LINES_VARIANCE #undef SCAN_LINES_PERIOD #define SCAN_LINES_STRENGTH 0.0 #define SCAN_LINES_VARIANCE 1.0 #define SCAN_LINES_PERIOD 1.0 #endif #if !defined(APERTURE_GRILLE_STRENGTH) || !defined(APERTURE_GRILLE_PERIOD) #undef APERTURE_GRILLE_STRENGTH #undef APERTURE_GRILLE_PERIOD #define APERTURE_GRILLE_STRENGTH 0.0 #define APERTURE_GRILLE_PERIOD 1.0 #endif #if !defined(FLICKER_STRENGTH) || !defined(FLICKER_FREQUENCY) #undef FLICKER_STRENGTH #undef FLICKER_FREQUENCY #define FLICKER_STRENGTH 0.0 #define FLICKER_FREQUENCY 1.0 #endif #if !defined(NOISE_CONTENT_STRENGTH) || !defined(NOISE_UNIFORM_STRENGTH) #undef NOISE_CONTENT_STRENGTH #undef NOISE_UNIFORM_STRENGTH #define NOISE_CONTENT_STRENGTH 0.0 #define NOISE_UNIFORM_STRENGTH 0.0 #endif #if !defined(BLOOM_SPREAD) || !defined(BLOOM_STRENGTH) #undef BLOOM_SPREAD #undef BLOOM_STRENGTH #define BLOOM_SPREAD 0.0 #define BLOOM_STRENGTH 0.0 #endif #ifndef FADE_FACTOR #define FADE_FACTOR 1.00 #endif // Constants #define PI 3.1415926535897932384626433832795 #ifdef BLOOM_SPREAD // Golden spiral samples used for bloom. // [x, y, weight] weight is inverse of distance. const vec3[24] bloom_samples = { vec3( 0.1693761725038636, 0.9855514761735895, 1), vec3(-1.333070830962943, 0.4721463328627773, 0.7071067811865475), vec3(-0.8464394909806497, -1.51113870578065, 0.5773502691896258), vec3( 1.554155680728463, -1.2588090085709776, 0.5), vec3( 1.681364377589461, 1.4741145918052656, 0.4472135954999579), vec3(-1.2795157692199817, 2.088741103228784, 0.4082482904638631), vec3(-2.4575847530631187, -0.9799373355024756, 0.3779644730092272), vec3( 0.5874641440200847, -2.7667464429345077, 0.35355339059327373), vec3( 2.997715703369726, 0.11704939884745152, 0.3333333333333333), vec3( 0.41360842451688395, 3.1351121305574803, 0.31622776601683794), vec3(-3.167149933769243, 0.9844599011770256, 0.30151134457776363), vec3(-1.5736713846521535, -3.0860263079123245, 0.2886751345948129), vec3( 2.888202648340422, -2.1583061557896213, 0.2773500981126146), vec3( 2.7150778983300325, 2.5745586041105715, 0.2672612419124244), vec3(-2.1504069972377464, 3.2211410627650165, 0.2581988897471611), vec3(-3.6548858794907493, -1.6253643308191343, 0.25), vec3( 1.0130775986052671, -3.9967078676335834, 0.24253562503633297), vec3( 4.229723673607257, 0.33081361055181563, 0.23570226039551587), vec3( 0.40107790291173834, 4.340407413572593, 0.22941573387056174), vec3(-4.319124570236028, 1.159811599693438, 0.22360679774997896), vec3(-1.9209044802827355, -4.160543952132907, 0.2182178902359924), vec3( 3.8639122286635708, -2.6589814382925123, 0.21320071635561041), vec3( 3.3486228404946234, 3.4331800232609, 0.20851441405707477), vec3(-2.8769733643574344, 3.9652268864187157, 0.20412414523193154) }; #endif void mainImage(out vec4 fragColor, in vec2 fragCoord) { // Get texture coordinates vec2 uv = fragCoord.xy / iResolution.xy; #ifdef CURVE // Curve texture coordinates to mimic non-flat CRT monior uv = (uv - 0.5) * 2.0; uv.xy *= 1.0 + pow((abs(vec2(uv.y, uv.x)) / vec2(CURVE)), vec2(2.0)); uv = (uv / 2.0) + 0.5; #endif // Retrieve colors from appropriate locations fragColor.r = texture(iChannel0, vec2(uv.x + 0.0003 * COLOR_FRINGING_SPREAD, uv.y + 0.0003 * COLOR_FRINGING_SPREAD)).x; fragColor.g = texture(iChannel0, vec2(uv.x + 0.0000 * COLOR_FRINGING_SPREAD, uv.y - 0.0006 * COLOR_FRINGING_SPREAD)).y; fragColor.b = texture(iChannel0, vec2(uv.x - 0.0006 * COLOR_FRINGING_SPREAD, uv.y + 0.0000 * COLOR_FRINGING_SPREAD)).z; fragColor.a = texture(iChannel0, uv).a; // Add faint ghost images fragColor.r += 0.04 * GHOSTING_STRENGTH * texture(iChannel0, GHOSTING_SPREAD * vec2(+0.025, -0.027) + uv.xy).x; fragColor.g += 0.02 * GHOSTING_STRENGTH * texture(iChannel0, GHOSTING_SPREAD * vec2(-0.022, -0.020) + uv.xy).y; fragColor.b += 0.04 * GHOSTING_STRENGTH * texture(iChannel0, GHOSTING_SPREAD * vec2(-0.020, -0.018) + uv.xy).z; // Quadratically darken everything fragColor.rgb = mix(fragColor.rgb, fragColor.rgb*fragColor.rgb, DARKEN_MIX); // Vignette effect fragColor.rgb *= VIGNETTE_BRIGHTNESS * pow(uv.x * uv.y * (1.0-uv.x) * (1.0-uv.y), VIGNETTE_SPREAD); // Tint all colors fragColor.rgb *= vec3(TINT); // NOTE: At this point, RGB values may be above 1 // Add scan lines effect fragColor.rgb *= mix( 1.0, SCAN_LINES_VARIANCE/2.0*(1.0 + sin(2*PI* uv.y * iResolution.y/SCAN_LINES_PERIOD)), SCAN_LINES_STRENGTH ); // Add aperture grille int aperture_grille_step = int(8 * mod(fragCoord.x, APERTURE_GRILLE_PERIOD) / APERTURE_GRILLE_PERIOD); float aperture_grille_mask; if (aperture_grille_step < 3) aperture_grille_mask = 0.0; else if (aperture_grille_step < 4) aperture_grille_mask = mod(8*fragCoord.x, APERTURE_GRILLE_PERIOD) / APERTURE_GRILLE_PERIOD; else if (aperture_grille_step < 7) aperture_grille_mask = 1.0; else if (aperture_grille_step < 8) aperture_grille_mask = mod(-8*fragCoord.x, APERTURE_GRILLE_PERIOD) / APERTURE_GRILLE_PERIOD; fragColor.rgb *= 1.0 - APERTURE_GRILLE_STRENGTH*aperture_grille_mask; // Add flicker fragColor *= 1.0 - FLICKER_STRENGTH/2.0*(1.0 + sin(2*PI*FLICKER_FREQUENCY*iTime)); // Add noise // NOTE: Hard-coded noise distributions float noiseContent = smoothstep(0.4, 0.6, fract(sin(uv.x * uv.y * (1.0-uv.x) * (1.0-uv.y) * iTime * 4096.0) * 65536.0)); float noiseUniform = smoothstep(0.4, 0.6, fract(sin(uv.x * uv.y * (1.0-uv.x) * (1.0-uv.y) * iTime * 8192.0) * 65536.0)); fragColor.rgb *= clamp(noiseContent + 1.0 - NOISE_CONTENT_STRENGTH, 0.0, 1.0); fragColor.rgb = clamp(fragColor.rgb + noiseUniform * NOISE_UNIFORM_STRENGTH, 0.0, 1.0); // NOTE: At this point, RGB values are again within [0, 1] // Remove output outside of screen bounds if (uv.x < 0.0 || uv.x > 1.0) fragColor.rgb *= 0.0; if (uv.y < 0.0 || uv.y > 1.0) fragColor.rgb *= 0.0; #ifdef BLOOM_SPREAD // Add bloom vec2 step = BLOOM_SPREAD * vec2(1.414) / iResolution.xy; for (int i = 0; i < 24; i++) { vec3 bloom_sample = bloom_samples[i]; vec4 neighbor = texture(iChannel0, uv + bloom_sample.xy * step); float luminance = 0.299 * neighbor.r + 0.587 * neighbor.g + 0.114 * neighbor.b; fragColor += luminance * bloom_sample.z * neighbor * BLOOM_STRENGTH; } fragColor = clamp(fragColor, 0.0, 1.0); #endif // Add fade effect to smoothen out color transitions // NOTE: May need to be iTime/iTimeDelta dependent fragColor = vec4(FADE_FACTOR*fragColor.rgb, FADE_FACTOR); }