diff --git a/crates/effects/src/catalog.rs b/crates/effects/src/catalog.rs index f41b4b2e..3a1c3a1b 100644 --- a/crates/effects/src/catalog.rs +++ b/crates/effects/src/catalog.rs @@ -87,6 +87,7 @@ const CPU_ONLY_CONTROLS: &[(&str, &[&str])] = &[ ), ("ec.noise.addgrain", &["viewingMode", "preset", "color/redBalance", "color/greenBalance", "color/blueBalance", "previewRegion/"]), ("ec.blur.cameralens", &["irisProperties/diffractionFringe", "blurMap/blurMapLayer"]), + ("ec.distort.liquify", &["viewFreezeAreaMask", "viewMesh"]), ]; /// Whether the GPU kernel of effect `id` covers the instance's current settings (see diff --git a/crates/effects/src/distort2.rs b/crates/effects/src/distort2.rs index 43378528..8f430a96 100644 --- a/crates/effects/src/distort2.rs +++ b/crates/effects/src/distort2.rs @@ -354,7 +354,7 @@ fn magnify(ctx: &EffectCtx, mut b: Buf) -> Buf { } /// Magnify's Blending Mode choices after "None". -const MAGNIFY_MODES: [BlendMode; 17] = [ +pub const MAGNIFY_MODES: [BlendMode; 17] = [ BlendMode::Normal, BlendMode::Add, BlendMode::Multiply, diff --git a/crates/effects/src/distort4.rs b/crates/effects/src/distort4.rs index c576fead..d1d1b0c2 100644 --- a/crates/effects/src/distort4.rs +++ b/crates/effects/src/distort4.rs @@ -325,7 +325,9 @@ pub fn build_mesh(strokes: &[LiquifyStroke], w: f64, h: f64, frozen: &(dyn Fn(f6 type MeshCache = Mutex>>; -fn cached_mesh(ctx: &EffectCtx) -> Arc { +/// Liquify's displacement mesh for the instance (replayed strokes, cached; also used by the +/// GPU kernels). +pub fn liquify_mesh(ctx: &EffectCtx) -> Arc { use std::hash::{Hash, Hasher}; let text = ctx.params.s("distortionMesh"); let mask = ctx.params.f("freezeAreaMask").round() as usize; @@ -369,7 +371,7 @@ fn liquify(ctx: &EffectCtx, mut b: Buf) -> Buf { if ctx.params.s("distortionMesh").trim().is_empty() && !show_freeze && !show_mesh { return b; } - let mesh = cached_mesh(ctx); + let mesh = liquify_mesh(ctx); let off = ctx.params.v2("distortionMeshOffset"); let mesh_color = ctx.params.color("meshColor"); let src = b.img.clone(); diff --git a/crates/effects/src/lib.rs b/crates/effects/src/lib.rs index bc1cd520..972378e2 100644 --- a/crates/effects/src/lib.rs +++ b/crates/effects/src/lib.rs @@ -71,7 +71,8 @@ pub use color_fx::{ }; pub use color2::Curve; pub use distort::transform_shutter; -pub use distort2::parse_mesh; +pub use distort2::{MAGNIFY_MODES, parse_mesh}; +pub use distort4::liquify_mesh; use effectcraft_keyframe::Value; use effectcraft_project::build::Ids; use effectcraft_project::{GroupKind, ParamUi, PropGroup, Property}; @@ -79,6 +80,7 @@ pub use effectcraft_raster::{AuxChannels, Image}; pub use misc::{INVERT_ALPHA, INVERT_CHANNELS, glow_operation}; pub use sim::particle_state; pub use sim3::playground_state; +pub use stylize2::strobe_on; // CPU helpers the GPU kernels share (effectcraft-gpu). pub use blur2::{camera_lens_plain, camera_lens_spans}; pub use generate::gen_mode; @@ -809,7 +811,33 @@ pub const GPU_EFFECTS: &[&str] = &[ // effectcraft-gpu fx_key (keying, matte, channel) // // effectcraft-gpu fx_stylize (stylize, distort) - // + "ec.stylize.posterize", + "ec.stylize.threshold", + "ec.stylize.ccthreshold", + "ec.stylize.ccthresholdrgb", + "ec.stylize.strobe", + "ec.stylize.ccvignette", + "ec.stylize.scatter", + "ec.stylize.brushstrokes", + "ec.stylize.roughenedges", + "ec.stylize.texturize", + "ec.stylize.motiontile", + "ec.stylize.cckaleida", + "ec.stylize.ccrepetile", + "ec.distort.mirror", + "ec.distort.offset", + "ec.distort.polar", + "ec.distort.spherize", + "ec.distort.cornerpin", + "ec.distort.opticscompensation", + "ec.distort.magnify", + "ec.distort.ccslant", + "ec.distort.ccsmear", + "ec.distort.ccsplit", + "ec.distort.ccsplit2", + "ec.distort.cctiler", + "ec.distort.ccgriddler", + "ec.distort.liquify", // effectcraft-gpu fx_noise (noise, blur, time) // ]; diff --git a/crates/effects/src/stylize2.rs b/crates/effects/src/stylize2.rs index 06c09d2e..b957b4e4 100644 --- a/crates/effects/src/stylize2.rs +++ b/crates/effects/src/stylize2.rs @@ -223,7 +223,8 @@ fn scatter(ctx: &EffectCtx, mut b: Buf) -> Buf { // ---- Strobe Light ---- -pub(crate) fn strobe_on(time: f64, duration: f64, period: f64, prob: f64, seed: u32) -> bool { +/// Whether Strobe Light flashes at `time` (also used by the GPU kernels). +pub fn strobe_on(time: f64, duration: f64, period: f64, prob: f64, seed: u32) -> bool { if period <= 0.0 { return false; } diff --git a/crates/gpu/src/effects.rs b/crates/gpu/src/effects.rs index 8169164a..a70c0477 100644 --- a/crates/gpu/src/effects.rs +++ b/crates/gpu/src/effects.rs @@ -113,6 +113,8 @@ fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option { "ec.blur.gaussian" => gaussian(e, ctx, b), "ec.blur.fastbox" => box_blur(e, ctx, b), "ec.blur.directional" => directional(e, ctx, b), + "ec.stylize.glow" if crate::fx_stylize::glow_extra(ctx) => crate::fx_stylize::glow(e, ctx, b), + "ec.distort.transform" if crate::fx_stylize::transform_blur(ctx) => crate::fx_stylize::transform(e, ctx, b), "ec.stylize.glow" => glow(e, ctx, b), "ec.perspective.dropshadow" => drop_shadow(e, ctx, b), "ec.distort.transform" => transform(e, ctx, b), diff --git a/crates/gpu/src/fx_stylize.rs b/crates/gpu/src/fx_stylize.rs index 725b2eac..7b3d4891 100644 --- a/crates/gpu/src/fx_stylize.rs +++ b/crates/gpu/src/fx_stylize.rs @@ -1,19 +1,889 @@ //! GPU effects, stylize and distort family (kernels in `shaders/fx_stylize.wgsl`, every entry point prefixed //! `fxs_`): the CPU effects' exact steps with the pixel loops as compute kernels. +//! +//! Parameter conversions, padding, output geometry and anything the CPU computes in f64 with a +//! floor or a wrap (tile indices, wrapped coordinates, brush cells) are computed here exactly as +//! the CPU does and uploaded as per-column / per-row / per-cell tables; the kernels do the +//! per-pixel work. Glow's other Glow Operations / Arbitrary Map and Transform's shutter-angle +//! motion blur extend the base kernels in `effects.rs` (see [`glow_extra`], [`transform_blur`]). +use std::f64::consts::{FRAC_PI_2, TAU}; + +use effectcraft_color::BlendMode; use effectcraft_effects::EffectCtx; +use effectcraft_geom::{Mat3, vec2}; +use effectcraft_raster::Sampling; -use crate::context::Enc; -use crate::effects::GBuf; +use crate::context::{Enc, Params}; +use crate::effects::{GBuf, gaussian_blur}; +use crate::ops; /// Compute entry points in `fx_stylize.wgsl`. -pub(crate) const KERNELS: &[&str] = &[]; +pub(crate) const KERNELS: &[&str] = &[ + "fxs_point", + "fxs_warp", + "fxs_table", + "fxs_griddler", + "fxs_motiontile", + "fxs_repetile", + "fxs_magnify", + "fxs_scatter", + "fxs_brush", + "fxs_roughen", + "fxs_tex_luma", + "fxs_texturize", + "fxs_glow_map", + "fxs_glow_op", + "fxs_add", + "fxs_div", +]; /// Effect ids implemented here. -pub(crate) const IDS: &[&str] = &[]; +pub(crate) const IDS: &[&str] = &[ + "ec.stylize.posterize", + "ec.stylize.threshold", + "ec.stylize.ccthreshold", + "ec.stylize.ccthresholdrgb", + "ec.stylize.strobe", + "ec.stylize.ccvignette", + "ec.stylize.scatter", + "ec.stylize.brushstrokes", + "ec.stylize.roughenedges", + "ec.stylize.texturize", + "ec.stylize.motiontile", + "ec.stylize.cckaleida", + "ec.stylize.ccrepetile", + "ec.distort.mirror", + "ec.distort.offset", + "ec.distort.polar", + "ec.distort.spherize", + "ec.distort.cornerpin", + "ec.distort.opticscompensation", + "ec.distort.magnify", + "ec.distort.ccslant", + "ec.distort.ccsmear", + "ec.distort.ccsplit", + "ec.distort.ccsplit2", + "ec.distort.cctiler", + "ec.distort.ccgriddler", + "ec.distort.liquify", +]; /// 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.stylize.posterize" => posterize(e, ctx, b), + "ec.stylize.threshold" => threshold(e, ctx, b), + "ec.stylize.ccthreshold" => cc_threshold(e, ctx, b), + "ec.stylize.ccthresholdrgb" => cc_threshold_rgb(e, ctx, b), + "ec.stylize.strobe" => strobe(e, ctx, b), + "ec.stylize.ccvignette" => vignette(e, ctx, b), + "ec.stylize.scatter" => scatter(e, ctx, b), + "ec.stylize.brushstrokes" => brush_strokes(e, ctx, b), + "ec.stylize.roughenedges" => roughen_edges(e, ctx, b), + "ec.stylize.texturize" => texturize(e, ctx, b), + "ec.stylize.motiontile" => motion_tile(e, ctx, b), + "ec.stylize.cckaleida" => kaleida(e, ctx, b), + "ec.stylize.ccrepetile" => repetile(e, ctx, b), + "ec.distort.mirror" => mirror(e, ctx, b), + "ec.distort.offset" => offset(e, ctx, b), + "ec.distort.polar" => polar(e, ctx, b), + "ec.distort.spherize" => spherize(e, ctx, b), + "ec.distort.cornerpin" => corner_pin(e, ctx, b), + "ec.distort.opticscompensation" => optics_compensation(e, ctx, b), + "ec.distort.magnify" => magnify(e, ctx, b), + "ec.distort.ccslant" => cc_slant(e, ctx, b), + "ec.distort.ccsmear" => cc_smear(e, ctx, b), + "ec.distort.ccsplit" => { + let s = ctx.params.f("split") / 100.0; + cc_split(e, ctx, b, s, s) + } + "ec.distort.ccsplit2" => cc_split(e, ctx, b, ctx.params.f("split1") / 100.0, ctx.params.f("split2") / 100.0), + "ec.distort.cctiler" => cc_tiler(e, ctx, b), + "ec.distort.ccgriddler" => cc_griddler(e, ctx, b), + "ec.distort.liquify" => liquify(e, ctx, b), + _ => None, + } +} + +/// Run a same-size per-pixel kernel over the buffer. +fn run(e: &mut Enc, entry: &str, p: &Params, mut b: GBuf, data: Option<&wgpu::Buffer>) -> Option { + let out = e.image(b.img.width, b.img.height); + e.pixels(entry, p, &b.img, None, &out, data); + b.img = out; + Some(b) +} + +/// util::layer_rect. +fn layer_rect(ctx: &EffectCtx, b: &GBuf) -> (f64, f64, f64, f64) { + (b.offset[0], b.offset[1], ctx.layer_size[0] * b.scale, ctx.layer_size[1] * b.scale) +} + +// ---------------------------------------------------------------- per-pixel (fxs_point) + +fn point(e: &mut Enc, mode: u32, f: impl FnOnce(&mut Params), b: GBuf) -> Option { + let mut p = Params::default(); + p.u[0][0] = mode; + f(&mut p); + run(e, "fxs_point", &p, b, None) +} + +fn posterize(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let lv = ctx.params.f("level").round().clamp(2.0, 255.0) as f32 - 1.0; + point(e, 1, |p| p.f[0][0] = lv, b) +} + +fn threshold(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let t = ctx.params.f("level") as f32 / 255.0; + point(e, 2, |p| p.f[0][0] = t, b) +} + +fn cc_threshold(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let t = ctx.params.f("threshold") as f32 / 255.0; + let blend = ctx.params.f("blend") as f32 / 100.0; + let (ch, inv) = (ctx.params.e("channel"), ctx.params.b("invert") as u32); + point( + e, + 3, + |p| { + p.u[0][1] = ch; + p.u[0][2] = inv; + p.f[0] = [t, blend, 0.0, 0.0]; + }, + b, + ) +} + +fn cc_threshold_rgb(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let t = [ctx.params.f("redThreshold"), ctx.params.f("greenThreshold"), ctx.params.f("blueThreshold")].map(|v| v as f32 / 255.0); + let inv = [ctx.params.b("invertRed"), ctx.params.b("invertGreen"), ctx.params.b("invertBlue")].map(u32::from); + let blend = ctx.params.f("blend") as f32 / 100.0; + point( + e, + 4, + |p| { + p.u[0] = [4, inv[0], inv[1], inv[2]]; + p.f[0] = [t[0], t[1], t[2], blend]; + }, + b, + ) +} + +fn strobe(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let on = effectcraft_effects::strobe_on( + ctx.time, + ctx.params.f("strobeDuration"), + ctx.params.f("strobePeriod"), + ctx.params.f("randomStrobeProbability") / 100.0, + ctx.seed, + ); + if !on { + return Some(b); + } + let blend = (ctx.params.f("blendWithOriginal") as f32 / 100.0).clamp(0.0, 1.0); + if ctx.params.e("strobe") == 1 { + // Image::scale_alpha. + if (blend - 1.0).abs() < 1e-7 { + return Some(b); + } + return point(e, 6, |p| p.f[0][0] = blend, b); + } + let s = ctx.params.color("strobeColor"); + let op = ctx.params.e("strobeOperator"); + point( + e, + 5, + |p| { + p.u[0][1] = op; + p.f[0] = [s[0], s[1], s[2], blend]; + }, + b, + ) +} + +fn vignette(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let amt = ctx.params.f("amount") as f32 / 100.0; + let fov = ctx.params.f("angleOfView").clamp(0.0, 179.0).to_radians(); + if amt == 0.0 || fov <= 0.0 { + return Some(b); + } + let c = b.to_px(ctx.params.v2("center")); + let pin = ctx.params.f("pinHighlights") as f32 / 100.0; + let (lw, lh) = (ctx.layer_size[0] * b.scale, ctx.layer_size[1] * b.scale); + let half_diag = (lw * lw + lh * lh).sqrt() * 0.5; + let dist = half_diag.max(1.0) / (fov * 0.5).tan(); + point( + e, + 7, + |p| { + p.f[0] = [c.0 as f32, c.1 as f32, dist as f32, amt]; + p.f[1][0] = pin; + }, + b, + ) +} + +// ---------------------------------------------------------------- warps (fxs_warp) + +fn warp(e: &mut Enc, mode: u32, f: impl FnOnce(&mut Params), b: GBuf, data: Option<&wgpu::Buffer>) -> Option { + let mut p = Params::default(); + p.u[0][0] = mode; + f(&mut p); + run(e, "fxs_warp", &p, b, data) +} + +fn mirror(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let c = b.to_px(ctx.params.v2("center")); + let a = ctx.params.f("angle").to_radians(); + warp(e, 1, |p| p.f[0] = [c.0 as f32, c.1 as f32, a.cos() as f32, a.sin() as f32], b, None) +} + +fn spherize(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let r = (ctx.params.f("radius") * b.scale).max(1.0); + let c = b.to_px(ctx.params.v2("center")); + warp(e, 2, |p| p.f[0] = [c.0 as f32, c.1 as f32, r as f32, 0.0], b, None) +} + +fn polar(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let amt = ctx.params.f("interpolation") / 100.0; + let to_polar = ctx.params.e("conversion") == 1; + let (w, h) = (b.img.width as f64, b.img.height as f64); + let (cx, cy) = (w / 2.0, h / 2.0); + let rmax = cx.min(cy); + warp( + e, + 3, + |p| { + p.u[0][1] = to_polar as u32; + p.f[0] = [w as f32, h as f32, cx as f32, cy as f32]; + p.f[1] = [rmax as f32, amt as f32, 0.0, 0.0]; + }, + b, + None, + ) +} + +fn cc_slant(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option { + let k = ctx.params.f("slant") / 100.0; + let height = ctx.params.f("height") / 100.0; + if k == 0.0 && (height - 1.0).abs() < 1e-12 { + return Some(b); + } + let stretch = ctx.params.b("stretching"); + let xs = if stretch { 1.0 / (1.0 + k * k).sqrt() } else { 1.0 }; + let hinv = if height.abs() < 1e-6 { 1e6 } else { 1.0 / height }; + if !ctx.adjustment { + let lean = (k * ctx.layer_size[1] * b.scale * height.max(1.0)).abs(); + let grow = ((height - 1.0).max(0.0) * ctx.layer_size[1] * b.scale).max(lean); + b.pad(e, grow.ceil().min(4096.0) as u32)?; + } + let fl = b.to_px(ctx.params.v2("floor")); + warp( + e, + 4, + |p| { + p.f[0] = [fl.0 as f32, fl.1 as f32, hinv as f32, k as f32]; + p.f[1][0] = xs as f32; + }, + b, + None, + ) +} + +fn cc_smear(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let from = b.to_px(ctx.params.v2("from")); + let to = b.to_px(ctx.params.v2("to")); + let reach = ctx.params.f("reach") / 100.0; + let radius = (ctx.params.f("radius") * b.scale).max(0.5); + let (dx, dy) = (to.0 - from.0, to.1 - from.1); + let l2 = dx * dx + dy * dy; + if l2 < 1e-12 || reach == 0.0 { + return Some(b); + } + warp( + e, + 5, + |p| { + p.f[0] = [from.0 as f32, from.1 as f32, dx as f32, dy as f32]; + p.f[1] = [l2 as f32, reach as f32, radius as f32, 0.0]; + }, + b, + None, + ) +} + +/// distort3::split_impl (CC Split / CC Split 2). +fn cc_split(e: &mut Enc, ctx: &EffectCtx, b: GBuf, s1: f64, s2: f64) -> Option { + if s1 == 0.0 && s2 == 0.0 { + return Some(b); + } + let a = b.to_px(ctx.params.v2("pointA")); + let c = b.to_px(ctx.params.v2("pointB")); + let (dx, dy) = (c.0 - a.0, c.1 - a.1); + let len = (dx * dx + dy * dy).sqrt(); + if len < 1e-6 { + return Some(b); + } + let (ux, uy) = (dx / len, dy / len); + warp( + e, + 6, + |p| { + p.f[0] = [a.0 as f32, a.1 as f32, ux as f32, uy as f32]; + p.f[1] = [len as f32, s1 as f32, s2 as f32, (len * 0.5) as f32]; + }, + b, + None, + ) +} + +fn optics_compensation(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option { + let fov = ctx.params.f("fieldOfView").clamp(0.0, 179.0).to_radians(); + if fov < 1e-6 { + return Some(b); + } + let reverse = ctx.params.b("reverseLensDistortion"); + let (lx, ly, lw, lh) = layer_rect(ctx, &b); + let r_ref = match ctx.params.e("fovOrientation") { + 1 => lh * 0.5, + 2 => (lw * lw + lh * lh).sqrt() * 0.5, + _ => lw * 0.5, + } + .max(1.0); + let half = fov * 0.5; + let f = r_ref / half.tan(); + let optimal = ctx.params.b("optimalPixels"); + let resize = if optimal { 0 } else { ctx.params.e("resize") }; + let corners = [(lx, ly), (lx + lw, ly), (lx, ly + lh), (lx + lw, ly + lh)]; + if reverse && resize > 0 && !ctx.adjustment { + let c = b.to_px(ctx.params.v2("viewCenter")); + let rc = corners.iter().map(|p| ((p.0 - c.0).powi(2) + (p.1 - c.1).powi(2)).sqrt()).fold(0.0, f64::max); + let th = rc / r_ref * half; + let cap = lw.max(lh) * [0.0, 0.5, 1.5, 3.5][resize.min(3) as usize]; + let grow = if th < FRAC_PI_2 - 1e-3 { (f * th.tan() - rc).max(0.0) } else { cap }; + b.pad(e, grow.min(cap).min(4096.0).ceil() as u32)?; + } + let c = b.to_px(ctx.params.v2("viewCenter")); + let fit = if optimal { + let rc = corners.iter().map(|p| ((p.0 - c.0).powi(2) + (p.1 - c.1).powi(2)).sqrt()).fold(1.0, f64::max); + if reverse { + let th = (rc / r_ref * half).min(FRAC_PI_2 - 1e-3); + (f * th.tan() / rc).max(1.0) + } else { + ((rc / f).atan() / half * r_ref / rc).min(1.0) + } + } else { + 1.0 + }; + warp( + e, + 7, + |p| { + p.u[0][1] = reverse as u32; + p.f[0] = [c.0 as f32, c.1 as f32, fit as f32, f as f32]; + p.f[1] = [r_ref as f32, half as f32, 0.0, 0.0]; + }, + b, + None, + ) +} + +fn kaleida(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let c = b.to_px(ctx.params.v2("center")); + let size = ctx.params.f("size").max(1.0) / 100.0; + let mode = ctx.params.e("mirroring"); + let rot = ctx.params.f("rotation").to_radians(); + let n = [6.0, 8.0, 4.0, 12.0, 3.0][mode.min(4) as usize]; + let seg = TAU / n; + warp( + e, + 8, + |p| { + p.u[0][1] = (mode != 3) as u32; + p.f[0] = [c.0 as f32, c.1 as f32, size as f32, rot as f32]; + p.f[1][0] = seg as f32; + }, + b, + None, + ) +} + +fn liquify(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + // View Freeze Area Mask / View Mesh overlays render on the CPU (catalog CPU_ONLY_CONTROLS). + if ctx.params.b("viewFreezeAreaMask") || ctx.params.b("viewMesh") { + return None; + } + if ctx.params.s("distortionMesh").trim().is_empty() { + return Some(b); + } + let pct = ctx.params.f("distortionPercentage") / 100.0; + let mesh = effectcraft_effects::liquify_mesh(ctx); + if mesh.nx < 2 || mesh.ny < 2 { + return None; + } + let off = ctx.params.v2("distortionMeshOffset"); + let data: Vec = mesh.d.iter().flat_map(|d| [d[0] as f32, d[1] as f32]).collect(); + let buf = e.data(&data); + let (scale, o) = (b.scale, b.offset); + warp( + e, + 9, + |p| { + p.u[0] = [9, mesh.nx as u32, mesh.ny as u32, 0]; + p.f[0] = [mesh.cell as f32, off[0] as f32, off[1] as f32, pct as f32]; + p.f[1] = [scale as f32, o[0] as f32, o[1] as f32, 0.0]; + }, + b, + Some(&buf), + ) +} + +// ---------------------------------------------------------------- separable tables + +/// Per-column `fx(x + 0.5)` then per-row `fy(y + 0.5)`, `k` values each. +fn axis_table(w: u32, h: u32, fx: impl Fn(f64) -> [f64; K], fy: impl Fn(f64) -> [f64; K]) -> Vec { + let mut v = Vec::with_capacity((w + h) as usize * K); + v.extend((0..w).flat_map(|x| fx(x as f64 + 0.5).map(|t| t as f32))); + v.extend((0..h).flat_map(|y| fy(y as f64 + 0.5).map(|t| t as f32))); + v +} + +fn table(e: &mut Enc, b: GBuf, data: &[f32], repeat: bool, blend: f32) -> Option { + let mut p = Params::default(); + p.u[0] = [b.img.width, repeat as u32, 0, 0]; + p.f[0][0] = blend; + let buf = e.data(data); + run(e, "fxs_table", &p, b, Some(&buf)) +} + +fn offset(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let shift = ctx.params.v2("shift"); + let (w, h) = (b.img.width as f64, b.img.height as f64); + let blend = ctx.params.f("blend") as f32 / 100.0; + let (cx, cy) = (w / 2.0, h / 2.0); + let (sx, sy) = b.to_px(shift); + let data = axis_table(b.img.width, b.img.height, |x| [(x - (sx - cx)).rem_euclid(w)], |y| [(y - (sy - cy)).rem_euclid(h)]); + table(e, b, &data, false, blend.max(0.0)) +} + +fn cc_tiler(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let s = (ctx.params.f("scale") / 100.0).max(0.001); + let c = b.to_px(ctx.params.v2("center")); + let blend = (ctx.params.f("blendWithOriginal") / 100.0) as f32; + let (x0, y0, w, h) = layer_rect(ctx, &b); + let (w, h) = (w.max(1.0), h.max(1.0)); + let data = axis_table(b.img.width, b.img.height, |x| [x0 + (c.0 + (x - c.0) / s - x0).rem_euclid(w)], |y| [y0 + (c.1 + (y - c.1) / s - y0).rem_euclid(h)]); + table(e, b, &data, true, blend) +} + +fn cc_griddler(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let hs = (ctx.params.f("horizontalScale") / 100.0).max(0.001); + let vs = (ctx.params.f("verticalScale") / 100.0).max(0.001); + let rot = ctx.params.f("rotation").to_radians(); + let cut = ctx.params.b("cutTiles"); + let (x0, y0, lw, _) = layer_rect(ctx, &b); + let t = (ctx.params.f("tileSize") / 100.0 * lw).max(1.0); + if (hs - 1.0).abs() < 1e-9 && (vs - 1.0).abs() < 1e-9 && rot.abs() < 1e-9 && !cut { + return Some(b); + } + let (sr, cr) = rot.sin_cos(); + let centre = |p: f64, o: f64| { + let tc = o + (((p - o) / t).floor() + 0.5) * t; + [tc, p - tc] + }; + let data = axis_table(b.img.width, b.img.height, |x| centre(x, x0), |y| centre(y, y0)); + let mut p = Params::default(); + p.u[0] = [b.img.width, cut as u32, 0, 0]; + p.f[0] = [cr as f32, sr as f32, hs as f32, vs as f32]; + p.f[1][0] = (0.45 * t) as f32; + let buf = e.data(&data); + run(e, "fxs_griddler", &p, b, Some(&buf)) +} + +fn motion_tile(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let s = b.scale; + let (lw, lh) = (ctx.layer_size[0], ctx.layer_size[1]); + if lw <= 0.0 || lh <= 0.0 { + return Some(b); + } + let tc = ctx.params.v2("tileCenter"); + let tw = (lw * ctx.params.f("tileWidth") / 100.0).max(0.01); + let th = (lh * ctx.params.f("tileHeight") / 100.0).max(0.01); + let mirror = ctx.params.b("mirrorEdges"); + let phase = ctx.params.f("phase") / 360.0; + let hshift = ctx.params.b("horizontalPhaseShift"); + let (nw, nh, noff) = if ctx.adjustment { + (b.img.width, b.img.height, b.offset) + } else { + let ow = (lw * ctx.params.f("outputWidth") / 100.0).max(1.0 / s); + let oh = (lh * ctx.params.f("outputHeight") / 100.0).max(1.0 / s); + ((ow * s).round().max(1.0) as u32, (oh * s).round().max(1.0) as u32, [(ow - lw) * 0.5 * s, (oh - lh) * 0.5 * s]) + }; + if !e.g.fits(nw, nh) { + return None; + } + let split = |t: f64| [t.floor(), t - t.floor()]; + let data = axis_table(nw, nh, |x| split(((x - noff[0]) / s - tc[0]) / tw + 0.5), |y| split(((y - noff[1]) / s - tc[1]) / th + 0.5)); + let mut p = Params::default(); + p.u[0] = [nw, hshift as u32, mirror as u32, 0]; + p.f[0] = [phase as f32, (lw * s) as f32, (lh * s) as f32, 0.0]; + p.f[1] = [b.offset[0] as f32, b.offset[1] as f32, 0.0, 0.0]; + let buf = e.data(&data); + let out = e.image(nw, nh); + e.pixels("fxs_motiontile", &p, &b.img, None, &out, Some(&buf)); + Some(GBuf { img: out, offset: noff, scale: s }) +} + +fn repetile(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let s = b.scale; + let (r, l, d, u) = ( + ctx.params.f("expandRight").max(0.0) * s, + ctx.params.f("expandLeft").max(0.0) * s, + ctx.params.f("expandDown").max(0.0) * s, + ctx.params.f("expandUp").max(0.0) * s, + ); + if ctx.adjustment || r + l + d + u < 0.5 { + return Some(b); + } + let (x0, y0, w, h) = layer_rect(ctx, &b); + if w < 1.0 || h < 1.0 { + return Some(b); + } + let (nw, nh) = ((w + l + r).round().max(1.0) as u32, (h + u + d).round().max(1.0) as u32); + if !e.g.fits(nw, nh) { + return None; + } + // stylize2::tile_coord. + let tile = |v: f64, size: f64| { + let i = (v / size).floor(); + [i, v - i * size] + }; + let data = axis_table(nw, nh, |x| tile(x - l, w), |y| tile(y - u, h)); + let mut p = Params::default(); + p.u[0] = [nw, ctx.params.e("tiling"), 0, 0]; + p.f[0] = [x0 as f32, y0 as f32, w as f32, h as f32]; + let buf = e.data(&data); + let out = e.image(nw, nh); + e.pixels("fxs_repetile", &p, &b.img, None, &out, Some(&buf)); + Some(GBuf { img: out, offset: [l, u], scale: s }) +} + +fn magnify(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option { + let square = ctx.params.e("shape") == 1; + let c = b.to_px(ctx.params.v2("center")); + let mag = (ctx.params.f("magnification") / 100.0).max(0.01); + let link = ctx.params.e("link"); + let mut size = ctx.params.f("size") * b.scale; + let mut feather = ctx.params.f("feather") * b.scale; + if link >= 1 { + size *= mag; + } + if link == 2 { + feather *= mag; + } + let op = (ctx.params.f("opacity") / 100.0) as f32; + let mode = ctx.params.e("blendingMode") as usize; + let blend = effectcraft_effects::MAGNIFY_MODES.get(mode.wrapping_sub(1)).copied(); + if ctx.params.b("resizeLayer") && link == 0 && !ctx.adjustment { + let (lx, ly, lw, lh) = layer_rect(ctx, &b); + let reach = size.max(0.0); + let over = [lx - (c.0 - reach), (c.0 + reach) - (lx + lw), ly - (c.1 - reach), (c.1 + reach) - (ly + lh)].into_iter().fold(0.0, f64::max); + b.pad(e, over.min(4096.0).ceil() as u32)?; + } + let c = b.to_px(ctx.params.v2("center")); + let axis = |p: f64, c: f64| { + let d = p - c; + let sp = c + d / mag; + [d, sp, sp.floor() + 0.5] + }; + let data = axis_table(b.img.width, b.img.height, |x| axis(x, c.0), |y| axis(y, c.1)); + let (kind, mode_id) = match blend { + None => (0, 0), + Some(BlendMode::Normal) => (1, 0), + Some(m) => (2, ops::mode_id(m)), + }; + let mut p = Params::default(); + p.u[0] = [b.img.width, square as u32, ctx.params.e("scaling"), kind]; + p.u[1] = [mode_id, ctx.seed ^ 0x3a61, 0, 0]; + p.f[0] = [size as f32, feather as f32, op, mag as f32]; + p.f[1][0] = (size - feather) as f32; + let buf = e.data(&data); + run(e, "fxs_magnify", &p, b, Some(&buf)) +} + +// ---------------------------------------------------------------- stylize2.rs + +fn scatter(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let amt = ctx.params.f("amount") * b.scale; + if amt <= 0.0 { + return Some(b); + } + let seed = if ctx.params.b("randomizeEveryFrame") { ctx.seed ^ ((ctx.time * 1000.0) as u32).wrapping_mul(0x9e37_79b9) } else { ctx.seed }; + let mut p = Params::default(); + p.u[0] = [ctx.params.e("grain"), seed, 0, 0]; + p.f[0][0] = amt as f32; + run(e, "fxs_scatter", &p, b, None) +} + +fn brush_strokes(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option { + let ang = ctx.params.f("strokeAngle"); + let size = (ctx.params.f("brushSize") * b.scale).max(0.5); + let len = ctx.params.f("strokeLength") * b.scale; + let density = ctx.params.f("strokeDensity") as f32; + let rnd = ctx.params.f("strokeRandomness"); + let blend = ctx.params.f("blendWithOriginal") as f32 / 100.0; + let seed = ctx.seed ^ 0xb705; + let cell = (size * 2.0).max(1.0); + let (w, h) = (b.img.width, b.img.height); + let cell_of = |i: u32| (i as f64 / cell).floor() as i64 as u32; + let (ncx, ncy) = (cell_of(w.saturating_sub(1)) + 1, cell_of(h.saturating_sub(1)) + 1); + if (ncx as u64 * ncy as u64 * 6 + (w + h) as u64) >= 1 << 24 { + return None; + } + let mut data: Vec = (0..w).map(|x| cell_of(x) as f32).chain((0..h).map(|y| cell_of(y) as f32)).collect(); + data.reserve((ncx * ncy * 6) as usize); + for cy in 0..ncy { + for cx in 0..ncx { + let hh = |k: u32| effectcraft_raster::hash_noise(cx.wrapping_add(k.wrapping_mul(7919)), cy, seed) as f64; + let a = (ang + (hh(0) - 0.5) * rnd * 60.0).to_radians(); + let l = len * (0.5 + hh(1)); + let (dx, dy) = (a.sin(), -a.cos()); + let jit = (hh(2) - 0.5) * size; + let n = l.ceil().clamp(1.0, 32.0); + let m = if (hh(3) as f32) < density.min(1.0) { 1.0 } else { 0.0 }; + data.extend([dx as f32, dy as f32, l as f32, jit as f32, n as f32, m]); + } + } + let mut p = Params::default(); + p.u[0] = [w, ncx, ctx.params.e("paintSurface"), h]; + p.f[0][0] = blend; + let buf = e.data(&data); + run(e, "fxs_brush", &p, b, Some(&buf)) +} + +fn roughen_edges(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option { + let kind = ctx.params.e("edgeType"); + let ec = ctx.params.color("edgeColor"); + let border = ctx.params.f("border") * b.scale; + let sharp = ctx.params.f("edgeSharpness").max(0.01) as f32; + let infl = ctx.params.f("fractalInfluence") as f32; + let scale = (ctx.params.f("scale") * b.scale * 0.1).max(0.5) as f32; + let (ox, oy) = b.to_px(ctx.params.v2("offset")); + let oct = ctx.params.f("complexity") as f32; + let evo = ctx.params.f("evolution") as f32 / 360.0; + let st = ctx.params.f("stretch") as f32; + let (sx, sy) = (1.0 + st.max(0.0), 1.0 + (-st).max(0.0)); + let cycle = ctx.params.b("evolutionOptions/cycleEvolution").then(|| ctx.params.f("evolutionOptions/cycle").round().max(1.0) as f32); + let (evo, evo_w, c) = match cycle { + Some(c) => { + let ev = evo.rem_euclid(c); + (ev, ev / c, c) + } + None => (evo, 0.0, 0.0), + }; + let (fm, im, sm, colored) = match kind { + 1 => (1.0, 1.0, 1.0, true), + 2 => (1.0, 1.0, 3.0, false), + 3 => (3.0, 1.0, 1.5, false), + 4 => (1.5, 1.5, 1.0, false), + 5 => (1.5, 1.5, 1.0, true), + 6 => (0.6, 1.2, 2.5, false), + 7 => (0.6, 1.2, 2.5, true), + _ => (1.0, 1.0, 1.0, false), + }; + let extra = if kind == 4 || kind == 5 { 1.0 } else { 0.0 }; + let seed = ctx.seed ^ 0x40f1 ^ (ctx.params.f("evolutionOptions/randomSeed").round() as i64 as u32).wrapping_mul(0x9e37_79b9); + // util::gauss_plane of the alpha (edges repeated); the kernel reads .w. + let d = if border > 0.05 { gaussian_blur(e, &b.img, border * 0.5, border * 0.5, true) } else { b.img.clone() }; + let mut p = Params::default(); + p.u[0] = [seed, cycle.is_some() as u32, colored as u32, 0]; + p.f[0] = [ox as f32, oy as f32, scale * sx, scale * sy]; + p.f[1] = [fm, evo, c, evo_w]; + p.f[2] = [oct + extra, infl, im, sharp]; + p.f[3] = [ec[0], ec[1], ec[2], sm]; + let out = e.image(b.img.width, b.img.height); + e.pixels("fxs_roughen", &p, &b.img, Some(&d), &out, None); + b.img = out; + Some(b) +} + +fn texturize(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option { + let ang = ctx.params.f("lightDirection").to_radians(); + let k = ctx.params.f("textureContrast") as f32; + if k == 0.0 { + return Some(b); + } + let Some(tex) = ctx.layer_param("textureLayer", true) else { return Some(b) }; + if tex.buf.img.is_empty() { + return None; + } + // transition::place_layer, per column / row in the texture buffer's pixels. + let (ls, os) = (ctx.layer_size, tex.size); + let inv = 1.0 / b.scale.max(1e-9); + let mode = ctx.params.e("texturePlacement"); + let place = |v: f64, k: usize| { + let p = (v - b.offset[k]) * inv; + let q = match mode { + 0 if os[0] >= 1.0 && os[1] >= 1.0 => p.rem_euclid(os[k]), + 1 => p + (os[k] - ls[k]) * 0.5, + _ if ls[0] > 0.0 && ls[1] > 0.0 => p * os[k] / ls[k], + _ => p, + }; + [q * tex.buf.scale + tex.buf.offset[k]] + }; + let data = axis_table(b.img.width, b.img.height, |x| place(x, 0), |y| place(y, 1)); + let timg = e.g.upload_image(&tex.buf.img)?; + let mut p = Params::default(); + p.u[0][0] = b.img.width; + let buf = e.data(&data); + let luma = e.image(b.img.width, b.img.height); + e.pixels("fxs_tex_luma", &p, &b.img, Some(&timg), &luma, Some(&buf)); + let s = 0.7 * b.scale; + let hgt = gaussian_blur(e, &luma, s, s, true); + let mut p = Params::default(); + p.f[0] = [ang.cos() as f32, -ang.sin() as f32, k, 0.0]; + let out = e.image(b.img.width, b.img.height); + e.pixels("fxs_texturize", &p, &b.img, Some(&hgt), &out, None); + b.img = out; + Some(b) +} + +// ---------------------------------------------------------------- distort.rs + +fn corner_pin(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option { + let (w, h) = (ctx.layer_size[0].max(1.0), ctx.layer_size[1].max(1.0)); + let q = ["ul", "ur", "lr", "ll"].map(|k| { + let p = b.to_px(ctx.params.v2(k)); + vec2(p.0, p.1) + }); + let quad = Mat3::square_to_quad(q); + let o = b.to_px([0.0, 0.0]); + let m = quad * Mat3::scale(vec2(1.0 / (w * b.scale), 1.0 / (h * b.scale))) * Mat3::translate(vec2(-o.0, -o.1)); + let empty = e.image(b.img.width, b.img.height); + b.img = ops::warp(e, &empty, &b.img, &m, Sampling::Bilinear, BlendMode::Normal, 1.0, 0, None); + Some(b) +} + +// ---------------------------------------------------------------- Glow / Transform extras + +/// Glow settings the base kernel in `effects.rs` leaves to the CPU (another Glow Operation or +/// the Arbitrary Map) and [`glow`] implements. +pub(crate) fn glow_extra(ctx: &EffectCtx) -> bool { + ctx.params.e("colors") == 2 || effectcraft_effects::glow_operation(ctx) != BlendMode::Add +} + +fn dims_xy(dim: u32) -> (f64, f64) { + match dim { + 1 => (1.0, 0.0), + 2 => (0.0, 1.0), + _ => (1.0, 1.0), + } +} + +/// misc::glow with any Glow Operation and the Arbitrary Map. +pub(crate) fn glow(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option { + let thr = ctx.params.f("threshold") as f32 / 100.0; + let radius = ctx.params.f("radius") * b.scale; + let intensity = ctx.params.f("intensity") as f32; + let colors = ctx.params.e("colors"); + let (kx, ky) = dims_xy(ctx.params.e("glowDimensions")); + if !ctx.adjustment { + b.pad(e, (radius * 1.5).ceil() as u32 + 2)?; + } + let mut p = Params::default(); + p.u[0] = [(colors == 1) as u32, (ctx.params.e("based") == 0) as u32, ctx.params.e("colorLooping"), 0]; + p.f[0] = [thr, ctx.params.f("colorLoops") as f32, (ctx.params.f("colorPhase") / 360.0) as f32, ctx.params.f("abMidpoint") as f32 / 100.0]; + let bright = e.image(b.img.width, b.img.height); + if colors == 2 { + // Arbitrary Map: Curves tables per channel (see effects::curves). + let mut parts = ctx.params.s("arbitraryMap").split('|'); + let curves: Vec> = (0..3).map(|_| parts.next().and_then(effectcraft_effects::Curve::parse)).collect(); + let mut data = vec![0.0f32]; + let mut offs = [-1.0f32; 3]; + for (i, c) in curves.iter().enumerate() { + let Some(c) = c else { continue }; + let (xs, ys, ms, lut) = c.tables(); + offs[i] = data.len() as f32; + data.push(xs.len() as f32); + data.extend_from_slice(xs); + data.extend_from_slice(ys); + data.extend_from_slice(ms); + data.extend_from_slice(lut); + } + p.f[1] = [offs[0], offs[1], offs[2], 0.0]; + let buf = e.data(&data); + e.pixels("fxs_glow_map", &p, &b.img, None, &bright, Some(&buf)); + } else { + p.f[1] = ctx.params.color("colorA"); + p.f[2] = ctx.params.color("colorB"); + e.pixels("glow_bright", &p, &b.img, None, &bright, None); + } + let s = (radius / 2.0).max(0.5); + let blurred = gaussian_blur(e, &bright, s * kx, s * ky, false); + let operation = ctx.params.e("operation"); + let glow_op = effectcraft_effects::glow_operation(ctx); + let mut p = Params::default(); + p.f[0][0] = intensity; + let out = e.image(b.img.width, b.img.height); + if operation == 0 && glow_op != BlendMode::Add { + p.u[0][0] = ops::mode_id(glow_op); + e.pixels("fxs_glow_op", &p, &b.img, Some(&blurred), &out, None); + } else { + p.u[0][0] = operation.min(2); + e.pixels("glow_combine", &p, &b.img, Some(&blurred), &out, None); + } + b.img = out; + Some(b) +} + +/// Transform renders with motion blur (a shutter and a host to read the parameters at other +/// times): [`transform`] implements it, the base kernel in `effects.rs` the sharp case. +pub(crate) fn transform_blur(ctx: &EffectCtx) -> bool { + ctx.env.host.is_some() && effectcraft_effects::transform_shutter(ctx).is_some() +} + +/// distort::transform_matrix. +fn transform_matrix(pr: &effectcraft_effects::Params, b: &GBuf) -> Mat3 { + let anchor = b.to_px(pr.v2("anchor")); + let pos = b.to_px(pr.v2("position")); + let sh = pr.f("scaleHeight"); + let sw = if pr.b("uniform") { sh } else { pr.f("scaleWidth") }; + Mat3::translate(vec2(pos.0, pos.1)) + * Mat3::rotate_deg(pr.f("rotation")) + * Mat3::skew_deg(pr.f("skew"), pr.f("skewAxis")) + * Mat3::scale(vec2(sw / 100.0, sh / 100.0)) + * Mat3::translate(vec2(-anchor.0, -anchor.1)) +} + +/// distort::transform's motion blur: the transform at Samples instants across the shutter, +/// averaged. +pub(crate) fn transform(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option { + let (angle, phase, n) = effectcraft_effects::transform_shutter(ctx)?; + let host = ctx.env.host?; + let sampling = if ctx.params.e("sampling") == 1 { Sampling::Bicubic } else { Sampling::Bilinear }; + let fd = 1.0 / ctx.fps(); + let (w, h) = (b.img.width, b.img.height); + let mut acc = e.image(w, h); + let mut count = 0u32; + for k in 0..n { + let t = ctx.time + (phase + angle * k as f64 / (n - 1) as f64) / 360.0 * fd; + let Some(pr) = host.params_at(t) else { continue }; + let op = pr.f("opacity") as f32 / 100.0; + let empty = e.image(w, h); + let one = ops::warp(e, &empty, &b.img, &transform_matrix(&pr, &b), sampling, BlendMode::Normal, op, 0, None); + let sum = e.image(w, h); + e.pixels("fxs_add", &Params::default(), &acc, Some(&one), &sum, None); + acc = sum; + count += 1; + } + if count == 0 { + // The CPU renders the frame sharp. + return None; + } + let mut p = Params::default(); + p.f[0][0] = count as f32; + let out = e.image(w, h); + e.pixels("fxs_div", &p, &acc, None, &out, None); + b.img = out; + Some(b) } diff --git a/crates/gpu/src/shaders/fx_stylize.wgsl b/crates/gpu/src/shaders/fx_stylize.wgsl index 01d1c54d..9c29925d 100644 --- a/crates/gpu/src/shaders/fx_stylize.wgsl +++ b/crates/gpu/src/shaders/fx_stylize.wgsl @@ -1,3 +1,808 @@ // GPU effects (stylize and distort family): see src/fx_stylize.rs. Each entry point mirrors the CPU effect in // effectcraft-effects operation for operation. Every name here is prefixed `fxs_` (the -// family files share one module). +// family files share one module). Coordinates are pixel centres (x + 0.5, y + 0.5) in buffer +// pixels, as in util::remap. Per-column / per-row tables in `data` hold coordinates the CPU +// computes in f64 (anything with a floor or a wrap), so tile and wrap boundaries match exactly. + +const FXS_TAU: f32 = 6.2831855; +const FXS_PI: f32 = 3.1415927; +const FXS_HALF_PI: f32 = 1.5707964; + +fn fxs_inside(p: vec2) -> bool { + let d = out_dims(); + return p.x < d.x && p.y < d.y; +} + +fn fxs_centre(p: vec2) -> vec2 { + return vec2(f32(p.x) + 0.5, f32(p.y) + 0.5); +} + +// util::unpremul: straight colour (zero below alpha 1e-6) and alpha (≥ 0). +fn fxs_unpremul(px: vec4) -> vec4 { + let a = px.w; + if (a > 1e-6) { + return vec4(fxs_qdiv(px.xyz, a), a); + } + return vec4(0.0, 0.0, 0.0, max(a, 0.0)); +} + +// util::smoothstep. +fn fxs_smoothstep(e0: f32, e1: f32, x: f32) -> f32 { + if (abs(e1 - e0) < 1e-9) { + return select(1.0, 0.0, x < e0); + } + let t = clamp((x - e0) / (e1 - e0), 0.0, 1.0); + return t * t * (3.0 - 2.0 * t); +} + +// x / a rounded like the CPU's IEEE division (GPU division may be off by an ulp, which flips +// thresholds on 8 / 16 bpc colours that sit exactly on them): one residual correction. +fn fxs_qdiv(x: vec3, a: f32) -> vec3 { + let q = x / a; + let r = fma(-q, vec3(a), x); + return q + r / a; +} + +fn fxs_rem2(i: i32) -> i32 { + return ((i % 2) + 2) % 2; +} + +// ---------------------------------------------------------------- per-pixel colour effects +// u[0].x = mode: +// 1 Posterize (misc::posterize): f[0].x = levels − 1 +// 2 Threshold (misc::threshold): f[0].x = level +// 3 CC Threshold: u[0].yz = (channel, invert); f[0].xy = (threshold, blend) +// 4 CC Threshold RGB: u[0].yzw = invert R, G, B; f[0] = (thresholds, blend) +// 5 Strobe Light on colour: u[0].y = operator; f[0] = (strobe colour, blend) +// 6 Strobe Light transparent (Image::scale_alpha): f[0].x = factor +// 7 CC Vignette: f[0] = (cx, cy, distance, amount); f[1].x = pin highlights + +fn fxs_bin(v: f32, t: f32, inv: bool) -> f32 { + return select(0.0, 1.0, (v >= t) != inv); +} + +fn fxs_strobe_op(op: u32, v: f32, k: f32) -> f32 { + switch op { + case 1u: { return v + k; } + case 2u: { return max(v - k, 0.0); } + case 3u: { return v * k; } + case 4u: { return abs(v - k); } + case 5u: { return 1.0 - (1.0 - v) * (1.0 - k); } + case 6u: { return max(v, k); } + case 7u: { return min(v, k); } + default: { return k; } + } +} + +@compute @workgroup_size(16, 16) +fn fxs_point(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fxs_inside(p)) { + return; + } + let px = textureLoad(src, p, 0); + let mode = P.u[0].x; + let a = px.w; + if (mode == 6u) { + textureStore(out, p, px * P.f[0].x); + return; + } + if (a <= 0.0) { + textureStore(out, p, px); + return; + } + switch mode { + case 1u: { + let lv = P.f[0].x; + let c = clamp(fxs_qdiv(px.xyz, a), vec3(0.0), vec3(1.0)) * lv; + let o = vec3(round_away(c.x), round_away(c.y), round_away(c.z)) / lv; + textureStore(out, p, vec4(o * a, a)); + } + case 2u: { + let v = select(0.0, 1.0, luminance(fxs_qdiv(px.xyz, a)) >= P.f[0].x); + textureStore(out, p, vec4(vec3(v) * a, a)); + } + case 3u: { + let u = fxs_unpremul(px); + let c = u.xyz; + let t = P.f[0].x; + let blend = P.f[0].y; + let inv = P.u[0].z != 0u; + var o = c; + var na = u.w; + switch P.u[0].y { + case 1u: { + o = vec3(fxs_bin(c.x, t, inv), fxs_bin(c.y, t, inv), fxs_bin(c.z, t, inv)); + } + case 2u: { + o = vec3(fxs_bin(rgb_to_hsl(c).y, t, inv)); + } + case 3u: { + na = fxs_bin(u.w, t, inv); + } + default: { + o = vec3(fxs_bin(luminance(c), t, inv)); + } + } + o = o + (c - o) * blend; + na = na + (u.w - na) * blend; + textureStore(out, p, vec4(o * na, na)); + } + case 4u: { + let c = fxs_qdiv(px.xyz, a); + let t = P.f[0].xyz; + let o = vec3(fxs_bin(c.x, t.x, P.u[0].y != 0u), fxs_bin(c.y, t.y, P.u[0].z != 0u), fxs_bin(c.z, t.z, P.u[0].w != 0u)); + let r = o + (c - o) * P.f[0].w; + textureStore(out, p, vec4(r * a, a)); + } + case 5u: { + let c = px.xyz / a; + let k = P.f[0].xyz; + let op = P.u[0].y; + let o = vec3(fxs_strobe_op(op, c.x, k.x), fxs_strobe_op(op, c.y, k.y), fxs_strobe_op(op, c.z, k.z)); + let r = o + (c - o) * P.f[0].w; + textureStore(out, p, vec4(r * a, a)); + } + case 7u: { + let u = fxs_unpremul(px); + let q = fxs_centre(p); + let dx = q.x - P.f[0].x; + let dy = q.y - P.f[0].y; + let r = sqrt(dx * dx + dy * dy) / P.f[0].z; + let f0 = 1.0 / (1.0 + r * r); + let f = f0 * f0; + var k = 1.0 - P.f[0].w * (1.0 - f); + let pin = P.f[1].x; + if (pin > 0.0) { + let l = clamp(luminance(u.xyz), 0.0, 1.0); + k += (1.0 - k) * pin * l; + } + textureStore(out, p, vec4(max(u.xyz * k, vec3(0.0)) * u.w, u.w)); + } + default: { + textureStore(out, p, px); + } + } +} + +// ---------------------------------------------------------------- inverse-mapped warps +// u[0].x = mode (all sample bilinear, transparent outside, unless noted): +// 1 Mirror (distort::mirror): f[0] = (cx, cy, nx, ny) +// 2 Spherize (distort::spherize): f[0] = (cx, cy, radius) +// 3 Polar Coordinates (distort::polar, edges repeated): u[0].y = to polar; +// f[0] = (w, h, cx, cy); f[1] = (rmax, interpolation) +// 4 CC Slant (distort3::cc_slant): f[0] = (floor x, floor y, 1 / height, slant); f[1].x = x scale +// 5 CC Smear (distort3::cc_smear): f[0] = (from x, from y, dx, dy); f[1] = (len², reach, radius) +// 6 CC Split (distort3::split_impl): f[0] = (ax, ay, ux, uy); f[1] = (len, split 1, split 2, reach) +// 7 Optics Compensation (distort2::optics_compensation): u[0].y = reverse; +// f[0] = (cx, cy, fit, f); f[1] = (reference radius, half FOV) +// 8 CC Kaleida (stylize2::kaleida, edges repeated): u[0].y = mirror; +// f[0] = (cx, cy, size, rotation); f[1].x = segment angle +// 9 Liquify (distort4::liquify): u[0].xy = (_, mesh nx, mesh ny); f[0] = (cell, mesh offset x, y, +// percentage); f[1] = (scale, buffer offset x, y); data = mesh offsets (x, y) per node + +fn fxs_mesh_d(i: u32, j: u32) -> vec2 { + let k = (j * P.u[0].y + i) * 2u; + return vec2(data[k], data[k + 1u]); +} + +// distort4::Mesh::at. +fn fxs_mesh_at(x: f32, y: f32) -> vec2 { + let cell = P.f[0].x; + let nx = P.u[0].y; + let ny = P.u[0].z; + let fx = x / cell; + let fy = y / cell; + if (fx < 0.0 || fy < 0.0 || fx > f32(nx - 1u) || fy > f32(ny - 1u)) { + return vec2(0.0); + } + let x0 = min(u32(floor(fx)), nx - 2u); + let y0 = min(u32(floor(fy)), ny - 2u); + let x1 = min(x0 + 1u, nx - 1u); + let y1 = min(y0 + 1u, ny - 1u); + let tx = fx - f32(x0); + let ty = fy - f32(y0); + let a = fxs_mesh_d(x0, y0); + let b = fxs_mesh_d(x1, y0); + let c = fxs_mesh_d(x0, y1); + let d = fxs_mesh_d(x1, y1); + let top = a + (b - a) * tx; + let bot = c + (d - c) * tx; + return top + (bot - top) * ty; +} + +@compute @workgroup_size(16, 16) +fn fxs_warp(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fxs_inside(p)) { + return; + } + let q = fxs_centre(p); + let x = q.x; + let y = q.y; + var s = q; + var clamped = false; + switch P.u[0].x { + case 1u: { + let n = P.f[0].zw; + let d = (x - P.f[0].x) * n.x + (y - P.f[0].y) * n.y; + if (d > 0.0) { + s = vec2(x - 2.0 * d * n.x, y - 2.0 * d * n.y); + } + } + case 2u: { + let c = P.f[0].xy; + let r = P.f[0].z; + let dx = x - c.x; + let dy = y - c.y; + let d = sqrt(dx * dx + dy * dy); + if (d < r && d != 0.0) { + let nd = d / r; + let k = (asin(nd) / FXS_HALF_PI) / nd; + s = vec2(c.x + dx * k, c.y + dy * k); + } + } + case 3u: { + clamped = true; + let w = P.f[0].x; + let h = P.f[0].y; + let cx = P.f[0].z; + let cy = P.f[0].w; + let rmax = P.f[1].x; + var pp: vec2; + if (P.u[0].y != 0u) { + let dx = x - cx; + let dy = y - cy; + let a0 = atan2(dx, -dy); + let a = a0 - floor(a0 / FXS_TAU) * FXS_TAU; + let r = sqrt(dx * dx + dy * dy) / rmax; + pp = vec2(a / FXS_TAU * w, h - r * h); + } else { + let a = x / w * FXS_TAU; + let r = (h - y) / h * rmax; + pp = vec2(cx + r * sin(a), cy - r * cos(a)); + } + let amt = P.f[1].y; + s = vec2(x + (pp.x - x) * amt, y + (pp.y - y) * amt); + } + case 4u: { + let fl = P.f[0].xy; + s = vec2(fl.x + (x - fl.x - (fl.y - y) * P.f[0].w) / P.f[1].x, fl.y + (y - fl.y) * P.f[0].z); + } + case 5u: { + let fr = P.f[0].xy; + let dx = P.f[0].z; + let dy = P.f[0].w; + let t = clamp(((x - fr.x) * dx + (y - fr.y) * dy) / P.f[1].x, 0.0, 1.0); + let px = fr.x + dx * t; + let py = fr.y + dy * t; + let d = sqrt((x - px) * (x - px) + (y - py) * (y - py)); + let w0 = max(1.0 - d / P.f[1].z, 0.0); + let w = w0 * w0 * (3.0 - 2.0 * w0); + let k = P.f[1].y * w * t; + s = vec2(x - dx * k, y - dy * k); + } + case 6u: { + let a = P.f[0].xy; + let ux = P.f[0].z; + let uy = P.f[0].w; + let nx = -uy; + let ny = ux; + let len = P.f[1].x; + let reach = P.f[1].w; + let rx = x - a.x; + let ry = y - a.y; + let t = (rx * ux + ry * uy) / len; + let sd = rx * nx + ry * ny; + if (t >= 0.0 && t <= 1.0) { + let amt = select(P.f[1].z, P.f[1].y, sd >= 0.0); + let g = amt * reach * sin(FXS_PI * t); + let ad = abs(sd); + if (ad < g) { + textureStore(out, p, vec4(0.0)); + return; + } + let push = g * max(1.0 - (ad - g) / reach, 0.0); + let ns = select(-1.0, 1.0, sd >= 0.0) * (ad - push); + s = vec2(x + nx * (ns - sd), y + ny * (ns - sd)); + } + } + case 7u: { + let c = P.f[0].xy; + let fit = P.f[0].z; + let f = P.f[0].w; + let r_ref = P.f[1].x; + let half = P.f[1].y; + let dx = x - c.x; + let dy = y - c.y; + let r = sqrt(dx * dx + dy * dy) * fit; + if (r >= 1e-9) { + var rs: f32; + if (P.u[0].y != 0u) { + rs = atan(r / f) / half * r_ref; + } else { + let th = r / r_ref * half; + if (th >= FXS_HALF_PI - 1e-4) { + textureStore(out, p, vec4(0.0)); + return; + } + rs = f * tan(th); + } + let k = rs / r * fit; + s = vec2(c.x + dx * k, c.y + dy * k); + } + } + case 8u: { + clamped = true; + let c = P.f[0].xy; + let rot = P.f[0].w; + let seg = P.f[1].x; + let dx = x - c.x; + let dy = y - c.y; + let r = sqrt(dx * dx + dy * dy) / P.f[0].z; + let a0 = atan2(dy, dx) - rot; + var a = a0 - floor(a0 / seg) * seg; + if (P.u[0].y != 0u && a > seg * 0.5) { + a = seg - a; + } + s = vec2(c.x + r * cos(a + rot), c.y + r * sin(a + rot)); + } + case 9u: { + let scale = P.f[1].x; + let o = P.f[1].yz; + let lx = (x - o.x) / scale; + let ly = (y - o.y) / scale; + let d = fxs_mesh_at(lx - P.f[0].y, ly - P.f[0].z); + if (d.x == 0.0 && d.y == 0.0) { + textureStore(out, p, textureLoad(src, p, 0)); + return; + } + let pct = P.f[0].w; + s = vec2((lx + d.x * pct) * scale + o.x, (ly + d.y * pct) * scale + o.y); + } + default: {} + } + if (clamped) { + textureStore(out, p, sample_bilinear_clamped(src, s.x, s.y)); + } else { + textureStore(out, p, sample_bilinear(src, s.x, s.y)); + } +} + +// ---------------------------------------------------------------- separable table warps +// The source point is (data[x], data[w + y]) (computed in f64 per column and per row), then +// mixed with the original: out = sample + (orig − sample) · blend (Offset, CC Tiler). +// u[0] = (buffer width, repeat edges); f[0].x = blend +@compute @workgroup_size(16, 16) +fn fxs_table(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fxs_inside(p)) { + return; + } + let w = P.u[0].x; + let sx = data[u32(p.x)]; + let sy = data[w + u32(p.y)]; + var s: vec4; + if (P.u[0].y != 0u) { + s = sample_bilinear_clamped(src, sx, sy); + } else { + s = sample_bilinear(src, sx, sy); + } + let o = textureLoad(src, p, 0); + textureStore(out, p, s + (o - s) * P.f[0].x); +} + +// CC Griddler (distort2::cc_griddler). data: per column (tile centre x, local x), per row +// (tile centre y, local y). u[0] = (buffer width, cut tiles); f[0] = (cos, sin, h scale, v scale); +// f[1].x = cut half size (0.45 · tile) +@compute @workgroup_size(16, 16) +fn fxs_griddler(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fxs_inside(p)) { + return; + } + let w = P.u[0].x; + let x = u32(p.x); + let y = u32(p.y); + let tcx = data[2u * x]; + let lx = data[2u * x + 1u]; + let tcy = data[2u * w + 2u * y]; + let ly = data[2u * w + 2u * y + 1u]; + if (P.u[0].y != 0u && max(abs(lx), abs(ly)) > P.f[1].x) { + textureStore(out, p, vec4(0.0)); + return; + } + let cr = P.f[0].x; + let sr = P.f[0].y; + let rx = lx * cr + ly * sr; + let ry = -lx * sr + ly * cr; + textureStore(out, p, sample_bilinear(src, tcx + rx / P.f[0].z, tcy + ry / P.f[0].w)); +} + +// Motion Tile (stylize2::motion_tile) into a new output size. data: per output column (tile +// index, fraction) of tx, then per row of ty. u[0] = (output width, horizontal phase shift, +// mirror edges); f[0] = (phase, layer w · scale, layer h · scale); f[1].xy = source buffer offset +@compute @workgroup_size(16, 16) +fn fxs_motiontile(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fxs_inside(p)) { + return; + } + let w = P.u[0].x; + let x = u32(p.x); + let y = u32(p.y); + let ixc = data[2u * x]; + let fxc = data[2u * x + 1u]; + let iyr = data[2u * w + 2u * y]; + let fyr = data[2u * w + 2u * y + 1u]; + let phase = P.f[0].x; + var i = ixc; + var fx = fxc; + var j = iyr; + var fy = fyr; + if (P.u[0].y != 0u) { + let t = fxc + phase * iyr; + i = ixc + floor(t); + fx = t - floor(t); + } else { + let t = fyr + phase * ixc; + j = iyr + floor(t); + fy = t - floor(t); + } + if (P.u[0].z != 0u) { + if (fxs_rem2(i32(i)) == 1) { + fx = 1.0 - fx; + } + if (fxs_rem2(i32(j)) == 1) { + fy = 1.0 - fy; + } + } + textureStore(out, p, sample_bilinear(src, fx * P.f[0].y + P.f[1].x, fy * P.f[0].z + P.f[1].y)); +} + +// CC RepeTile (stylize2::repetile) into a new output size. data: per output column (tile index, +// position in the tile), then per row. u[0] = (output width, tiling); f[0] = layer rect (x0, y0, w, h) +@compute @workgroup_size(16, 16) +fn fxs_repetile(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fxs_inside(p)) { + return; + } + let w = P.u[0].x; + let x = u32(p.x); + let y = u32(p.y); + let ix = i32(data[2u * x]); + var fx = data[2u * x + 1u]; + let iy = i32(data[2u * w + 2u * y]); + var fy = data[2u * w + 2u * y + 1u]; + let odd = fxs_rem2(ix + iy) == 1; + var flip_x = false; + var flip_y = false; + switch P.u[0].y { + case 1u: { + flip_x = fxs_rem2(ix) == 1; + flip_y = fxs_rem2(iy) == 1; + } + case 2u: { + flip_x = odd; + } + case 3u: { + flip_y = odd; + } + case 4u: { + flip_x = odd; + flip_y = odd; + } + default: {} + } + let r = P.f[0]; + if (flip_x) { + fx = r.z - fx; + } + if (flip_y) { + fy = r.w - fy; + } + textureStore(out, p, sample_bilinear(src, r.x + fx, r.y + fy)); +} + +// ---------------------------------------------------------------- Magnify (distort2::magnify) +// data: per column (dx, sx, floor(sx) + 0.5), then per row (dy, sy, floor(sy) + 0.5). +// u[0] = (buffer width, square, scaling, blend: 0 none, 1 normal, 2 blend mode); +// u[1] = (blend mode id, seed); f[0] = (size, feather, opacity, magnification); f[1].x = size − feather +@compute @workgroup_size(16, 16) +fn fxs_magnify(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fxs_inside(p)) { + return; + } + let bw = P.u[0].x; + let x = u32(p.x); + let y = u32(p.y); + let dx = data[3u * x]; + let sx = data[3u * x + 1u]; + let nx = data[3u * x + 2u]; + let dy = data[3u * bw + 3u * y]; + let sy = data[3u * bw + 3u * y + 1u]; + let ny = data[3u * bw + 3u * y + 2u]; + var d: f32; + if (P.u[0].y != 0u) { + d = max(abs(dx), abs(dy)); + } else { + d = sqrt(dx * dx + dy * dy); + } + let size = P.f[0].x; + let feather = P.f[0].y; + var w: f32; + if (feather > 0.01) { + w = 1.0 - fxs_smoothstep(P.f[1].x, size, d); + } else { + w = clamp(size - d + 0.5, 0.0, 1.0); + } + w = w * P.f[0].z; + let px = textureLoad(src, p, 0); + let blend = P.u[0].w; + if (w <= 0.0) { + if (blend == 0u) { + textureStore(out, p, vec4(0.0)); + } else { + textureStore(out, p, px); + } + return; + } + var m: vec4; + switch P.u[0].z { + case 1u: { + m = sample_bicubic(src, sx, sy); + } + case 2u: { + let seed = P.u[1].y; + let k = max(1.0 - 1.0 / P.f[0].w, 0.0); + let j1 = (hash_noise(x, y ^ (1u << 20u), seed) - 0.5) * k; + let j2 = (hash_noise(x, y ^ (2u << 20u), seed) - 0.5) * k; + m = sample_bilinear(src, nx + j1, ny + j2); + } + default: { + m = sample_bilinear(src, nx, ny); + } + } + if (blend == 0u) { + textureStore(out, p, m * w); + } else if (blend == 1u) { + textureStore(out, p, px + (m - px) * w); + } else { + textureStore(out, p, blend_pixel(P.u[1].x, px, m * w, 0.5)); + } +} + +// ---------------------------------------------------------------- Scatter (stylize2::scatter) +// u[0] = (grain, seed); f[0].x = amount +@compute @workgroup_size(16, 16) +fn fxs_scatter(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fxs_inside(p)) { + return; + } + let grain = P.u[0].x; + let seed = P.u[0].y; + let amt = P.f[0].x; + let x = u32(p.x); + let y = u32(p.y); + var dx = 0.0; + var dy = 0.0; + if (grain != 2u) { + dx = (hash_noise(x, y, seed) - 0.5) * 2.0 * amt; + } + if (grain != 1u) { + dy = (hash_noise(x, y, seed ^ 0x5bd1u) - 0.5) * 2.0 * amt; + } + textureStore(out, p, tex_get(src, i32(round_away(f32(p.x) + dx)), i32(round_away(f32(p.y) + dy)))); +} + +// ---------------------------------------------------------------- Brush Strokes (stylize2::brush_strokes) +// data: cell column per x (w), cell row per y (h), then per cell (dx, dy, length, jitter, samples, +// painted). u[0] = (buffer width, cell columns, paint surface, buffer height); f[0].x = blend +@compute @workgroup_size(16, 16) +fn fxs_brush(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fxs_inside(p)) { + return; + } + let w = P.u[0].x; + let h = P.u[0].w; + let cx = u32(data[u32(p.x)]); + let cy = u32(data[w + u32(p.y)]); + let base = w + h + (cy * P.u[0].y + cx) * 6u; + let dx = data[base]; + let dy = data[base + 1u]; + let l = data[base + 2u]; + let jit = data[base + 3u]; + let n = u32(data[base + 4u]); + let m = data[base + 5u]; + let q = fxs_centre(p); + let bx = q.x - dy * jit; + let by = q.y + dx * jit; + var acc = vec4(0.0); + for (var i = 0u; i < n; i++) { + var t = 0.0; + if (n != 1u) { + t = (f32(i) / f32(n - 1u) - 0.5) * l; + } + acc += sample_bilinear_clamped(src, bx + dx * t, by + dy * t); + } + let stroke = acc / f32(n); + let orig = textureLoad(src, p, 0); + let surface = P.u[0].z; + var base_px = orig; + if (surface == 1u) { + base_px = vec4(0.0); + } else if (surface == 2u) { + base_px = vec4(1.0); + } else if (surface == 3u) { + base_px = vec4(0.0, 0.0, 0.0, 1.0); + } + var painted: vec4; + if (surface == 2u || surface == 3u) { + let k = 1.0 - stroke.w * m; + painted = stroke * m + base_px * k; + } else { + painted = base_px + (stroke - base_px) * m; + } + textureStore(out, p, painted + (orig - painted) * P.f[0].x); +} + +// ---------------------------------------------------------------- Roughen Edges (stylize2::roughen_edges) + +// noise::fbm. +fn fxs_fbm(u: f32, v: f32, z: f32, seed: u32, octaves0: f32) -> f32 { + let octaves = clamp(octaves0, 1.0, 20.0); + let n = u32(ceil(octaves)); + let frac = octaves - floor(octaves); + var sum = 0.0; + var norm = 0.0; + var amp = 1.0; + var f = 1.0; + for (var o = 0u; o < n; o++) { + var w = 1.0; + if (o + 1u == n && frac > 0.0) { + w = frac; + } + sum += value_noise(u * f, v * f, z + f32(o) * 7.31, seed + o) * amp * w; + norm += amp * w; + amp *= 0.5; + f *= 2.0; + } + return sum / max(norm, 1e-6); +} + +// aux = the (blurred) alpha in .w. u[0] = (seed, cycle evolution, coloured); +// f[0] = (offset x, offset y, scale · stretch x, scale · stretch y); f[1] = (frequency, evolution, +// cycle, cycle weight); f[2] = (octaves, influence, influence multiplier, sharpness); +// f[3] = (edge colour, sharpness multiplier) +@compute @workgroup_size(16, 16) +fn fxs_roughen(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fxs_inside(p)) { + return; + } + let px = textureLoad(src, p, 0); + if (px.w <= 0.0) { + textureStore(out, p, px); + return; + } + let dv = textureLoad(aux, p, 0).w; + let fm = P.f[1].x; + let nx = (f32(p.x) + 0.5 - P.f[0].x) / P.f[0].z * fm; + let ny = (f32(p.y) + 0.5 - P.f[0].y) / P.f[0].w * fm; + let seed = P.u[0].x; + let evo = P.f[1].y; + let oct = P.f[2].x; + var n = fxs_fbm(nx, ny, evo, seed, oct); + if (P.u[0].y != 0u) { + n += (fxs_fbm(nx, ny, evo - P.f[1].z, seed, oct) - n) * P.f[1].w; + } + let infl = P.f[2].y; + let s = dv - 0.5 + (n - 0.5) * infl * P.f[2].z * 0.5; + let m = clamp(s * P.f[2].w * P.f[3].w * 4.0 + 0.5, 0.0, 1.0); + let u = fxs_unpremul(px); + var c = u.xyz; + if (P.u[0].z != 0u) { + let band = 1.0 - fxs_smoothstep(0.5, 0.9, dv + (n - 0.5) * infl * 0.5); + c = c + (P.f[3].xyz - c) * band; + } + let a = u.w * m; + textureStore(out, p, vec4(c * a, a)); +} + +// ---------------------------------------------------------------- Texturize (stylize2::texturize) + +// The texture layer placed into the buffer (transition::place_layer) as luminance +// (Plane::luma). aux = the texture layer's buffer; data: per column x, then per row y in its +// pixels. u[0].x = buffer width +@compute @workgroup_size(16, 16) +fn fxs_tex_luma(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fxs_inside(p)) { + return; + } + let w = P.u[0].x; + let t = sample_bilinear(aux, data[u32(p.x)], data[w + u32(p.y)]); + let l = luminance(fxs_unpremul(t).xyz); + textureStore(out, p, vec4(l)); +} + +// Relief shading. aux = the blurred texture luminance; f[0] = (light x, light y, contrast) +@compute @workgroup_size(16, 16) +fn fxs_texturize(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fxs_inside(p)) { + return; + } + let gx = (tex_get_clamped(aux, p.x + 1, p.y).x - tex_get_clamped(aux, p.x - 1, p.y).x) * 0.5; + let gy = (tex_get_clamped(aux, p.x, p.y + 1).x - tex_get_clamped(aux, p.x, p.y - 1).x) * 0.5; + let shade = max(1.0 - P.f[0].z * 4.0 * (gx * P.f[0].x + gy * P.f[0].y), 0.0); + let px = textureLoad(src, p, 0); + textureStore(out, p, vec4(px.xyz * shade, px.w)); +} + +// ---------------------------------------------------------------- Glow (misc::glow) extras + +// Bright pass with the Arbitrary Map: as `glow_bright` (same f[0] / u[0] layout, see +// kernels.wgsl) with the red, green and blue curves at data offsets f[1].xyz (−1 = identity). +@compute @workgroup_size(16, 16) +fn fxs_glow_map(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fxs_inside(p)) { + return; + } + let px = textureLoad(src, p, 0); + let a = px.w; + if (a <= 0.0) { + textureStore(out, p, vec4(0.0)); + return; + } + let thr = P.f[0].x; + var l = luminance(px.xyz / a); + if (P.u[0].y != 0u) { + l = a; + } + let k = clamp((l - thr) / max(1.0 - thr, 1e-3), 0.0, 1.0); + let t = glow_ab_t(l); + let c = vec3(curve_eval(P.f[1].x, t), curve_eval(P.f[1].y, t), curve_eval(P.f[1].z, t)); + textureStore(out, p, vec4(c * k * a, k * a)); +} + +// On Top with another Glow Operation: blend_pixel(mode, original, glow · intensity, 0). +// aux = the blurred glow; u[0].x = blend mode id; f[0].x = intensity +@compute @workgroup_size(16, 16) +fn fxs_glow_op(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fxs_inside(p)) { + return; + } + let g = textureLoad(aux, p, 0) * P.f[0].x; + var o = blend_pixel(P.u[0].x, textureLoad(src, p, 0), g, 0.0); + o.w = clamp(o.w, 0.0, 1.0); + textureStore(out, p, o); +} + +// ---------------------------------------------------------------- accumulation (Transform motion blur) + +// out = src + aux +@compute @workgroup_size(16, 16) +fn fxs_add(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fxs_inside(p)) { + return; + } + textureStore(out, p, textureLoad(src, p, 0) + textureLoad(aux, p, 0)); +} + +// out = src / f[0].x +@compute @workgroup_size(16, 16) +fn fxs_div(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + if (!fxs_inside(p)) { + return; + } + textureStore(out, p, textureLoad(src, p, 0) / P.f[0].x); +} diff --git a/crates/gpu/src/tests.rs b/crates/gpu/src/tests.rs index f795b58d..7a3a1712 100644 --- a/crates/gpu/src/tests.rs +++ b/crates/gpu/src/tests.rs @@ -511,7 +511,7 @@ fn effect_chains_and_mixed_stacks() { let mut l = s.footage(80, 50); s.effect(&mut l, "ec.color.exposure", &[("master/exposure", n(0.7))]); s.effect(&mut l, "ec.blur.gaussian", &[("blurriness", n(5.0))]); - s.effect(&mut l, "ec.stylize.posterize", &[]); + s.effect(&mut l, "ec.stylize.coloremboss", &[]); s.effect(&mut l, "ec.stylize.glow", &[]); s.effect(&mut l, "ec.channel.invert", &[("blend", n(50.0))]); s.push(l); diff --git a/crates/gpu/src/tests_adjust.rs b/crates/gpu/src/tests_adjust.rs index ae7f88da..bc6be0cf 100644 --- a/crates/gpu/src/tests_adjust.rs +++ b/crates/gpu/src/tests_adjust.rs @@ -57,7 +57,7 @@ fn gpu_adjustment_chains_match_the_cpu() { #[test] fn mixed_adjustment_chains_fall_back_per_effect() { let mut stack = gpu_stack(); - stack.insert(1, ("ec.stylize.posterize", vec![])); + stack.insert(1, ("ec.stylize.coloremboss", vec![])); stack.push(("ec.stylize.glow", vec![("threshold", n(30.0))])); for depth in [BitDepth::Bpc8, BitDepth::Bpc32] { let s = adjustment_scene(depth, &stack, true); @@ -93,7 +93,7 @@ impl FxTarget for Probe { fn adjustment_stacks_split_into_gpu_runs_and_cpu_steps() { let Some(g) = gpu() else { return }; let mut stack = gpu_stack(); - stack.insert(1, ("ec.stylize.posterize", vec![])); + stack.insert(1, ("ec.stylize.coloremboss", vec![])); let s = adjustment_scene(BitDepth::Bpc32, &stack, false); let mut r = Renderer::new(&s.p, &Pattern, RenderOpts { backend: Backend::Gpu, ..opts() }); r.accel = Some(g); diff --git a/crates/gpu/src/tests_fx_stylize.rs b/crates/gpu/src/tests_fx_stylize.rs index a11811aa..1d11f801 100644 --- a/crates/gpu/src/tests_fx_stylize.rs +++ b/crates/gpu/src/tests_fx_stylize.rs @@ -1,2 +1,206 @@ //! GPU stylize and distort 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::{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 pt(x: f64, y: f64) -> Value { + Value::Vec2([x, y]) +} + +#[test] +fn posterize_thresholds_strobe_vignette() { + effect_case("ec.stylize.posterize", &[("level", n(4.0))]); + effect_case("ec.stylize.posterize", &[("level", n(9.0))]); + effect_case("ec.stylize.threshold", &[("level", n(100.0))]); + effect_case("ec.stylize.threshold", &[("level", n(181.0))]); + effect_case("ec.stylize.ccthreshold", &[]); + effect_case("ec.stylize.ccthreshold", &[("channel", e(1)), ("invert", on()), ("threshold", n(90.0))]); + effect_case("ec.stylize.ccthreshold", &[("channel", e(2)), ("blend", n(30.0))]); + effect_case("ec.stylize.ccthreshold", &[("channel", e(3)), ("threshold", n(60.0))]); + effect_case( + "ec.stylize.ccthresholdrgb", + &[("redThreshold", n(70.0)), ("greenThreshold", n(150.0)), ("blueThreshold", n(110.0)), ("invertGreen", on()), ("blend", n(25.0))], + ); + effect_case( + "ec.stylize.strobe", + &[("strobeDuration", n(0.5)), ("strobeOperator", e(1)), ("strobeColor", c(0.2, 0.5, 0.9)), ("blendWithOriginal", n(30.0))], + ); + effect_case("ec.stylize.strobe", &[("strobeDuration", n(0.5)), ("strobeOperator", e(4)), ("strobeColor", c(0.6, 0.1, 0.3))]); + effect_case("ec.stylize.strobe", &[("strobeDuration", n(0.5)), ("strobe", e(1)), ("blendWithOriginal", n(40.0))]); + effect_case("ec.stylize.strobe", &[("strobeDuration", n(0.1)), ("strobePeriod", n(0.2)), ("randomStrobeProbability", n(50.0))]); + effect_case("ec.stylize.ccvignette", &[("amount", n(80.0))]); + effect_case("ec.stylize.ccvignette", &[("amount", n(-60.0)), ("pinHighlights", n(50.0)), ("angleOfView", n(120.0)), ("center", pt(20.0, 15.0))]); +} + +#[test] +fn scatter_brush_strokes() { + effect_case("ec.stylize.scatter", &[("amount", n(5.0))]); + effect_case("ec.stylize.scatter", &[("amount", n(3.0)), ("grain", e(1))]); + effect_case("ec.stylize.scatter", &[("amount", n(7.0)), ("grain", e(2)), ("randomizeEveryFrame", on())]); + effect_case("ec.stylize.brushstrokes", &[]); + effect_case( + "ec.stylize.brushstrokes", + &[("strokeAngle", n(40.0)), ("brushSize", n(3.0)), ("strokeLength", n(10.0)), ("paintSurface", e(2)), ("blendWithOriginal", n(20.0))], + ); + effect_case("ec.stylize.brushstrokes", &[("paintSurface", e(1)), ("strokeDensity", n(0.6)), ("strokeRandomness", n(2.0)), ("brushSize", n(1.3))]); + effect_case("ec.stylize.brushstrokes", &[("paintSurface", e(3)), ("strokeLength", n(25.0))]); +} + +#[test] +fn roughen_edges() { + effect_case("ec.stylize.roughenedges", &[]); + effect_case("ec.stylize.roughenedges", &[("edgeType", e(1)), ("border", n(4.0)), ("edgeColor", c(0.1, 0.6, 0.9))]); + effect_case( + "ec.stylize.roughenedges", + &[ + ("edgeType", e(5)), + ("evolutionOptions/cycleEvolution", on()), + ("evolutionOptions/cycle", n(2.0)), + ("evolution", n(500.0)), + ("stretch", n(2.0)), + ("complexity", n(3.0)), + ], + ); + effect_case("ec.stylize.roughenedges", &[("edgeType", e(7)), ("border", n(0.0)), ("scale", n(60.0)), ("evolutionOptions/randomSeed", n(9.0))]); +} + +#[test] +fn tiles_kaleida_repetile() { + effect_case("ec.stylize.motiontile", &[("tileWidth", n(50.0)), ("tileHeight", n(40.0))]); + effect_case( + "ec.stylize.motiontile", + &[("outputWidth", n(150.0)), ("outputHeight", n(130.0)), ("mirrorEdges", on()), ("phase", n(70.0)), ("tileWidth", n(45.0))], + ); + effect_case( + "ec.stylize.motiontile", + &[("horizontalPhaseShift", on()), ("phase", n(33.0)), ("tileCenter", pt(30.0, 17.0)), ("tileHeight", n(55.0)), ("outputWidth", n(80.0))], + ); + effect_case("ec.stylize.cckaleida", &[]); + effect_case("ec.stylize.cckaleida", &[("mirroring", e(2)), ("rotation", n(20.0)), ("size", n(150.0))]); + effect_case("ec.stylize.ccrepetile", &[("expandRight", n(10.0)), ("expandUp", n(4.0))]); + effect_case("ec.stylize.ccrepetile", &[("expandLeft", n(12.0)), ("expandDown", n(9.0)), ("tiling", e(1))]); + effect_case("ec.stylize.ccrepetile", &[("expandLeft", n(30.0)), ("expandRight", n(30.0)), ("expandUp", n(20.0)), ("tiling", e(4))]); + effect_case("ec.distort.cctiler", &[]); + effect_case("ec.distort.cctiler", &[("scale", n(70.0)), ("blendWithOriginal", n(30.0)), ("center", pt(30.0, 18.0))]); + effect_case("ec.distort.ccgriddler", &[("rotation", n(20.0))]); + effect_case("ec.distort.ccgriddler", &[("cutTiles", on()), ("horizontalScale", n(120.0)), ("verticalScale", n(80.0)), ("tileSize", n(17.0))]); +} + +#[test] +fn mirror_offset_polar_spherize_corner_pin() { + effect_case("ec.distort.mirror", &[("angle", n(30.0))]); + effect_case("ec.distort.mirror", &[("angle", n(200.0)), ("center", pt(27.0, 19.0))]); + effect_case("ec.distort.offset", &[("shift", pt(10.0, 5.0))]); + effect_case("ec.distort.offset", &[("shift", pt(50.3, 20.7)), ("blend", n(40.0))]); + effect_case("ec.distort.polar", &[("interpolation", n(100.0))]); + effect_case("ec.distort.polar", &[("interpolation", n(60.0)), ("conversion", e(1))]); + effect_case("ec.distort.spherize", &[("radius", n(20.0))]); + effect_case("ec.distort.spherize", &[("radius", n(35.0)), ("center", pt(30.0, 25.0))]); + effect_case("ec.distort.cornerpin", &[("ul", pt(5.0, 3.0)), ("ur", pt(60.0, 8.0)), ("lr", pt(66.0, 40.0)), ("ll", pt(2.0, 44.0))]); + effect_case("ec.distort.cornerpin", &[("ul", pt(-8.0, 6.0)), ("ur", pt(75.0, -4.0)), ("lr", pt(50.0, 30.0)), ("ll", pt(12.0, 50.0))]); +} + +#[test] +fn optics_magnify() { + effect_case("ec.distort.opticscompensation", &[("fieldOfView", n(60.0))]); + effect_case("ec.distort.opticscompensation", &[("fieldOfView", n(90.0)), ("reverseLensDistortion", on()), ("fovOrientation", e(2))]); + effect_case( + "ec.distort.opticscompensation", + &[("fieldOfView", n(110.0)), ("reverseLensDistortion", on()), ("resize", e(1)), ("viewCenter", pt(30.0, 20.0))], + ); + effect_case("ec.distort.opticscompensation", &[("fieldOfView", n(120.0)), ("optimalPixels", on()), ("fovOrientation", e(1))]); + effect_case("ec.distort.magnify", &[("size", n(15.0))]); + effect_case( + "ec.distort.magnify", + &[("shape", e(1)), ("feather", n(6.0)), ("size", n(18.0)), ("scaling", e(1)), ("blendingMode", e(3)), ("magnification", n(270.0))], + ); + effect_case("ec.distort.magnify", &[("scaling", e(2)), ("blendingMode", e(0)), ("size", n(20.0)), ("opacity", n(70.0))]); + effect_case("ec.distort.magnify", &[("size", n(25.0)), ("center", pt(60.0, 10.0)), ("resizeLayer", on()), ("blendingMode", e(5))]); +} + +#[test] +fn cc_slant_smear_split() { + effect_case("ec.distort.ccslant", &[("slant", n(30.0))]); + effect_case("ec.distort.ccslant", &[("slant", n(-20.0)), ("stretching", on()), ("height", n(120.0))]); + effect_case("ec.distort.ccsmear", &[]); + effect_case("ec.distort.ccsmear", &[("reach", n(-40.0)), ("radius", n(20.0)), ("to", pt(40.0, 10.0))]); + effect_case("ec.distort.ccsplit", &[]); + effect_case("ec.distort.ccsplit2", &[("split1", n(30.0)), ("split2", n(80.0)), ("pointB", pt(60.0, 30.0))]); +} + +#[test] +fn liquify_render() { + let mesh = "warp 20 50 0 0 0 10,10 20,15 30,20\ntwirlClockwise 24 80 0 0 0 40,25 42,27"; + effect_case("ec.distort.liquify", &[("distortionMesh", Value::Str(mesh.into()))]); + effect_case( + "ec.distort.liquify", + &[("distortionMesh", Value::Str(mesh.into())), ("distortionPercentage", n(60.0)), ("distortionMeshOffset", pt(3.0, -2.0))], + ); +} + +#[test] +fn glow_operations_and_arbitrary_map() { + effect_case("ec.stylize.glow", &[("glowOperation", e(2)), ("threshold", n(30.0)), ("radius", n(8.0))]); + effect_case("ec.stylize.glow", &[("glowOperation", e(3)), ("colors", e(1)), ("intensity", n(2.0))]); + effect_case("ec.stylize.glow", &[("colors", e(2)), ("arbitraryMap", Value::Str("0,0 0.5,0.8 1,1|0,0.2 1,0.6|".into())), ("threshold", n(20.0))]); + effect_case("ec.stylize.glow", &[("colors", e(2)), ("arbitraryMap", Value::Str("0,1 1,0||0,0 1,1".into())), ("operation", e(1)), ("glowOperation", e(4))]); +} + +/// Texturize with a texture layer (Tile / Center / Stretch), composited. +#[test] +fn texturize_layer() { + for depth in [BitDepth::Bpc8, BitDepth::Bpc32] { + for placement in 0..3 { + let mut s = Scene::new(depth); + let bg = s.solid([0.15, 0.1, 0.2], 97, 61); + s.push(bg); + let mut tex = s.footage(50, 36); + tex.switches.video = false; + let tid = s.push(tex); + let mut l = s.footage(70, 44); + s.effect( + &mut l, + "ec.stylize.texturize", + &[("textureLayer", Value::Layer(Some(tid.0))), ("texturePlacement", e(placement)), ("textureContrast", n(1.5)), ("lightDirection", n(60.0))], + ); + set(&mut l, "transform/rotation", Value::Scalar(8.0)); + s.push(l); + let label = format!("texturize {placement} {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); + } + } +} + +/// Transform's own shutter angle: the effect's rotation and position animate. +#[test] +fn transform_motion_blur() { + 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, "ec.distort.transform", &[("useCompositionShutterAngle", Value::Bool(false)), ("shutterAngle", n(180.0))]); + let rot = l.props.prop_mut("effects/#1/rotation").expect("transform rotation"); + rot.keys = vec![Keyframe::new(Tick::ZERO, Value::Scalar(0.0)), Keyframe::new(Tick::from_seconds_f64(1.0), Value::Scalar(120.0))]; + let pos = l.props.prop_mut("effects/#1/position").expect("transform position"); + pos.keys = vec![Keyframe::new(Tick::ZERO, pt(35.0, 22.0)), Keyframe::new(Tick::from_seconds_f64(1.0), pt(95.0, 40.0))]; + s.push(l); + let label = format!("transform motion blur {depth:?}"); + check(&label, compare_at(&s, opts(), Tick::from_seconds_f64(0.3)), 0.0); + check(&format!("{label} half"), compare_at(&s, RenderOpts { scale: 0.5, ..opts() }, Tick::from_seconds_f64(0.3)), 0.0); + } +} diff --git a/docs/effects.md b/docs/effects.md index e35ae5f6..833a0d7b 100644 --- a/docs/effects.md +++ b/docs/effects.md @@ -5,7 +5,7 @@ EffectCraft ships 306 effects with 3009 parameters in total, grouped into the same categories as the Effects & Presets panel. Every effect is our own implementation, written from public behaviour descriptions and standard image-processing literature. Parameter names, order, popup options, units, defaults and ranges follow the reference application so that projects, expressions and muscle memory carry over. -- **GPU**: 59 effects also run on the GPU compositor with identical results. +- **GPU**: 86 effects also run on the GPU compositor with identical results. - **32**: 306 effects process 32-bit float (HDR, overbright) pixels without clamping. - **Status**: 306 implemented in full, 0 partial (what is missing is listed). @@ -136,32 +136,32 @@ Effects are addressed by id (`ec..`) or display name in commands | CC Bender | `ec.distort.ccbender` | 5 | | 32 | Implemented | | CC Blobbylize | `ec.distort.ccblobbylize` | 15 | | 32 | Implemented | | CC Flo Motion | `ec.distort.ccflomotion` | 7 | | 32 | Implemented | -| CC Griddler | `ec.distort.ccgriddler` | 5 | | 32 | Implemented | +| CC Griddler | `ec.distort.ccgriddler` | 5 | GPU | 32 | Implemented | | CC Lens | `ec.distort.cclens` | 3 | GPU | 32 | Implemented | | CC Page Turn | `ec.distort.ccpageturn` | 7 | | 32 | Implemented | | CC Power Pin | `ec.distort.ccpowerpin` | 10 | | 32 | Implemented | | CC Ripple Pulse | `ec.distort.ccripplepulse` | 5 | | 32 | Implemented | -| CC Slant | `ec.distort.ccslant` | 4 | | 32 | Implemented | -| CC Smear | `ec.distort.ccsmear` | 4 | | 32 | Implemented | -| CC Split | `ec.distort.ccsplit` | 3 | | 32 | Implemented | -| CC Split 2 | `ec.distort.ccsplit2` | 4 | | 32 | Implemented | -| CC Tiler | `ec.distort.cctiler` | 3 | | 32 | Implemented | -| Corner Pin | `ec.distort.cornerpin` | 4 | | 32 | Implemented | +| CC Slant | `ec.distort.ccslant` | 4 | GPU | 32 | Implemented | +| CC Smear | `ec.distort.ccsmear` | 4 | GPU | 32 | Implemented | +| CC Split | `ec.distort.ccsplit` | 3 | GPU | 32 | Implemented | +| CC Split 2 | `ec.distort.ccsplit2` | 4 | GPU | 32 | Implemented | +| CC Tiler | `ec.distort.cctiler` | 3 | GPU | 32 | Implemented | +| Corner Pin | `ec.distort.cornerpin` | 4 | GPU | 32 | Implemented | | Detail-preserving Upscale | `ec.distort.upscale` | 4 | | 32 | Implemented | | Displacement Map | `ec.distort.displacementmap` | 8 | GPU | 32 | Implemented | -| Liquify | `ec.distort.liquify` | 12 | | 32 | Implemented | -| Magnify | `ec.distort.magnify` | 10 | | 32 | Implemented | +| Liquify | `ec.distort.liquify` | 12 | GPU | 32 | Implemented | +| Magnify | `ec.distort.magnify` | 10 | GPU | 32 | Implemented | | Mesh Warp | `ec.distort.meshwarp` | 3 | GPU | 32 | Implemented | -| Mirror | `ec.distort.mirror` | 2 | | 32 | Implemented | -| Offset | `ec.distort.offset` | 2 | | 32 | Implemented | -| Optics Compensation | `ec.distort.opticscompensation` | 6 | | 32 | Implemented | -| Polar Coordinates | `ec.distort.polar` | 2 | | 32 | Implemented | +| Mirror | `ec.distort.mirror` | 2 | GPU | 32 | Implemented | +| Offset | `ec.distort.offset` | 2 | GPU | 32 | Implemented | +| Optics Compensation | `ec.distort.opticscompensation` | 6 | GPU | 32 | Implemented | +| Polar Coordinates | `ec.distort.polar` | 2 | GPU | 32 | Implemented | | Puppet | `ec.distort.puppet` | 1 | | 32 | Implemented | | Reshape | `ec.distort.reshape` | 6 | | 32 | Implemented | | Ripple | `ec.distort.ripple` | 7 | GPU | 32 | Implemented | | Rolling Shutter Repair | `ec.distort.rollingshutterrepair` | 5 | | 32 | Implemented | | Smear | `ec.distort.smear` | 8 | | 32 | Implemented | -| Spherize | `ec.distort.spherize` | 2 | | 32 | Implemented | +| Spherize | `ec.distort.spherize` | 2 | GPU | 32 | Implemented | | Transform | `ec.distort.transform` | 12 | GPU | 32 | Implemented | | Turbulent Displace | `ec.distort.turbulentdisplace` | 11 | GPU | 32 | Implemented | | Twirl | `ec.distort.twirl` | 3 | GPU | 32 | Implemented | @@ -340,31 +340,31 @@ Effects are addressed by id (`ec..`) or display name in commands | Effect | Id | Params | GPU | 32 | Status | |---|---|---:|:-:|:-:|---| -| Brush Strokes | `ec.stylize.brushstrokes` | 7 | | 32 | Implemented | +| Brush Strokes | `ec.stylize.brushstrokes` | 7 | GPU | 32 | Implemented | | CC Block Load | `ec.stylize.ccblockload` | 5 | | 32 | Implemented | | CC Burn Film | `ec.stylize.ccburnfilm` | 3 | | 32 | Implemented | | CC Glass | `ec.stylize.ccglass` | 17 | | 32 | Implemented | | CC HexTile | `ec.stylize.cchextile` | 5 | | 32 | Implemented | -| CC Kaleida | `ec.stylize.cckaleida` | 4 | | 32 | Implemented | +| CC Kaleida | `ec.stylize.cckaleida` | 4 | GPU | 32 | Implemented | | CC Mr. Smoothie | `ec.stylize.ccmrsmoothie` | 7 | | 32 | Implemented | | CC Plastic | `ec.stylize.ccplastic` | 18 | | 32 | Implemented | -| CC RepeTile | `ec.stylize.ccrepetile` | 5 | | 32 | Implemented | -| CC Threshold | `ec.stylize.ccthreshold` | 4 | | 32 | Implemented | -| CC Threshold RGB | `ec.stylize.ccthresholdrgb` | 7 | | 32 | Implemented | -| CC Vignette | `ec.stylize.ccvignette` | 4 | | 32 | Implemented | +| CC RepeTile | `ec.stylize.ccrepetile` | 5 | GPU | 32 | Implemented | +| CC Threshold | `ec.stylize.ccthreshold` | 4 | GPU | 32 | Implemented | +| CC Threshold RGB | `ec.stylize.ccthresholdrgb` | 7 | GPU | 32 | Implemented | +| CC Vignette | `ec.stylize.ccvignette` | 4 | GPU | 32 | Implemented | | Cartoon | `ec.stylize.cartoon` | 12 | | 32 | Implemented | | Color Emboss | `ec.stylize.coloremboss` | 4 | | 32 | Implemented | | Emboss | `ec.stylize.emboss` | 4 | GPU | 32 | Implemented | | Find Edges | `ec.stylize.findedges` | 2 | GPU | 32 | Implemented | | Glow | `ec.stylize.glow` | 14 | GPU | 32 | Implemented | | Mosaic | `ec.stylize.mosaic` | 3 | GPU | 32 | Implemented | -| Motion Tile | `ec.stylize.motiontile` | 8 | | 32 | Implemented | -| Posterize | `ec.stylize.posterize` | 1 | | 32 | Implemented | -| Roughen Edges | `ec.stylize.roughenedges` | 13 | | 32 | Implemented | -| Scatter | `ec.stylize.scatter` | 3 | | 32 | Implemented | -| Strobe Light | `ec.stylize.strobe` | 7 | | 32 | Implemented | -| Texturize | `ec.stylize.texturize` | 4 | | 32 | Implemented | -| Threshold | `ec.stylize.threshold` | 1 | | 32 | Implemented | +| Motion Tile | `ec.stylize.motiontile` | 8 | GPU | 32 | Implemented | +| Posterize | `ec.stylize.posterize` | 1 | GPU | 32 | Implemented | +| Roughen Edges | `ec.stylize.roughenedges` | 13 | GPU | 32 | Implemented | +| Scatter | `ec.stylize.scatter` | 3 | GPU | 32 | Implemented | +| Strobe Light | `ec.stylize.strobe` | 7 | GPU | 32 | Implemented | +| Texturize | `ec.stylize.texturize` | 4 | GPU | 32 | Implemented | +| Threshold | `ec.stylize.threshold` | 1 | GPU | 32 | Implemented | ## Text