M13.9: gpu stylize and distort (posterize, threshold, CC Threshold / RGB, strobe, CC Vignette, scatter, brush strokes, roughen edges, texturize, motion tile, CC Kaleida, CC RepeTile, mirror, offset, polar, spherize, corner pin, optics compensation, magnify, CC Slant / Smear / Split / Split 2 / Tiler / Griddler, liquify; glow operations and arbitrary map; transform motion blur)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-10-03 08:16:58 -04:00
parent 47f61debcb
commit 2e5d35dcee
12 changed files with 1957 additions and 44 deletions

View File

@ -87,6 +87,7 @@ const CPU_ONLY_CONTROLS: &[(&str, &[&str])] = &[
), ),
("ec.noise.addgrain", &["viewingMode", "preset", "color/redBalance", "color/greenBalance", "color/blueBalance", "previewRegion/"]), ("ec.noise.addgrain", &["viewingMode", "preset", "color/redBalance", "color/greenBalance", "color/blueBalance", "previewRegion/"]),
("ec.blur.cameralens", &["irisProperties/diffractionFringe", "blurMap/blurMapLayer"]), ("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 /// Whether the GPU kernel of effect `id` covers the instance's current settings (see

View File

@ -354,7 +354,7 @@ fn magnify(ctx: &EffectCtx, mut b: Buf) -> Buf {
} }
/// Magnify's Blending Mode choices after "None". /// Magnify's Blending Mode choices after "None".
const MAGNIFY_MODES: [BlendMode; 17] = [ pub const MAGNIFY_MODES: [BlendMode; 17] = [
BlendMode::Normal, BlendMode::Normal,
BlendMode::Add, BlendMode::Add,
BlendMode::Multiply, BlendMode::Multiply,

View File

@ -325,7 +325,9 @@ pub fn build_mesh(strokes: &[LiquifyStroke], w: f64, h: f64, frozen: &(dyn Fn(f6
type MeshCache = Mutex<HashMap<u64, Arc<Mesh>>>; type MeshCache = Mutex<HashMap<u64, Arc<Mesh>>>;
fn cached_mesh(ctx: &EffectCtx) -> Arc<Mesh> { /// Liquify's displacement mesh for the instance (replayed strokes, cached; also used by the
/// GPU kernels).
pub fn liquify_mesh(ctx: &EffectCtx) -> Arc<Mesh> {
use std::hash::{Hash, Hasher}; use std::hash::{Hash, Hasher};
let text = ctx.params.s("distortionMesh"); let text = ctx.params.s("distortionMesh");
let mask = ctx.params.f("freezeAreaMask").round() as usize; 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 { if ctx.params.s("distortionMesh").trim().is_empty() && !show_freeze && !show_mesh {
return b; return b;
} }
let mesh = cached_mesh(ctx); let mesh = liquify_mesh(ctx);
let off = ctx.params.v2("distortionMeshOffset"); let off = ctx.params.v2("distortionMeshOffset");
let mesh_color = ctx.params.color("meshColor"); let mesh_color = ctx.params.color("meshColor");
let src = b.img.clone(); let src = b.img.clone();

View File

@ -71,7 +71,8 @@ pub use color_fx::{
}; };
pub use color2::Curve; pub use color2::Curve;
pub use distort::transform_shutter; 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_keyframe::Value;
use effectcraft_project::build::Ids; use effectcraft_project::build::Ids;
use effectcraft_project::{GroupKind, ParamUi, PropGroup, Property}; 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 misc::{INVERT_ALPHA, INVERT_CHANNELS, glow_operation};
pub use sim::particle_state; pub use sim::particle_state;
pub use sim3::playground_state; pub use sim3::playground_state;
pub use stylize2::strobe_on;
// CPU helpers the GPU kernels share (effectcraft-gpu). // CPU helpers the GPU kernels share (effectcraft-gpu).
pub use blur2::{camera_lens_plain, camera_lens_spans}; pub use blur2::{camera_lens_plain, camera_lens_spans};
pub use generate::gen_mode; pub use generate::gen_mode;
@ -809,7 +811,33 @@ pub const GPU_EFFECTS: &[&str] = &[
// effectcraft-gpu fx_key (keying, matte, channel) // effectcraft-gpu fx_key (keying, matte, channel)
// //
// effectcraft-gpu fx_stylize (stylize, distort) // 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) // effectcraft-gpu fx_noise (noise, blur, time)
// //
]; ];

View File

@ -223,7 +223,8 @@ fn scatter(ctx: &EffectCtx, mut b: Buf) -> Buf {
// ---- Strobe Light ---- // ---- 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 { if period <= 0.0 {
return false; return false;
} }

View File

@ -113,6 +113,8 @@ fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
"ec.blur.gaussian" => gaussian(e, ctx, b), "ec.blur.gaussian" => gaussian(e, ctx, b),
"ec.blur.fastbox" => box_blur(e, ctx, b), "ec.blur.fastbox" => box_blur(e, ctx, b),
"ec.blur.directional" => directional(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.stylize.glow" => glow(e, ctx, b),
"ec.perspective.dropshadow" => drop_shadow(e, ctx, b), "ec.perspective.dropshadow" => drop_shadow(e, ctx, b),
"ec.distort.transform" => transform(e, ctx, b), "ec.distort.transform" => transform(e, ctx, b),

View File

@ -1,19 +1,889 @@
//! GPU effects, stylize and distort family (kernels in `shaders/fx_stylize.wgsl`, every entry point prefixed //! 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. //! `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_effects::EffectCtx;
use effectcraft_geom::{Mat3, vec2};
use effectcraft_raster::Sampling;
use crate::context::Enc; use crate::context::{Enc, Params};
use crate::effects::GBuf; use crate::effects::{GBuf, gaussian_blur};
use crate::ops;
/// Compute entry points in `fx_stylize.wgsl`. /// 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. /// 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. /// 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<GBuf> { pub(crate) fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let _ = (e, id, ctx, b); match id {
None "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<GBuf> {
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<GBuf> {
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<GBuf> {
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<GBuf> {
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<GBuf> {
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<GBuf> {
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<GBuf> {
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<GBuf> {
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<GBuf> {
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<GBuf> {
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<GBuf> {
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<GBuf> {
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<GBuf> {
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<GBuf> {
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<GBuf> {
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<GBuf> {
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<GBuf> {
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<GBuf> {
// 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<f32> = 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<const K: usize>(w: u32, h: u32, fx: impl Fn(f64) -> [f64; K], fy: impl Fn(f64) -> [f64; K]) -> Vec<f32> {
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<GBuf> {
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<GBuf> {
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<GBuf> {
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<GBuf> {
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<GBuf> {
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<GBuf> {
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<GBuf> {
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<GBuf> {
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<GBuf> {
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<f32> = (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<GBuf> {
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<GBuf> {
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<GBuf> {
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<GBuf> {
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<Option<effectcraft_effects::Curve>> = (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<GBuf> {
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)
} }

View File

@ -1,3 +1,808 @@
// GPU effects (stylize and distort family): see src/fx_stylize.rs. Each entry point mirrors the CPU effect in // 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 // 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<i32>) -> bool {
let d = out_dims();
return p.x < d.x && p.y < d.y;
}
fn fxs_centre(p: vec2<i32>) -> vec2<f32> {
return vec2<f32>(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<f32>) -> vec4<f32> {
let a = px.w;
if (a > 1e-6) {
return vec4<f32>(fxs_qdiv(px.xyz, a), a);
}
return vec4<f32>(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<f32>, a: f32) -> vec3<f32> {
let q = x / a;
let r = fma(-q, vec3<f32>(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<u32>) {
let p = vec2<i32>(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<f32>(0.0), vec3<f32>(1.0)) * lv;
let o = vec3<f32>(round_away(c.x), round_away(c.y), round_away(c.z)) / lv;
textureStore(out, p, vec4<f32>(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<f32>(vec3<f32>(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<f32>(fxs_bin(c.x, t, inv), fxs_bin(c.y, t, inv), fxs_bin(c.z, t, inv));
}
case 2u: {
o = vec3<f32>(fxs_bin(rgb_to_hsl(c).y, t, inv));
}
case 3u: {
na = fxs_bin(u.w, t, inv);
}
default: {
o = vec3<f32>(fxs_bin(luminance(c), t, inv));
}
}
o = o + (c - o) * blend;
na = na + (u.w - na) * blend;
textureStore(out, p, vec4<f32>(o * na, na));
}
case 4u: {
let c = fxs_qdiv(px.xyz, a);
let t = P.f[0].xyz;
let o = vec3<f32>(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<f32>(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<f32>(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<f32>(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<f32>(max(u.xyz * k, vec3<f32>(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<f32> {
let k = (j * P.u[0].y + i) * 2u;
return vec2<f32>(data[k], data[k + 1u]);
}
// distort4::Mesh::at.
fn fxs_mesh_at(x: f32, y: f32) -> vec2<f32> {
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<f32>(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<u32>) {
let p = vec2<i32>(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<f32>(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<f32>(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<f32>;
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<f32>(a / FXS_TAU * w, h - r * h);
} else {
let a = x / w * FXS_TAU;
let r = (h - y) / h * rmax;
pp = vec2<f32>(cx + r * sin(a), cy - r * cos(a));
}
let amt = P.f[1].y;
s = vec2<f32>(x + (pp.x - x) * amt, y + (pp.y - y) * amt);
}
case 4u: {
let fl = P.f[0].xy;
s = vec2<f32>(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<f32>(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<f32>(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<f32>(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<f32>(0.0));
return;
}
rs = f * tan(th);
}
let k = rs / r * fit;
s = vec2<f32>(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<f32>(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<f32>((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<u32>) {
let p = vec2<i32>(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<f32>;
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<u32>) {
let p = vec2<i32>(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<f32>(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<u32>) {
let p = vec2<i32>(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<u32>) {
let p = vec2<i32>(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<u32>) {
let p = vec2<i32>(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<f32>(0.0));
} else {
textureStore(out, p, px);
}
return;
}
var m: vec4<f32>;
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<u32>) {
let p = vec2<i32>(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<u32>) {
let p = vec2<i32>(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<f32>(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<f32>(0.0);
} else if (surface == 2u) {
base_px = vec4<f32>(1.0);
} else if (surface == 3u) {
base_px = vec4<f32>(0.0, 0.0, 0.0, 1.0);
}
var painted: vec4<f32>;
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<u32>) {
let p = vec2<i32>(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<f32>(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<u32>) {
let p = vec2<i32>(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<f32>(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<u32>) {
let p = vec2<i32>(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<f32>(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<u32>) {
let p = vec2<i32>(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<f32>(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<f32>(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<f32>(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<u32>) {
let p = vec2<i32>(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<u32>) {
let p = vec2<i32>(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<u32>) {
let p = vec2<i32>(gid.xy);
if (!fxs_inside(p)) {
return;
}
textureStore(out, p, textureLoad(src, p, 0) / P.f[0].x);
}

View File

@ -511,7 +511,7 @@ fn effect_chains_and_mixed_stacks() {
let mut l = s.footage(80, 50); let mut l = s.footage(80, 50);
s.effect(&mut l, "ec.color.exposure", &[("master/exposure", n(0.7))]); 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.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.stylize.glow", &[]);
s.effect(&mut l, "ec.channel.invert", &[("blend", n(50.0))]); s.effect(&mut l, "ec.channel.invert", &[("blend", n(50.0))]);
s.push(l); s.push(l);

View File

@ -57,7 +57,7 @@ fn gpu_adjustment_chains_match_the_cpu() {
#[test] #[test]
fn mixed_adjustment_chains_fall_back_per_effect() { fn mixed_adjustment_chains_fall_back_per_effect() {
let mut stack = gpu_stack(); 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))])); stack.push(("ec.stylize.glow", vec![("threshold", n(30.0))]));
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] { for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
let s = adjustment_scene(depth, &stack, true); 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() { fn adjustment_stacks_split_into_gpu_runs_and_cpu_steps() {
let Some(g) = gpu() else { return }; let Some(g) = gpu() else { return };
let mut stack = gpu_stack(); 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 s = adjustment_scene(BitDepth::Bpc32, &stack, false);
let mut r = Renderer::new(&s.p, &Pattern, RenderOpts { backend: Backend::Gpu, ..opts() }); let mut r = Renderer::new(&s.p, &Pattern, RenderOpts { backend: Backend::Gpu, ..opts() });
r.accel = Some(g); r.accel = Some(g);

View File

@ -1,2 +1,206 @@
//! GPU stylize and distort family effects vs the CPU effects (the oracle): direct on a buffer at full and half //! 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. //! 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);
}
}

View File

@ -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. 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. - **32**: 306 effects process 32-bit float (HDR, overbright) pixels without clamping.
- **Status**: 306 implemented in full, 0 partial (what is missing is listed). - **Status**: 306 implemented in full, 0 partial (what is missing is listed).
@ -136,32 +136,32 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
| CC Bender | `ec.distort.ccbender` | 5 | | 32 | Implemented | | CC Bender | `ec.distort.ccbender` | 5 | | 32 | Implemented |
| CC Blobbylize | `ec.distort.ccblobbylize` | 15 | | 32 | Implemented | | CC Blobbylize | `ec.distort.ccblobbylize` | 15 | | 32 | Implemented |
| CC Flo Motion | `ec.distort.ccflomotion` | 7 | | 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 Lens | `ec.distort.cclens` | 3 | GPU | 32 | Implemented |
| CC Page Turn | `ec.distort.ccpageturn` | 7 | | 32 | Implemented | | CC Page Turn | `ec.distort.ccpageturn` | 7 | | 32 | Implemented |
| CC Power Pin | `ec.distort.ccpowerpin` | 10 | | 32 | Implemented | | CC Power Pin | `ec.distort.ccpowerpin` | 10 | | 32 | Implemented |
| CC Ripple Pulse | `ec.distort.ccripplepulse` | 5 | | 32 | Implemented | | CC Ripple Pulse | `ec.distort.ccripplepulse` | 5 | | 32 | Implemented |
| CC Slant | `ec.distort.ccslant` | 4 | | 32 | Implemented | | CC Slant | `ec.distort.ccslant` | 4 | GPU | 32 | Implemented |
| CC Smear | `ec.distort.ccsmear` | 4 | | 32 | Implemented | | CC Smear | `ec.distort.ccsmear` | 4 | GPU | 32 | Implemented |
| CC Split | `ec.distort.ccsplit` | 3 | | 32 | Implemented | | CC Split | `ec.distort.ccsplit` | 3 | GPU | 32 | Implemented |
| CC Split 2 | `ec.distort.ccsplit2` | 4 | | 32 | Implemented | | CC Split 2 | `ec.distort.ccsplit2` | 4 | GPU | 32 | Implemented |
| CC Tiler | `ec.distort.cctiler` | 3 | | 32 | Implemented | | CC Tiler | `ec.distort.cctiler` | 3 | GPU | 32 | Implemented |
| Corner Pin | `ec.distort.cornerpin` | 4 | | 32 | Implemented | | Corner Pin | `ec.distort.cornerpin` | 4 | GPU | 32 | Implemented |
| Detail-preserving Upscale | `ec.distort.upscale` | 4 | | 32 | Implemented | | Detail-preserving Upscale | `ec.distort.upscale` | 4 | | 32 | Implemented |
| Displacement Map | `ec.distort.displacementmap` | 8 | GPU | 32 | Implemented | | Displacement Map | `ec.distort.displacementmap` | 8 | GPU | 32 | Implemented |
| Liquify | `ec.distort.liquify` | 12 | | 32 | Implemented | | Liquify | `ec.distort.liquify` | 12 | GPU | 32 | Implemented |
| Magnify | `ec.distort.magnify` | 10 | | 32 | Implemented | | Magnify | `ec.distort.magnify` | 10 | GPU | 32 | Implemented |
| Mesh Warp | `ec.distort.meshwarp` | 3 | GPU | 32 | Implemented | | Mesh Warp | `ec.distort.meshwarp` | 3 | GPU | 32 | Implemented |
| Mirror | `ec.distort.mirror` | 2 | | 32 | Implemented | | Mirror | `ec.distort.mirror` | 2 | GPU | 32 | Implemented |
| Offset | `ec.distort.offset` | 2 | | 32 | Implemented | | Offset | `ec.distort.offset` | 2 | GPU | 32 | Implemented |
| Optics Compensation | `ec.distort.opticscompensation` | 6 | | 32 | Implemented | | Optics Compensation | `ec.distort.opticscompensation` | 6 | GPU | 32 | Implemented |
| Polar Coordinates | `ec.distort.polar` | 2 | | 32 | Implemented | | Polar Coordinates | `ec.distort.polar` | 2 | GPU | 32 | Implemented |
| Puppet | `ec.distort.puppet` | 1 | | 32 | Implemented | | Puppet | `ec.distort.puppet` | 1 | | 32 | Implemented |
| Reshape | `ec.distort.reshape` | 6 | | 32 | Implemented | | Reshape | `ec.distort.reshape` | 6 | | 32 | Implemented |
| Ripple | `ec.distort.ripple` | 7 | GPU | 32 | Implemented | | Ripple | `ec.distort.ripple` | 7 | GPU | 32 | Implemented |
| Rolling Shutter Repair | `ec.distort.rollingshutterrepair` | 5 | | 32 | Implemented | | Rolling Shutter Repair | `ec.distort.rollingshutterrepair` | 5 | | 32 | Implemented |
| Smear | `ec.distort.smear` | 8 | | 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 | | Transform | `ec.distort.transform` | 12 | GPU | 32 | Implemented |
| Turbulent Displace | `ec.distort.turbulentdisplace` | 11 | GPU | 32 | Implemented | | Turbulent Displace | `ec.distort.turbulentdisplace` | 11 | GPU | 32 | Implemented |
| Twirl | `ec.distort.twirl` | 3 | GPU | 32 | Implemented | | Twirl | `ec.distort.twirl` | 3 | GPU | 32 | Implemented |
@ -340,31 +340,31 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
| Effect | Id | Params | GPU | 32 | Status | | 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 Block Load | `ec.stylize.ccblockload` | 5 | | 32 | Implemented |
| CC Burn Film | `ec.stylize.ccburnfilm` | 3 | | 32 | Implemented | | CC Burn Film | `ec.stylize.ccburnfilm` | 3 | | 32 | Implemented |
| CC Glass | `ec.stylize.ccglass` | 17 | | 32 | Implemented | | CC Glass | `ec.stylize.ccglass` | 17 | | 32 | Implemented |
| CC HexTile | `ec.stylize.cchextile` | 5 | | 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 Mr. Smoothie | `ec.stylize.ccmrsmoothie` | 7 | | 32 | Implemented |
| CC Plastic | `ec.stylize.ccplastic` | 18 | | 32 | Implemented | | CC Plastic | `ec.stylize.ccplastic` | 18 | | 32 | Implemented |
| CC RepeTile | `ec.stylize.ccrepetile` | 5 | | 32 | Implemented | | CC RepeTile | `ec.stylize.ccrepetile` | 5 | GPU | 32 | Implemented |
| CC Threshold | `ec.stylize.ccthreshold` | 4 | | 32 | Implemented | | CC Threshold | `ec.stylize.ccthreshold` | 4 | GPU | 32 | Implemented |
| CC Threshold RGB | `ec.stylize.ccthresholdrgb` | 7 | | 32 | Implemented | | CC Threshold RGB | `ec.stylize.ccthresholdrgb` | 7 | GPU | 32 | Implemented |
| CC Vignette | `ec.stylize.ccvignette` | 4 | | 32 | Implemented | | CC Vignette | `ec.stylize.ccvignette` | 4 | GPU | 32 | Implemented |
| Cartoon | `ec.stylize.cartoon` | 12 | | 32 | Implemented | | Cartoon | `ec.stylize.cartoon` | 12 | | 32 | Implemented |
| Color Emboss | `ec.stylize.coloremboss` | 4 | | 32 | Implemented | | Color Emboss | `ec.stylize.coloremboss` | 4 | | 32 | Implemented |
| Emboss | `ec.stylize.emboss` | 4 | GPU | 32 | Implemented | | Emboss | `ec.stylize.emboss` | 4 | GPU | 32 | Implemented |
| Find Edges | `ec.stylize.findedges` | 2 | GPU | 32 | Implemented | | Find Edges | `ec.stylize.findedges` | 2 | GPU | 32 | Implemented |
| Glow | `ec.stylize.glow` | 14 | GPU | 32 | Implemented | | Glow | `ec.stylize.glow` | 14 | GPU | 32 | Implemented |
| Mosaic | `ec.stylize.mosaic` | 3 | GPU | 32 | Implemented | | Mosaic | `ec.stylize.mosaic` | 3 | GPU | 32 | Implemented |
| Motion Tile | `ec.stylize.motiontile` | 8 | | 32 | Implemented | | Motion Tile | `ec.stylize.motiontile` | 8 | GPU | 32 | Implemented |
| Posterize | `ec.stylize.posterize` | 1 | | 32 | Implemented | | Posterize | `ec.stylize.posterize` | 1 | GPU | 32 | Implemented |
| Roughen Edges | `ec.stylize.roughenedges` | 13 | | 32 | Implemented | | Roughen Edges | `ec.stylize.roughenedges` | 13 | GPU | 32 | Implemented |
| Scatter | `ec.stylize.scatter` | 3 | | 32 | Implemented | | Scatter | `ec.stylize.scatter` | 3 | GPU | 32 | Implemented |
| Strobe Light | `ec.stylize.strobe` | 7 | | 32 | Implemented | | Strobe Light | `ec.stylize.strobe` | 7 | GPU | 32 | Implemented |
| Texturize | `ec.stylize.texturize` | 4 | | 32 | Implemented | | Texturize | `ec.stylize.texturize` | 4 | GPU | 32 | Implemented |
| Threshold | `ec.stylize.threshold` | 1 | | 32 | Implemented | | Threshold | `ec.stylize.threshold` | 1 | GPU | 32 | Implemented |
## Text ## Text