out vec4 outcolor; in vec2 frag_texture_coord; uniform vec2 resolution; uniform vec2 src_image_size; uniform float brightness; uniform vec4 tone_data; uniform bool crt_emulation; uniform bool apply_mask; uniform sampler2D iChannel0; //============================================================== // CRTS SHADER PORTABILITY //============================================================== #define CrtsF1 float #define CrtsF2 vec2 #define CrtsF3 vec3 #define CrtsF4 vec4 #define CrtsFractF1 fract #define CrtsRcpF1(x) (1.0/(x)) #define CrtsSatF1(x) clamp((x),0.0,1.0) CrtsF1 CrtsMax3F1(CrtsF1 a, CrtsF1 b, CrtsF1 c) { return max(a, max(b, c)); } //============================================================== // FETCH FUNCTION //============================================================== CrtsF3 CrtsFetch(CrtsF2 uv) { const float bias = 0.002533333; return max(texture(iChannel0, uv, -16.0).rgb, vec3(bias)); } //============================================================== // PHOSPHOR MASK //============================================================== CrtsF3 CrtsMask(CrtsF2 pos, CrtsF1 dark) { #ifdef CRTS_MASK_GRILLE CrtsF3 m = CrtsF3(dark, dark, dark); CrtsF1 x = CrtsFractF1(pos.x * (1.0/3.0)); if(x < (1.0/3.0)) m.r = 1.0; else if(x < (2.0/3.0)) m.g = 1.0; else m.b = 1.0; return m; #endif #ifdef CRTS_MASK_GRILLE_LITE CrtsF3 m = CrtsF3(1.0, 1.0, 1.0); CrtsF1 x = CrtsFractF1(pos.x * (1.0/3.0)); if(x < (1.0/3.0)) m.r = dark; else if(x < (2.0/3.0)) m.g = dark; else m.b = dark; return m; #endif #ifdef CRTS_MASK_NONE return CrtsF3(1.0, 1.0, 1.0); #endif #ifdef CRTS_MASK_SHADOW pos.x += pos.y * 3.0; CrtsF3 m = CrtsF3(dark, dark, dark); CrtsF1 x = CrtsFractF1(pos.x * (1.0/6.0)); if(x < (1.0/3.0)) m.r = 1.0; else if(x < (2.0/3.0)) m.g = 1.0; else m.b = 1.0; return m; #endif } //============================================================== // CRTS FILTER //============================================================== CrtsF3 CrtsFilter(CrtsF2 ipos, CrtsF2 inputSizeDivOutputSize, CrtsF2 halfInputSize, CrtsF2 rcpInputSize, CrtsF2 rcpOutputSize, CrtsF2 twoDivOutputSize, CrtsF1 inputHeight, CrtsF2 warp, CrtsF1 thin, CrtsF1 blur, CrtsF1 mask, CrtsF4 tone) { // Apply warp // Convert to {-1 to 1} range CrtsF2 pos = ipos * twoDivOutputSize - CrtsF2(1.0, 1.0); // Distort pushes image outside {-1 to 1} range pos *= CrtsF2( 1.0 + (pos.y * pos.y) * warp.x, 1.0 + (pos.x * pos.x) * warp.y); // Vignette disabled - use rounded corners in composite shader instead CrtsF1 vin = 1.0; // Leave in {0 to inputSize} pos = pos * halfInputSize + halfInputSize; // Snap to center of first scanline CrtsF1 y0 = floor(pos.y - 0.5) + 0.5; // Snap to center of one of four pixels CrtsF1 x0 = floor(pos.x - 1.5) + 0.5; // Initial UV position CrtsF2 p = CrtsF2(x0 * rcpInputSize.x, y0 * rcpInputSize.y); // Fetch 4 nearest texels from 2 nearest scanlines CrtsF3 colA0 = CrtsFetch(p); p.x += rcpInputSize.x; CrtsF3 colA1 = CrtsFetch(p); p.x += rcpInputSize.x; CrtsF3 colA2 = CrtsFetch(p); p.x += rcpInputSize.x; CrtsF3 colA3 = CrtsFetch(p); p.y += rcpInputSize.y; CrtsF3 colB3 = CrtsFetch(p); p.x -= rcpInputSize.x; CrtsF3 colB2 = CrtsFetch(p); p.x -= rcpInputSize.x; CrtsF3 colB1 = CrtsFetch(p); p.x -= rcpInputSize.x; CrtsF3 colB0 = CrtsFetch(p); // Vertical filter - Scanline intensity using cosine wave CrtsF1 off = pos.y - y0; CrtsF1 pi2 = 6.28318530717958; CrtsF1 hlf = 0.5; CrtsF1 scanA = cos(min(0.5, off * thin ) * pi2) * hlf + hlf; CrtsF1 scanB = cos(min(0.5, (-off) * thin + thin) * pi2) * hlf + hlf; // Horizontal kernel is simple gaussian filter CrtsF1 off0 = pos.x - x0; CrtsF1 off1 = off0 - 1.0; CrtsF1 off2 = off0 - 2.0; CrtsF1 off3 = off0 - 3.0; CrtsF1 pix0 = exp2(blur * off0 * off0); CrtsF1 pix1 = exp2(blur * off1 * off1); CrtsF1 pix2 = exp2(blur * off2 * off2); CrtsF1 pix3 = exp2(blur * off3 * off3); CrtsF1 pixT = CrtsRcpF1(pix0 + pix1 + pix2 + pix3); // Get rid of wrong pixels on edge pixT *= vin; scanA *= pixT; scanB *= pixT; // Apply horizontal and vertical filters CrtsF3 color = (colA0 * pix0 + colA1 * pix1 + colA2 * pix2 + colA3 * pix3) * scanA + (colB0 * pix0 + colB1 * pix1 + colB2 * pix2 + colB3 * pix3) * scanB; // Apply phosphor mask color *= CrtsMask(ipos, mask); // Tonal control, start by protecting from /0 CrtsF1 peak = max(1.0 / (256.0 * 65536.0), CrtsMax3F1(color.r, color.g, color.b)); // Compute the ratios of {R,G,B} CrtsF3 ratio = color * CrtsRcpF1(peak); // Apply tonal curve to peak value peak = pow(peak, tone.x); peak = peak * CrtsRcpF1(peak * tone.y + tone.z); // Apply saturation ratio = pow(ratio, CrtsF3(tone.w, tone.w, tone.w)); // Reconstruct color return ratio * peak; } //============================================================== // MAIN //============================================================== void main() { // Add half_pixel offset to reduce aliasing from warp vec2 half_pixel = 0.5 / src_image_size; vec2 fragCoord = vec2(frag_texture_coord.x, 1.0 - frag_texture_coord.y) + half_pixel; if(crt_emulation) { outcolor.rgb = CrtsFilter( fragCoord.xy * resolution + vec2(0.5), src_image_size / resolution, src_image_size * vec2(0.5), 1.0 / src_image_size, 1.0 / resolution, 2.0 / resolution, src_image_size.y, vec2(1.0 / 24.0, 1.0 / 16.0), INPUT_THIN, INPUT_BLUR, INPUT_MASK, tone_data ); outcolor.rgb *= brightness; } else { outcolor.rgb = texture(iChannel0, fragCoord, -16.0).rgb; } // Apply rounded corner mask (matching composite shader) only when bloom is disabled if(apply_mask) { vec2 ipos = frag_texture_coord * resolution; vec2 pos = ipos * (2.0 / resolution) - vec2(1.0); vec2 warp = vec2(1.0 / 24.0, 1.0 / 16.0); pos *= vec2(1.0 + (pos.y * pos.y) * warp.x, 1.0 + (pos.x * pos.x) * warp.y); vec2 uv = (pos + vec2(1.0)) * 0.5; float corner_radius = 0.05; vec2 edge_distance = abs(pos) - vec2(1.0 - corner_radius); float dist = length(max(edge_distance, 0.0)); float edge_softness = 0.003; float mask = smoothstep(corner_radius + edge_softness, corner_radius - edge_softness, dist); mask *= (uv.x >= 0.0 && uv.x <= 1.0 && uv.y >= 0.0 && uv.y <= 1.0) ? 1.0 : 0.0; outcolor = vec4(outcolor.rgb * mask, 1.0); } else { outcolor = vec4(outcolor.rgb, 1.0); } }