Merge M13.9 gpu stylize and distort (fx_stylize)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-10-03 08:32:18 -04:00
commit 0fbde2f4ec
12 changed files with 2213 additions and 49 deletions

View File

@ -88,6 +88,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.noise.removegrain", &["temporalFiltering/enabled"]),
("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

@ -72,7 +72,8 @@ pub use color_fx::{
};
pub use color2::{Curve, auto_correct_settings, equalize_tables, luma_clip_points, shadow_highlight_amounts};
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};
@ -80,6 +81,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;
@ -860,7 +862,37 @@ pub const GPU_EFFECTS: &[&str] = &[
"ec.color.cccolorneutralizer",
"ec.color.colorstabilizer",
// 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",
"ec.distort.twirllegacy",
"ec.distort.ccripplepulse",
"ec.distort.ccpowerpin",
"ec.distort.ccflomotion",
// effectcraft-gpu fx_noise (noise, blur, time)
"ec.noise.turbulent",
"ec.noise.median",

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

File diff suppressed because it is too large Load Diff

View File

@ -1,3 +1,925 @@
// 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
// 10 Twirl (Legacy) (distort3::twirl_legacy): f[0] = (cx, cy, radius, angle)
// 11 CC Ripple Pulse (distort3::cc_ripple_pulse): u[0].y = render bump; f[0] = (cx, cy, front,
// speed); f[1] = (time span, wavelength, height)
// 12 CC Power Pin, full perspective (distort3::cc_power_pin): f[2..5] = inverse of the
// square → quad matrix (rows); f[5] = source rect in layer pixels (x0, y0, w, h);
// f[6] = (scale, buffer offset x, y)
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]);
}
// distort3::cc_ripple_pulse's wave.
fn fxs_pulse(r: f32) -> f32 {
let front = P.f[0].z;
let speed = P.f[0].w;
let span = P.f[1].x;
let age = (front - r) / max(speed, 1e-9);
if (age < 0.0 || age > span) {
return 0.0;
}
let fade = 1.0 - age / span;
return sin(FXS_TAU * (r - front) / P.f[1].y) * fade;
}
// 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);
}
case 10u: {
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) {
let a = -P.f[0].w * (1.0 - d / r);
let sn = sin(a);
let co = cos(a);
s = vec2<f32>(c.x + dx * co - dy * sn, c.y + dx * sn + dy * co);
}
}
case 11u: {
let c = P.f[0].xy;
let dx = x - c.x;
let dy = y - c.y;
let r = sqrt(dx * dx + dy * dy);
let height = P.f[1].z;
let d = fxs_pulse(r) * height;
var u = vec2<f32>(0.0);
if (r > 1e-9) {
u = vec2<f32>(dx / r, dy / r);
}
var o = sample_bilinear(src, x - u.x * d, y - u.y * d);
if (P.u[0].y != 0u && d != 0.0) {
let slope = (fxs_pulse(r + 0.5) - fxs_pulse(r - 0.5)) * height;
let k = clamp(1.0 - slope * 0.5, 0.0, 2.0);
o = vec4<f32>(o.xyz * k, o.w);
}
textureStore(out, p, o);
return;
}
case 12u: {
let r0 = P.f[2];
let r1 = P.f[3];
let r2 = P.f[4];
var u = r0.x * x + r0.y * y + r0.z;
var v = r1.x * x + r1.y * y + r1.z;
let w = r2.x * x + r2.y * y + r2.z;
if (w != 1.0 && w != 0.0) {
u = u / w;
v = v / w;
}
if (u < -1e-9 || u > 1.0 + 1e-9 || v < -1e-9 || v > 1.0 + 1e-9) {
textureStore(out, p, vec4<f32>(0.0));
return;
}
let rc = P.f[5];
let lx = rc.x + u * rc.z;
let ly = rc.y + v * rc.w;
let o = P.f[6];
s = vec2<f32>(lx * o.x + o.y, ly * o.x + o.z);
}
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));
}
}
// ---------------------------------------------------------------- CC Flo Motion (distort4::flo_motion)
// One knot's pull at layer point q: f[k] = (knot x, knot y, amount, _).
fn fxs_flo_knot(q: vec2<f32>, k: vec4<f32>) -> vec2<f32> {
if (k.z == 0.0) {
return q;
}
let d = q - k.xy;
let r = sqrt(d.x * d.x + d.y * d.y) / P.f[2].y;
let w = powz(1.0 - min(r, 1.0), P.f[2].x);
let s = max(1.0 - clamp(k.z, -4.0, 0.95) * w, 0.02);
return k.xy + d * s;
}
// u[0] = (supersampling, tile edges); f[0], f[1] = knots; f[2] = (falloff, radius, layer w,
// layer h); f[3] = (scale, buffer offset x, y)
@compute @workgroup_size(16, 16)
fn fxs_flomotion(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = vec2<i32>(gid.xy);
if (!fxs_inside(p)) {
return;
}
let ss = P.u[0].x;
let scale = P.f[3].x;
let off = P.f[3].yz;
let lw = P.f[2].z;
let lh = P.f[2].w;
var acc = vec4<f32>(0.0);
for (var sy = 0u; sy < ss; sy++) {
for (var sx = 0u; sx < ss; sx++) {
let px = f32(p.x) + (f32(sx) + 0.5) / f32(ss);
let py = f32(p.y) + (f32(sy) + 0.5) / f32(ss);
var q = vec2<f32>((px - off.x) / scale, (py - off.y) / scale);
q = fxs_flo_knot(q, P.f[0]);
q = fxs_flo_knot(q, P.f[1]);
if (P.u[0].y != 0u) {
q = vec2<f32>(q.x - floor(q.x / lw) * lw, q.y - floor(q.y / lh) * lh);
}
acc += sample_bilinear(src, q.x * scale + off.x, q.y * scale + off.y);
}
}
textureStore(out, p, acc / f32(ss * ss));
}
// ---------------------------------------------------------------- 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,223 @@
//! 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 twirl_legacy_ripple_pulse_power_pin() {
effect_case("ec.distort.twirllegacy", &[("angle", n(90.0))]);
effect_case("ec.distort.twirllegacy", &[("angle", n(-250.0)), ("radius", n(80.0)), ("center", pt(30.0, 18.0))]);
effect_case("ec.distort.ccripplepulse", &[("pulseLevel", n(80.0)), ("timeSpan", n(0.5))]);
effect_case("ec.distort.ccripplepulse", &[("pulseLevel", n(-60.0)), ("amplitude", n(150.0)), ("renderBump", on()), ("center", pt(25.0, 20.0))]);
effect_case("ec.distort.ccpowerpin", &[("topLeft", pt(5.0, 3.0)), ("bottomRight", pt(62.0, 38.0))]);
effect_case("ec.distort.ccpowerpin", &[("topRight", pt(80.0, -6.0)), ("bottomLeft", pt(-4.0, 50.0)), ("expandLeft", n(20.0)), ("expandBottom", n(-10.0))]);
}
#[test]
fn flo_motion() {
effect_case("ec.distort.ccflomotion", &[]);
effect_case("ec.distort.ccflomotion", &[("antialiasing", e(2)), ("tileEdges", on()), ("amount1", n(-150.0)), ("falloff", n(0.7))]);
effect_case("ec.distort.ccflomotion", &[("antialiasing", e(0)), ("amount2", n(0.0)), ("knot1", pt(20.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**: 118 effects also run on the GPU compositor with identical results.
- **GPU**: 150 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).
@ -101,7 +101,7 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
| Color Balance | `ec.color.colorbalance` | 10 | GPU | 32 | Implemented |
| Color Balance (HLS) | `ec.color.colorbalancehls` | 3 | GPU | 32 | Implemented |
| Color Link | `ec.color.colorlink` | 6 | | 32 | Implemented |
| Color Stabilizer | `ec.color.colorstabilizer` | 6 | | 32 | Implemented |
| Color Stabilizer | `ec.color.colorstabilizer` | 6 | GPU | 32 | Implemented |
| Colorama | `ec.color.colorama` | 20 | GPU | 32 | Implemented |
| Curves | `ec.color.curves` | 0 | GPU | 32 | Implemented |
| Equalize | `ec.color.equalize` | 2 | GPU | 32 | Implemented |
@ -135,37 +135,37 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
| CC Bend It | `ec.distort.ccbendit` | 4 | | 32 | Implemented |
| 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 Flo Motion | `ec.distort.ccflomotion` | 7 | GPU | 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 Power Pin | `ec.distort.ccpowerpin` | 10 | GPU | 32 | Implemented |
| CC Ripple Pulse | `ec.distort.ccripplepulse` | 5 | GPU | 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 |
| Twirl (Legacy) | `ec.distort.twirllegacy` | 3 | | 32 | Implemented |
| Twirl (Legacy) | `ec.distort.twirllegacy` | 3 | GPU | 32 | Implemented |
| Warp | `ec.distort.warp` | 5 | | 32 | Implemented |
| Warp Stabilizer | `ec.distort.warpstabilizer` | 20 | | 32 | Implemented |
| Wave Warp | `ec.distort.wavewarp` | 7 | 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