diff options
| author | Peter Fors <peter.fors@mindkiller.com> | 2025-10-09 22:07:52 +0200 |
|---|---|---|
| committer | Peter Fors <peter.fors@mindkiller.com> | 2025-10-09 22:07:52 +0200 |
| commit | 030724a9aea346e4a9843d5842fb28c6d6c4cf1a (patch) | |
| tree | f06fb84aaef64b2f4e2d81b3d2d3eef71bad83ec /shaders/gl_crt_fragment.glsl | |
| parent | 412b2ef851516c1de8ba5006ddd284192cbcaf9b (diff) | |
Rearrangement and refactoring and optimizations and more accuracy
Diffstat (limited to 'shaders/gl_crt_fragment.glsl')
| -rw-r--r-- | shaders/gl_crt_fragment.glsl | 202 |
1 files changed, 202 insertions, 0 deletions
diff --git a/shaders/gl_crt_fragment.glsl b/shaders/gl_crt_fragment.glsl new file mode 100644 index 0000000..73dcfec --- /dev/null +++ b/shaders/gl_crt_fragment.glsl @@ -0,0 +1,202 @@ +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); + } +} |
