diff options
| -rw-r--r-- | src/effects/ntsc.wgsl | 207 | ||||
| -rw-r--r-- | src/shaders/debug/debug_print.wgsl | 9 | ||||
| -rw-r--r-- | src/shaders/math/noise.wgsl | 7 | ||||
| -rw-r--r-- | src/shaders/render/scratch_lines.wgsl | 36 | ||||
| -rw-r--r-- | workspaces/main/music.track | 2 |
5 files changed, 193 insertions, 68 deletions
diff --git a/src/effects/ntsc.wgsl b/src/effects/ntsc.wgsl index 4e72f0a..8d8d536 100644 --- a/src/effects/ntsc.wgsl +++ b/src/effects/ntsc.wgsl @@ -1,74 +1,193 @@ -// NTSC post-process effect with fisheye/barrel distortion #include "sequence_uniforms" #include "render/fullscreen_uv_vs" #include "math/noise" #include "debug/debug_print" -const vignetteRounding = 160.0f; -const vignetteSmoothness = 0.7f; -const fisheyeStrength = vec2f(.1, .24); +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 C64Colors = array<vec3f, 16>( + 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<f32>; @group(0) @binding(2) var<uniform> uniforms: UniformsSequenceParams; // Barrel (fisheye) distortion: strength > 0 = barrel, < 0 = pincushion -fn fisheye(uv: vec2f, strength: f32) -> vec2f { - let r2 = uv * uv; - return uv * 1.2 * (1.0 + fisheyeStrength * strength * r2); +fn fisheye(uv: vec2f, scale: f32) -> vec2f { + const strength = vec2f(0.1, 0.24); + return uv * (1.0 + scale * strength * (uv * uv).yx) * 0.65 + .5; } fn vignette(uv: vec2f) -> f32 { - let uv2 = uv * 1.99; - let amount = 1.0 - sqrt(pow(abs(uv2.x), vignetteRounding) + pow(abs(uv2.y), vignetteRounding)); - let vhard = smoothstep(0., vignetteSmoothness, amount); - return vhard; + 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 = textureSample(input_texture, input_sampler, 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<f32, 2 * 12 + 1>( + 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<f32, 2 * 12 + 1>( + 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)); } @fragment fn fs_main(in: VertexOutput) -> @location(0) vec4f { let t = uniforms.time; + let bt = uniforms.beat_phase; // Fisheye/barrel distortion - let uv = (fisheye(in.st, 0.18) + 1.) * .5; -// uv = vignette(uv); + var uv = fisheye(in.st, 0.18); + let mframe = round(bt * 2.) % 2.; + uv.y += mframe * SCAN_FLICKER / uniforms.resolution.y; // flicker at target resolution - // Black outside screen edges - if (uv.x < 0.0 || uv.x > 1.0 || uv.y < 0.0 || uv.y > 1.0) { - return vec4f(0.0, 0.0, 0.0, 1.0); - } - // Chroma separation (horizontal RGB bleeding) - let bleed = 2.5 / uniforms.resolution.x; - let r = textureSample(input_texture, input_sampler, uv + vec2f( bleed, 0.0)).r; - let g = textureSample(input_texture, input_sampler, uv ).g; - let b = textureSample(input_texture, input_sampler, uv - vec2f( bleed, 0.0)).b; - var col = vec3f(r, g, b); + // 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; } - // Scanlines - let scan = sin(uv.y * uniforms.resolution.y * 3.14159265) * 0.5 + 0.5; - col *= 0.82 + 0.18 * scan; + 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; + uv = clamp(uv, vec2f(0.), vec2f(1.)); - // Per-pixel temporal noise - let pixel = floor(uv * uniforms.resolution); - let n = hash_2f(pixel + vec2f(t * 47.3, t * 31.7)) * 0.08 - 0.04; - col += n; - - // Horizontal jitter line (random scanline rolling artifact) - let jitter_y = fract(t * 0.37); - let jitter_band = abs(uv.y - jitter_y); - if (jitter_band < 0.003) { - col += hash_1f(uv.x * 100.0 + t) * 0.15; + // 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.)); - // Vignette (stronger at corners due to fisheye) - let vig = in.uv * 2.0 - 1.0; - col *= 1.0 - dot(vig, vig) * 0.45; + // 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) - col.r *= 1.04; - col.g *= 1.01; - col.b *= 0.94; + 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; - col = debug_f32(col, in.position.xy / uniforms.resolution, vec2f(10., 10.), 3.2); - return vec4f(clamp(col, vec3f(0.0), vec3f(1.0)), 1.0); + + let v_strength = vignette(uv); + let scanl = (0.82 + 0.18 * sin(PI * uv.y * uniforms.resolution.y / 2.)); + var col = clamp(scanl * yiqa_to_rgba(signal), vec4f(0.0), vec4f(1.0)); + let id = round(hash_1f(round(t * .02 + uv.x + YSIZE * uv.y / 8.) + 3. * round(hash_1f(t * 533.) * 24.)) * 16.); + let border_col = vec4f(C64Colors[u32(id)], 1.); + col = mix(border_col, col, v_strength); + // 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; } diff --git a/src/shaders/debug/debug_print.wgsl b/src/shaders/debug/debug_print.wgsl index 22b5838..7e6debe 100644 --- a/src/shaders/debug/debug_print.wgsl +++ b/src/shaders/debug/debug_print.wgsl @@ -11,6 +11,8 @@ // each component holds 4 chars (MSB = leftmost char) // e.g. "Hi!" -> vec4u(0x48692100u, 0u, 0u, 0u), 3u +const ink_color = vec4f(1.0, 1.0, 0.0, 1.0); // yellow + // Returns lit pixel (0 or 1) for ASCII code [0x20-0x7E], row r [0-7], col c [0-7]. // Encoding: 2 u32s per glyph; hi covers rows 0-3, lo covers rows 4-7. // Within each u32, row r occupies bits [(3-r%4)*8+7 : (3-r%4)*8]. @@ -217,8 +219,7 @@ fn _dbg_char(ascii: u32, r: u32, c: u32) -> u32 { // pixel_pos : @builtin(position).xy // origin : top-left corner of text in screen pixels // value : f32 to display (format: [-]DDD.DDD, 64×8 px) -fn debug_f32(col: vec3f, pixel_pos: vec2f, origin: vec2f, value: f32) -> vec3f { - let ink_color = vec3f(1.0, 1.0, 0.0); // yellow +fn debug_f32(col: vec4f, pixel_pos: vec2f, origin: vec2f, value: f32) -> vec4f { let lp = pixel_pos - origin; if (lp.x < 0.0 || lp.x >= 64.0 || lp.y < 0.0 || lp.y >= 8.0) { return col; @@ -256,8 +257,8 @@ fn debug_f32(col: vec3f, pixel_pos: vec2f, origin: vec2f, value: f32) -> vec3f { // // Example — "Hello" (5 chars, ASCII 0x48 0x65 0x6C 0x6C 0x6F): // debug_str(col, pos.xy, origin, vec4u(0x48656C6Cu, 0x6F000000u, 0u, 0u), 5u) -fn debug_str(col: vec3f, pixel_pos: vec2f, origin: vec2f, s: vec4u, len: u32) -> vec3f { - let ink_color = vec3f(1.0, 1.0, 0.0); // yellow +fn debug_str(col: vec4f, pixel_pos: vec2f, origin: vec2f, s: vec4u, len: u32) -> +vec4f { let lp = pixel_pos - origin; let max_w = f32(len * 8u); if (lp.x < 0.0 || lp.x >= max_w || lp.y < 0.0 || lp.y >= 8.0) { diff --git a/src/shaders/math/noise.wgsl b/src/shaders/math/noise.wgsl index dd97e02..dff6619 100644 --- a/src/shaders/math/noise.wgsl +++ b/src/shaders/math/noise.wgsl @@ -21,6 +21,13 @@ fn hash_2f(p: vec2f) -> f32 { return fract(sin(h) * 43758.5453123); } +// variant for vec2f -> f32 hashing +fn hash_2f_alt(p: vec2f) -> f32 { + var p3 = fract(p.xyx * .1376); + p3 += dot(p3, p3.yzx + 33.33); + return fract((p3.x + p3.y) * p3.z); +} + // Hash: vec2f -> vec2f // 2D coordinate to 2D hash (from Shadertoy 4djSRW) fn hash_2f_2f(p: vec2f) -> vec2f { diff --git a/src/shaders/render/scratch_lines.wgsl b/src/shaders/render/scratch_lines.wgsl index 04ed6f8..6123486 100644 --- a/src/shaders/render/scratch_lines.wgsl +++ b/src/shaders/render/scratch_lines.wgsl @@ -12,24 +12,22 @@ #include "math/noise" fn scratch_lines(uv: vec2f, resolution: vec2f, time: f32) -> f32 { - // Quantize to ~24fps to simulate film frame rate - let t = floor(time * 24.0); + // Quantize to ~24fps to simulate film frame rate + let t = floor(time * 24.0); - var intensity = 0.0; - for (var i = 0; i < 8; i++) { - let seed = f32(i) * 17.3 + t; - // ~25% chance this scratch is visible this frame - let visible = step(0.75, hash_1f(seed + 3.1)); - // Random vertical position [0, 1] - let y = hash_1f(seed + 7.9); - // Thickness in pixels [1, 4] - let thickness = hash_1f(seed + 13.5) * 3.0 + 1.0; - // Brightness [0.5, 1.0] - let brightness = hash_1f(seed + 21.7) * 0.5 + 0.5; - // Soft linear falloff from line center - let dy = abs(uv.y - y) * resolution.y; - let line = clamp(1.0 - dy / thickness, 0.0, 1.0); - intensity += line * brightness * visible; - } - return clamp(intensity, 0.0, 1.0); + // Scanlines + let scan = abs(sin(uv.y * uniforms.resolution.y * 3.14159265)); + var intensity = 0.2 * scan; + + // Per-pixel temporal noise + let pixel = floor(uv * uniforms.resolution * .4); + let n = hash_2f(pixel + vec2f(time * 47.3, time * 31.7)) * 0.04 - 0.02; + intensity += n; + + // Horizontal jitter line (random scanline rolling artifact) + let jitter_y = hash_1f(fract(t * 0.37)); + let jitter_band = abs(uv.y - jitter_y); + let scratch = smoothstep(5. / uniforms.resolution.y, 0., jitter_band); + intensity += scratch * 0.11 * hash_1f(uv.x * 100.0 + t); + return clamp(intensity, 0.0, 1.0); } diff --git a/workspaces/main/music.track b/workspaces/main/music.track index 7ac016c..231ac3b 100644 --- a/workspaces/main/music.track +++ b/workspaces/main/music.track @@ -10,7 +10,7 @@ # # TIMING: Unit-less (1 unit = 4 beats at 120 BPM = 2 seconds) -BPM 120 +BPM 184 SAMPLE ASSET_KICK_1 SAMPLE ASSET_KICK_2 |
