summaryrefslogtreecommitdiff
path: root/src/effects
diff options
context:
space:
mode:
Diffstat (limited to 'src/effects')
-rw-r--r--src/effects/ntsc.wgsl160
-rw-r--r--src/effects/ntsc_effect.h14
-rw-r--r--src/effects/ntsc_rgb.wgsl20
-rw-r--r--src/effects/ntsc_yiq.wgsl19
-rw-r--r--src/effects/shaders.cc5
-rw-r--r--src/effects/shaders.h3
6 files changed, 57 insertions, 164 deletions
diff --git a/src/effects/ntsc.wgsl b/src/effects/ntsc.wgsl
deleted file mode 100644
index 3ee02bc..0000000
--- a/src/effects/ntsc.wgsl
+++ /dev/null
@@ -1,160 +0,0 @@
-// NTSC post-process effect: fisheye distortion, scanlines, and color bleeding.
-// Produces a retro CRT/NTSC look using YIQ color space and C64-style dithering.
-#include "sequence_uniforms"
-#include "render/fullscreen_uv_vs"
-#include "math/noise"
-#include "math/color"
-#include "math/color_c64"
-#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);
-
-
-@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, 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);
-}
-
-// returns Luma, chroma subcarrier level, phase, transparency
-fn get_luma_chroma_phase_a(uv: vec2f) -> vec4f {
- let rgba = textureSample(input_texture, input_sampler, uv);
- return rgba_to_luma_chroma_phase(rgba, uv.y, YSIZE);
-}
-
-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);
-}
-
-// 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_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 = get_luma_chroma_phase_a(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;
-}
diff --git a/src/effects/ntsc_effect.h b/src/effects/ntsc_effect.h
index d8624ea..4737291 100644
--- a/src/effects/ntsc_effect.h
+++ b/src/effects/ntsc_effect.h
@@ -1,12 +1,22 @@
-// NTSC post-process effect with fisheye distortion
+// NTSC post-process effects: RGB and YIQ input variants.
#pragma once
#include "effects/shaders.h"
#include "gpu/wgsl_effect.h"
+// RGB input: converts RGB → YIQ internally (standard post-process use).
struct Ntsc : public WgslEffect {
Ntsc(const GpuContext& ctx, const std::vector<std::string>& inputs,
const std::vector<std::string>& outputs, float start_time,
float end_time)
: WgslEffect(ctx, inputs, outputs, start_time, end_time,
- ntsc_shader_wgsl) {}
+ ntsc_rgb_shader_wgsl) {}
+};
+
+// YIQ input: input texture already stores luma/chroma/phase (e.g. RotatingCube output).
+struct NtscYiq : public WgslEffect {
+ NtscYiq(const GpuContext& ctx, const std::vector<std::string>& inputs,
+ const std::vector<std::string>& outputs, float start_time,
+ float end_time)
+ : WgslEffect(ctx, inputs, outputs, start_time, end_time,
+ ntsc_yiq_shader_wgsl) {}
};
diff --git a/src/effects/ntsc_rgb.wgsl b/src/effects/ntsc_rgb.wgsl
new file mode 100644
index 0000000..09adbf1
--- /dev/null
+++ b/src/effects/ntsc_rgb.wgsl
@@ -0,0 +1,20 @@
+// NTSC post-process effect (RGB input): fisheye distortion, scanlines, color bleeding.
+// Input texture is in RGB color space; converted to YIQ for NTSC processing.
+#include "sequence_uniforms"
+#include "render/fullscreen_uv_vs"
+#include "math/noise"
+#include "math/color"
+#include "math/color_c64"
+#include "debug/debug_print"
+
+@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;
+
+// RGB input: sample texture and convert to luma/chroma/phase
+fn sample_ntsc_signal(uv: vec2f) -> vec4f {
+ let rgba = textureSample(input_texture, input_sampler, uv);
+ return rgba_to_luma_chroma_phase(rgba, uv.y, YSIZE);
+}
+
+#include "render/ntsc_common"
diff --git a/src/effects/ntsc_yiq.wgsl b/src/effects/ntsc_yiq.wgsl
new file mode 100644
index 0000000..8ab36d3
--- /dev/null
+++ b/src/effects/ntsc_yiq.wgsl
@@ -0,0 +1,19 @@
+// NTSC post-process effect (YIQ input): fisheye distortion, scanlines, color bleeding.
+// Input texture already stores luma/chroma/phase (e.g. from RotatingCube output).
+#include "sequence_uniforms"
+#include "render/fullscreen_uv_vs"
+#include "math/noise"
+#include "math/color"
+#include "math/color_c64"
+#include "debug/debug_print"
+
+@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;
+
+// YIQ input: texture already stores luma/chroma/phase — pass through directly
+fn sample_ntsc_signal(uv: vec2f) -> vec4f {
+ return textureSample(input_texture, input_sampler, uv);
+}
+
+#include "render/ntsc_common"
diff --git a/src/effects/shaders.cc b/src/effects/shaders.cc
index 44dfa0b..8a81bb0 100644
--- a/src/effects/shaders.cc
+++ b/src/effects/shaders.cc
@@ -55,6 +55,8 @@ void InitShaderComposer() {
AssetId::ASSET_SHADER_DEBUG_DEBUG_PRINT);
register_if_exists("render/scratch_lines",
AssetId::ASSET_SHADER_RENDER_SCRATCH_LINES);
+ register_if_exists("render/ntsc_common",
+ AssetId::ASSET_SHADER_RENDER_NTSC_COMMON);
register_if_exists("render/fullscreen_vs",
AssetId::ASSET_SHADER_RENDER_FULLSCREEN_VS);
register_if_exists("render/fullscreen_uv_vs",
@@ -103,7 +105,8 @@ const char* flash_shader_wgsl = SafeGetAsset(AssetId::ASSET_SHADER_FLASH);
const char* scene1_shader_wgsl = SafeGetAsset(AssetId::ASSET_SHADER_SCENE1);
const char* scene2_shader_wgsl = SafeGetAsset(AssetId::ASSET_SHADER_SCENE2);
const char* scratch_shader_wgsl = SafeGetAsset(AssetId::ASSET_SHADER_SCRATCH);
-const char* ntsc_shader_wgsl = SafeGetAsset(AssetId::ASSET_SHADER_NTSC);
+const char* ntsc_rgb_shader_wgsl = SafeGetAsset(AssetId::ASSET_SHADER_NTSC_RGB);
+const char* ntsc_yiq_shader_wgsl = SafeGetAsset(AssetId::ASSET_SHADER_NTSC_YIQ);
// Compute shaders
const char* gen_noise_compute_wgsl =
diff --git a/src/effects/shaders.h b/src/effects/shaders.h
index 0686b7e..527a8a3 100644
--- a/src/effects/shaders.h
+++ b/src/effects/shaders.h
@@ -17,7 +17,8 @@ extern const char* flash_shader_wgsl;
extern const char* scene1_shader_wgsl;
extern const char* scene2_shader_wgsl;
extern const char* scratch_shader_wgsl;
-extern const char* ntsc_shader_wgsl;
+extern const char* ntsc_rgb_shader_wgsl;
+extern const char* ntsc_yiq_shader_wgsl;
// Compute shaders
extern const char* gen_noise_compute_wgsl;