mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
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:
1 parent
2e5d35dcee
commit
7f6e9c5e41
5 files changed
+256
-5
No files matched your search
@@ -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)
|
||||
//
|
||||
];
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
@@ -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 |
|
||||
|
||||
Reference in new issue
Block a user