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 | |
| parent | 412b2ef851516c1de8ba5006ddd284192cbcaf9b (diff) | |
Rearrangement and refactoring and optimizations and more accuracy
Diffstat (limited to 'shaders')
| -rw-r--r-- | shaders/gl_bloom_blur_fragment.glsl | 50 | ||||
| -rw-r--r-- | shaders/gl_bloom_composite_fragment.glsl | 38 | ||||
| -rw-r--r-- | shaders/gl_bloom_extract_fragment.glsl | 34 | ||||
| -rw-r--r-- | shaders/gl_crt_fragment.glsl | 202 | ||||
| -rw-r--r-- | shaders/gl_phosphor_persistence_fragment.glsl | 17 | ||||
| -rw-r--r-- | shaders/gl_upscale_warp_fragment.glsl | 36 | ||||
| -rw-r--r-- | shaders/gl_vertex.glsl | 9 |
7 files changed, 386 insertions, 0 deletions
diff --git a/shaders/gl_bloom_blur_fragment.glsl b/shaders/gl_bloom_blur_fragment.glsl new file mode 100644 index 0000000..94eff93 --- /dev/null +++ b/shaders/gl_bloom_blur_fragment.glsl @@ -0,0 +1,50 @@ +out vec4 outcolor; +in vec2 frag_texture_coord; + +uniform sampler2D source; +uniform bool horizontal; + +// ========== QUALITY SETTINGS ========== +// Change this to switch between quality levels: +// 0 = Fastest (5-tap, best for integrated GPUs) +// 1 = Medium quality (9-tap, good balance) +// 2 = High quality (17-tap, beautiful bloom but expensive) +#define BLUR_QUALITY 2 + +#if BLUR_QUALITY == 0 + // 5-tap gaussian blur (3 samples each direction) + const int SAMPLE_COUNT = 3; + const float weight[3] = float[](0.3829249226, 0.2419707245, 0.0606531529); + const float blur_radius = 1.0; +#elif BLUR_QUALITY == 1 + // 9-tap gaussian blur (5 samples each direction) + const int SAMPLE_COUNT = 5; + const float weight[5] = float[](0.227027, 0.1945946, 0.1216216, 0.054054, 0.016216); + const float blur_radius = 1.0; +#else + // 17-tap gaussian blur (9 samples each direction) + const int SAMPLE_COUNT = 9; + const float weight[9] = float[](0.1964825501511404, 0.1782316199485724, 0.12149164501415554, 0.0652229951888931, 0.027835877787234808, 0.009270061520867907, 0.0024201487396289743, 0.0004963317290261215, 0.0000801326238056394); + const float blur_radius = 1.0; +#endif + +void main() { + vec2 tex_offset = 1.0 / vec2(textureSize(source, 0)); + vec3 result = texture(source, frag_texture_coord).rgb * weight[0]; + + if(horizontal) { + for(int i = 1; i < SAMPLE_COUNT; ++i) { + float offset = tex_offset.x * float(i) * blur_radius; + result += texture(source, frag_texture_coord + vec2(offset, 0.0)).rgb * weight[i]; + result += texture(source, frag_texture_coord - vec2(offset, 0.0)).rgb * weight[i]; + } + } else { + for(int i = 1; i < SAMPLE_COUNT; ++i) { + float offset = tex_offset.y * float(i) * blur_radius; + result += texture(source, frag_texture_coord + vec2(0.0, offset)).rgb * weight[i]; + result += texture(source, frag_texture_coord - vec2(0.0, offset)).rgb * weight[i]; + } + } + + outcolor = vec4(result, 1.0); +} diff --git a/shaders/gl_bloom_composite_fragment.glsl b/shaders/gl_bloom_composite_fragment.glsl new file mode 100644 index 0000000..5977e91 --- /dev/null +++ b/shaders/gl_bloom_composite_fragment.glsl @@ -0,0 +1,38 @@ +out vec4 outcolor; +in vec2 frag_texture_coord; + +uniform sampler2D crt_texture; +uniform sampler2D bloom_texture; +uniform float bloom_strength; +uniform vec2 bloom_size; +uniform vec2 src_image_size; +uniform vec2 viewport_size; + +void main() { + vec3 crt_color = texture(crt_texture, frag_texture_coord).rgb; + vec3 bloom_color = texture(bloom_texture, frag_texture_coord).rgb; + + // Calculate warp boundary mask to clip bloom outside CRT area + vec2 ipos = frag_texture_coord * viewport_size; + // Convert to normalized device coordinates [-1, 1] + vec2 pos = ipos * (2.0 / viewport_size) - vec2(1.0); + // Apply barrel distortion + 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); + // Convert back to UV + vec2 uv = (pos + vec2(1.0)) * 0.5; + + // Rounded corner cutoff (CRT bezel effect) - only clips the corners + float corner_radius = 0.05; // Radius of corner rounding (0.0 = sharp corners, 0.2 = very rounded) + vec2 edge_distance = abs(pos) - vec2(1.0 - corner_radius); + float dist = length(max(edge_distance, 0.0)); + + // Antialiased edge using smoothstep (creates soft 1-2 pixel transition) + float edge_softness = 0.003; // Controls antialiasing width (smaller = sharper) + float mask = smoothstep(corner_radius + edge_softness, corner_radius - edge_softness, dist); + + // Also check bounds + mask *= (uv.x >= 0.0 && uv.x <= 1.0 && uv.y >= 0.0 && uv.y <= 1.0) ? 1.0 : 0.0; + + outcolor = vec4((crt_color + bloom_color * bloom_strength) * mask, 1.0); +} diff --git a/shaders/gl_bloom_extract_fragment.glsl b/shaders/gl_bloom_extract_fragment.glsl new file mode 100644 index 0000000..6d0a806 --- /dev/null +++ b/shaders/gl_bloom_extract_fragment.glsl @@ -0,0 +1,34 @@ +out vec4 outcolor; +in vec2 frag_texture_coord; + +uniform sampler2D source; +uniform float threshold; + +// Bloom curve selection: +// 0 = Linear (default) - simple linear fade +// 1 = Smooth - smoothstep for more gradual fade-in +// 2 = Power - adjustable power curve +#define BLOOM_CURVE 0 + +void main() { + vec3 color = texture(source, frag_texture_coord).rgb; + // Calculate perceptual luminance + float brightness = dot(color, vec3(0.2126, 0.7152, 0.0722)); + + // Inverted bloom: full bloom at threshold and above, fades below threshold + // At brightness >= threshold: bloom_amount = 1.0 (full bloom) + // At brightness = 0: bloom_amount = 0.0 (no bloom) + float bloom_amount = min(brightness / threshold, 1.0); + +#if BLOOM_CURVE == 1 + // Smooth curve - more gradual fade-in using smoothstep + bloom_amount = smoothstep(0.0, 1.0, bloom_amount); +#elif BLOOM_CURVE == 2 + // Power curve - adjust exponent for different feel + // < 1.0 = gentler fade, > 1.0 = steeper fade + bloom_amount = pow(bloom_amount, 0.7); +#endif + // BLOOM_CURVE == 0 uses linear (no modification needed) + + outcolor = vec4(color * bloom_amount, 1.0); +} 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); + } +} diff --git a/shaders/gl_phosphor_persistence_fragment.glsl b/shaders/gl_phosphor_persistence_fragment.glsl new file mode 100644 index 0000000..1633a2e --- /dev/null +++ b/shaders/gl_phosphor_persistence_fragment.glsl @@ -0,0 +1,17 @@ +out vec4 outcolor; +in vec2 frag_texture_coord; + +uniform sampler2D current_frame; +uniform sampler2D previous_frame; +uniform float decay; + +void main() { + vec3 current = texture(current_frame, frag_texture_coord).rgb; + vec3 previous = texture(previous_frame, frag_texture_coord).rgb; + + // Mix current frame with decayed previous frame + // Higher decay = more trail (0.5 = subtle, 0.7 = noticeable) + vec3 result = max(current, previous * decay); + + outcolor = vec4(result, 1.0); +} diff --git a/shaders/gl_upscale_warp_fragment.glsl b/shaders/gl_upscale_warp_fragment.glsl new file mode 100644 index 0000000..26f0202 --- /dev/null +++ b/shaders/gl_upscale_warp_fragment.glsl @@ -0,0 +1,36 @@ +out vec4 outcolor; +in vec2 frag_texture_coord; + +uniform sampler2D source; +uniform vec2 resolution; +uniform vec2 src_image_size; + +void main() { + // Match CRT shader coordinate system exactly + vec2 half_pixel = 0.5 / src_image_size; + vec2 fragCoord = vec2(frag_texture_coord.x, 1.0 - frag_texture_coord.y) + half_pixel; + + // Convert to pixel coordinates and add pixel centering offset + vec2 ipos = fragCoord * resolution + vec2(0.5); + + // Convert to normalized device coordinates [-1, 1] + vec2 pos = ipos * (2.0 / resolution) - vec2(1.0); + + // Apply barrel distortion (match CRT warp) + 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 + ); + + // Convert back to UV coordinates + vec2 uv = (pos + vec2(1.0)) * 0.5; + + // Sample from source with boundary check + if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) { + outcolor = vec4(0.0, 0.0, 0.0, 1.0); + } else { + // Sample directly (Y-flip already applied at line 11) + outcolor = texture(source, uv, -16.0); + } +} diff --git a/shaders/gl_vertex.glsl b/shaders/gl_vertex.glsl new file mode 100644 index 0000000..707df72 --- /dev/null +++ b/shaders/gl_vertex.glsl @@ -0,0 +1,9 @@ +in vec2 position; +in vec2 texture_coord; +out vec2 frag_texture_coord; + +void main() { + gl_Position = vec4(position, 0.0, 1.0); + frag_texture_coord = texture_coord; +} + |
