diff options
| author | skal <pascal.massimino@gmail.com> | 2026-03-09 01:37:57 +0100 |
|---|---|---|
| committer | skal <pascal.massimino@gmail.com> | 2026-03-09 01:37:57 +0100 |
| commit | cc13cf590ae524b509486d26029a9cc949a5d404 (patch) | |
| tree | cbd8cc9d1c38bf7d0edde2e88d5709f5f7812927 /src/effects | |
| parent | ad0f386e1b5cd9148ad278c7f016734af00cd0bc (diff) | |
add dithering
Diffstat (limited to 'src/effects')
| -rw-r--r-- | src/effects/ntsc.wgsl | 90 |
1 files changed, 79 insertions, 11 deletions
diff --git a/src/effects/ntsc.wgsl b/src/effects/ntsc.wgsl index 8d8d536..ad29c79 100644 --- a/src/effects/ntsc.wgsl +++ b/src/effects/ntsc.wgsl @@ -22,7 +22,8 @@ const CROSS_TALK = 0.1; const CROSS_Y_INTERFERENCE = 30.; const CHROMA_MOD_FREQ = (0.4 * PI); -const C64Colors = array<vec3f, 16>( +const NUM_COLORS : u32 = 16; +const C64Colors = array<vec3f, NUM_COLORS>( 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 @@ -51,7 +52,7 @@ const C64Colors = array<vec3f, 16>( // 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.65 + .5; + return uv * (1.0 + scale * strength * (uv * uv).yx) * 0.60 + .50; } fn vignette(uv: vec2f) -> f32 { @@ -84,7 +85,7 @@ fn yiqa_to_rgba(yiq: vec4f) -> vec4f { // 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 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); @@ -123,16 +124,80 @@ fn randomized_f32(p: vec2f, t: f32) -> f32 { return hash_2f_alt(vec2f(p * 0.152 + t * 1500. + 50.0)); } +const BAYER_8X8 = array<u32, 64>( + 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 = round(bt * 2.) % 2.; + 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); @@ -141,7 +206,6 @@ fn randomized_f32(p: vec2f, t: f32) -> f32 { 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.)); // luma fringing let d = (BLUR_SIZE + y_interf * 100.0) / XSIZE; @@ -176,17 +240,21 @@ fn randomized_f32(p: vec2f, t: f32) -> f32 { 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.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); + 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; |
