summaryrefslogtreecommitdiff
path: root/shaders
diff options
context:
space:
mode:
authorPeter Fors <peter.fors@mindkiller.com>2025-10-09 22:07:52 +0200
committerPeter Fors <peter.fors@mindkiller.com>2025-10-09 22:07:52 +0200
commit030724a9aea346e4a9843d5842fb28c6d6c4cf1a (patch)
treef06fb84aaef64b2f4e2d81b3d2d3eef71bad83ec /shaders
parent412b2ef851516c1de8ba5006ddd284192cbcaf9b (diff)
Rearrangement and refactoring and optimizations and more accuracy
Diffstat (limited to 'shaders')
-rw-r--r--shaders/gl_bloom_blur_fragment.glsl50
-rw-r--r--shaders/gl_bloom_composite_fragment.glsl38
-rw-r--r--shaders/gl_bloom_extract_fragment.glsl34
-rw-r--r--shaders/gl_crt_fragment.glsl202
-rw-r--r--shaders/gl_phosphor_persistence_fragment.glsl17
-rw-r--r--shaders/gl_upscale_warp_fragment.glsl36
-rw-r--r--shaders/gl_vertex.glsl9
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;
+}
+