diff --git a/crates/effects/src/lib.rs b/crates/effects/src/lib.rs index bc1cd520..ebb44d9e 100644 --- a/crates/effects/src/lib.rs +++ b/crates/effects/src/lib.rs @@ -807,6 +807,29 @@ pub const GPU_EFFECTS: &[&str] = &[ "ec.key.linearcolor", "ec.keying.keylight", // effectcraft-gpu fx_key (keying, matte, channel) + "ec.key.colorkey", + "ec.key.luma", + "ec.key.colorrange", + "ec.key.extract", + "ec.key.differencematte", + "ec.key.colordifference", + "ec.key.screen", + "ec.key.spill", + "ec.key.advancedspill", + "ec.key.keycleaner", + "ec.key.unmult", + "ec.matte.simplechoker", + "ec.matte.mattechoker", + "ec.channel.setmatte", + "ec.channel.setchannels", + "ec.channel.shiftchannels", + "ec.channel.removecolormatting", + "ec.channel.arithmetic", + "ec.channel.solidcomposite", + "ec.channel.combiner", + "ec.channel.blend", + "ec.channel.calculations", + "ec.channel.compoundarithmetic", // // effectcraft-gpu fx_stylize (stylize, distort) // diff --git a/crates/gpu/src/fx_color.rs b/crates/gpu/src/fx_color.rs index ee6ab7e8..f411e97e 100644 --- a/crates/gpu/src/fx_color.rs +++ b/crates/gpu/src/fx_color.rs @@ -235,7 +235,7 @@ fn morph(e: &mut Enc, img: &GpuImage, r: u32, modes: [u32; 4]) -> GpuImage { } /// util::morph_frac (blend between the two nearest integer radii). -fn morph_frac(e: &mut Enc, img: &GpuImage, radius: f64, modes: [u32; 4]) -> GpuImage { +pub(crate) fn morph_frac(e: &mut Enc, img: &GpuImage, radius: f64, modes: [u32; 4]) -> GpuImage { if radius <= 0.0 { return img.clone(); } @@ -253,8 +253,8 @@ fn morph_frac(e: &mut Enc, img: &GpuImage, radius: f64, modes: [u32; 4]) -> GpuI out } -const ERODE_X: [u32; 4] = [0, 2, 2, 2]; -const DILATE_X: [u32; 4] = [1, 2, 2, 2]; +pub(crate) const ERODE_X: [u32; 4] = [0, 2, 2, 2]; +pub(crate) const DILATE_X: [u32; 4] = [1, 2, 2, 2]; // ---------------------------------------------------------------- Key Light diff --git a/crates/gpu/src/fx_key.rs b/crates/gpu/src/fx_key.rs index 05625d4c..b4709520 100644 --- a/crates/gpu/src/fx_key.rs +++ b/crates/gpu/src/fx_key.rs @@ -1,19 +1,551 @@ -//! GPU effects, keying, matte and channel family (kernels in `shaders/fx_key.wgsl`, every entry point prefixed -//! `fxk_`): the CPU effects' exact steps with the pixel loops as compute kernels. +//! GPU effects, keying, matte and channel family (kernels in `shaders/fx_key.wgsl`, every entry +//! point prefixed `fxk_`): the CPU effects' exact steps with the pixel loops as compute kernels. +//! +//! The keyers (Color Key, Luma Key, Color Range, Extract, Difference Matte, Color Difference +//! Key, Screen Key) build their matte in a plane pass, run the CPU's edge thin (morphology) and +//! feather (Gaussian) on it, and apply it in a second pass; spill suppression, Unmult and the +//! channel effects are one per-pixel pass (`fxk_point`). Simple / Matte Choker and Key Cleaner +//! work on the alpha plane and put it back with `util::set_alpha` (straight colour kept, the +//! transparent pixels' colour from a small blur). Advanced Spill Suppressor's Standard method +//! finds the dominant key primary with a GPU sum reduction (`fxk_reduce`). Other layers (layer +//! parameters) are resampled into the buffer like `util::fit_layer` (`fxk_fit`). +use effectcraft_color::rgb_to_hsl; use effectcraft_effects::EffectCtx; +use effectcraft_effects::util::{SRC_ORDER, Src}; -use crate::context::Enc; -use crate::effects::GBuf; +use crate::context::{Enc, GpuImage, Params}; +use crate::effects::{GBuf, gaussian_blur}; +use crate::fx_color::{DILATE_X, ERODE_X, morph_frac}; /// Compute entry points in `fx_key.wgsl`. -pub(crate) const KERNELS: &[&str] = &[]; +pub(crate) const KERNELS: &[&str] = &["fxk_point", "fxk_fit", "fxk_reduce"]; /// Effect ids implemented here. -pub(crate) const IDS: &[&str] = &[]; +pub(crate) const IDS: &[&str] = &[ + "ec.key.colorkey", + "ec.key.luma", + "ec.key.colorrange", + "ec.key.extract", + "ec.key.differencematte", + "ec.key.colordifference", + "ec.key.screen", + "ec.key.spill", + "ec.key.advancedspill", + "ec.key.keycleaner", + "ec.key.unmult", + "ec.matte.simplechoker", + "ec.matte.mattechoker", + "ec.channel.setmatte", + "ec.channel.setchannels", + "ec.channel.shiftchannels", + "ec.channel.removecolormatting", + "ec.channel.arithmetic", + "ec.channel.solidcomposite", + "ec.channel.combiner", + "ec.channel.blend", + "ec.channel.calculations", + "ec.channel.compoundarithmetic", +]; /// Run effect `id` (one of [`IDS`]); `None` = this parameter combination runs on the CPU. pub(crate) fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option { - let _ = (e, id, ctx, b); - None + match id { + "ec.key.colorkey" | "ec.key.luma" | "ec.key.colorrange" | "ec.key.extract" => matte_key(e, id, ctx, b), + "ec.key.differencematte" => difference_matte(e, ctx, b), + "ec.key.screen" => screen_key(e, ctx, b), + "ec.key.advancedspill" => advanced_spill(e, ctx, b), + "ec.key.keycleaner" => key_cleaner(e, ctx, b), + "ec.matte.simplechoker" => simple_choker(e, ctx, b), + "ec.matte.mattechoker" => matte_choker(e, ctx, b), + "ec.channel.setchannels" => set_channels(e, ctx, b), + _ => point(e, id, ctx, b), + } +} + +// ---------------------------------------------------------------- helpers + +/// One `fxk_point` pass over `src`'s size. +fn run(e: &mut Enc, p: &Params, src: &GpuImage, aux: Option<&GpuImage>, data: Option<&wgpu::Buffer>) -> GpuImage { + let out = e.image(src.width, src.height); + e.pixels("fxk_point", p, src, aux, &out, data); + out +} + +fn op(code: u32) -> Params { + let mut p = Params::default(); + p.u[0][0] = code; + p +} + +fn src_index(s: Src) -> u32 { + SRC_ORDER.iter().position(|x| *x == s).unwrap_or(10) as u32 +} + +/// Layer parameter `id` resampled into the buffer's pixel grid (`util::fit_layer`): +/// `Some(None)` when no layer is chosen (the effect then uses its own layer). +fn fitted(e: &mut Enc, ctx: &EffectCtx, b: &GBuf, id: &str, masks_and_effects: bool, stretch: bool) -> Option> { + let Some(o) = ctx.layer_param(id, masks_and_effects) else { return Some(None) }; + let (w, h) = (b.img.width, b.img.height); + if o.buf.img.is_empty() { + return Some(Some(e.image(w, h))); + } + let ls = ctx.layer_size; + let os = o.size; + let (sx, sy) = if stretch && ls[0] > 0.0 && ls[1] > 0.0 { (os[0] / ls[0], os[1] / ls[1]) } else { (1.0, 1.0) }; + let (dx, dy) = if stretch { (0.0, 0.0) } else { ((os[0] - ls[0]) * 0.5, (os[1] - ls[1]) * 0.5) }; + let inv = 1.0 / b.scale.max(1e-9); + // Per column / row: the other layer's buffer coordinate. + let mut data = Vec::with_capacity((w + h) as usize); + data.extend((0..w).map(|x| (((x as f64 + 0.5 - b.offset[0]) * inv * sx + dx) * o.buf.scale + o.buf.offset[0]) as f32)); + data.extend((0..h).map(|y| (((y as f64 + 0.5 - b.offset[1]) * inv * sy + dy) * o.buf.scale + o.buf.offset[1]) as f32)); + let src = e.g.upload_image(&o.buf.img)?; + let buf = e.data(&data); + let out = e.image(w, h); + e.pixels("fxk_fit", &Params::default(), &src, None, &out, Some(&buf)); + Some(Some(out)) +} + +/// `util::layer_or_self`. +fn layer_or_self(e: &mut Enc, ctx: &EffectCtx, b: &GBuf, id: &str, stretch: bool) -> Option { + Some(fitted(e, ctx, b, id, true, stretch)?.unwrap_or_else(|| b.img.clone())) +} + +/// keying::thin_feather on a matte plane (x channel). +fn thin_feather(e: &mut Enc, m: GpuImage, thin: f64, feather: f64) -> GpuImage { + let m = if thin > 0.0 { + morph_frac(e, &m, thin, ERODE_X) + } else if thin < 0.0 { + morph_frac(e, &m, -thin, DILATE_X) + } else { + m + }; + if feather > 0.0 { gaussian_blur(e, &m, feather * 0.5, feather * 0.5, true) } else { m } +} + +/// keying::apply_matte (view 0 final, 1 source, 2 matte). +fn apply_matte(e: &mut Enc, b: GBuf, m: &GpuImage, view: u32) -> GBuf { + if view == 1 { + return b; + } + let mut p = op(5); + p.u[0][1] = view; + let img = run(e, &p, &b.img, Some(m), None); + GBuf { img, ..b } +} + +/// keying::primary. +fn primary(c: [f32; 4]) -> [u32; 3] { + if c[1] >= c[0] && c[1] >= c[2] { + [1, 0, 2] + } else if c[2] >= c[0] && c[2] >= c[1] { + [2, 0, 1] + } else { + [0, 1, 2] + } +} + +/// util::set_alpha with the new alpha in `na`'s x channel. +fn set_alpha(e: &mut Enc, img: &GpuImage, na: &GpuImage) -> GpuImage { + let fill = gaussian_blur(e, img, 3.0, 3.0, true); + let (rows, row) = e.image_rows(&fill); + let mut p = op(27); + p.u[3][0] = row; + run(e, &p, img, Some(na), Some(&rows)) +} + +// ---------------------------------------------------------------- keyers + +fn matte_key(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option { + let pr = ctx.params; + let f = |k: &str| pr.f(k) as f32; + let mut p = Params::default(); + let mut thin = 0.0; + let mut feather = 0.0; + match id { + "ec.key.colorkey" => { + let k = pr.color("keyColor"); + p.u[0][0] = 1; + p.f[0] = [k[0], k[1], k[2], f("colorTolerance") / 255.0]; + thin = pr.f("edgeThin") * b.scale; + feather = pr.f("edgeFeather") * b.scale; + } + "ec.key.luma" => { + p.u[0] = [2, pr.e("keyType"), 0, 0]; + p.f[0] = [f("threshold") / 255.0, f("tolerance") / 255.0, 0.0, 0.0]; + thin = pr.f("edgeThin") * b.scale; + feather = pr.f("edgeFeather") * b.scale; + } + "ec.key.colorrange" => { + p.u[0] = [3, pr.e("colorSpace"), 0, 0]; + p.f[0] = [f("minL"), f("minA"), f("minB"), 0.0]; + p.f[1] = [f("maxL"), f("maxA"), f("maxB"), 0.0]; + p.f[2][0] = f("fuzziness"); + } + _ => { + p.u[0] = [4, pr.e("channel"), pr.b("invert") as u32, 0]; + p.f[0] = [f("blackPoint"), f("whitePoint"), f("blackSoftness"), f("whiteSoftness")]; + } + } + let m = run(e, &p, &b.img, None, None); + let m = thin_feather(e, m, thin, feather); + Some(apply_matte(e, b, &m, 0)) +} + +fn difference_matte(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let pr = ctx.params; + let view = pr.e("view"); + if view == 1 { + return Some(b); + } + let blur = pr.f("blurBeforeDifference") * b.scale; + let other = layer_or_self(e, ctx, &b, "differenceLayer", pr.e("ifLayerSizesDiffer") == 1)?; + let (src, other) = if blur > 0.05 { + (gaussian_blur(e, &b.img, blur * 0.5, blur * 0.5, true), gaussian_blur(e, &other, blur * 0.5, blur * 0.5, true)) + } else { + (b.img.clone(), other) + }; + let mut p = op(6); + p.f[0] = [(pr.f("matchingTolerance") / 100.0) as f32, (pr.f("matchingSoftness") / 100.0) as f32, 0.0, 0.0]; + let m = run(e, &p, &src, Some(&other), None); + Some(apply_matte(e, b, &m, if view == 2 { 3 } else { 4 })) +} + +/// keying::secondary's screen-key matte and composite. +fn screen_key(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let pr = ctx.params; + let view = pr.e("view"); + if view == 3 { + return Some(b); + } + let sc = pr.color("screenColor"); + let ix = primary(sc); + let (pi, o1, o2) = (ix[0] as usize, ix[1] as usize, ix[2] as usize); + let gain = pr.f("screenGain") as f32 / 100.0; + let bal = pr.f("screenBalance") as f32 / 100.0; + let cb = pr.f("clipBlack") as f32 / 100.0; + let cw = pr.f("clipWhite") as f32 / 100.0; + let preblur = pr.f("screenPreblur") * b.scale; + let sg = pr.f("screenShrinkGrow") * b.scale; + let soft = pr.f("screenSoftness") * b.scale; + let (hi, lo) = if sc[o1] > sc[o2] { (sc[o1], sc[o2]) } else { (sc[o2], sc[o1]) }; + let sd = (sc[pi] - (bal * hi + (1.0 - bal) * lo)).max(1e-3); + let key_src = if preblur > 0.0 { gaussian_blur(e, &b.img, preblur * 0.5, preblur * 0.5, true) } else { b.img.clone() }; + let mut p = op(22); + p.u[1] = [ix[0], ix[1], ix[2], 0]; + p.f[0] = [sd, gain, bal, cb]; + p.f[1][0] = cw; + let mut m = run(e, &p, &key_src, None, None); + if sg < 0.0 { + m = morph_frac(e, &m, -sg, ERODE_X); + } else if sg > 0.0 { + m = morph_frac(e, &m, sg, DILATE_X); + } + if soft > 0.0 { + m = gaussian_blur(e, &m, soft * 0.5, soft * 0.5, true); + } + let mut p = op(23); + p.u[0] = [23, view, (pr.e("edgeColor") == 1) as u32, 0]; + p.u[1] = [ix[0], ix[1], ix[2], 0]; + p.f[0] = [sc[0], sc[1], sc[2], bal]; + p.f[1][0] = pr.f("despill") as f32 / 100.0; + let img = run(e, &p, &b.img, Some(&m), None); + Some(GBuf { img, ..b }) +} + +fn advanced_spill(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let pr = ctx.params; + let ultra = pr.e("method") == 1; + let key = pr.color("ultraSettings/keyColor"); + let mut p = op(8); + let sums = if ultra { + let ix = primary(key); + p.u[1] = [ix[0], ix[1], ix[2], 0]; + None + } else { + // Standard: the dominant key primary over the whole buffer (sum reduction). + p.u[1][3] = 1; + let mut cur = b.img.clone(); + let mut first = true; + while first || cur.width > 1 || cur.height > 1 { + let out = e.image(cur.width.div_ceil(16), cur.height.div_ceil(16)); + let mut rp = Params::default(); + rp.u[0][0] = first as u32; + e.pixels("fxk_reduce", &rp, &cur, None, &out, None); + cur = out; + first = false; + } + Some(cur) + }; + let tol = if ultra { pr.get("ultraSettings/tolerance").map(|v| v.as_f64()).unwrap_or(100.0) as f32 / 100.0 } else { 1.0 }; + let f = |k: &str| pr.f(k) as f32 / 100.0; + let (desat, neutral) = if ultra { (f("ultraSettings/desaturate"), f("ultraSettings/spillColorCorrection")) } else { (0.0, 0.0) }; + p.f[0] = [f("suppression"), f("ultraSettings/spillRange"), f("ultraSettings/lumaCorrection"), tol]; + p.f[1] = [desat, neutral, rgb_to_hsl(key[0], key[1], key[2]).0, 0.0]; + let img = run(e, &p, &b.img, sums.as_ref(), None); + Some(GBuf { img, ..b }) +} + +fn key_cleaner(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let pr = ctx.params; + let r = (pr.f("additionalEdgeRadius") * b.scale).max(0.0); + let strength = pr.f("strength") as f32 / 100.0; + let chatter = pr.b("reduceChatter"); + let mut a = run(e, &op(24), &b.img, None, None); + if chatter { + a = gaussian_blur(e, &a, 1.0, 1.0, true); + } + let est = (r > 0.0 && strength > 0.0).then(|| gaussian_blur(e, &b.img, r * 0.5, r * 0.5, true)); + let rows = est.as_ref().map(|img| e.image_rows(img)); + let mut p = op(25); + p.u[0][1] = chatter as u32; + p.u[3] = [rows.as_ref().map_or(0, |r| r.1), rows.is_some() as u32, 0, 0]; + p.f[0] = [pr.f("alphaContrast") as f32 / 100.0, strength, 0.0, 0.0]; + let img = run(e, &p, &b.img, Some(&a), rows.as_ref().map(|r| &r.0)); + Some(GBuf { img, ..b }) +} + +// ---------------------------------------------------------------- mattes + +fn simple_choker(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option { + let c = ctx.params.f("chokeMatte") * b.scale; + if c.abs() > 1e-6 { + if c < 0.0 && !ctx.adjustment { + b.pad(e, (-c).ceil() as u32 + 1)?; + } + let a = run(e, &op(24), &b.img, None, None); + let na = if c > 0.0 { morph_frac(e, &a, c, ERODE_X) } else { morph_frac(e, &a, -c, DILATE_X) }; + b.img = set_alpha(e, &b.img, &na); + } + if ctx.params.e("view") == 1 { + b.img = run(e, &op(28), &b.img, None, None); + } + Some(b) +} + +fn matte_choker(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option { + let pr = ctx.params; + let g1 = pr.f("geometricSoftness1") * b.scale; + let c1 = pr.f("choke1") as f32; + let s1 = pr.f("grayLevelSoftness1") as f32 / 100.0; + let g2 = pr.f("geometricSoftness2") * b.scale; + let c2 = pr.f("choke2") as f32; + let s2 = pr.f("grayLevelSoftness2") as f32 / 100.0; + let it = pr.f("iterations").round().clamp(1.0, 100.0) as usize; + if (c1 < 0.0 || c2 < 0.0 || g1 > 0.0 || g2 > 0.0) && !ctx.adjustment { + b.pad(e, ((g1 + g2) * 1.5 * it as f64).ceil() as u32 + 1)?; + } + let mut a = run(e, &op(24), &b.img, None, None); + let stage = |e: &mut Enc, a: GpuImage, geo: f64, choke: f32, gray: f32| -> GpuImage { + let a = if geo > 0.0 { gaussian_blur(e, &a, geo * 0.5, geo * 0.5, true) } else { a }; + let t = 0.5 + choke / 255.0 * 0.5; + let w = gray.max(1e-3); + if (t - 0.5).abs() < 1e-6 && (w - 1.0).abs() < 1e-6 { + return a; + } + let mut p = op(26); + p.f[0] = [t, w, 0.0, 0.0]; + run(e, &p, &a, None, None) + }; + for _ in 0..it { + a = stage(e, a, g1, c1, s1); + a = stage(e, a, g2, c2, s2); + } + b.img = set_alpha(e, &b.img, &a); + Some(b) +} + +// ---------------------------------------------------------------- channel + +const SET_CHANNEL_LAYERS: [&str; 4] = ["sourceLayer1", "sourceLayer2", "sourceLayer3", "sourceLayer4"]; + +const SHIFT_ORDER: [Src; 11] = + [Src::Alpha, Src::Red, Src::Green, Src::Blue, Src::Luminance, Src::Hue, Src::Lightness, Src::Saturation, Src::Full, Src::Half, Src::Off]; + +const MATTE_ORDER: [Src; 10] = [Src::Red, Src::Green, Src::Blue, Src::Alpha, Src::Luminance, Src::Hue, Src::Lightness, Src::Saturation, Src::Full, Src::Off]; + +fn set_channels(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let pr = ctx.params; + let s = ["setRedTo", "setGreenTo", "setBlueTo", "setAlphaTo"].map(|id| src_index(effectcraft_effects::util::src_at(pr.e(id)))); + let stretch = pr.b("stretchLayersToFit"); + let mut layers = vec![]; + for id in SET_CHANNEL_LAYERS { + layers.push(fitted(e, ctx, &b, id, true, stretch)?); + } + if layers.iter().all(Option::is_none) { + return route(e, b, s); + } + let mut vals = e.image(b.img.width, b.img.height); + for (k, layer) in layers.iter().enumerate() { + let mut p = op(20); + p.u[0] = [20, k as u32, s[k], 0]; + vals = run(e, &p, &vals, Some(layer.as_ref().unwrap_or(&b.img)), None); + } + let img = run(e, &op(21), &vals, None, None); + Some(GBuf { img, ..b }) +} + +/// channel::route. +fn route(e: &mut Enc, b: GBuf, s: [u32; 4]) -> Option { + if s == [0, 1, 2, 3] { + return Some(b); + } + let mut p = op(11); + p.u[1] = s; + let img = run(e, &p, &b.img, None, None); + Some(GBuf { img, ..b }) +} + +fn point(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option { + let pr = ctx.params; + let f = |k: &str| pr.f(k) as f32; + let mut p = Params::default(); + let mut aux = None; + match id { + "ec.key.spill" => { + let k = pr.color("colorToSuppress"); + let amt = f("suppression") / 100.0; + if pr.e("colorAccuracy") == 1 { + let kg = (k[0] + k[1] + k[2]) / 3.0; + let kc = [k[0] - kg, k[1] - kg, k[2] - kg]; + let kn = (kc[0] * kc[0] + kc[1] * kc[1] + kc[2] * kc[2]).sqrt(); + if kn < 1e-5 { + return Some(b); + } + p.u[0] = [7, 1, 0, 0]; + p.f[0] = [kc[0] / kn, kc[1] / kn, kc[2] / kn, amt]; + } else { + let ix = primary(k); + p.u[0] = [7, 0, 0, 0]; + p.u[1] = [ix[0], ix[1], ix[2], 0]; + p.f[0][3] = amt; + } + } + "ec.key.colordifference" => { + let view = pr.e("view"); + if view == 0 { + return Some(b); + } + let accurate = pr.e("colorMatchingAccuracy") == 1; + let lin = |c: [f32; 3]| if accurate { c.map(|v| effectcraft_color::srgb_to_linear(v.clamp(0.0, 1.0))) } else { c }; + let k = pr.color("keyColor"); + let key = lin([k[0], k[1], k[2]]); + let ix = primary([key[0], key[1], key[2], 1.0]); + let (pi, o1, o2) = (ix[0] as usize, ix[1] as usize, ix[2] as usize); + let lv = |pre: &str| ["InBlack", "InWhite", "Gamma", "OutBlack", "OutWhite"].map(|s| f(&format!("{pre}{s}"))); + let (pa, pb) = (lv("partialA"), lv("partialB")); + p.u[0] = [9, view, accurate as u32, 0]; + p.u[1] = [ix[0], ix[1], ix[2], 0]; + p.f[0] = [(key[pi] - key[o1]).max(1e-3), (key[pi] - key[o2]).max(1e-3), f("blackLevel"), f("whiteLevel")]; + p.f[1] = [pa[0], pa[1], pa[2], pa[3]]; + p.f[2] = [pa[4], pb[4], f("gamma"), 0.0]; + p.f[3] = [pb[0], pb[1], pb[2], pb[3]]; + } + "ec.key.unmult" => { + let white = pr.e("backgroundColor") == 1; + let level = if white { pr.f("whiteLevel") } else { pr.f("blackLevel") }.clamp(0.0, 100.0) as f32 / 100.0; + p.u[0] = [10, white as u32, pr.b("removeColorMatting") as u32, pr.b("clipHdrResults") as u32]; + p.f[0] = [level, pr.f("softness").clamp(0.0, 100.0) as f32 / 100.0, 0.0, 0.0]; + } + "ec.channel.shiftchannels" => { + let s = ["takeRedFrom", "takeGreenFrom", "takeBlueFrom", "takeAlphaFrom"].map(|id| src_index(SHIFT_ORDER[(pr.e(id) as usize).min(10)])); + return route(e, b, s); + } + "ec.channel.removecolormatting" => { + p.u[0] = [12, pr.b("clipHdr") as u32, 0, 0]; + p.f[0] = pr.color("backgroundColor"); + } + "ec.channel.arithmetic" => { + p.u[0] = [13, pr.e("operator"), pr.b("clip") as u32, 0]; + p.f[0] = [f("redValue") / 255.0, f("greenValue") / 255.0, f("blueValue") / 255.0, 0.0]; + } + "ec.channel.solidcomposite" => { + const MODES: [effectcraft_color::BlendMode; 12] = { + use effectcraft_color::BlendMode::*; + [Normal, Add, Multiply, Screen, Overlay, SoftLight, HardLight, Darken, Lighten, Difference, Color, Luminosity] + }; + let c = pr.color("color"); + let opa = (f("opacity") / 100.0 * c[3]).clamp(0.0, 1.0); + p.u[0] = [14, crate::ops::mode_id(MODES[(pr.e("blendingMode") as usize).min(11)]), 0, 0]; + p.f[0] = [c[0] * opa, c[1] * opa, c[2] * opa, opa]; + p.f[1][0] = f("sourceOpacity") / 100.0; + } + "ec.channel.setmatte" => { + let src = MATTE_ORDER[(pr.e("takeMatteFrom") as usize).min(9)]; + aux = fitted(e, ctx, &b, "takeMatteFromLayer", true, pr.b("stretchMatteToFit"))?; + let pre_ok = !matches!(src, Src::Alpha | Src::Full | Src::Off); + let flags = pr.b("invertMatte") as u32 + | (pr.b("compositeMatteWithOriginal") as u32) << 1 + | ((pr.b("premultiplyMatteLayer") && pre_ok) as u32) << 2 + | (aux.is_none() as u32) << 3 + | ((src == Src::Alpha) as u32) << 4; + p.u[0] = [15, src_index(src), flags, 0]; + } + "ec.channel.combiner" => { + if pr.b("useSecondLayer") { + aux = Some(layer_or_self(e, ctx, &b, "sourceLayer", true)?); + } + let flags = pr.b("invert") as u32 | (pr.b("solidAlpha") as u32) << 1 | (aux.is_some() as u32) << 2; + p.u[0] = [16, pr.e("from"), pr.e("to"), flags]; + } + "ec.channel.blend" => { + let orig = (f("blendWithOriginal") / 100.0).clamp(0.0, 1.0); + if orig >= 1.0 { + return Some(b); + } + aux = Some(layer_or_self(e, ctx, &b, "blendWithLayer", pr.e("ifLayerSizesDiffer") == 1)?); + p.u[0] = [17, pr.e("mode"), 0, 0]; + p.f[0][0] = orig; + } + "ec.channel.calculations" => { + const MODES: [effectcraft_color::BlendMode; 22] = { + use effectcraft_color::BlendMode::*; + [ + Normal, + Darken, + Multiply, + ColorBurn, + LinearBurn, + Add, + Lighten, + Screen, + ColorDodge, + LinearDodge, + Overlay, + SoftLight, + HardLight, + LinearLight, + VividLight, + PinLight, + HardMix, + Difference, + Exclusion, + Hue, + Saturation, + Luminosity, + ] + }; + if pr.get("secondLayer").and_then(|v| v.as_layer()).is_some() { + aux = Some(layer_or_self(e, ctx, &b, "secondLayer", pr.b("stretchSecondLayerToFit"))?); + } + let flags = + pr.b("invertInput") as u32 | (pr.b("invertSecondLayer") as u32) << 1 | (pr.b("preserveTransparency") as u32) << 2 | (aux.is_some() as u32) << 3; + let mode = MODES[(pr.e("blendingMode") as usize).min(MODES.len() - 1)]; + p.u[0] = [18, pr.e("inputChannel"), pr.e("secondLayerChannel"), crate::ops::mode_id(mode)]; + p.u[1][0] = flags; + p.f[0][0] = (f("secondLayerOpacity") / 100.0).clamp(0.0, 1.0); + } + "ec.channel.compoundarithmetic" => { + let orig = (f("blendWithOriginal") / 100.0).clamp(0.0, 1.0); + if orig >= 1.0 { + return Some(b); + } + aux = Some(layer_or_self(e, ctx, &b, "secondSource", pr.b("stretchSecondSourceToFit"))?); + p.u[0] = [19, pr.e("operator"), pr.e("operateOnChannels"), pr.e("overflowBehavior")]; + p.f[0][0] = orig; + } + _ => return None, + } + let img = run(e, &p, &b.img, aux.as_ref(), None); + Some(GBuf { img, ..b }) } diff --git a/crates/gpu/src/shaders/fx_key.wgsl b/crates/gpu/src/shaders/fx_key.wgsl index 4b251f3a..0ac62639 100644 --- a/crates/gpu/src/shaders/fx_key.wgsl +++ b/crates/gpu/src/shaders/fx_key.wgsl @@ -1,3 +1,796 @@ -// GPU effects (keying, matte and channel family): see src/fx_key.rs. Each entry point mirrors the CPU effect in -// effectcraft-effects operation for operation. Every name here is prefixed `fxk_` (the -// family files share one module). +// GPU effects (keying, matte and channel family): see src/fx_key.rs. Each entry point mirrors +// the CPU effect in effectcraft-effects operation for operation. Every name here is prefixed +// `fxk_` (the family files share one module). + +// util::unpremul: (straight colour, alpha); colour 0 when alpha ≤ 1e-6. +fn fxk_straight(px: vec4) -> vec3 { + if (px.w > 1e-6) { + return px.xyz / px.w; + } + return vec3(0.0); +} + +fn fxk_premul(c: vec3, a: f32) -> vec4 { + return vec4(c * a, a); +} + +// Pixel `p` of the third image, copied into `data` as RGBA f32 rows of P.u[3].x pixels. +fn fxk_rows(p: vec2) -> vec4 { + let i = (u32(p.y) * P.u[3].x + u32(p.x)) * 4u; + return vec4(data[i], data[i + 1u], data[i + 2u], data[i + 3u]); +} + +// keying::to_lab. +fn fxk_cbrt(t: f32) -> f32 { + return sign(t) * pow(abs(t), 1.0 / 3.0); +} + +fn fxk_lab_f(t: f32) -> f32 { + if (t > 0.008856) { + return fxk_cbrt(t); + } + return 7.787 * t + 16.0 / 116.0; +} + +// keying::range_components. +fn fxk_range_components(space: u32, c: vec3) -> vec3 { + if (space == 1u) { + let y = 0.299 * c.x + 0.587 * c.y + 0.114 * c.z; + let u = -0.14713 * c.x - 0.28886 * c.y + 0.436 * c.z; + let v = 0.615 * c.x - 0.51499 * c.y - 0.10001 * c.z; + return vec3(y * 255.0, u * 255.0 + 128.0, v * 255.0 + 128.0); + } + if (space == 2u) { + return c * 255.0; + } + let cl = clamp(c, vec3(0.0), vec3(1.0)); + let l = vec3(srgb_to_linear(cl.x), srgb_to_linear(cl.y), srgb_to_linear(cl.z)); + let x = (0.4124 * l.x + 0.3576 * l.y + 0.1805 * l.z) / 0.95047; + let yy = 0.2126 * l.x + 0.7152 * l.y + 0.0722 * l.z; + let z = (0.0193 * l.x + 0.1192 * l.y + 0.9505 * l.z) / 1.08883; + let fx = fxk_lab_f(x); + let fy = fxk_lab_f(yy); + let fz = fxk_lab_f(z); + return vec3((116.0 * fy - 16.0) * 2.55, 500.0 * (fx - fy) + 128.0, 200.0 * (fy - fz) + 128.0); +} + +// keying::matte_levels. +fn fxk_matte_levels(v: f32, in_b: f32, in_w: f32, gamma: f32, out_b: f32, out_w: f32) -> f32 { + var t = clamp((v - in_b / 255.0) / max((in_w - in_b) / 255.0, 1e-4), 0.0, 1.0); + t = powz(t, 1.0 / max(gamma, 0.01)); + return clamp(out_b / 255.0 + t * (out_w - out_b) / 255.0, 0.0, 1.0); +} + +// keying::secondary. +fn fxk_secondary(c: vec3, o1: u32, o2: u32, bal: f32) -> f32 { + var hi = c[o2]; + var lo = c[o1]; + if (c[o1] > c[o2]) { + hi = c[o1]; + lo = c[o2]; + } + return bal * hi + (1.0 - bal) * lo; +} + +// channel::arithmetic's 8-bit quantisation. +fn fxk_q(v: f32) -> u32 { + return u32(round_away(clamp(v, 0.0, 1.0) * 255.0)); +} + +// channel2::extract. +fn fxk_extract(px: vec4, ch: u32, inv: bool) -> vec4 { + let c = fxk_straight(px); + let a = max(px.w, 0.0); + var g: f32; + switch ch { + case 0u: { + if (inv) { + return fxk_premul(1.0 - c, a); + } + return px; + } + case 1u: { g = luminance(c) * a; } + case 2u: { g = c.x * a; } + case 3u: { g = c.y * a; } + case 4u: { g = c.z * a; } + default: { g = a; } + } + if (inv) { + g = 1.0 - g; + } + return vec4(g, g, g, 1.0); +} + +// channel2::arith. +fn fxk_arith(op: u32, a: f32, b: f32) -> f32 { + switch op { + case 0u: { return b; } + case 1u: { return a + b; } + case 2u: { return a - b; } + case 3u: { return a * b; } + case 4u: { return abs(a - b); } + case 5u: { return f32(fxk_q(a) & fxk_q(b)) / 255.0; } + case 6u: { return f32(fxk_q(a) | fxk_q(b)) / 255.0; } + case 7u: { return f32(fxk_q(a) ^ fxk_q(b)) / 255.0; } + case 8u, 11u: { return max(a, b); } + case 9u, 10u: { return min(a, b); } + case 12u: { return a + b - a * b; } + case 13u: { + if (a < 0.5) { + return 2.0 * a * b; + } + return 1.0 - 2.0 * (1.0 - a) * (1.0 - b); + } + default: { + if (b < 0.5) { + return 2.0 * a * b; + } + return 1.0 - 2.0 * (1.0 - a) * (1.0 - b); + } + } +} + +// channel2::overflow. +fn fxk_overflow(op: u32, v: f32, how: u32) -> f32 { + if (how == 0u) { + return clamp(v, 0.0, 1.0); + } + if (how == 1u) { + let m = 1.0001; + let r = v - m * floor(v / m); + return min(select(r, 0.0, r >= m || r < 0.0), 1.0); + } + if (op == 1u) { + return v * 0.5; + } + if (op == 2u) { + return (v + 1.0) * 0.5; + } + return clamp(v, 0.0, 1.0); +} + +// Per-pixel keying / matte / channel operations. u[0].x = op (see src/fx_key.rs for each +// effect's parameter layout): +// 1 Color Key matte 2 Luma Key matte 3 Color Range matte 4 Extract matte +// 5 apply a matte (aux.x; u[0].y = 0 final, 2 matte view, 3 Difference Matte's matte view, +// 4 Difference Matte's output) +// 6 Difference Matte matte (src and aux blurred) +// 7 Spill Suppressor 8 Advanced Spill Suppressor (aux = 1×1 excess sums when u[1].w) +// 9 Color Difference Key 10 Unmult 11 route channels (u[1] = sources) +// 12 Remove Color Matting 13 Arithmetic 14 Solid Composite 15 Set Matte +// 16 Channel Combiner 17 Blend 18 Calculations 19 Compound Arithmetic +// 20 Set Channels: channel u[0].y of the value image from aux; 21 Set Channels: premultiply +// 22 Screen Key matte 23 Screen Key output 24 alpha plane 25 Key Cleaner output +// 26 Matte Choker ramp 27 util::set_alpha (aux.x = alpha, data = fill) +// 28 matte view (alpha as opaque grey) +@compute @workgroup_size(16, 16) +fn fxk_point(@builtin(global_invocation_id) gid: vec3) { + let dims = out_dims(); + let p = vec2(gid.xy); + if (p.x >= dims.x || p.y >= dims.y) { + return; + } + let px = textureLoad(src, p, 0); + let c = fxk_straight(px); + let a = max(px.w, 0.0); + let u0 = P.u[0]; + let u1 = P.u[1]; + let f0 = P.f[0]; + var o = px; + switch u0.x { + case 1u: { + let k = f0.xyz; + let d = max(max(abs(c.x - k.x), abs(c.y - k.y)), abs(c.z - k.z)); + o = vec4(select(1.0, 0.0, d <= f0.w)); + } + case 2u: { + let l = luminance(c); + let thr = f0.x; + let tol = f0.y; + var m: f32; + switch u0.y { + case 0u: { + if (tol > 0.0) { + m = clamp((thr + tol - l) / tol, 0.0, 1.0); + } else { + m = select(1.0, 0.0, l > thr); + } + } + case 2u: { m = select(1.0, 0.0, abs(l - thr) <= tol); } + case 3u: { m = select(0.0, 1.0, abs(l - thr) <= tol); } + default: { + if (tol > 0.0) { + m = clamp((l - (thr - tol)) / tol, 0.0, 1.0); + } else { + m = select(1.0, 0.0, l < thr); + } + } + } + o = vec4(m); + } + case 3u: { + let v = fxk_range_components(u0.y, c); + var out_d = 0.0; + for (var i = 0u; i < 3u; i++) { + out_d = max(max(out_d, P.f[0][i] - v[i]), v[i] - P.f[1][i]); + } + let fuzz = P.f[2].x; + var m = 1.0; + if (out_d <= 0.0) { + m = 0.0; + } else if (fuzz > 0.0) { + m = clamp(out_d / fuzz, 0.0, 1.0); + } + o = vec4(m); + } + case 4u: { + var v: f32; + switch u0.y { + case 1u: { v = c.x; } + case 2u: { v = c.y; } + case 3u: { v = c.z; } + case 4u: { v = a; } + default: { v = luminance(c); } + } + v = 255.0 * v; + let bp = f0.x; + let wp = f0.y; + let bs = f0.z; + let ws = f0.w; + var k = 1.0; + if (v < bp) { + k = select(0.0, clamp(1.0 - (bp - v) / bs, 0.0, 1.0), bs > 0.0); + } else if (v > wp) { + k = select(0.0, clamp(1.0 - (v - wp) / ws, 0.0, 1.0), ws > 0.0); + } + if (u0.z != 0u) { + k = 1.0 - k; + } + o = vec4(k); + } + case 5u: { + let m = textureLoad(aux, p, 0).x; + switch u0.y { + case 2u: { + let k = clamp(m, 0.0, 1.0); + let v = px.w * k; + o = vec4(v, v, v, 1.0); + } + case 3u: { + let v = m * px.w; + o = vec4(v, v, v, px.w); + } + case 4u: { o = px * m; } + default: { o = px * clamp(m, 0.0, 1.0); } + } + } + case 6u: { + let q = textureLoad(aux, p, 0); + let oc = fxk_straight(q); + let oa = max(q.w, 0.0); + let d = max(max(max(abs(c.x - oc.x), abs(c.y - oc.y)), abs(c.z - oc.z)), abs(a - oa)); + var m: f32; + if (d <= f0.x) { + m = 0.0; + } else if (f0.y <= 1e-6) { + m = 1.0; + } else { + m = clamp((d - f0.x) / f0.y, 0.0, 1.0); + } + o = vec4(m); + } + case 7u: { + if (px.w <= 0.0) { + textureStore(out, p, px); + return; + } + var s = px.xyz / px.w; + if (u0.y == 1u) { + let g = (s.x + s.y + s.z) / 3.0; + let dir = f0.xyz; + let proj = (s.x - g) * dir.x + (s.y - g) * dir.y + (s.z - g) * dir.z; + if (proj > 0.0) { + s = s - proj * dir * f0.w; + } + } else { + let spill = max(s[u1.x] - max(s[u1.y], s[u1.z]), 0.0); + s[u1.x] = s[u1.x] - spill * f0.w; + } + o = vec4(s * px.w, px.w); + } + case 8u: { + if (px.w <= 0.0) { + textureStore(out, p, px); + return; + } + var pi = u1.x; + var o1 = u1.y; + var o2 = u1.z; + if (u1.w != 0u) { + let ex = textureLoad(aux, vec2(0, 0), 0); + if (ex.y >= ex.x && ex.y >= ex.z) { + pi = 1u; + o1 = 0u; + o2 = 2u; + } else if (ex.z >= ex.x) { + pi = 2u; + o1 = 0u; + o2 = 1u; + } else { + pi = 0u; + o1 = 1u; + o2 = 2u; + } + } + var s = px.xyz / px.w; + let amt = f0.x; + let spill_range = f0.y; + let luma = f0.z; + let tol = f0.w; + let desat = P.f[1].x; + let neutral = P.f[1].y; + let key_hue = P.f[1].z; + var w = 1.0; + if (tol < 1.0) { + let h = rgb_to_hsl(s).x; + let d0 = abs(h - key_hue); + let d = min(d0, 1.0 - d0); + w = clamp(1.0 - d / max(tol * 0.5, 1e-4), 0.0, 1.0); + } + let mx = max(s[o1], s[o2]); + let avg = 0.5 * (s[o1] + s[o2]); + let lim = mx + (avg - mx) * spill_range; + let spill = max(s[pi] - lim, 0.0) * amt * w; + s[pi] = s[pi] - spill; + let lw = vec3(0.2126, 0.7152, 0.0722); + let back = spill * lw[pi] * luma + spill * neutral / 3.0; + s = s + back; + if (desat > 0.0 && spill > 0.0) { + let l = luminance(s); + let k = min(desat * min(spill * 4.0, 1.0), 1.0); + s = s + (vec3(l) - s) * k; + } + o = vec4(s * px.w, px.w); + } + case 9u: { + var cc = c; + if (u0.z != 0u) { + let cl = clamp(cc, vec3(0.0), vec3(1.0)); + cc = vec3(srgb_to_linear(cl.x), srgb_to_linear(cl.y), srgb_to_linear(cl.z)); + } + let pa = P.f[1]; + let pb = P.f[3]; + let ua = 1.0 - clamp((cc[u1.x] - cc[u1.y]) / f0.x, 0.0, 1.0); + let ub = 1.0 - clamp((cc[u1.x] - cc[u1.z]) / f0.y, 0.0, 1.0); + let ca = fxk_matte_levels(ua, pa.x, pa.y, pa.z, pa.w, P.f[2].x); + let cb = fxk_matte_levels(ub, pb.x, pb.y, pb.z, pb.w, P.f[2].y); + let um = max(ua, ub); + let cm = fxk_matte_levels(max(ca, cb), f0.z, f0.w, P.f[2].z, 0.0, 255.0); + var view = u0.y; + if (view == 8u) { + let right = p.x * 2 >= dims.x; + let bottom = p.y * 2 >= dims.y; + if (!right && !bottom) { + view = 2u; + } else if (right && !bottom) { + view = 4u; + } else if (!right && bottom) { + view = 6u; + } else { + view = 7u; + } + } + var g = -1.0; + switch view { + case 1u: { g = ua; } + case 2u: { g = ca; } + case 3u: { g = ub; } + case 4u: { g = cb; } + case 5u: { g = um; } + case 6u: { g = cm; } + default: {} + } + if (g >= 0.0) { + let m = g * a; + o = vec4(m, m, m, 1.0); + } else { + o = px * cm; + } + } + case 10u: { + let white = u0.y != 0u; + var d: f32; + var start: f32; + if (white) { + d = 1.0 - min(min(c.x, c.y), c.z); + start = 1.0 - f0.x; + } else { + d = max(max(c.x, c.y), c.z); + start = f0.x; + } + d = clamp(d, 0.0, 1.0); + let width = f0.y * (1.0 - start); + var m: f32; + if (width > 1e-6) { + m = clamp((d - start) / width, 0.0, 1.0); + } else { + m = select(0.0, 1.0, d > start); + } + if (m <= 1e-6) { + o = vec4(0.0); + } else { + let bg = select(0.0, 1.0, white); + var oc = c; + if (u0.z != 0u) { + oc = (c - bg * (1.0 - m)) / m; + } + if (u0.w != 0u) { + oc = clamp(oc, vec3(0.0), vec3(1.0)); + } + o = fxk_premul(oc, a * m); + } + } + case 11u: { + let v = vec4(dst_pick(u1.x, c, a), dst_pick(u1.y, c, a), dst_pick(u1.z, c, a), dst_pick(u1.w, c, a)); + o = fxk_premul(v.xyz, clamp(v.w, 0.0, 1.0)); + } + case 12u: { + if (px.w <= 1e-6) { + o = vec4(0.0); + } else { + for (var i = 0; i < 3; i++) { + let s = px[i] / px.w; + var v = max(s - f0[i] * (1.0 - px.w), 0.0); + if (u0.y != 0u) { + v = min(v, px.w); + } + o[i] = v; + } + } + } + case 13u: { + if (px.w <= 0.0) { + textureStore(out, p, px); + return; + } + let s = px.xyz / px.w; + var r = vec3(0.0); + for (var i = 0u; i < 3u; i++) { + let v = s[i]; + let kv = f0[i]; + let iv = fxk_q(v); + let ik = u32(round_away(kv * 255.0)); + var x: f32; + switch u0.y { + case 0u: { x = f32(iv & ik) / 255.0; } + case 1u: { x = f32(iv | ik) / 255.0; } + case 2u: { x = f32(iv ^ ik) / 255.0; } + case 3u: { x = v + kv; } + case 4u: { x = v - kv; } + case 5u: { x = abs(v - kv); } + case 6u: { x = min(v, kv); } + case 7u: { x = max(v, kv); } + case 8u: { x = select(v, 0.0, v > kv); } + case 9u: { x = select(v, 0.0, v < kv); } + case 10u: { x = select(0.0, 1.0, v >= kv); } + case 11u: { x = v * kv; } + default: { x = 1.0 - (1.0 - v) * (1.0 - kv); } + } + if (u0.z != 0u) { + r[i] = clamp(x, 0.0, 1.0); + } else { + r[i] = max(x, 0.0); + } + } + o = vec4(r * px.w, px.w); + } + case 14u: { + var b = blend_pixel(u0.y, f0, px * P.f[1].x, 0.5); + b.w = clamp(b.w, 0.0, 1.0); + o = vec4(max(b.xyz, vec3(0.0)), b.w); + } + case 15u: { + var mc = c; + var ma = a; + if ((u0.z & 8u) == 0u) { + let q = textureLoad(aux, p, 0); + mc = fxk_straight(q); + ma = max(q.w, 0.0); + } + var m = dst_pick(u0.y, mc, ma); + if ((u0.z & 4u) != 0u) { + m = m * ma; + } + m = clamp(m, 0.0, 1.0); + let inv = (u0.z & 1u) != 0u; + if (inv) { + m = 1.0 - m; + } + let own_m = (u0.z & 8u) != 0u; + let is_alpha = (u0.z & 16u) != 0u; + var na = m; + if ((u0.z & 2u) != 0u && (!own_m || !is_alpha || inv)) { + na = a * m; + } + o = fxk_premul(c, clamp(na, 0.0, 1.0)); + } + case 16u: { + let ch_from = u0.y; + let ch_to = u0.z; + let inv = (u0.w & 1u) != 0u; + let solid = (u0.w & 2u) != 0u; + let second = (u0.w & 4u) != 0u; + var sc = c; + var sa = a; + if (second) { + let q = textureLoad(aux, p, 0); + sc = fxk_straight(q); + sa = max(q.w, 0.0); + } + if (sa <= 0.0 && ch_from != 7u && !second) { + if (solid) { + o = fxk_premul(c, 1.0); + } + textureStore(out, p, o); + return; + } + var na = a; + var r: vec3; + if (ch_from <= 3u) { + switch ch_from { + case 0u: { + let hsl = rgb_to_hsl(sc); + r = vec3(hsl.x, hsl.z, hsl.y); + } + case 1u: { r = hsl_to_rgb(sc.x, sc.z, sc.y); } + case 2u: { + let y = 0.299 * sc.x + 0.587 * sc.y + 0.114 * sc.z; + r = vec3(y, 0.492 * (sc.z - y) + 0.5, 0.877 * (sc.x - y) + 0.5); + } + default: { + let y = sc.x; + let uu = sc.y - 0.5; + let vv = sc.z - 0.5; + let rr = y + vv / 0.877; + let bb = y + uu / 0.492; + let gg = (y - 0.299 * rr - 0.114 * bb) / 0.587; + r = vec3(rr, gg, bb); + } + } + if (inv) { + r = 1.0 - r; + } + } else { + var v: f32; + switch ch_from { + case 4u: { v = sc.x; } + case 5u: { v = sc.y; } + case 6u: { v = sc.z; } + case 7u: { v = sa; } + case 8u: { v = rgb_to_hsl(sc).z; } + case 9u: { v = rgb_to_hsl(sc).x; } + case 10u: { v = rgb_to_hsl(sc).y; } + case 11u: { v = luminance(sc); } + case 12u: { v = max(max(sc.x, sc.y), sc.z); } + default: { v = min(min(sc.x, sc.y), sc.z); } + } + if (inv) { + v = 1.0 - v; + } + switch ch_to { + case 0u: { r = vec3(v, 0.0, 0.0); } + case 1u: { r = vec3(0.0, v, 0.0); } + case 2u: { r = vec3(0.0, 0.0, v); } + case 3u: { + na = clamp(v, 0.0, 1.0); + r = vec3(1.0); + } + case 5u: { r = hsl_to_rgb(v, 1.0, 0.5); } + case 6u: { r = hsl_to_rgb(0.0, clamp(v, 0.0, 1.0), 0.5); } + case 8u: { + let hsl = rgb_to_hsl(c); + r = hsl_to_rgb(hsl.x, clamp(hsl.y * v, 0.0, 1.0), hsl.z); + } + default: { r = vec3(v); } + } + } + if (solid) { + na = 1.0; + } + o = fxk_premul(max(r, vec3(0.0)), na); + } + case 17u: { + let q = textureLoad(aux, p, 0); + var bl: vec4; + switch u0.y { + case 0u: { bl = q; } + case 1u, 2u: { + let oa = q.w; + if (px.w <= 0.0 || oa <= 0.0) { + bl = px; + } else { + let hsl = rgb_to_hsl(c); + let ohsl = rgb_to_hsl(fxk_straight(q)); + var h2 = ohsl.x; + var s2 = ohsl.y; + if (u0.y == 2u) { + if (hsl.y > 1e-4) { + s2 = hsl.y; + } else { + h2 = 0.0; + s2 = 0.0; + } + } + let rgb = hsl_to_rgb(h2, s2, hsl.z); + bl = px + (fxk_premul(rgb, a) - px) * oa; + } + } + case 3u: { bl = vec4(min(px.xyz, q.xyz), px.w); } + default: { bl = max(px, q); } + } + o = bl + (px - bl) * f0.x; + } + case 18u: { + let flags = u1.x; + var r = fxk_extract(px, u0.y, (flags & 1u) != 0u); + if ((flags & 8u) != 0u) { + let s = fxk_extract(textureLoad(aux, p, 0), u0.z, (flags & 2u) != 0u) * f0.x; + r = blend_pixel(u0.w, r, s, 0.5); + } + if ((flags & 4u) != 0u && r.w > 1e-6) { + r = fxk_premul(fxk_straight(r), px.w); + } + r.w = clamp(r.w, 0.0, 1.0); + o = r; + } + case 19u: { + let q = textureLoad(aux, p, 0); + let oc = fxk_straight(q); + let oa = max(q.w, 0.0); + var rc = c; + var ra = a; + if (u0.z != 2u) { + for (var i = 0u; i < 3u; i++) { + rc[i] = fxk_overflow(u0.y, fxk_arith(u0.y, c[i], oc[i]), u0.w); + } + } + if (u0.z != 0u) { + ra = fxk_overflow(u0.y, fxk_arith(u0.y, a, oa), u0.w); + } + let res = fxk_premul(rc, ra); + o = res + (px - res) * f0.x; + } + case 20u: { + let q = textureLoad(aux, p, 0); + o = px; + o[u0.y] = dst_pick(u0.z, fxk_straight(q), max(q.w, 0.0)); + } + case 21u: { + o = fxk_premul(px.xyz, clamp(px.w, 0.0, 1.0)); + } + case 22u: { + let d = c[u1.x] - fxk_secondary(c, u1.y, u1.z, f0.z); + let t = clamp(d / f0.x * f0.y, 0.0, 1.0); + o = vec4(clamp((1.0 - t - f0.w) / max(P.f[1].x - f0.w, 1e-3), 0.0, 1.0)); + } + case 23u: { + let k = clamp(textureLoad(aux, p, 0).x, 0.0, 1.0); + let alpha = a * k; + if (u0.y == 1u) { + o = vec4(alpha, alpha, alpha, 1.0); + } else if (u0.y == 2u) { + var v = 0.5; + if (alpha <= 1e-3) { + v = 0.0; + } else if (alpha >= 0.999) { + v = 1.0; + } + o = vec4(v, v, v, 1.0); + } else { + let screen = f0.xyz; + var fg = c; + if (!(u0.z != 0u || k <= 1e-4)) { + let s = 1.0 - k; + for (var i = 0; i < 3; i++) { + fg[i] = min(max(c[i] - s * screen[i], 0.0) / k, max(c[i], 1.0)); + } + } + let spill = max(fg[u1.x] - fxk_secondary(fg, u1.y, u1.z, f0.w), 0.0); + fg[u1.x] = fg[u1.x] - spill * P.f[1].x; + o = fxk_premul(fg, alpha); + } + } + case 24u: { + o = vec4(px.w); + } + case 25u: { + var na = textureLoad(aux, p, 0).x; + if (u0.y != 0u) { + na = clamp((na - 0.5) * 1.5 + 0.5, 0.0, 1.0); + } + var cc = c; + let a0 = a; + if (P.u[3].y != 0u && a0 > 1e-4 && a0 < 0.999) { + let q = fxk_rows(p); + if (q.w > 1e-6) { + let ea = q.w; + if (ea > 1e-4) { + let t = f0.y * (1.0 - a0); + cc = cc + (q.xyz / ea - cc) * t; + } + } + } + na = clamp((na - 0.5) * f0.x + 0.5, 0.0, 1.0); + if (a0 <= 1e-6) { + na = 0.0; + } + o = fxk_premul(cc, na); + } + case 26u: { + o = vec4(clamp((px.x - f0.x) / f0.y + 0.5, 0.0, 1.0)); + } + case 27u: { + let na = clamp(textureLoad(aux, p, 0).x, 0.0, 1.0); + var cc = c; + if (px.w <= 1e-4) { + cc = fxk_straight(fxk_rows(p)); + } + o = fxk_premul(cc, na); + } + case 28u: { + let v = clamp(px.w, 0.0, 1.0); + o = vec4(v, v, v, 1.0); + } + default: {} + } + textureStore(out, p, o); +} + +// util::fit_layer: bilinear sample of the other layer (src) at the buffer coordinates in data +// (one per column, then one per row). +@compute @workgroup_size(16, 16) +fn fxk_fit(@builtin(global_invocation_id) gid: vec3) { + let dims = out_dims(); + let p = vec2(gid.xy); + if (p.x >= dims.x || p.y >= dims.y) { + return; + } + let x = data[u32(p.x)]; + let y = data[u32(dims.x) + u32(p.y)]; + textureStore(out, p, sample_bilinear(src, x, y)); +} + +// Sum of 16×16 blocks; u[0].x = first pass: sum Advanced Spill Suppressor's per-channel excess +// of each primary over the other two (straight colour × alpha), else sum the values. +@compute @workgroup_size(16, 16) +fn fxk_reduce(@builtin(global_invocation_id) gid: vec3) { + let dims = out_dims(); + let p = vec2(gid.xy); + if (p.x >= dims.x || p.y >= dims.y) { + return; + } + let sd = vec2(textureDimensions(src)); + var acc = vec4(0.0); + for (var j = 0; j < 16; j++) { + for (var i = 0; i < 16; i++) { + let q = vec2(p.x * 16 + i, p.y * 16 + j); + if (q.x >= sd.x || q.y >= sd.y) { + continue; + } + let v = textureLoad(src, q, 0); + if (P.u[0].x != 0u) { + let c = fxk_straight(v); + let a = max(v.w, 0.0); + acc += vec4( + max(c.x - max(c.y, c.z), 0.0) * a, + max(c.y - max(c.x, c.z), 0.0) * a, + max(c.z - max(c.x, c.y), 0.0) * a, + 0.0, + ); + } else { + acc += v; + } + } + } + textureStore(out, p, acc); +} diff --git a/crates/gpu/src/tests_fx_key.rs b/crates/gpu/src/tests_fx_key.rs index ae615fe7..25f2d9fe 100644 --- a/crates/gpu/src/tests_fx_key.rs +++ b/crates/gpu/src/tests_fx_key.rs @@ -1,2 +1,218 @@ -//! GPU keying, matte and channel family effects vs the CPU effects (the oracle): direct on a buffer at full and half -//! resolution, as adjustment, and composited at 8 and 32 bpc. +//! GPU keying, matte and channel family effects vs the CPU effects (the oracle): direct on a +//! buffer at full and half resolution, as adjustment, and composited at 8 and 32 bpc. + +use effectcraft_keyframe::Value; +use effectcraft_project::BitDepth; +use effectcraft_render::RenderOpts; +use effectcraft_time::Tick; + +use crate::tests::{Scene, c, check, compare_at, effect_case, n, opts, set}; + +fn e(v: u32) -> Value { + Value::Enum(v) +} + +fn on() -> Value { + Value::Bool(true) +} + +fn off() -> Value { + Value::Bool(false) +} + +/// [`effect_case`] for hard thresholds (8-bit quantisation, bitwise operators): the composited +/// renders may flip a few pixels sitting on the threshold after the layer transform's float +/// rounding, so up to 0.1 % of them may differ by any amount. +fn discontinuous_case(id: &str, vals: &[(&str, Value)]) { + crate::tests::effect_direct(id, vals, false); + crate::tests::effect_direct(id, vals, true); + for depth in [BitDepth::Bpc8, BitDepth::Bpc32] { + let mut s = Scene::new(depth); + let bg = s.solid([0.15, 0.1, 0.2], 97, 61); + s.push(bg); + let mut l = s.footage(70, 44); + s.effect(&mut l, id, vals); + set(&mut l, "transform/rotation", Value::Scalar(8.0)); + s.push(l); + check(&format!("{id} {vals:?} {depth:?}"), compare_at(&s, opts(), Tick::ZERO), 0.001); + check(&format!("{id} {vals:?} {depth:?} half"), compare_at(&s, RenderOpts { scale: 0.5, ..opts() }, Tick::ZERO), 0.001); + } +} + +#[test] +fn colour_luma_range_extract_keys() { + effect_case("ec.key.colorkey", &[("keyColor", c(0.7, 0.6, 0.5)), ("colorTolerance", n(60.0))]); + effect_case("ec.key.colorkey", &[("keyColor", c(0.5, 0.6, 0.5)), ("colorTolerance", n(70.0)), ("edgeThin", n(1.5)), ("edgeFeather", n(4.0))]); + effect_case("ec.key.colorkey", &[("keyColor", c(0.5, 0.6, 0.5)), ("colorTolerance", n(70.0)), ("edgeThin", n(-2.0))]); + effect_case("ec.key.luma", &[("threshold", n(120.0)), ("tolerance", n(40.0))]); + effect_case("ec.key.luma", &[("keyType", e(0)), ("threshold", n(140.0)), ("tolerance", n(30.0)), ("edgeFeather", n(3.0))]); + effect_case("ec.key.luma", &[("keyType", e(2)), ("threshold", n(128.0)), ("tolerance", n(25.0)), ("edgeThin", n(1.0))]); + effect_case("ec.key.luma", &[("keyType", e(3)), ("threshold", n(100.0)), ("tolerance", n(50.0))]); + effect_case("ec.key.colorrange", &[("fuzziness", n(20.0))]); + effect_case("ec.key.colorrange", &[("colorSpace", e(1)), ("minL", n(60.0)), ("maxA", n(150.0)), ("minB", n(90.0)), ("fuzziness", n(15.0))]); + effect_case("ec.key.colorrange", &[("colorSpace", e(2)), ("minL", n(80.0)), ("maxA", n(180.0)), ("minB", n(40.0)), ("fuzziness", n(30.0))]); + effect_case("ec.key.extract", &[("blackPoint", n(60.0)), ("whitePoint", n(200.0)), ("blackSoftness", n(30.0)), ("whiteSoftness", n(20.0))]); + discontinuous_case("ec.key.extract", &[("channel", e(1)), ("blackPoint", n(90.0)), ("invert", on())]); + effect_case("ec.key.extract", &[("channel", e(4)), ("whitePoint", n(200.0)), ("whiteSoftness", n(40.0))]); +} + +#[test] +fn difference_colour_difference_and_screen_keys() { + effect_case("ec.key.differencematte", &[]); + effect_case("ec.key.differencematte", &[("view", e(2)), ("matchingTolerance", n(0.0)), ("blurBeforeDifference", n(3.0))]); + effect_case("ec.key.colordifference", &[("keyColor", c(0.2, 0.3, 0.8))]); + effect_case( + "ec.key.colordifference", + &[ + ("keyColor", c(0.3, 0.8, 0.2)), + ("colorMatchingAccuracy", e(1)), + ("partialAInBlack", n(20.0)), + ("partialAGamma", n(1.4)), + ("partialBOutWhite", n(200.0)), + ("blackLevel", n(10.0)), + ("gamma", n(0.8)), + ], + ); + for view in [1, 2, 3, 4, 5, 6, 8] { + effect_case("ec.key.colordifference", &[("keyColor", c(0.2, 0.3, 0.8)), ("view", e(view)), ("partialBInWhite", n(180.0))]); + } + effect_case("ec.key.screen", &[("screenColor", c(0.4, 0.75, 0.3))]); + effect_case( + "ec.key.screen", + &[ + ("screenColor", c(0.3, 0.4, 0.8)), + ("screenGain", n(130.0)), + ("screenBalance", n(30.0)), + ("despill", n(60.0)), + ("clipBlack", n(10.0)), + ("clipWhite", n(85.0)), + ("screenPreblur", n(2.0)), + ("screenShrinkGrow", n(-1.5)), + ("screenSoftness", n(3.0)), + ], + ); + effect_case("ec.key.screen", &[("screenColor", c(0.8, 0.4, 0.3)), ("screenShrinkGrow", n(2.0)), ("edgeColor", e(1))]); + for view in [1, 2] { + effect_case("ec.key.screen", &[("screenColor", c(0.4, 0.75, 0.3)), ("view", e(view))]); + } +} + +#[test] +fn spill_unmult_and_key_cleaner() { + effect_case("ec.key.spill", &[("colorToSuppress", c(0.2, 0.9, 0.1))]); + effect_case("ec.key.spill", &[("colorToSuppress", c(0.9, 0.8, 0.1)), ("colorAccuracy", e(1)), ("suppression", n(150.0))]); + effect_case("ec.key.advancedspill", &[]); + effect_case("ec.key.advancedspill", &[("suppression", n(80.0)), ("ultraSettings/spillRange", n(20.0)), ("ultraSettings/lumaCorrection", n(50.0))]); + effect_case( + "ec.key.advancedspill", + &[ + ("method", e(1)), + ("ultraSettings/keyColor", c(0.9, 0.2, 0.3)), + ("ultraSettings/tolerance", n(40.0)), + ("ultraSettings/desaturate", n(50.0)), + ("ultraSettings/spillColorCorrection", n(30.0)), + ], + ); + effect_case("ec.key.unmult", &[]); + effect_case("ec.key.unmult", &[("backgroundColor", e(1)), ("whiteLevel", n(90.0)), ("softness", n(40.0)), ("clipHdrResults", off())]); + effect_case("ec.key.unmult", &[("blackLevel", n(30.0)), ("softness", n(0.0)), ("removeColorMatting", off())]); + effect_case("ec.key.keycleaner", &[]); + effect_case("ec.key.keycleaner", &[("additionalEdgeRadius", n(6.0)), ("reduceChatter", on()), ("alphaContrast", n(250.0)), ("strength", n(60.0))]); +} + +#[test] +fn chokers() { + effect_case("ec.matte.simplechoker", &[("chokeMatte", n(1.5))]); + effect_case("ec.matte.simplechoker", &[("chokeMatte", n(-2.5))]); + effect_case("ec.matte.simplechoker", &[("chokeMatte", n(-1.0)), ("view", e(1))]); + effect_case("ec.matte.mattechoker", &[]); + effect_case( + "ec.matte.mattechoker", + &[ + ("geometricSoftness1", n(2.0)), + ("choke1", n(-40.0)), + ("geometricSoftness2", n(3.0)), + ("choke2", n(30.0)), + ("grayLevelSoftness2", n(40.0)), + ("iterations", n(2.0)), + ], + ); +} + +#[test] +fn channel_effects() { + effect_case("ec.channel.setchannels", &[("setRedTo", e(2)), ("setGreenTo", e(4)), ("setBlueTo", e(5)), ("setAlphaTo", e(6))]); + effect_case("ec.channel.shiftchannels", &[("takeRedFrom", e(3)), ("takeGreenFrom", e(7)), ("takeAlphaFrom", e(4))]); + effect_case("ec.channel.removecolormatting", &[("backgroundColor", c(0.3, 0.5, 0.7))]); + effect_case("ec.channel.removecolormatting", &[("backgroundColor", c(0.8, 0.2, 0.1)), ("clipHdr", off())]); + for op in [0, 2, 3, 4, 8, 10, 12] { + discontinuous_case("ec.channel.arithmetic", &[("operator", e(op)), ("redValue", n(120.0)), ("greenValue", n(60.0)), ("blueValue", n(200.0))]); + } + effect_case("ec.channel.arithmetic", &[("operator", e(3)), ("redValue", n(200.0)), ("clip", off())]); + for mode in [0, 2, 4, 9, 11] { + effect_case("ec.channel.solidcomposite", &[("blendingMode", e(mode)), ("color", c(0.8, 0.3, 0.2)), ("opacity", n(70.0)), ("sourceOpacity", n(80.0))]); + } + effect_case("ec.channel.setmatte", &[("takeMatteFrom", e(4))]); + effect_case("ec.channel.setmatte", &[("takeMatteFrom", e(3)), ("invertMatte", on())]); + effect_case("ec.channel.setmatte", &[("takeMatteFrom", e(1)), ("compositeMatteWithOriginal", off()), ("premultiplyMatteLayer", off())]); + for (from, to) in [(0, 0), (1, 0), (2, 0), (3, 0), (8, 4), (9, 5), (10, 6), (11, 3), (12, 8), (13, 7), (7, 1)] { + effect_case("ec.channel.combiner", &[("from", e(from)), ("to", e(to))]); + } + effect_case("ec.channel.combiner", &[("from", e(4)), ("to", e(2)), ("invert", on()), ("solidAlpha", on())]); + effect_case("ec.channel.blend", &[("mode", e(1)), ("blendWithOriginal", n(30.0))]); + effect_case("ec.channel.calculations", &[("inputChannel", e(1))]); + effect_case("ec.channel.calculations", &[("inputChannel", e(0)), ("invertInput", on())]); + effect_case("ec.channel.compoundarithmetic", &[("operator", e(3)), ("operateOnChannels", e(1))]); +} + +/// The channel and keying effects that read another layer, composited. +#[test] +fn layer_inputs() { + let cases: Vec<(&str, Vec<(&str, Value)>)> = vec![ + ("ec.key.differencematte", vec![("matchingTolerance", n(10.0)), ("matchingSoftness", n(20.0))]), + ("ec.key.differencematte", vec![("ifLayerSizesDiffer", e(1)), ("blurBeforeDifference", n(2.0)), ("view", e(2))]), + ("ec.channel.setmatte", vec![("takeMatteFrom", e(4)), ("stretchMatteToFit", off())]), + ("ec.channel.setmatte", vec![("takeMatteFrom", e(3))]), + ("ec.channel.setchannels", vec![("setGreenTo", e(0)), ("setAlphaTo", e(4))]), + ("ec.channel.combiner", vec![("useSecondLayer", on()), ("from", e(11)), ("to", e(7))]), + ("ec.channel.blend", vec![("mode", e(0)), ("blendWithOriginal", n(40.0))]), + ("ec.channel.blend", vec![("mode", e(2))]), + ("ec.channel.blend", vec![("mode", e(3)), ("ifLayerSizesDiffer", e(1))]), + ("ec.channel.blend", vec![("mode", e(4))]), + ("ec.channel.calculations", vec![("secondLayerChannel", e(2)), ("blendingMode", e(2)), ("secondLayerOpacity", n(70.0))]), + ("ec.channel.calculations", vec![("inputChannel", e(4)), ("blendingMode", e(10)), ("preserveTransparency", on()), ("invertSecondLayer", on())]), + ("ec.channel.compoundarithmetic", vec![("operator", e(1)), ("overflowBehavior", e(2))]), + ("ec.channel.compoundarithmetic", vec![("operator", e(4)), ("operateOnChannels", e(1)), ("blendWithOriginal", n(25.0))]), + ("ec.channel.compoundarithmetic", vec![("operator", e(13)), ("operateOnChannels", e(2))]), + ]; + let layer_param = |id: &str| match id { + "ec.key.differencematte" => vec!["differenceLayer"], + "ec.channel.setmatte" => vec!["takeMatteFromLayer"], + "ec.channel.setchannels" => vec!["sourceLayer2", "sourceLayer4"], + "ec.channel.combiner" => vec!["sourceLayer"], + "ec.channel.blend" => vec!["blendWithLayer"], + "ec.channel.calculations" => vec!["secondLayer"], + _ => vec!["secondSource"], + }; + for depth in [BitDepth::Bpc8, BitDepth::Bpc32] { + for (id, vals) in &cases { + let mut s = Scene::new(depth); + let bg = s.solid([0.15, 0.1, 0.2], 97, 61); + s.push(bg); + let mut other = s.footage(50, 36); + other.switches.video = false; + let oid = s.push(other); + let mut l = s.footage(70, 44); + let mut vals = vals.clone(); + for p in layer_param(id) { + vals.push((p, Value::Layer(Some(oid.0)))); + } + s.effect(&mut l, id, &vals); + set(&mut l, "transform/rotation", Value::Scalar(8.0)); + s.push(l); + let label = format!("{id} {vals:?} {depth:?}"); + check(&label, compare_at(&s, opts(), Tick::ZERO), 0.0); + check(&format!("{label} half"), compare_at(&s, RenderOpts { scale: 0.5, ..opts() }, Tick::ZERO), 0.0); + } + } +}