M13.9: gpu Twirl (Legacy), CC Ripple Pulse, CC Power Pin (full perspective), CC Flo Motion

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Brandon ThomasandClaude Opus 5.5 committed 2026-10-03 08:25:22 -04:00
1 parent 2e5d35dcee
commit 7f6e9c5e41
5 files changed
+256 -5

No files matched your search

+4
View File
@@ -838,6 +838,10 @@ pub const GPU_EFFECTS: &[&str] = &[
"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)
//
];
+113
View File
@@ -36,6 +36,7 @@ pub(crate) const KERNELS: &[&str] = &[
"fxs_glow_op",
"fxs_add",
"fxs_div",
"fxs_flomotion",
];
/// Effect ids implemented here.
@@ -67,6 +68,10 @@ pub(crate) const IDS: &[&str] = &[
"ec.distort.cctiler",
"ec.distort.ccgriddler",
"ec.distort.liquify",
"ec.distort.twirllegacy",
"ec.distort.ccripplepulse",
"ec.distort.ccpowerpin",
"ec.distort.ccflomotion",
];
/// Run effect `id` (one of [`IDS`]); `None` = this parameter combination runs on the CPU.
@@ -102,6 +107,10 @@ pub(crate) fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<G
"ec.distort.cctiler" => cc_tiler(e, ctx, b),
"ec.distort.ccgriddler" => cc_griddler(e, ctx, b),
"ec.distort.liquify" => liquify(e, ctx, b),
"ec.distort.twirllegacy" => twirl_legacy(e, ctx, b),
"ec.distort.ccripplepulse" => ripple_pulse(e, ctx, b),
"ec.distort.ccpowerpin" => power_pin(e, ctx, b),
"ec.distort.ccflomotion" => flo_motion(e, ctx, b),
_ => None,
}
}
@@ -438,6 +447,110 @@ fn liquify(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
)
}
fn twirl_legacy(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let ang = ctx.params.f("angle").to_radians();
if ang == 0.0 {
return Some(b);
}
let (lw, lh) = (ctx.layer_size[0] * b.scale, ctx.layer_size[1] * b.scale);
let r = (ctx.params.f("radius") / 100.0 * lw.min(lh) * 0.5).max(1.0);
let c = b.to_px(ctx.params.v2("center"));
warp(e, 10, |p| p.f[0] = [c.0 as f32, c.1 as f32, r as f32, ang as f32], b, None)
}
fn ripple_pulse(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let level = ctx.params.f("pulseLevel") / 100.0;
let amp = ctx.params.f("amplitude") / 100.0;
if level == 0.0 || amp == 0.0 {
return Some(b);
}
let c = b.to_px(ctx.params.v2("center"));
let span = ctx.params.f("timeSpan").max(0.01);
let size = ctx.layer_size[0].max(ctx.layer_size[1]) * b.scale;
let speed = size * 0.5 / span;
let lambda = (size * 0.06).max(2.0);
let height = level * amp * lambda * 0.5;
let front = speed * ctx.time.max(0.0);
let bump = ctx.params.b("renderBump");
warp(
e,
11,
|p| {
p.u[0][1] = bump as u32;
p.f[0] = [c.0 as f32, c.1 as f32, front as f32, speed as f32];
p.f[1] = [span as f32, lambda as f32, height as f32, 0.0];
},
b,
None,
)
}
fn power_pin(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
// Perspective below 100 % (a bilinear inverse by Newton iterations with finite
// differences) renders on the CPU.
if ctx.params.f("perspective") / 100.0 < 1.0 {
return None;
}
let (lw, lh) = (ctx.layer_size[0].max(1.0), ctx.layer_size[1].max(1.0));
let ex = [ctx.params.f("expandTop"), ctx.params.f("expandLeft"), ctx.params.f("expandRight"), ctx.params.f("expandBottom")];
let (x0, y0) = (-ex[1] / 100.0 * lw, -ex[0] / 100.0 * lh);
let (x1, y1) = (lw + ex[2] / 100.0 * lw, lh + ex[3] / 100.0 * lh);
let corners = [ctx.params.v2("topLeft"), ctx.params.v2("topRight"), ctx.params.v2("bottomRight"), ctx.params.v2("bottomLeft")];
if !ctx.adjustment {
let mut need = 0.0f64;
for c in &corners {
let (px, py) = b.to_px(*c);
need = need.max(-px).max(-py).max(px - b.img.width as f64).max(py - b.img.height as f64);
}
if need > 0.0 {
b.pad(e, need.ceil().min(4096.0) as u32 + 1)?;
}
}
let q = corners.map(|c| {
let p = b.to_px(c);
vec2(p.0, p.1)
});
let Some(inv) = Mat3::square_to_quad(q).inverse() else { return Some(b) };
let r = inv.0.map(|r| [r[0] as f32, r[1] as f32, r[2] as f32, 0.0]);
let (sc, off) = (b.scale, b.offset);
warp(
e,
12,
|p| {
p.f[2] = r[0];
p.f[3] = r[1];
p.f[4] = r[2];
p.f[5] = [x0 as f32, y0 as f32, (x1 - x0) as f32, (y1 - y0) as f32];
p.f[6] = [sc as f32, off[0] as f32, off[1] as f32, 0.0];
},
b,
None,
)
}
fn flo_motion(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let knots = [(ctx.params.v2("knot1"), ctx.params.f("amount1") / 100.0), (ctx.params.v2("knot2"), ctx.params.f("amount2") / 100.0)];
if knots.iter().all(|k| k.1 == 0.0) {
return Some(b);
}
let ss = match ctx.params.e("antialiasing") {
0 => 1,
1 => 2,
_ => 3,
};
let falloff = ctx.params.f("falloff").max(0.01);
let (lw, lh) = (ctx.layer_size[0].max(1.0), ctx.layer_size[1].max(1.0));
let radius = (lw * lw + lh * lh).sqrt() * 0.5;
let mut p = Params::default();
p.u[0] = [ss, ctx.params.b("tileEdges") as u32, 0, 0];
for (i, (k, a)) in knots.iter().enumerate() {
p.f[i] = [k[0] as f32, k[1] as f32, *a as f32, 0.0];
}
p.f[2] = [falloff as f32, radius as f32, lw as f32, lh as f32];
p.f[3] = [b.scale as f32, b.offset[0] as f32, b.offset[1] as f32, 0.0];
run(e, "fxs_flomotion", &p, b, None)
}
// ---------------------------------------------------------------- separable tables
/// Per-column `fx(x + 0.5)` then per-row `fy(y + 0.5)`, `k` values each.
+117
View File
@@ -180,12 +180,31 @@ fn fxs_point(@builtin(global_invocation_id) gid: vec3<u32>) {
// 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;
@@ -360,6 +379,60 @@ fn fxs_warp(@builtin(global_invocation_id) gid: vec3<u32>) {
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) {
@@ -369,6 +442,50 @@ fn fxs_warp(@builtin(global_invocation_id) gid: vec3<u32>) {
}
}
// ---------------------------------------------------------------- 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).
+17
View File
@@ -141,6 +141,23 @@ fn cc_slant_smear_split() {
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";
+5 -5
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**: 86 effects also run on the GPU compositor with identical results.
- **GPU**: 90 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).
@@ -135,12 +135,12 @@ 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 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 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 |
@@ -165,7 +165,7 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
| 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 |