//! GPU effects, pixel ports of part C (kernels in `shaders/fx_pixel2.wgsl`, every entry point //! prefixed `fp2_`): CC Cross Blur, CC Radial Blur, CC Vector Blur, Reduce Interlace Flicker, //! CC Composite, Color Link, Cineon Converter, HDR Compander, HDR Highlight Compression, Grow //! Bounds, CC Overbrights, CC Block Load, CC Burn Film, CC Glass, CC HexTile, CC Mr. Smoothie, //! CC Plastic, Inner/Outer Key, CC Simple Wire Removal and Basic 3D, with the CPU effects' //! exact steps (padding, box-blur radii, parameter conversions in f64 on the CPU). //! //! What only depends on parameters and geometry is prepared on the CPU and shared with the CPU //! effect: Inner/Outer Key's trimap and Cleanup strokes (`effects::inner_outer_plan`), CC Glass / //! Plastic's light (`effects::BumpLight`). Color Link's sample statistics (sorting, trimmed //! means) run on the CPU over the read-back layer (`effects::color_link_sample`) unless a Source //! Layer is chosen; the colouring runs here. use effectcraft_color::BlendMode; use effectcraft_effects::EffectCtx; use effectcraft_raster::Image; use crate::context::{Enc, GpuImage, Params}; use crate::effects::{GBuf, box_passes, gaussian_blur}; use crate::ops::mode_id; /// Compute entry points in `fx_pixel2.wgsl`. pub(crate) const KERNELS: &[&str] = &[ "fp2_cross", "fp2_box", "fp2_radial", "fp2_plane", "fp2_vector", "fp2_point", "fp2_block_means", "fp2_block_load", "fp2_burn", "fp2_bump", "fp2_hextile", "fp2_smoothie", "fp2_io_prep", "fp2_io_known", "fp2_io_est", "fp2_io_strokes", "fp2_io_final", "fp2_wire", "fp2_basic3d", ]; /// Effect ids implemented here. pub(crate) const IDS: &[&str] = &[ "ec.blur.cccross", "ec.blur.ccradial", "ec.blur.ccvector", "ec.blur.reduceflicker", "ec.channel.cccomposite", "ec.color.colorlink", "ec.utility.cineon", "ec.utility.hdrcompander", "ec.utility.hdrcompression", "ec.utility.growbounds", "ec.utility.ccoverbrights", "ec.stylize.ccblockload", "ec.stylize.ccburnfilm", "ec.stylize.ccglass", "ec.stylize.cchextile", "ec.stylize.ccmrsmoothie", "ec.stylize.ccplastic", "ec.key.innerouter", "ec.key.ccsimplewireremoval", "ec.obsolete.basic3d", ]; /// 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 { match id { "ec.blur.cccross" => cross_blur(e, ctx, b), "ec.blur.ccradial" => radial_blur(e, ctx, b), "ec.blur.ccvector" => vector_blur(e, ctx, b), "ec.blur.reduceflicker" => reduce_flicker(e, ctx, b), "ec.utility.growbounds" => grow_bounds(e, ctx, b), "ec.stylize.ccblockload" => block_load(e, ctx, b), "ec.stylize.ccburnfilm" => burn_film(e, ctx, b), "ec.stylize.ccglass" | "ec.stylize.ccplastic" => bump(e, id, ctx, b), "ec.stylize.cchextile" => hextile(e, ctx, b), "ec.stylize.ccmrsmoothie" => smoothie(e, ctx, b), "ec.key.innerouter" => inner_outer(e, ctx, b), "ec.key.ccsimplewireremoval" => wire_removal(e, ctx, b), "ec.obsolete.basic3d" => basic_3d(e, ctx, b), _ => point(e, id, ctx, b), } } /// A same-size per-pixel kernel over the buffer. fn run(e: &mut Enc, entry: &str, p: &Params, mut b: GBuf, aux: Option<&GpuImage>, data: Option<&wgpu::Buffer>) -> Option { let out = e.scratch(b.img.width, b.img.height); e.pixels(entry, p, &b.img, aux, &out, data); b.img = out; Some(b) } /// A one-value plane (x) of `img` (`fp2_plane` kind / property), softened by util::gauss_plane. fn plane(e: &mut Enc, img: &GpuImage, kind: u32, prop: u32, soft: f64) -> GpuImage { let mut p = Params::default(); p.u[0] = [kind, prop, 0, 0]; let pl = e.scratch(img.width, img.height); e.pixels("fp2_plane", &p, img, None, &pl, None); gauss_plane(e, &pl, soft, soft) } /// util::gauss_plane on every channel: 3 box passes per axis (edges repeated), each window /// summed directly (the CPU sums in f64; see `fp2_box`). pub(crate) fn gauss_plane(e: &mut Enc, img: &GpuImage, sx: f64, sy: f64) -> GpuImage { let mut cur = img.clone(); for (vertical, sigma) in [(0, sx), (1, sy)] { if sigma <= 0.05 { continue; } for r in crate::effects::box_radii(sigma, 3) { if r == 0 { continue; } let mut p = Params::default(); p.u[0] = [r as u32, vertical, 0, 0]; let out = e.scratch(cur.width, cur.height); e.pixels("fp2_box", &p, &cur, None, &out, None); cur = out; } } cur } // ---------------------------------------------------------------- blurs (blur2, blur3) fn cross_blur(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option { let rx = (ctx.params.f("radiusX").max(0.0) * b.scale).round() as usize; let ry = (ctx.params.f("radiusY").max(0.0) * b.scale).round() as usize; if rx == 0 && ry == 0 { return Some(b); } if !ctx.adjustment { b.pad(e, (rx.max(ry) * 3) as u32 + 1)?; } let repeat = ctx.adjustment; let hz = box_passes(e, &b.img, &[rx; 3], &[], repeat); let vt = box_passes(e, &b.img, &[], &[ry; 3], repeat); let mut p = Params::default(); p.u[0][0] = ctx.params.e("transferMode"); let out = e.scratch(b.img.width, b.img.height); e.pixels("fp2_cross", &p, &hz, Some(&vt), &out, None); b.img = out; Some(b) } fn radial_blur(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { let amount = ctx.params.f("amount"); if amount == 0.0 { return Some(b); } let c = b.to_px(ctx.params.v2("center")); let mut p = Params::default(); p.u[0][0] = ctx.params.e("type"); p.f[0] = [c.0 as f32, c.1 as f32, ((amount / 100.0).clamp(-1.0, 1.0) * 0.5) as f32, (amount.to_radians() * 0.25) as f32]; p.f[1][0] = ctx.params.f("quality").clamp(1.0, 100.0) as f32; run(e, "fp2_radial", &p, b, None, None) } fn vector_blur(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { let amount = ctx.params.f("amount").max(0.0) * b.scale; if amount < 0.5 { return Some(b); } let soft = ctx.params.f("mapSoftness").max(0.0) * 0.5 * b.scale; let map = plane(e, &b.img, 0, 0, soft); let mut p = Params::default(); p.u[0][0] = ctx.params.e("type"); p.f[0] = [amount as f32, ctx.params.f("angleOffset").to_radians() as f32, 0.0, 0.0]; run(e, "fp2_vector", &p, b, Some(&map), None) } fn reduce_flicker(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option { let s = ctx.params.f("softness").max(0.0) * b.scale; if s < 0.05 { return Some(b); } b.img = gaussian_blur(e, &b.img, 0.0, s, true); Some(b) } fn grow_bounds(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option { let px = (ctx.params.f("pixels") * b.scale).round().max(0.0) as u32; if !ctx.adjustment { b.pad(e, px)?; } Some(b) } // ---------------------------------------------------------------- per-pixel colour effects /// channel2::cc_composite's Composite Original modes. fn composite_mode(m: u32) -> BlendMode { match m { 2 => BlendMode::Add, 3 => BlendMode::Multiply, 4 => BlendMode::Screen, 5 => BlendMode::Overlay, 6 => BlendMode::SoftLight, 7 => BlendMode::HardLight, 8 => BlendMode::Darken, 9 => BlendMode::Lighten, 10 => BlendMode::Difference, 11 => BlendMode::Hue, 12 => BlendMode::Saturation, 13 => BlendMode::Color, 14 => BlendMode::Luminosity, 15 => BlendMode::StencilAlpha, _ => BlendMode::Normal, } } fn point(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option { let pr = ctx.params; let mut p = Params::default(); let mut original = None; match id { "ec.utility.cineon" => { // utility::Cineon's offset and knee, in the CPU's f32. let black = pr.f("tenBitBlackPoint") as f32; let white = pr.f("tenBitWhitePoint") as f32; let rolloff = pr.f("highlightRolloff") as f32; let offset = |black: f32, white: f32| 10f32.powf((black - white) * 0.002 / 0.6); let knee = 1.0 - (rolloff / 100.0).clamp(0.0, 0.99); p.u[0] = [0, pr.e("conversionType"), 0, 0]; p.f[0] = [offset(black, white), knee, white, pr.f("gamma") as f32]; p.f[1] = [pr.f("internalBlackPoint") as f32, pr.f("internalWhitePoint") as f32, 0.0, 0.0]; p.f[2] = [offset(95.0, 685.0), knee, 685.0, 1.7]; } "ec.utility.hdrcompander" => { p.u[0] = [1, (pr.e("mode") == 1) as u32, 0, 0]; p.f[0] = [pr.f("gain").max(1e-6) as f32, pr.f("gamma").max(1e-3) as f32, 0.0, 0.0]; } "ec.utility.hdrcompression" => { let t = (pr.f("amount") / 100.0).clamp(0.0, 1.0) as f32; if t <= 0.0 { return Some(b); } p.u[0][0] = 2; p.f[0][0] = 1.0 - 0.5 * t; } "ec.utility.ccoverbrights" => { p.u[0] = [3, pr.e("channel"), 0, 0]; p.f[0] = pr.color("highlightColor"); } "ec.channel.cccomposite" => { if let Some(source) = ctx.env.host.and_then(|h| h.original()) { let fitted = effectcraft_effects::util::fit_buffer(source, b.img.width, b.img.height, b.offset, b.scale); original = Some(e.g.upload_image(&fitted)?); } p.u[1][0] = original.is_some() as u32; let m = pr.e("compositeOriginal"); let mode = if m == 1 { BlendMode::Normal } else { composite_mode(m) }; p.u[0] = [6, (m == 1) as u32, mode_id(mode), pr.b("rgbOnly") as u32]; p.f[0][0] = (pr.f("opacity") as f32 / 100.0).clamp(0.0, 1.0); } "ec.color.colorlink" => { // The sample statistics run on the CPU (the layer's own pixels are read back). let own = if ctx.layer_param("sourceLayer", false).is_none() { Some(e.download(&b.img)?) } else { None }; let (c, sa) = effectcraft_effects::color_link_sample(ctx, own.as_ref())?; let op = (pr.f("opacity") / 100.0).clamp(0.0, 1.0) as f32; let stencil = pr.b("stencilOriginalAlpha") as u32; if pr.e("sampleSource") >= 6 { p.u[0] = [4, stencil, 0, 0]; p.f[0] = [sa, op, 0.0, 0.0]; } else { p.u[0] = [5, stencil, mode_id(effectcraft_effects::color_link_mode(ctx)), 0]; p.f[0] = [c[0] * op, c[1] * op, c[2] * op, op]; } } _ => return None, } run(e, "fp2_point", &p, b, original.as_ref(), None) } // ---------------------------------------------------------------- CC Block Load (stylize3) fn block_load(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option { let c = (ctx.params.f("completion") / 100.0).clamp(0.0, 1.0); if c >= 1.0 { return Some(b); } let (w, h) = (b.img.width as usize, b.img.height as usize); let nlev = (w.max(h).max(2) as f64).log2().ceil() as u32; let lp = c * (nlev + 1) as f64; let done = lp.floor() as u32; let frac = (lp - done as f64) as f32; let size = |k: u32| 1usize << (nlev - k.min(nlev)); let means = |e: &mut Enc, s: usize| { let mut p = Params::default(); p.u[0][0] = s as u32; let g = e.scratch(w.div_ceil(s).max(1) as u32, h.div_ceil(s).max(1) as u32); e.pixels("fp2_block_means", &p, &b.img, None, &g, None); g }; let s_new = size(done); let new = means(e, s_new); let old = (done > 0).then(|| (size(done - 1), means(e, size(done - 1)))); let flags = ctx.params.b("scanlines") as u32 | (ctx.params.b("smoothing") as u32) << 1 | (ctx.params.b("bilinear") as u32) << 2 | (old.is_some() as u32) << 3 | (ctx.params.b("startCleared") as u32) << 4; let mut p = Params::default(); p.u[0] = [s_new as u32, old.as_ref().map_or(1, |o| o.0 as u32), flags, 0]; p.f[0] = [frac, h.div_ceil(s_new) as f32, 0.0, 0.0]; let out = e.scratch(w as u32, h as u32); e.pixels("fp2_block_load", &p, &new, old.as_ref().map(|o| &o.1), &out, None); b.img = out; Some(b) } // ---------------------------------------------------------------- CC Burn Film (stylize3) fn burn_film(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { let burn = (ctx.params.f("burn") / 100.0).clamp(0.0, 1.0) as f32; if burn <= 0.0 { return Some(b); } let c = b.to_px(ctx.params.v2("center")); let seed = ctx.params.f("randomSeed").max(0.0) as u32; let s = b.scale; let diag = (ctx.layer_size[0].hypot(ctx.layer_size[1]) * s).max(1.0); let feat = (60.0 * s).max(1e-3); let mut p = Params::default(); p.u[0][0] = seed.wrapping_add(17); p.f[0] = [c.0 as f32, c.1 as f32, diag as f32, feat as f32]; p.f[1] = [1.0 - burn * 1.25, 0.12 * (burn * 10.0).min(1.0), 0.0, 0.0]; run(e, "fp2_burn", &p, b, None, None) } // ---------------------------------------------------------------- CC Glass / CC Plastic (stylize3) fn bump(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option { let glass = id == "ec.stylize.ccglass"; // stylize3::height_field. let src = crate::fx_key::fitted(e, ctx, &b, effectcraft_effects::bump_layer_id(ctx), true, false)?.unwrap_or_else(|| b.img.clone()); let soft = ctx.params.f("softness") * b.scale * 0.5; let mut hf = plane(e, &src, 1, ctx.params.e("property"), soft); let height = ctx.params.f("height") as f32 / 100.0; let mut disp = 0.0; if glass { disp = (ctx.params.f("displacement") * b.scale * 0.25) as f32; } else { let lo = (ctx.params.f("cutMin") / 100.0) as f32; let hi = (ctx.params.f("cutMax") / 100.0) as f32; if lo > 0.0 || hi < 1.0 { hf = clamp_plane(e, &hf, lo.min(hi), hi.max(lo)); } } let l = effectcraft_effects::BumpLight::from(ctx, &geo(&b)); let mut p = Params::default(); p.u[0] = [glass as u32, l.point as u32, 0, 0]; p.f[0] = [height * 25.0, disp, (height != 0.0) as u32 as f32, l.height]; p.f[1] = [l.intensity, l.ambient, l.diffuse, l.specular]; p.f[2] = [l.shininess, l.metal, l.pos.0 as f32, l.pos.1 as f32]; p.f[3] = [l.color[0], l.color[1], l.color[2], 0.0]; p.f[4] = [l.dir[0], l.dir[1], l.dir[2], 0.0]; run(e, "fp2_bump", &p, b, Some(&hf), None) } /// An empty CPU buffer with `b`'s geometry (for CPU helpers that only read positions). fn geo(b: &GBuf) -> effectcraft_effects::Buf { effectcraft_effects::Buf { img: Image { width: b.img.width, height: b.img.height, data: vec![] }, offset: b.offset, scale: b.scale } } /// Clamp a plane (x) into [lo, hi] (Plane::map with f32::clamp). fn clamp_plane(e: &mut Enc, pl: &GpuImage, lo: f32, hi: f32) -> GpuImage { let mut p = Params::default(); p.u[0] = [3, 0, 0, 0]; p.f[0] = [lo, hi, 0.0, 0.0]; let out = e.scratch(pl.width, pl.height); e.pixels("fp2_plane", &p, pl, None, &out, None); out } // ---------------------------------------------------------------- CC HexTile / CC Mr. Smoothie (stylize3) fn hextile(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { let r = (ctx.params.f("radius") * b.scale).max(0.5); let c = b.to_px(ctx.params.v2("center")); let (sn, cs) = ctx.params.f("rotate").to_radians().sin_cos(); let mut p = Params::default(); p.u[0][0] = ctx.params.e("render"); p.f[0] = [c.0 as f32, c.1 as f32, r as f32, (ctx.params.f("smearing") / 100.0).clamp(0.0, 1.0) as f32]; p.f[1] = [sn as f32, cs as f32, 0.0, 0.0]; run(e, "fp2_hextile", &p, b, None, None) } fn smoothie(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { let flow = crate::fx_key::fitted(e, ctx, &b, "flowLayer", true, false)?.unwrap_or_else(|| b.img.clone()); let sm = ctx.params.f("smoothness") * b.scale * 0.5; let pl = plane(e, &flow, 2, ctx.params.e("property"), sm); let a = b.to_px(ctx.params.v2("sampleA")); let bb = b.to_px(ctx.params.v2("sampleB")); let mut p = Params::default(); p.f[0] = [a.0 as f32, a.1 as f32, bb.0 as f32, bb.1 as f32]; p.f[1] = [(ctx.params.f("phase") / 360.0) as f32, (ctx.params.e("colorLoop") + 1) as f32, 0.0, 0.0]; run(e, "fp2_smoothie", &p, b, Some(&pl), None) } // ---------------------------------------------------------------- Inner/Outer Key (keying2) fn inner_outer(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { let Some(plan) = effectcraft_effects::inner_outer_plan(ctx, &geo(&b)) else { return Some(b) }; let (w, h) = (b.img.width, b.img.height); let mut tri = Image::new(w, h); for (px, &t) in tri.data.iter_mut().zip(&plan.tri.data) { px[0] = t; } let tri = e.g.upload_image(&tri)?; // The matte (x, or w of the estimate) and the estimate's colours. let (mut matte, est) = if plan.unknown { let t = e.scratch(w, h); e.pixels("fp2_io_prep", &Params::default(), &b.img, Some(&tri), &t, None); let known = |e: &mut Enc, side: u32| { let mut p = Params::default(); p.u[0][0] = side; let k = e.scratch(w, h); e.pixels("fp2_io_known", &p, &t, None, &k, None); gauss_plane(e, &k, plan.sigma, plan.sigma) }; let fg = known(e, 0); let bg = known(e, 1); let (rows, stride) = e.image_rows(&t); let mut p = Params::default(); p.u[0][0] = stride; let est = e.scratch(w, h); e.pixels("fp2_io_est", &p, &fg, Some(&bg), &est, Some(&rows)); (est.clone(), Some(est)) } else { (tri, None) }; let from_w = est.is_some() as u32; if !plan.strokes.is_empty() || from_w != 0 { let n = plan.strokes.len(); let mut data = Vec::with_capacity(n + n * (w * h) as usize); data.extend(plan.strokes.iter().map(|s| s.0)); for (_, cov) in &plan.strokes { data.extend_from_slice(cov); } let buf = e.data(&data); let mut p = Params::default(); p.u[0] = [n as u32, from_w, 0, 0]; let out = e.scratch(w, h); e.pixels("fp2_io_strokes", &p, &matte, None, &out, Some(&buf)); matte = out; } if plan.thin.abs() > 0.01 { let modes = if plan.thin < 0.0 { crate::fx_color::DILATE_X } else { crate::fx_color::ERODE_X }; matte = crate::fx_color::morph_frac(e, &matte, plan.thin.abs(), modes); } if plan.feather > 0.05 { matte = gauss_plane(e, &matte, plan.feather * 0.5, plan.feather * 0.5); } let mut p = Params::default(); p.u[0] = [0, from_w, plan.invert as u32, 0]; p.f[0] = [plan.threshold, plan.original, 0.0, 0.0]; match est { Some(est) => { let (rows, stride) = e.image_rows(&est); p.u[0][0] = stride; run(e, "fp2_io_final", &p, b, Some(&matte), Some(&rows)) } None => run(e, "fp2_io_final", &p, b, Some(&matte), None), } } // ---------------------------------------------------------------- CC Simple Wire Removal (keying2) fn wire_removal(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { let half = (ctx.params.f("thickness") * b.scale * 0.5).max(0.0); if half <= 0.0 { return Some(b); } // Frame Offset reads another frame from the effect host: that renders on the CPU. if ctx.params.e("removalStyle") == 1 && ctx.params.f("frameOffset").round() != 0.0 && ctx.env.host.is_some() { return None; } let slope = (ctx.params.f("slope") / 100.0).clamp(0.0, 1.0) as f32; let mirror = (ctx.params.f("mirrorBlend") / 100.0).clamp(0.0, 1.0) as f32; let a = b.to_px(ctx.params.v2("pointA")); let z = b.to_px(ctx.params.v2("pointB")); let (dx, dy) = (z.0 - a.0, z.1 - a.1); let len = (dx * dx + dy * dy).sqrt(); if len < 1e-6 { return Some(b); } let (tx, ty) = (dx / len, dy / len); let mut p = Params::default(); p.u[0][0] = ctx.params.e("removalStyle"); p.f[0] = [a.0 as f32, a.1 as f32, -ty as f32, tx as f32]; p.f[1] = [half as f32, (half * slope as f64) as f32, mirror, 0.0]; run(e, "fp2_wire", &p, b, None, None) } // ---------------------------------------------------------------- Basic 3D (obsolete) fn basic_3d(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { let swivel = ctx.params.f("swivel").to_radians(); let tilt = ctx.params.f("tilt").to_radians(); let dist = ctx.params.f("distanceToImage") * b.scale; let spec_on = ctx.params.b("specularHighlight"); let wire = ctx.params.b("preview/drawPreviewWireframe"); if swivel == 0.0 && tilt == 0.0 && dist == 0.0 && !spec_on && !wire { return Some(b); } let (lx, ly, lw, lh) = (b.offset[0], b.offset[1], ctx.layer_size[0] * b.scale, ctx.layer_size[1] * b.scale); let (cx, cy) = (lx + lw * 0.5, ly + lh * 0.5); let f = lw.max(lh).max(1.0) * 1.4; let (cs, ss) = (swivel.cos(), swivel.sin()); let (ct, st) = (tilt.cos(), tilt.sin()); let v3 = |v: [f64; 3]| [v[0] as f32, v[1] as f32, v[2] as f32, 0.0]; let l = [-0.5f64, -0.6, -1.0]; let k = 1.0 / (l[0] * l[0] + l[1] * l[1] + l[2] * l[2]).sqrt(); let mut p = Params::default(); p.u[0] = [spec_on as u32, wire as u32, 0, 0]; p.f[0] = [cx as f32, cy as f32, f as f32, dist as f32]; p.f[1] = v3([cs, 0.0, -ss]); p.f[2] = v3([ss * st, ct, cs * st]); p.f[3] = v3([ss * ct, -st, cs * ct]); p.f[4] = v3(l.map(|v| v * k)); p.f[5] = [(lw * 0.5) as f32, (lh * 0.5) as f32, 0.0, 0.0]; run(e, "fp2_basic3d", &p, b, None, None) }