diff options
| author | Gabriel Schneider <[email protected]> | 2026-08-16 15:49:12 -0300 |
|---|---|---|
| committer | Gabriel Schneider <[email protected]> | 2026-08-18 23:44:42 -0300 |
| commit | 1551e409c31992437cb2fa864f576d45c8433801 (patch) | |
| tree | e2fae8451f87b735a1360c7c2e383fdc40165789 /shaders/crt.frag.glsl | |
| parent | be2a9957708cbf0c478ca861c4a1f0f227bbfe10 (diff) | |
| download | pardes-1551e409c31992437cb2fa864f576d45c8433801.tar.gz pardes-1551e409c31992437cb2fa864f576d45c8433801.zip | |
big slow change: prebuilt shaders (SPIR-V/Metal), core gui reflow, docs, web + snapshot refresh
Diffstat (limited to 'shaders/crt.frag.glsl')
| -rw-r--r-- | shaders/crt.frag.glsl | 173 |
1 files changed, 148 insertions, 25 deletions
diff --git a/shaders/crt.frag.glsl b/shaders/crt.frag.glsl index 1c9b15a8..e43d5b86 100644 --- a/shaders/crt.frag.glsl +++ b/shaders/crt.frag.glsl @@ -1,39 +1,162 @@ #version 450 -// The Crt builtin's post-process, one pass, fully static (no animation, no -// glitches). Screen point cc shows the scene at uv' = cc*(1 + BARREL*r2), -// and src/crt.zig maps mouse input through that same transform — keep them -// equal or clicks drift near the edges. +// 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 BARREL = 0.012; // barrel curvature; MUST match src/crt.zig barrel -const float VIGNETTE = 0.07; // corner darkening: *= 1 - VIGNETTE*r2^2 -const vec2 PHOSPHOR = vec2(2.0, 2.0); // uv quantize cell in scene px (~1/7 of a glyph cell) -const float SCAN = 0.15; // scanline depth, one raised-cosine band per PHOSPHOR.y rows -const float CHROMA = 0.001; // R/B radial sample offset, grows toward the edges -const float BLOOM = 0.4; // bright-pass neighbor glow added back -const float BLOOM_MIN = 0.5; // channel level where the bright pass starts -const float LIFT = 1.08; // output brightness lift (offsets scan+vignette losses) +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; +} + +vec3 sceneSrgb(vec2 uv) { + vec2 warped = sceneUv(uv); + vec3 center = texture(u_scene, warped).rgb; + 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 vec3( + texture(u_scene, warped + chroma).r, + center.g, + texture(u_scene, warped - chroma).b + ); +} + +vec3 effectSceneLinear(vec2 uv) { + return srgbToLinear(sceneSrgb(uv)); +} void main() { - vec2 size = vec2(textureSize(u_scene, 0)); + 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) * 0.5 + 0.5; + 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. + o_col = vec4(clamp(sceneSrgb(uv), 0.0, 1.0), 1.0); + return; + } + vec3 col = vec3(0.0); - if (all(greaterThanEqual(uv, vec2(0.0))) && all(lessThanEqual(uv, vec2(1.0)))) { - vec2 q = (floor(uv * size / PHOSPHOR) + 0.5) * PHOSPHOR / size; - vec2 ca = cc * (CHROMA * r2); - col = vec3(texture(u_scene, q + ca).r, texture(u_scene, q).g, texture(u_scene, q - ca).b); - vec2 px = PHOSPHOR / size; - vec3 nb = texture(u_scene, q + vec2(px.x, 0.0)).rgb + texture(u_scene, q - vec2(px.x, 0.0)).rgb + - texture(u_scene, q + vec2(0.0, px.y)).rgb + texture(u_scene, q - vec2(0.0, px.y)).rgb; - col += max(nb * 0.25 - BLOOM_MIN, 0.0) * BLOOM; + 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; + + 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)); } - col *= 1.0 - VIGNETTE * r2 * r2; - col *= 1.0 - SCAN * (0.5 + 0.5 * cos(gl_FragCoord.y * 6.2831853 / PHOSPHOR.y)); - o_col = vec4(col * LIFT, 1.0); + o_col = vec4(linearToSrgb(clamp(col, 0.0, 1.0)), 1.0); } |
