diff options
Diffstat (limited to 'src/shaders/render/ntsc_common.wgsl')
| -rw-r--r-- | src/shaders/render/ntsc_common.wgsl | 143 |
1 files changed, 143 insertions, 0 deletions
diff --git a/src/shaders/render/ntsc_common.wgsl b/src/shaders/render/ntsc_common.wgsl new file mode 100644 index 0000000..61ea254 --- /dev/null +++ b/src/shaders/render/ntsc_common.wgsl @@ -0,0 +1,143 @@ +// Shared NTSC post-process constants and functions. +// Requires sample_ntsc_signal(uv: vec2f) -> vec4f to be defined by the includer. + +const PI = 3.14159265; +const TAU = 6.28318530718; + +const XSIZE = 54.0 * 8.; +const YSIZE = 33.0 * 8.; +const SCAN_FLICKER = 2.33; +const X_INTERFERENCE = 1.1; +const Y_INTERFERENCE = 0.101; +const LUMA_BRIGHTNESS = 1.1; +const CHROMA_SATURATION = 1.6; +const BLUR_SIZE = 0.2; +const LUMA_BLUR = 1.7; +const CHROMA_BLUR = 0.7; +const CHROMA_SIZE = 6.0; +const SUB_CARRIER = 2.1; +const CROSS_TALK = 0.1; +const CROSS_Y_INTERFERENCE = 30.; +const CHROMA_MOD_FREQ = (0.4 * PI); + +// Barrel (fisheye) distortion: strength > 0 = barrel, < 0 = pincushion +fn fisheye(uv: vec2f, scale: f32) -> vec2f { + const strength = vec2f(0.1, 0.24); + return uv * (1.0 + scale * strength * (uv * uv).yx) * 0.60 + .50; +} + +fn vignette(uv: vec2f) -> f32 { + const vignetteRounding = 160.0; + const vignetteSmoothness = 0.7; + let uv2 = 2.0 * uv - 1.0; + let amount = 1.0 - sqrt(pow(abs(uv2.x), vignetteRounding) + pow(abs(uv2.y), vignetteRounding)); + return smoothstep(0., vignetteSmoothness, amount); +} + +// 6-taps Luma horizontal filtering +// fs = 3.84 MHz (Nyquist 1.92 MHz) +// Passband: 0–2.8 MHz +// Stopband: 3.4–3.84 MHz (>20 dB atten.) +// => firpm(12, [0 2.8/3.842 3.4/3.842 1], [1 1 0 0]) +const luma_filter = array<f32, 2 * 6 + 1>( + 0.0102, 0.0214, 0.0387, -0.0018, -0.0785, -0.1572, + -0.1698, + 0.1275, 0.4924, 0.5381, 0.4924, 0.1275, -0.1698 +); +// Chroma: +// fs = 3.84 MHz +// Passband: 3.3–3.7 MHz (around fsc) +// Stopbands: 0–2.5 MHz (>40 dB) and 3.9+ MHz +// => firpm(12, [0 2.5/1.92 3.3/1.92 3.7/1.92 1], [0 0 1 1 0]) +const chroma_filter = array<f32, 2 * 6 + 1>( + -0.0123, -0.0456, -0.0892, 0.0234, 0.1678, 0.2984, + 0.3456, + 0.0000, 0.3456, 0.2984, 0.1678, 0.0234, -0.0892 +); + +fn get_value(uv: vec2f, off: f32, yscale: f32) -> vec4f { + return sample_ntsc_signal(uv + off * vec2f(1., yscale)); +} + +fn peak(y: f32, ypos: f32, scale: f32) -> f32 { + return clamp((y - 1.) * scale * log(abs(y - ypos)), 0.0, 1.0); +} + +fn get_signal(uv: vec2f, d: f32) -> vec4f { + var signal = vec4f(0.0); + for (var i = 0; i <= 12; i += 1) { + let offset = f32(i) - 6.0; + let suml = luma_filter[i] * get_value(uv, offset * d, 0.67); + let sumc = chroma_filter[i] * get_value(uv, offset * d * CHROMA_SIZE, 0.67); + signal += vec4f(suml.x, sumc.y, sumc.z, suml.a); + } + let base = sample_ntsc_signal(uv); + return mix(signal, base, vec4f(LUMA_BLUR, CHROMA_BLUR, CHROMA_BLUR, 1.)); +} + +fn randomized_f32(p: vec2f, t: f32) -> f32 { + return hash_2f_alt(vec2f(p * 0.152 + t * 1500. + 50.0)); +} + +@fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { + let t = uniforms.time; + let bt = uniforms.beat_phase; + + // Fisheye/barrel distortion + var uv = fisheye(in.st, 0.18); + let mframe = sin(bt * 32.) * 1.2; + uv.y += mframe * SCAN_FLICKER / uniforms.resolution.y; // flicker at target resolution + + // interference + let cur_line = round(uv.y * YSIZE); + var r = randomized_f32(vec2f(0.0, cur_line), uniforms.time); + if (r > 0.995) { r *= 3.0; } + + let x_interf = X_INTERFERENCE * r / XSIZE; + let y_interf = Y_INTERFERENCE * r * peak(uv.y, 0.2, 0.03); + uv.x += x_interf - y_interf; + + // luma fringing + let d = (BLUR_SIZE + y_interf * 100.0) / XSIZE; + var signal = get_signal(uv, d); + + // luma / chroma saturation + let lchroma = signal.y * CHROMA_SATURATION; + let phase = signal.z * TAU; + signal.x *= LUMA_BRIGHTNESS; + signal.y = lchroma * sin(phase); + signal.z = lchroma * cos(phase); + + // color subcarrier signal, crosstalk + let chroma_phase = t * 60.0 * PI * 0.6667; + let mod_phase = chroma_phase + dot(uv, vec2f(1.0, 0.1)) * CHROMA_MOD_FREQ * XSIZE * 2.0; + let scarrier = SUB_CARRIER * lchroma; + let i_mod = cos(mod_phase); + let q_mod = sin(mod_phase); + + signal.x *= 1.0 + CROSS_TALK * scarrier * q_mod - y_interf * CROSS_Y_INTERFERENCE; + signal.y *= 1.0 + scarrier * i_mod; + signal.z *= 1.0 + scarrier * q_mod; + + // convert back to rgb + var col = yiqa_to_rgba(signal); + // Slight NTSC warm tint (boost red/green, attenuate blue) + col *= vec4f(1.04, 1.01, .94, 1.); +// col = dither_c64(col, uv, XSIZE, YSIZE); + + let border_col = get_border_c64(uv, uniforms.beat_time, YSIZE); + + let v_strength = vignette(uv); + let scanl = 0.82 + 0.5 * sin(PI * uv.y * uniforms.resolution.y / 2.); + col = scanl * mix(border_col, col, v_strength); + col = clamp(col, vec4f(0.), vec4f(1.0)); + + // Black outside screen edges + if (uv.x <= 0.0 || uv.x >= 1.0 || uv.y <= 0.0 || uv.y >= 1.0) { + // discard; + } + + col = debug_f32(col, in.position.xy / 2., vec2f(100., 75.), uniforms.beat_time); + col = debug_str(col, in.position.xy / 2., vec2f(100., 150.), vec4u(0x48656C6Cu, 0x6F000000u, 0u, 0u), 5u); + return col; +} |
