summaryrefslogtreecommitdiff
path: root/src/shaders/math
diff options
context:
space:
mode:
Diffstat (limited to 'src/shaders/math')
-rw-r--r--src/shaders/math/color.wgsl36
-rw-r--r--src/shaders/math/color_c64.wgsl83
2 files changed, 119 insertions, 0 deletions
diff --git a/src/shaders/math/color.wgsl b/src/shaders/math/color.wgsl
index 9352053..0639bf7 100644
--- a/src/shaders/math/color.wgsl
+++ b/src/shaders/math/color.wgsl
@@ -1,4 +1,40 @@
// Common color space and tone mapping functions.
+// Includes: sRGB, ACES, HSV, YIQ (NTSC) conversions.
+// Use rgba_to_luma_chroma_phase() to encode YIQ into a temp buffer for NTSC-style effects.
+
+const TAU_COLOR = 6.28318530718;
+
+// RGB <-> YIQ (NTSC) color space
+fn rgba_to_yiqa(rgba: vec4f) -> vec4f {
+ const m = 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 m * rgba;
+}
+
+fn yiqa_to_rgba(yiq: vec4f) -> vec4f {
+ const m = 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 m * yiq;
+}
+
+// Convert RGBA to packed luma/chroma/phase signal for NTSC processing.
+// Returns vec4f(luma, chroma_level, phase/TAU, alpha).
+// ysize: virtual scanline count (e.g. 33.*8.); drives 22.5-degree inter-line phase shift.
+fn rgba_to_luma_chroma_phase(rgba: vec4f, uv_y: f32, ysize: f32) -> vec4f {
+ let yiq = rgba_to_yiqa(rgba);
+ let chroma_level = length(yiq.yz);
+ 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_COLOR, yiq.a);
+}
// sRGB to Linear approximation
// Note: Assumes input is in sRGB color space.
diff --git a/src/shaders/math/color_c64.wgsl b/src/shaders/math/color_c64.wgsl
new file mode 100644
index 0000000..ef8f073
--- /dev/null
+++ b/src/shaders/math/color_c64.wgsl
@@ -0,0 +1,83 @@
+// C64 palette and ordered-dither quantization.
+// Provides Dither() to snap any RGBA color to the nearest C64 color pair
+// via 8x8 Bayer threshold, suitable for NTSC and retro post-process effects.
+
+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
+ 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
+);
+
+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);
+}
+
+// Quantize col to the nearest C64 color pair using 8x8 Bayer dithering.
+// xsize/ysize: virtual pixel grid dimensions (e.g. 54.*8., 33.*8.).
+fn Dither(col: vec4f, uv: vec2f, xsize: f32, ysize: f32) -> 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));
+ return sqrt(color);
+}