#include "sequence_uniforms" #include "render/fullscreen_uv_vs" #include "math/noise" #include "debug/debug_print" 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); const NUM_COLORS : u32 = 16; const C64Colors = array( vec3f(13.0/255.0, 13.0/255.0, 13.0/255.0), // 0 black vec3f(242.0/255.0, 242.0/255.0, 242.0/255.0), // 1 white vec3f(89.0/255.0, 39.0/255.0, 33.0/255.0), // 2 red vec3f(170.0/255.0, 220.0/255.0, 240.0/255.0), // 3 cyan vec3f(101.0/255.0, 27.0/255.0, 109.0/255.0), // 4 purple vec3f(120.0/255.0, 198.0/255.0, 112.0/255.0), // 5 green vec3f(54.0/255.0, 53.0/255.0, 152.0/255.0), // 6 blue vec3f(226.0/255.0, 229.0/255.0, 115.0/255.0), // 7 yellow vec3f(141.0/255.0, 85.0/255.0, 50.0/255.0), // 8 orange vec3f(89.0/255.0, 63.0/255.0, 13.0/255.0), // 9 brown vec3f(189.0/255.0, 110.0/255.0, 115.0/255.0), // 10 pink vec3f(80.0/255.0, 80.0/255.0, 80.0/255.0), // 11 dark gray vec3f(124.0/255.0, 124.0/255.0, 124.0/255.0), // 12 gray vec3f(165.0/255.0, 242.0/255.0, 156.0/255.0), // 13 bright green vec3f(114.0/255.0, 111.0/255.0, 224.0/255.0), // 14 light blue vec3f(128.0/255.0, 128.0/255.0, 128.0/255.0) // 15 middle gray ); @group(0) @binding(0) var input_sampler: sampler; @group(0) @binding(1) var input_texture: texture_2d; @group(0) @binding(2) var uniforms: UniformsSequenceParams; // 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); } fn rgba_to_yiqa(rgba: vec4f) -> vec4f { const rgb_yiq_mat = mat4x4f( 0.299, 0.596, 0.211, 0., 0.587, -0.274, -0.523, 0., 0.114, -0.322, 0.312, 0., 0., 0., 0., 1., ); return rgb_yiq_mat * rgba; } fn yiqa_to_rgba(yiq: vec4f) -> vec4f { const yiq_rgb_mat = mat4x4f( 1.000, 1.000, 1.000, 0., 0.956, -0.272, -1.106, 0., 0.621, -0.647, 1.703, 0., 0., 0., 0., 1., ); return yiq_rgb_mat * yiq; } // returns Luma, chroma subcarrier level, phase, transparency fn get_luma_chroma_phase_a(uv: vec2f) -> vec4f { let rgba = Dither(textureSample(input_texture, input_sampler, uv), uv); let yiq = rgba_to_yiqa(textureSample(input_texture, input_sampler, uv)); let chroma_level = length(yiq.yz); // 22.5 degrees phase shift between lines // let mscanl = (uv.y * uniforms.resolution.y / 4.) % 2.0; let mscanl = (uv.y * YSIZE) % 2.0; let phase = atan2(yiq.y, yiq.z) - 0.3926991 + mscanl * 0.19634954; return vec4f(yiq.x, chroma_level, phase / TAU, yiq.a); } fn get_value(uv: vec2f, off: f32, yscale: f32) -> vec4f { return get_luma_chroma_phase_a(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); } // 12-taps horizontal filtering const luma_filter = array( 0.0105, 0.0134, 0.0057,-0.0242,-0.0824, -0.1562,-0.2078,-0.1850,-0.0546, 0.1626, 0.3852, 0.5095, 0.5163, 0.4678, 0.2844, 0.0515,-0.1308,-0.2082,-0.1891,-0.1206, -0.0511,-0.0065, 0.0114, 0.0127, 0.008 ); const chroma_filter = array( 0.001, 0.0010, 0.0001, 0.0002, -0.0003, 0.0062, 0.0120,-0.0079, 0.0978, 0.1059, -0.0394, 0.2732, 0.2941, 0.1529, -0.021, 0.1347, 0.0415,-0.0032, 0.0115, 0.002, -0.0001, 0.0002, 0.001, 0.001, 0.001 ); fn randomized_f32(p: vec2f, t: f32) -> f32 { return hash_2f_alt(vec2f(p * 0.152 + t * 1500. + 50.0)); } const BAYER_8X8 = array( 0, 32, 8, 40, 2, 34, 10, 42, 48, 16, 56, 24, 50, 18, 58, 26, 12, 44, 4, 36, 14, 46, 6, 38, 60, 28, 52, 20, 62, 30, 54, 22, 3, 35, 11, 43, 1, 33, 9, 41, 51, 19, 59, 27, 49, 17, 57, 25, 15, 47, 7, 39, 13, 45, 5, 37, 63, 31, 55, 23, 61, 29, 53, 21 ); const DistanceParam = 0.65; fn colorDistance(color: vec4f, c1: vec4f, c2: vec4f, frac: f32) -> f32 { return mix(distance(color, mix(c1, c2, frac)), distance(color, c1) + distance(color, c2), 0.5 * DistanceParam * DistanceParam); } fn Dither(col: vec4f, uv: vec2f) -> vec4f { let ix = u32(floor(uv.x * XSIZE / 2.)) % 8; let iy = u32(floor(uv.y * YSIZE / 2.)) % 8; let thresh = f32(BAYER_8X8[ix + 8 * iy]) / 64.; var color = min(col, vec4f(1.)); color *= color; // ~gamma var c1 = vec4f(0.); var c2 = vec4f(0.); var frac = 0.0; var best = 1.e5; for (var i = 0u; i < NUM_COLORS; i += 1u) { var p1 = vec4f(C64Colors[i], 1.); p1 *= p1; for (var j = i + 1u; j < NUM_COLORS; j += 1u) { var p2 = vec4f(C64Colors[j], 1.); p2 *= p2; // min-L2 optim let dc = p1 - p2; var a = dot(dc, p1 - color) / dot(dc, dc); a = clamp(a, 0., 1.); let dist = colorDistance(color, p1, p2, a); if (dist < best) { best = dist; c1 = p1; c2 = p2; frac = a; } } } color = mix(c1, c2, f32(frac > thresh)); // color = mix(c1, c2, frac); return sqrt(color); } fn get_border_col(uv: vec2f) -> vec4f { let t = uniforms.beat_time; let offset = uv.x + YSIZE * uv.y / 8.; let phase = 3. * round(hash_1f(t * 533.) * 24.); let id = round(hash_1f(round(sin(t * 1.6) + offset + phase)) * 8.); let border_col = vec4f(C64Colors[u32(id)], 1.); return border_col; } @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 lc_signal = vec4f(0.0); for (var i = 0; i < 25; i += 1) { let offset = f32(i) - 12.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); lc_signal += vec4f(suml.x, sumc.y, sumc.z, suml.a); } let base = get_luma_chroma_phase_a(uv); var signal = mix(base, lc_signal, vec4f(LUMA_BLUR, CHROMA_BLUR, CHROMA_BLUR, 1.)); // 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); // Slight NTSC warm tint (boost red/green, attenuate blue) signal *= vec4f(1.04, 1.01, .94, 1.); // 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; var col = yiqa_to_rgba(signal); col = Dither(col, uv); let border_col = get_border_col(uv); 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., 0., 0., 1.), 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; }