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.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

View File

@ -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,

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>>>;
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};
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();

View File

@ -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)
//
];

View File

@ -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;
}

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.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),

View File

@ -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<GBuf> {
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<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
// 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);
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);

View File

@ -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);

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
//! 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.
- **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.<category>.<name>`) 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.<category>.<name>`) 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