#version 450 // The Crt builtin's post-process. Screen point cc shows the scene at // uv' = cc*(1 + BARREL*r2), and src/gui/crt.zig maps mouse input through that // same transform — keep them equal or clicks drift near the edges. layout(set = 2, binding = 0) uniform sampler2D u_scene; layout(std140, set = 3, binding = 0) uniform CrtUniforms { float time_seconds; float frame; vec2 viewport; // x = CRT, y = ripple, z = glitch. One scene pass can combine them. vec4 effects; } u_crt; layout(location = 0) out vec4 o_col; const float TAU = 6.28318530718; const float BARREL = 0.016; // MUST match src/gui/crt.zig barrel const float EDGE_SOFTNESS_PX = 3.5; const float CHROMA_PX = 0.28; // radial R/B separation in physical pixels at r2=1 const float BLOOM_RADIUS_PX = 1.75; const float BLOOM_THRESHOLD = 0.60; // linear-light channel peak const float BLOOM_SOFTNESS = 0.25; const float BLOOM_STRENGTH = 0.045; const float SCAN_PERIOD_PX = 3.0; const float SCAN_STRENGTH = 0.055; const float MASK_PERIOD_PX = 3.0; const float MASK_STRENGTH = 0.015; const float VIGNETTE_STRENGTH = 0.11; // 11% only at the extreme corners const float HUM_STRENGTH = 0.0025; const float NOISE_STRENGTH = 0.0020; const float EXPOSURE = 1.012; vec3 srgbToLinear(vec3 c) { vec3 low = c / 12.92; vec3 high = pow((c + 0.055) / 1.055, vec3(2.4)); return mix(high, low, lessThanEqual(c, vec3(0.04045))); } vec3 linearToSrgb(vec3 c) { c = max(c, vec3(0.0)); vec3 low = c * 12.92; vec3 high = 1.055 * pow(c, vec3(1.0 / 2.4)) - 0.055; return mix(high, low, lessThanEqual(c, vec3(0.0031308))); } vec3 sceneLinear(vec2 uv) { return srgbToLinear(texture(u_scene, uv).rgb); } vec3 brightPass(vec2 uv) { vec3 c = sceneLinear(uv); float peak = max(c.r, max(c.g, c.b)); return c * smoothstep(BLOOM_THRESHOLD, BLOOM_THRESHOLD + BLOOM_SOFTNESS, peak); } float noiseAt(vec2 p) { vec3 q = fract(vec3(p.xyx) * 0.1031); q += dot(q, q.yzx + 33.33); return fract((q.x + q.y) * q.z); } // The non-CRT scene effects deliberately distort the source coordinates, // rather than remapping the final colors. Themes retain their palette and the // effects remain composable with the linear-light CRT treatment below. vec2 sceneUv(vec2 uv) { if (u_crt.effects.y > 0.5) { vec2 centered = uv - 0.5; float radius = length(centered); float envelope = 1.0 - smoothstep(0.04, 0.72, radius); float wave = sin(radius * 42.0 - u_crt.time_seconds * 5.2); uv += centered / max(radius, 0.00001) * wave * envelope * 0.0032; } if (u_crt.effects.z > 0.5) { float band = floor(uv.y * 96.0); float seed = noiseAt(vec2(band, floor(u_crt.time_seconds * 13.0))); float tear = smoothstep(0.955, 1.0, seed); uv.x += (noiseAt(vec2(seed, band)) - 0.5) * tear * 0.020; } return uv; } // The scene, ALPHA INCLUDED: with a see-through ground the grid arrives // premultiplied and partly empty, and a pass that dropped .a would turn the // desktop behind a `*_transparent` theme into opaque black the moment Crt, // Ripple or Glitch came on. vec4 sceneSrgb(vec2 uv) { vec2 warped = sceneUv(uv); vec4 center = texture(u_scene, warped); if (u_crt.effects.z <= 0.5) return center; float band = floor(warped.y * 96.0); float seed = noiseAt(vec2(band, floor(u_crt.time_seconds * 13.0))); float tear = smoothstep(0.955, 1.0, seed); vec2 chroma = vec2((0.35 + 1.25 * tear) / u_crt.viewport.x, 0.0); return vec4( texture(u_scene, warped + chroma).r, center.g, texture(u_scene, warped - chroma).b, center.a ); } vec3 effectSceneLinear(vec2 uv) { return srgbToLinear(sceneSrgb(uv).rgb); } void main() { vec2 size = u_crt.viewport; vec2 texel = 1.0 / size; vec2 cc = gl_FragCoord.xy / size * 2.0 - 1.0; float r2 = dot(cc, cc); vec2 uv = cc * (1.0 + BARREL * r2 * u_crt.effects.x) * 0.5 + 0.5; // Ripple and glitch are useful without the CRT. They only move samples; // skip every tube-specific light/mask/exposure adjustment. if (u_crt.effects.x <= 0.5) { // The scene texture is already sRGB. Avoid a needless transfer-curve // round trip so a coordinate-only effect preserves theme bytes. vec4 s = sceneSrgb(uv); o_col = vec4(clamp(s.rgb, 0.0, 1.0), clamp(s.a, 0.0, 1.0)); return; } vec3 col = vec3(0.0); // Outside the tube nothing is drawn, so nothing is covered either: the // old opaque black there would have punched a rectangle out of the // compositor's backdrop. float alpha = 0.0; float edge = min(min(uv.x, uv.y), min(1.0 - uv.x, 1.0 - uv.y)); if (edge > 0.0) { vec3 center = effectSceneLinear(uv); vec2 chroma = cc * (CHROMA_PX * r2) * texel; // Each radial sample follows the same ripple/glitch path from its own // pre-warp coordinate. This is also how the Metal kernel composes the // effects; sampling a once-warped center here made combined effects // diverge between the two GUI backends. col = vec3( effectSceneLinear(uv + chroma).r, center.g, effectSceneLinear(uv - chroma).b ); vec2 bloom_px = texel * BLOOM_RADIUS_PX; vec3 bloom = brightPass(uv + vec2(bloom_px.x, 0.0)) + brightPass(uv - vec2(bloom_px.x, 0.0)) + brightPass(uv + vec2(0.0, bloom_px.y)) + brightPass(uv - vec2(0.0, bloom_px.y)); col += bloom * (0.25 * BLOOM_STRENGTH); // The mask has unit mean and the scan loss stays small; neither needs // the old global lift that remapped every theme toward white. float scan_wave = 0.5 + 0.5 * cos(gl_FragCoord.y * TAU / SCAN_PERIOD_PX); col *= 1.0 - SCAN_STRENGTH * scan_wave * scan_wave; vec3 mask = 1.0 + MASK_STRENGTH * cos( gl_FragCoord.x * TAU / MASK_PERIOD_PX + vec3(0.0, TAU / 3.0, 2.0 * TAU / 3.0) ); col *= mask; vec2 edge_px = min(uv, 1.0 - uv) * size; float tube = smoothstep(0.0, EDGE_SOFTNESS_PX, min(edge_px.x, edge_px.y)); float vignette = 1.0 - VIGNETTE_STRENGTH * pow(clamp(r2 * 0.5, 0.0, 1.0), 1.6); float hum = 1.0 + HUM_STRENGTH * sin((uv.y * 1.35 - u_crt.time_seconds * 0.11) * TAU); col *= tube * vignette * hum * EXPOSURE; // The tube's edge falloff dims coverage as well as colour; everything // else here is a light adjustment and leaves it alone. alpha = sceneSrgb(uv).a * tube; float luma = dot(col, vec3(0.2126, 0.7152, 0.0722)); vec2 noise_seed = gl_FragCoord.xy + vec2(u_crt.frame * 17.0, u_crt.frame * 59.0); col += (noiseAt(noise_seed) - 0.5) * NOISE_STRENGTH * (0.15 + 0.85 * clamp(luma, 0.0, 1.0)); } o_col = vec4(linearToSrgb(clamp(col, 0.0, 1.0)), clamp(alpha, 0.0, 1.0)); }