Merge GPU distort/stylize effects into M12.7

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas
2026-10-03 02:47:03 -04:00
co-authored by Claude Opus 5.5
6 changed files with 1087 additions and 6 deletions
+2 -1
View File
@@ -414,7 +414,8 @@ fn grid_warp(src: &Image, w: u32, h: u32, nx: usize, ny: usize, dest: &[Pt], src
out
}
fn parse_mesh(s: &str) -> Vec<Pt> {
/// Mesh Warp's `mesh` parameter: the vertex offsets (layer pixels), row-major.
pub fn parse_mesh(s: &str) -> Vec<(f64, f64)> {
s.split(|c: char| c.is_whitespace() || c == ';')
.filter(|t| !t.is_empty())
.filter_map(|t| {
+12
View File
@@ -63,6 +63,7 @@ use std::sync::OnceLock;
pub use color_fx::{exposure_settings, levels_clip};
pub use color2::Curve;
pub use distort2::parse_mesh;
use effectcraft_keyframe::Value;
use effectcraft_project::build::Ids;
use effectcraft_project::{GroupKind, ParamUi, PropGroup, Property};
@@ -649,6 +650,17 @@ pub const GPU_EFFECTS: &[&str] = &[
"ec.color.exposure",
"ec.channel.invert",
"ec.distort.transform",
"ec.distort.turbulentdisplace",
"ec.distort.displacementmap",
"ec.distort.wavewarp",
"ec.distort.ripple",
"ec.distort.twirl",
"ec.distort.bulge",
"ec.distort.cclens",
"ec.distort.meshwarp",
"ec.stylize.mosaic",
"ec.stylize.findedges",
"ec.stylize.emboss",
];
/// Effects whose output depends on [`EffectCtx::time`] directly (not only through animated
+400 -5
View File
@@ -1,18 +1,413 @@
//! GPU effects, distortion and stylize family (kernels in `shaders/fx_distort.wgsl`): the CPU effects' exact
//! steps with the pixel loops as compute kernels.
//!
//! Parameter conversions, padding and anything with a per-row or per-column structure (map
//! coordinates, tile bounds, Mesh Warp's triangle buckets) are computed here in f64 exactly as
//! the CPU does and uploaded; the kernels do the per-pixel work.
use std::f64::consts::TAU;
use effectcraft_effects::EffectCtx;
use effectcraft_effects::util::{Src, src_at};
use crate::context::Enc;
use crate::context::{Enc, Params};
use crate::effects::GBuf;
/// Compute entry points in `fx_distort.wgsl`.
pub(crate) const KERNELS: &[&str] = &[];
pub(crate) const KERNELS: &[&str] = &[
"dst_twirl",
"dst_bulge",
"dst_wavewarp",
"dst_ripple",
"dst_cclens",
"dst_turbulent",
"dst_displace",
"dst_mesh",
"dst_mosaic_rows",
"dst_mosaic_tiles",
"dst_mosaic_out",
"dst_mosaic_sharp",
"dst_findedges",
"dst_emboss",
];
/// Effect ids implemented here.
pub(crate) const IDS: &[&str] = &[];
pub(crate) const IDS: &[&str] = &[
"ec.distort.turbulentdisplace",
"ec.distort.displacementmap",
"ec.distort.wavewarp",
"ec.distort.ripple",
"ec.distort.twirl",
"ec.distort.bulge",
"ec.distort.cclens",
"ec.distort.meshwarp",
"ec.stylize.mosaic",
"ec.stylize.findedges",
"ec.stylize.emboss",
];
/// 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> {
None
pub(crate) fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
match id {
"ec.distort.turbulentdisplace" => turbulent(e, ctx, b),
"ec.distort.displacementmap" => displacement_map(e, ctx, b),
"ec.distort.wavewarp" => wave_warp(e, ctx, b),
"ec.distort.ripple" => ripple(e, ctx, b),
"ec.distort.twirl" => twirl(e, ctx, b),
"ec.distort.bulge" => bulge(e, ctx, b),
"ec.distort.cclens" => cc_lens(e, ctx, b),
"ec.distort.meshwarp" => mesh_warp(e, ctx, b),
"ec.stylize.mosaic" => mosaic(e, ctx, b),
"ec.stylize.findedges" => find_edges(e, ctx, b),
"ec.stylize.emboss" => emboss(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)
}
/// Buf::layer_w / layer_h (distort.rs).
fn layer_wh(ctx: &EffectCtx) -> (f64, f64) {
(ctx.layer_size[0].max(1.0), ctx.layer_size[1].max(1.0))
}
/// util::layer_rect.
fn layer_rect(ctx: &EffectCtx, b: &GBuf) -> [f32; 4] {
[b.offset[0] as f32, b.offset[1] as f32, (ctx.layer_size[0] * b.scale) as f32, (ctx.layer_size[1] * b.scale) as f32]
}
// ---------------------------------------------------------------- distort.rs
fn twirl(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let ang = ctx.params.f("angle").to_radians();
let (lw, lh) = layer_wh(ctx);
let r = (ctx.params.f("radius") / 100.0 * lw.max(lh) * b.scale).max(1.0);
let c = b.to_px(ctx.params.v2("center"));
let mut p = Params::default();
p.f[0] = [c.0 as f32, c.1 as f32, r as f32, ang as f32];
run(e, "dst_twirl", &p, b, None)
}
fn bulge(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let rx = (ctx.params.f("hradius") * b.scale).max(1.0);
let ry = (ctx.params.f("vradius") * b.scale).max(1.0);
let c = b.to_px(ctx.params.v2("center"));
let h = ctx.params.f("height");
let taper = 1.0 + ctx.params.f("taperRadius").max(0.0) / 100.0 * 3.0;
let pin = ctx.params.b("pinAllEdges");
let (lw, lh) = layer_wh(ctx);
let (lw, lh) = (lw * b.scale, lh * b.scale);
let edge = (lw.min(lh) * 0.1).max(1.0);
let mut p = Params::default();
p.f[0] = [c.0 as f32, c.1 as f32, rx as f32, ry as f32];
p.f[1] = [h as f32, taper as f32, if pin { 1.0 } else { 0.0 }, edge as f32];
p.f[2] = [b.offset[0] as f32, b.offset[1] as f32, lw as f32, lh as f32];
run(e, "dst_bulge", &p, b, None)
}
fn wave_warp(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
let h = ctx.params.f("height") * b.scale;
let w = (ctx.params.f("width") * b.scale).max(1.0);
let dir = ctx.params.f("direction").to_radians();
let speed = ctx.params.f("speed");
let phase = ctx.params.f("phase").to_radians() + ctx.time * speed * TAU;
let (dx, dy) = (dir.sin(), -dir.cos());
if !ctx.adjustment {
b.pad(e, h.abs().ceil() as u32 + 1)?;
}
let (lw, lh) = layer_wh(ctx);
let rect = [b.offset[0] as f32, b.offset[1] as f32, (lw * b.scale).max(1.0) as f32, (lh * b.scale).max(1.0) as f32];
// The wave phase in cycles, (x·dx + y·dy) / width + phase / 2π, is separable: per column
// and per row (fraction, whole cycles) in f64, so the kernel's sum keeps the fraction exact
// to f32 (the circle shapes have vertical tangents where a phase error is amplified).
let (bw, bh) = (b.img.width as usize, b.img.height as usize);
let mut data = vec![0.0f32; 2 * (bw + bh)];
let split = |v: f64| (v - v.floor(), v.floor());
for x in 0..bw {
let (f, i) = split((x as f64 + 0.5) * dx / w);
data[x] = f as f32;
data[bw + x] = i as f32;
}
for y in 0..bh {
let (f, i) = split((y as f64 + 0.5) * dy / w + phase / TAU);
data[2 * bw + y] = f as f32;
data[2 * bw + bh + y] = i as f32;
}
let mut p = Params::default();
p.u[0] = [ctx.params.e("waveType"), ctx.params.e("pinning"), ctx.seed ^ 0x3a7e, bw as u32];
p.f[0] = [dx as f32, dy as f32, h as f32, 0.0];
p.f[2] = rect;
let buf = e.data(&data);
run(e, "dst_wavewarp", &p, b, Some(&buf))
}
fn ripple(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let (lw, lh) = layer_wh(ctx);
let r = (ctx.params.f("radius") / 100.0 * lw.max(lh) * 0.5 * b.scale).max(1.0);
let asymmetric = ctx.params.e("conversion") == 0;
let c = b.to_px(ctx.params.v2("center"));
let w = (ctx.params.f("waveWidth") * b.scale).max(1.0);
let h = ctx.params.f("waveHeight") * b.scale;
let phase = ctx.params.f("phase").to_radians() - ctx.time * ctx.params.f("speed") * TAU;
let mut p = Params::default();
p.u[0][0] = asymmetric as u32;
p.f[0] = [c.0 as f32, c.1 as f32, r as f32, w as f32];
p.f[1] = [h as f32, phase.rem_euclid(TAU) as f32, 0.0, 0.0];
run(e, "dst_ripple", &p, b, None)
}
// ---------------------------------------------------------------- distort2.rs
fn cc_lens(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let c = b.to_px(ctx.params.v2("center"));
let (lw, lh) = (ctx.layer_size[0] * b.scale, ctx.layer_size[1] * b.scale);
let r = (ctx.params.f("size") / 100.0 * lw.max(lh) * 0.5).max(0.5);
let k = 2f64.powf(ctx.params.f("convergence") / 50.0);
let mut p = Params::default();
p.f[0] = [c.0 as f32, c.1 as f32, r as f32, k as f32];
run(e, "dst_cclens", &p, b, None)
}
fn turbulent(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
let amount = ctx.params.f("amount") * b.scale;
if amount.abs() < 1e-9 {
return Some(b);
}
let kind = ctx.params.e("displacement");
let size = (ctx.params.f("size") * b.scale).max(1.0);
if ctx.params.b("resizeLayer") && !ctx.adjustment {
b.pad(e, amount.abs().ceil() as u32 + 1)?;
}
let off = b.to_px(ctx.params.v2("offset"));
let oct = ctx.params.f("complexity").clamp(1.0, 10.0);
let evo = ctx.params.f("evolution") / 360.0;
let cycle = ctx.params.b("evolutionOptions/cycleEvolution").then(|| ctx.params.f("evolutionOptions/cycle").round().max(1.0));
let falloff = if (3..=5).contains(&kind) { 0.3 } else { 0.5 };
let rect = layer_rect(ctx, &b);
let edge = ((ctx.layer_size[0] * b.scale).min(ctx.layer_size[1] * b.scale) * 0.1).max(1.0);
let seed = (ctx.seed ^ 0x7d15).wrapping_add((ctx.params.f("evolutionOptions/randomSeed") as i64 as u32).wrapping_mul(0x9e37_79b9));
let (z0, z1, t) = match cycle {
Some(c) => {
let z = evo.rem_euclid(c);
(z as f32, (z - c) as f32, (z / c) as f32)
}
None => (evo as f32, 0.0, 0.0),
};
let mut p = Params::default();
p.u[0] = [kind, ctx.params.e("pinning"), seed, oct.ceil().max(1.0) as u32];
p.u[1][0] = cycle.is_some() as u32;
p.f[0] = [off.0 as f32, off.1 as f32, size as f32, amount as f32];
p.f[1] = [z0, z1, t, (oct - oct.floor()) as f32];
p.f[2] = rect;
p.f[3] = [edge as f32, falloff, 0.0, 0.0];
run(e, "dst_turbulent", &p, b, None)
}
fn displacement_map(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
let hs = src_at(ctx.params.e("useForHorizontal"));
let vs = src_at(ctx.params.e("useForVertical"));
let mh = ctx.params.f("maxHorizontal") * b.scale;
let mv = ctx.params.f("maxVertical") * b.scale;
let wrap = ctx.params.b("wrapPixelsAround");
if ctx.params.b("expandOutput") && !wrap && !ctx.adjustment {
b.pad(e, mh.abs().max(mv.abs()).ceil() as u32)?;
}
let layer = ctx.layer_param("displacementMapLayer", false);
let behavior = ctx.params.e("displacementMapBehavior");
let (w, h) = (b.img.width as usize, b.img.height as usize);
let inv = 1.0 / b.scale.max(1e-9);
let ls = ctx.layer_size;
// Per column / row: the map layer's buffer coordinate (fit_layer / Tile Map) and whether
// the pixel lies inside the centred map (Center Map).
let mut data = vec![0.0f32; 2 * (w + h)];
let mut mode = 0u32;
let mut center = false;
let map = match &layer {
Some(o) => {
if o.buf.img.is_empty() {
return None;
}
let os = o.size;
let axis = |i: usize, k: usize| -> (f64, bool) {
let q = (i as f64 + 0.5 - b.offset[k]) * inv;
let map_q = match behavior {
2 => (q + (os[k] - ls[k]) * 0.5).rem_euclid(os[k].max(1.0)),
1 if ls[0] > 0.0 && ls[1] > 0.0 => q * (os[k] / ls[k]),
1 => q,
_ => q + (os[k] - ls[k]) * 0.5,
};
let c = q + (os[k] - ls[k]) * 0.5;
(map_q * o.buf.scale + o.buf.offset[k], (0.0..os[k]).contains(&c))
};
for x in 0..w {
let (m, inside) = axis(x, 0);
data[x] = m as f32;
data[w + h + x] = inside as u32 as f32;
}
for y in 0..h {
let (m, inside) = axis(y, 1);
data[w + y] = m as f32;
data[2 * w + h + y] = inside as u32 as f32;
}
mode = if behavior == 2 { 2 } else { 1 };
center = behavior == 0;
Some(e.g.upload_image(&o.buf.img)?)
}
None => None,
};
let idx = |s: Src| effectcraft_effects::util::SRC_ORDER.iter().position(|x| *x == s).unwrap_or(10) as u32;
let mut p = Params::default();
p.u[0] = [idx(hs), idx(vs), mode, wrap as u32 | (center as u32) << 1];
p.u[1] = [w as u32, h as u32, 0, 0];
p.f[0] = [mh as f32, mv as f32, 0.0, 0.0];
let buf = e.data(&data);
let out = e.image(b.img.width, b.img.height);
e.pixels("dst_displace", &p, &b.img, map.as_ref(), &out, Some(&buf));
b.img = out;
Some(b)
}
fn mesh_warp(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let rows = ctx.params.f("rows").round().clamp(1.0, 31.0) as usize;
let cols = ctx.params.f("columns").round().clamp(1.0, 31.0) as usize;
let q = ctx.params.f("quality").round().clamp(1.0, 10.0) as usize;
let offs = effectcraft_effects::parse_mesh(ctx.params.s("mesh"));
if offs.len() != (rows + 1) * (cols + 1) || offs.iter().all(|o| o.0 == 0.0 && o.1 == 0.0) {
return Some(b);
}
let (lw, lh) = (ctx.layer_size[0], ctx.layer_size[1]);
let vert = |i: usize, j: usize| {
let o = offs[j * (cols + 1) + i];
b.to_px([i as f64 / cols as f64 * lw + o.0, j as f64 / rows as f64 * lh + o.1])
};
let (nx, ny) = (cols * q, rows * q);
let mut dest = Vec::with_capacity((nx + 1) * (ny + 1));
let mut srcp = Vec::with_capacity((nx + 1) * (ny + 1));
for gj in 0..=ny {
for gi in 0..=nx {
let (ci, cj) = ((gi / q).min(cols - 1), (gj / q).min(rows - 1));
let (u, v) = ((gi - ci * q) as f64 / q as f64, (gj - cj * q) as f64 / q as f64);
let (p00, p10, p01, p11) = (vert(ci, cj), vert(ci + 1, cj), vert(ci, cj + 1), vert(ci + 1, cj + 1));
let x = (p00.0 * (1.0 - u) + p10.0 * u) * (1.0 - v) + (p01.0 * (1.0 - u) + p11.0 * u) * v;
let y = (p00.1 * (1.0 - u) + p10.1 * u) * (1.0 - v) + (p01.1 * (1.0 - u) + p11.1 * u) * v;
dest.push((x, y));
srcp.push(b.to_px([gi as f64 / nx as f64 * lw, gj as f64 / ny as f64 * lh]));
}
}
// distort2::grid_warp: triangles in order, bucketed per output row.
let idx = |i: usize, j: usize| j * (nx + 1) + i;
let hh = b.img.height as usize;
let mut tri_data: Vec<f32> = Vec::with_capacity(nx * ny * 24);
let mut buckets: Vec<Vec<u32>> = vec![Vec::new(); hh];
let mut t = 0u32;
for j in 0..ny {
for i in 0..nx {
let (a, b2, c, d) = (idx(i, j), idx(i + 1, j), idx(i + 1, j + 1), idx(i, j + 1));
for tri in [[a, b2, c], [a, c, d]] {
let [ia, ib, ic] = tri;
let (pa, pb, pc) = (dest[ia], dest[ib], dest[ic]);
let den: f64 = (pb.1 - pc.1) * (pa.0 - pc.0) + (pc.0 - pb.0) * (pa.1 - pc.1);
// Degenerate triangles are skipped per pixel on the CPU; leaving them out of
// the buckets is the same.
if den.abs() < 1e-12 {
continue;
}
let ys = [pa.1, pb.1, pc.1];
let y0 = (ys[0].min(ys[1]).min(ys[2]) - 0.5).floor().max(0.0) as usize;
let y1 = (ys[0].max(ys[1]).max(ys[2]) + 0.5).ceil().max(0.0) as usize;
for bucket in buckets.iter_mut().take(y1.min(hh)).skip(y0) {
bucket.push(t);
}
let (sa, sb, sc) = (srcp[ia], srcp[ib], srcp[ic]);
tri_data.extend(
[(pb.1 - pc.1) / den, (pc.0 - pb.0) / den, (pc.1 - pa.1) / den, (pa.0 - pc.0) / den, pc.0, pc.1, sa.0, sa.1, sb.0, sb.1, sc.0, sc.1]
.map(|v| v as f32),
);
t += 1;
}
}
}
let row_starts = tri_data.len();
let list_start = row_starts + hh + 1;
let total: usize = buckets.iter().map(Vec::len).sum();
if list_start + total >= 1 << 24 {
// Indices past 2^24 are not exact in the f32 data buffer.
return None;
}
let mut data = tri_data;
data.reserve(hh + 1 + total);
let mut n = 0usize;
for bucket in &buckets {
data.push(n as f32);
n += bucket.len();
}
data.push(n as f32);
for bucket in &buckets {
data.extend(bucket.iter().map(|&t| t as f32));
}
let mut p = Params::default();
p.u[0] = [row_starts as u32, list_start as u32, 0, 0];
let buf = e.data(&data);
run(e, "dst_mesh", &p, b, Some(&buf))
}
// ---------------------------------------------------------------- misc.rs (Stylize)
fn mosaic(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
let hb = ctx.params.f("horizontal").round().max(1.0) as usize;
let vb = ctx.params.f("vertical").round().max(1.0) as usize;
let (w, h) = (b.img.width as usize, b.img.height as usize);
if w == 0 || h == 0 {
return Some(b);
}
let cw = (w as f64 / hb as f64).max(1.0);
let ch = (h as f64 / vb as f64).max(1.0);
let mut p = Params::default();
if ctx.params.b("sharpColors") {
let centre = |i: usize, c: f64| (((i as f64 / c).floor() + 0.5) * c) as i64 as f32;
let data: Vec<f32> = (0..w).map(|x| centre(x, cw)).chain((0..h).map(|y| centre(y, ch))).collect();
p.u[0][0] = w as u32;
let buf = e.data(&data);
return run(e, "dst_mosaic_sharp", &p, b, Some(&buf));
}
let (nx, ny) = ((w as f64 / cw).ceil() as usize, (h as f64 / ch).ceil() as usize);
let tx: Vec<usize> = (0..w).map(|x| ((x as f64 / cw) as usize).min(nx - 1)).collect();
let ty: Vec<usize> = (0..h).map(|y| ((y as f64 / ch) as usize).min(ny - 1)).collect();
// Tile bounds: tile i covers the columns with tx == i (tx is non-decreasing).
let bounds = |t: &[usize], n: usize| -> Vec<f32> { (0..=n).map(|i| t.partition_point(|&v| v < i) as f32).collect() };
let mut data: Vec<f32> = tx.iter().chain(&ty).map(|&v| v as f32).collect();
data.extend(bounds(&tx, nx));
data.extend(bounds(&ty, ny));
p.u[0] = [w as u32, h as u32, nx as u32, ny as u32];
let buf = e.data(&data);
let rows = e.image(nx as u32, h as u32);
e.pixels("dst_mosaic_rows", &p, &b.img, None, &rows, Some(&buf));
let tiles = e.image(nx as u32, ny as u32);
e.pixels("dst_mosaic_tiles", &p, &rows, None, &tiles, Some(&buf));
let out = e.image(b.img.width, b.img.height);
e.pixels("dst_mosaic_out", &p, &tiles, None, &out, Some(&buf));
b.img = out;
Some(b)
}
fn find_edges(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let mut p = Params::default();
p.u[0][0] = ctx.params.b("invert") as u32;
p.f[0][0] = ctx.params.f("blend") as f32 / 100.0;
run(e, "dst_findedges", &p, b, None)
}
fn emboss(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let ang = ctx.params.f("direction").to_radians();
let relief = ctx.params.f("relief") * b.scale;
let (dx, dy) = (ang.cos() * relief, -ang.sin() * relief);
let mut p = Params::default();
p.f[0] = [dx as f32, dy as f32, ctx.params.f("contrast") as f32 / 100.0, ctx.params.f("blend") as f32 / 100.0];
run(e, "dst_emboss", &p, b, None)
}
+2
View File
@@ -161,4 +161,6 @@ mod tests_adjust;
#[cfg(test)]
mod tests_adv3d;
#[cfg(test)]
mod tests_fx_distort;
#[cfg(test)]
mod tests_particles;
+552
View File
@@ -1,2 +1,554 @@
// GPU effects (distort family): see src/fx_distort.rs. Each entry point mirrors the CPU effect in
// effectcraft-effects operation for operation; parameter layouts are documented per entry.
// Coordinates are pixel centres (x + 0.5, y + 0.5) in buffer pixels, as in util::remap.
const DST_TAU: f32 = 6.2831855;
const DST_HALF_PI: f32 = 1.5707964;
fn dst_centre(p: vec2<i32>) -> vec2<f32> {
return vec2<f32>(f32(p.x) + 0.5, f32(p.y) + 0.5);
}
fn dst_inside(p: vec2<i32>) -> bool {
let d = out_dims();
return p.x < d.x && p.y < d.y;
}
// ---------------------------------------------------------------- Twirl (distort::twirl)
// f[0] = (cx, cy, radius, angle in radians)
@compute @workgroup_size(16, 16)
fn dst_twirl(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = vec2<i32>(gid.xy);
if (!dst_inside(p)) {
return;
}
let q = dst_centre(p);
let c = P.f[0].xy;
let r = P.f[0].z;
let dx = q.x - c.x;
let dy = q.y - c.y;
let d = sqrt(dx * dx + dy * dy);
var s = q;
if (d < r) {
let t = 1.0 - d / r;
let a = -P.f[0].w * t * t;
let sn = sin(a);
let co = cos(a);
s = vec2<f32>(c.x + dx * co - dy * sn, c.y + dx * sn + dy * co);
}
textureStore(out, p, sample_bilinear(src, s.x, s.y));
}
// ---------------------------------------------------------------- Bulge (distort::bulge)
// f[0] = (cx, cy, rx, ry); f[1] = (height, taper, pin, edge); f[2] = layer rect (x, y, w, h)
@compute @workgroup_size(16, 16)
fn dst_bulge(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = vec2<i32>(gid.xy);
if (!dst_inside(p)) {
return;
}
let q = dst_centre(p);
let c = P.f[0].xy;
let rx = P.f[0].z;
let ry = P.f[0].w;
let dx = (q.x - c.x) / rx;
let dy = (q.y - c.y) / ry;
let d2 = dx * dx + dy * dy;
var s = q;
if (d2 < 1.0) {
var amt = P.f[1].x * pow(1.0 - d2, P.f[1].y) * 0.5;
if (P.f[1].z > 0.5) {
let l = P.f[2];
let edge = P.f[1].w;
let ex = clamp(min(q.x - l.x - 0.5, l.x + l.z - 0.5 - q.x) / edge, 0.0, 1.0);
let ey = clamp(min(q.y - l.y - 0.5, l.y + l.w - 0.5 - q.y) / edge, 0.0, 1.0);
amt *= min(ex, ey);
}
let k = 1.0 - amt;
s = vec2<f32>(c.x + dx * rx * k, c.y + dy * ry * k);
}
textureStore(out, p, sample_bilinear(src, s.x, s.y));
}
// ---------------------------------------------------------------- Wave Warp (distort::wave_warp)
fn dst_half_circle(u: f32) -> f32 {
let v = 2.0 * u - 1.0;
return sqrt(max(1.0 - v * v, 0.0));
}
fn dst_wave_noise(i: f32, seed: u32) -> f32 {
return hash_noise(bitcast<u32>(i32(i)), 0x5eedu, seed) * 2.0 - 1.0;
}
// distort::wave_shape at the phase `cf + ci` in cycles (t / 2π), split into a fraction cf in
// [0, 2) and whole cycles ci so the fraction keeps full precision.
fn dst_wave_shape(kind: u32, cf: f32, ci: f32, seed: u32) -> f32 {
let ph = cf - floor(cf);
switch kind {
case 1u: {
return select(-1.0, 1.0, ph < 0.5);
}
case 2u: {
return 1.0 - 4.0 * abs(ph - 0.5);
}
case 3u: {
return 2.0 * ph - 1.0;
}
case 4u: {
if (ph < 0.5) {
return dst_half_circle(ph * 2.0);
}
return -dst_half_circle(ph * 2.0 - 1.0);
}
case 5u: {
return 2.0 * dst_half_circle(fract(ph * 2.0)) - 1.0;
}
case 6u: {
return 1.0 - 2.0 * dst_half_circle(fract(ph * 2.0));
}
case 7u: {
return dst_wave_noise(floor(cf * 2.0) + 2.0 * ci, seed);
}
case 8u: {
let u = cf * 2.0;
let i = floor(u);
let f = u - i;
let s = f * f * (3.0 - 2.0 * f);
let n = i + 2.0 * ci;
return dst_wave_noise(n, seed) * (1.0 - s) + dst_wave_noise(n + 1.0, seed) * s;
}
default: {
return sin(ph * DST_TAU);
}
}
}
// distort::wave_pin.
fn dst_wave_pin(pin: u32, x: f32, y: f32, r: vec4<f32>) -> f32 {
let u = clamp((x - r.x - 0.5) / max(r.z - 1.0, 1.0), 0.0, 1.0);
let v = clamp((y - r.y - 0.5) / max(r.w - 1.0, 1.0), 0.0, 1.0);
let horiz = clamp(min(v, 1.0 - v) * 2.0, 0.0, 1.0);
let vert = clamp(min(u, 1.0 - u) * 2.0, 0.0, 1.0);
switch pin {
case 1u: { return min(horiz, vert); }
case 2u: { return min(sqrt((u - 0.5) * (u - 0.5) + (v - 0.5) * (v - 0.5)) * 2.0, 1.0); }
case 3u: { return u; }
case 4u: { return v; }
case 5u: { return 1.0 - u; }
case 6u: { return 1.0 - v; }
case 7u: { return horiz; }
case 8u: { return vert; }
default: { return 1.0; }
}
}
// u[0] = (wave type, pinning, seed, buffer width); f[0] = (dx, dy, height); f[2] = layer rect
// (x, y, w, h). data: the phase in cycles (along / width + phase / 2π) as per-column and per-row
// (fraction, whole cycles) pairs: fx (w), ix (w), fy (h), iy (h).
@compute @workgroup_size(16, 16)
fn dst_wavewarp(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = vec2<i32>(gid.xy);
if (!dst_inside(p)) {
return;
}
let q = dst_centre(p);
let dx = P.f[0].x;
let dy = P.f[0].y;
let w = P.u[0].w;
let x = u32(p.x);
let y = u32(p.y);
let cf = data[x] + data[2u * w + y];
let ci = data[w + x] + data[2u * w + u32(out_dims().y) + y];
let disp = dst_wave_shape(P.u[0].x, cf, ci, P.u[0].z) * P.f[0].z * dst_wave_pin(P.u[0].y, q.x, q.y, P.f[2]);
textureStore(out, p, sample_bilinear(src, q.x + dy * disp, q.y - dx * disp));
}
// ---------------------------------------------------------------- Ripple (distort::ripple)
// u[0].x = asymmetric; f[0] = (cx, cy, radius, wave width); f[1] = (wave height, phase mod 2π)
@compute @workgroup_size(16, 16)
fn dst_ripple(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = vec2<i32>(gid.xy);
if (!dst_inside(p)) {
return;
}
let q = dst_centre(p);
let c = P.f[0].xy;
let r = P.f[0].z;
let dx = q.x - c.x;
let dy = q.y - c.y;
let d = sqrt(dx * dx + dy * dy);
var s = q;
if (d < r && d != 0.0) {
let h = P.f[1].x;
let fall = 1.0 - d / r;
let arg = d / P.f[0].w * DST_TAU + P.f[1].y;
let o = sin(arg) * h * fall;
var t = 0.0;
if (P.u[0].x != 0u) {
t = cos(arg) * h * fall * 0.5;
}
let ux = dx / d;
let uy = dy / d;
s = vec2<f32>(q.x + ux * o - uy * t, q.y + uy * o + ux * t);
}
textureStore(out, p, sample_bilinear(src, s.x, s.y));
}
// ---------------------------------------------------------------- CC Lens (distort2::cc_lens)
// f[0] = (cx, cy, radius, convergence exponent)
@compute @workgroup_size(16, 16)
fn dst_cclens(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = vec2<i32>(gid.xy);
if (!dst_inside(p)) {
return;
}
let q = dst_centre(p);
let c = P.f[0].xy;
let r = P.f[0].z;
let dx = q.x - c.x;
let dy = q.y - c.y;
let d = sqrt(dx * dx + dy * dy);
if (d >= r) {
textureStore(out, p, vec4<f32>(0.0));
return;
}
var s = q;
if (d >= 1e-9) {
let g = powz(asin(min(d / r, 1.0)) / DST_HALF_PI, P.f[0].w);
let k = r * g / d;
s = vec2<f32>(c.x + dx * k, c.y + dy * k);
}
textureStore(out, p, sample_bilinear(src, s.x, s.y));
}
// ---------------------------------------------------------------- Turbulent Displace (distort2)
// distort2::fbm. u[0].w = octave count; f[1].w = fraction of the last octave; f[3].y = falloff.
fn dst_fbm(u: f32, v: f32, z: f32, seed: u32) -> f32 {
let n = P.u[0].w;
let frac = P.f[1].w;
let falloff = P.f[3].y;
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) * 5.17, seed + o * 101u) * amp * w;
norm += amp * w;
amp *= falloff;
f *= 2.0;
}
return sum / max(norm, 1e-6);
}
// The centred noise `n(u, v, s)` (cross-faded over the evolution cycle when u[1].x is set).
// f[1] = (z, z - cycle, cycle position t).
fn dst_turb_n(u: f32, v: f32, s: u32) -> f32 {
let seed = P.u[0].z + s;
if (P.u[1].x != 0u) {
let t = P.f[1].z;
return dst_fbm(u, v, P.f[1].x, seed) * (1.0 - t) + dst_fbm(u, v, P.f[1].y, seed) * t - 0.5;
}
return dst_fbm(u, v, P.f[1].x, seed) - 0.5;
}
// u[0] = (displacement kind, pinning, seed, octaves); u[1].x = cycle evolution;
// f[0] = (offset x, offset y, size, amount); f[1] = (z0, z1, t, last-octave fraction);
// f[2] = layer rect (x, y, w, h); f[3] = (edge, falloff)
@compute @workgroup_size(16, 16)
fn dst_turbulent(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = vec2<i32>(gid.xy);
if (!dst_inside(p)) {
return;
}
let q = dst_centre(p);
let kind = P.u[0].x;
let amount = P.f[0].w;
let u = (q.x - P.f[0].x) / P.f[0].z;
let v = (q.y - P.f[0].y) / P.f[0].z;
var d = vec2<f32>(0.0);
switch kind {
case 1u, 2u, 4u, 5u: {
let e = 0.05;
let gx = (dst_turb_n(u + e, v, 0u) - dst_turb_n(u - e, v, 0u)) / (2.0 * e);
let gy = (dst_turb_n(u, v + e, 0u) - dst_turb_n(u, v - e, 0u)) / (2.0 * e);
let k = amount * 0.25;
if (kind == 1u || kind == 4u) {
d = vec2<f32>(gx * k, gy * k);
} else {
d = vec2<f32>(-gy * k, gx * k);
}
}
case 6u: {
d = vec2<f32>(0.0, dst_turb_n(u, v, 17u) * amount);
}
case 7u: {
d = vec2<f32>(dst_turb_n(u, v, 0u) * amount, 0.0);
}
case 8u: {
let a = dst_turb_n(u, v, 0u) * amount;
d = vec2<f32>(a, a);
}
default: {
d = vec2<f32>(dst_turb_n(u, v, 0u) * amount, dst_turb_n(u, v, 17u) * amount);
}
}
let r = P.f[2];
let edge = P.f[3].x;
let l = clamp((q.x - r.x - 0.5) / edge, 0.0, 1.0);
let rr = clamp((r.x + r.z - 0.5 - q.x) / edge, 0.0, 1.0);
let t = clamp((q.y - r.y - 0.5) / edge, 0.0, 1.0);
let bt = clamp((r.y + r.w - 0.5 - q.y) / edge, 0.0, 1.0);
var k = 1.0;
switch P.u[0].y {
case 1u: { k = min(min(l, rr), min(t, bt)); }
case 2u: { k = min(t, bt); }
case 3u: { k = min(l, rr); }
case 4u: { k = l; }
case 5u: { k = t; }
case 6u: { k = rr; }
case 7u: { k = bt; }
default: {}
}
textureStore(out, p, sample_bilinear(src, q.x + d.x * k, q.y + d.y * k));
}
// ---------------------------------------------------------------- Displacement Map (distort2)
// util::pick for a straight colour (source index in util::SRC_ORDER).
fn dst_pick(s: u32, c: vec3<f32>, a: f32) -> f32 {
switch s {
case 0u: { return c.x; }
case 1u: { return c.y; }
case 2u: { return c.z; }
case 3u: { return a; }
case 4u: { return luminance(c); }
case 5u: { return rgb_to_hsl(c).x; }
case 6u: { return rgb_to_hsl(c).z; }
case 7u: { return rgb_to_hsl(c).y; }
case 8u: { return 1.0; }
case 9u: { return 0.5; }
default: { return 0.0; }
}
}
// u[0] = (horizontal source, vertical source, map mode: 0 self, 1 fitted layer (transparent
// outside), 2 tiled layer (edges repeat), flags: 1 wrap, 2 Center Map bounds);
// u[1] = (buffer width, buffer height); f[0] = (max horizontal, max vertical).
// data: per-column map x, per-row map y (the map layer's buffer pixels), per-column and
// per-row "inside the centred map" flags. aux = the map layer.
@compute @workgroup_size(16, 16)
fn dst_displace(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = vec2<i32>(gid.xy);
if (!dst_inside(p)) {
return;
}
let w = P.u[1].x;
let h = P.u[1].y;
let x = u32(p.x);
let y = u32(p.y);
let px = textureLoad(src, p, 0);
let flags = P.u[0].w;
if ((flags & 2u) != 0u && (data[w + h + x] < 0.5 || data[2u * w + h + y] < 0.5)) {
textureStore(out, p, px);
return;
}
var m = px;
let mode = P.u[0].z;
if (mode == 1u) {
m = sample_bilinear(aux, data[x], data[w + y]);
} else if (mode == 2u) {
m = sample_bilinear_clamped(aux, data[x], data[w + y]);
}
var c = vec3<f32>(0.0);
let a = m.w;
if (a > 1e-6) {
c = m.xyz / a;
}
let al = max(a, 0.0);
var dx = 0.0;
var dy = 0.0;
if (P.u[0].x != 10u) {
dx = (dst_pick(P.u[0].x, c, al) - 0.5) * 2.0 * P.f[0].x;
}
if (P.u[0].y != 10u) {
dy = (dst_pick(P.u[0].y, c, al) - 0.5) * 2.0 * P.f[0].y;
}
if (dx == 0.0 && dy == 0.0) {
textureStore(out, p, px);
return;
}
var sx = f32(p.x) + 0.5 + dx;
var sy = f32(p.y) + 0.5 + dy;
if ((flags & 1u) != 0u) {
let fw = f32(w);
let fh = f32(h);
sx = sx - floor(sx / fw) * fw;
sy = sy - floor(sy / fh) * fh;
textureStore(out, p, sample_bilinear_clamped(src, sx, sy));
} else {
textureStore(out, p, sample_bilinear(src, sx, sy));
}
}
// ---------------------------------------------------------------- Mesh Warp (distort2::grid_warp)
// u[0] = (row starts offset, list offset). data: 12 floats per triangle (the barycentric
// coefficients k11 k12 k21 k22 relative to vertex c, c, then the source points a, b, c), then
// per output row the start of its triangle list (height + 1 entries), then the lists (triangle
// indices in the CPU's bucket order).
@compute @workgroup_size(16, 16)
fn dst_mesh(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = vec2<i32>(gid.xy);
if (!dst_inside(p)) {
return;
}
let q = dst_centre(p);
let rs = P.u[0].x;
let lo = P.u[0].y;
let i0 = u32(data[rs + u32(p.y)]);
let i1 = u32(data[rs + u32(p.y) + 1u]);
var cur = vec4<f32>(0.0);
for (var i = i0; i < i1; i++) {
if (cur.w > 0.0) {
break;
}
let b = u32(data[lo + i]) * 12u;
let ex = q.x - data[b + 4u];
let ey = q.y - data[b + 5u];
let l1 = data[b] * ex + data[b + 1u] * ey;
let l2 = data[b + 2u] * ex + data[b + 3u] * ey;
let l3 = 1.0 - l1 - l2;
// The CPU tests against -1e-9 in f64; f32 rounding needs a wider margin so pixels on
// shared edges are not dropped by both triangles.
let e = -1e-5;
if (l1 < e || l2 < e || l3 < e) {
continue;
}
let sx = data[b + 6u] * l1 + data[b + 8u] * l2 + data[b + 10u] * l3;
let sy = data[b + 7u] * l1 + data[b + 9u] * l2 + data[b + 11u] * l3;
cur = sample_bilinear(src, sx, sy);
}
textureStore(out, p, cur);
}
// ---------------------------------------------------------------- Mosaic (misc::mosaic)
// data: column tile index (w), row tile index (h), tile column bounds (nx + 1), tile row bounds
// (ny + 1); u[0] = (w, h, nx, ny).
// Row sums of each tile's columns: out is nx × h.
@compute @workgroup_size(16, 16)
fn dst_mosaic_rows(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = vec2<i32>(gid.xy);
if (!dst_inside(p)) {
return;
}
let w = P.u[0].x;
let h = P.u[0].y;
let xb = w + h + u32(p.x);
var s = vec4<f32>(0.0);
for (var x = u32(data[xb]); x < u32(data[xb + 1u]); x++) {
s += textureLoad(src, vec2<i32>(i32(x), p.y), 0);
}
textureStore(out, p, s);
}
// Tile averages: out is nx × ny (src = the row sums).
@compute @workgroup_size(16, 16)
fn dst_mosaic_tiles(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = vec2<i32>(gid.xy);
if (!dst_inside(p)) {
return;
}
let w = P.u[0].x;
let h = P.u[0].y;
let xb = w + h + u32(p.x);
let yb = w + h + P.u[0].z + 1u + u32(p.y);
let y0 = u32(data[yb]);
let y1 = u32(data[yb + 1u]);
var s = vec4<f32>(0.0);
for (var y = y0; y < y1; y++) {
s += textureLoad(src, vec2<i32>(p.x, i32(y)), 0);
}
let n = f32((u32(data[xb + 1u]) - u32(data[xb])) * (y1 - y0));
textureStore(out, p, s / max(n, 1.0));
}
// Each pixel takes its tile's average (src = the tile averages).
@compute @workgroup_size(16, 16)
fn dst_mosaic_out(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = vec2<i32>(gid.xy);
if (!dst_inside(p)) {
return;
}
let tx = i32(data[u32(p.x)]);
let ty = i32(data[P.u[0].x + u32(p.y)]);
textureStore(out, p, textureLoad(src, vec2<i32>(tx, ty), 0));
}
// Sharp Colors: data = per-column / per-row source pixel; u[0].x = w.
@compute @workgroup_size(16, 16)
fn dst_mosaic_sharp(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = vec2<i32>(gid.xy);
if (!dst_inside(p)) {
return;
}
let sx = i32(data[u32(p.x)]);
let sy = i32(data[P.u[0].x + u32(p.y)]);
textureStore(out, p, tex_get_clamped(src, sx, sy));
}
// ---------------------------------------------------------------- Find Edges / Emboss (misc)
fn dst_blend_with(orig: vec4<f32>, fx: vec4<f32>, k: f32) -> vec4<f32> {
return fx + (orig - fx) * k;
}
fn dst_lum_at(x: i32, y: i32) -> f32 {
return luminance(tex_get_clamped(src, x, y).xyz);
}
// u[0].x = invert; f[0].x = blend with original.
@compute @workgroup_size(16, 16)
fn dst_findedges(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = vec2<i32>(gid.xy);
if (!dst_inside(p)) {
return;
}
let x = p.x;
let y = p.y;
let gx = dst_lum_at(x + 1, y - 1) + 2.0 * dst_lum_at(x + 1, y) + dst_lum_at(x + 1, y + 1) - dst_lum_at(x - 1, y - 1) - 2.0 * dst_lum_at(x - 1, y)
- dst_lum_at(x - 1, y + 1);
let gy = dst_lum_at(x - 1, y + 1) + 2.0 * dst_lum_at(x, y + 1) + dst_lum_at(x + 1, y + 1) - dst_lum_at(x - 1, y - 1) - 2.0 * dst_lum_at(x, y - 1)
- dst_lum_at(x + 1, y - 1);
let e = min(sqrt(gx * gx + gy * gy), 1.0);
var v = 1.0 - e;
if (P.u[0].x != 0u) {
v = e;
}
let px = textureLoad(src, p, 0);
let a = px.w;
textureStore(out, p, dst_blend_with(px, vec4<f32>(v * a, v * a, v * a, a), P.f[0].x));
}
// f[0] = (dx, dy, contrast, blend with original).
@compute @workgroup_size(16, 16)
fn dst_emboss(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = vec2<i32>(gid.xy);
if (!dst_inside(p)) {
return;
}
let q = dst_centre(p);
let d = P.f[0].xy;
let a = sample_bilinear_clamped(src, q.x + d.x, q.y + d.y);
let c = sample_bilinear_clamped(src, q.x - d.x, q.y - d.y);
let l = luminance(a.xyz) - luminance(c.xyz);
let v = clamp(0.5 + l * P.f[0].z, 0.0, 1.0);
let px = textureLoad(src, p, 0);
let al = px.w;
textureStore(out, p, dst_blend_with(px, vec4<f32>(v * al, v * al, v * al, al), P.f[0].w));
}
+119
View File
@@ -0,0 +1,119 @@
//! Distort / stylize GPU effects vs the CPU effects (the oracle): see `fx_distort.rs`.
use effectcraft_keyframe::Value;
use effectcraft_project::BitDepth;
use effectcraft_render::RenderOpts;
use effectcraft_time::Tick;
use crate::tests::{Scene, check, compare_at, effect_case, n, opts, set};
fn e(v: u32) -> Value {
Value::Enum(v)
}
fn on() -> Value {
Value::Bool(true)
}
#[test]
fn twirl_bulge_ripple_wave_warp() {
effect_case("ec.distort.twirl", &[("angle", n(120.0)), ("radius", n(40.0))]);
effect_case("ec.distort.twirl", &[("angle", n(-400.0)), ("radius", n(70.0)), ("center", Value::Vec2([25.0, 18.0]))]);
effect_case("ec.distort.bulge", &[("hradius", n(30.0)), ("vradius", n(20.0)), ("height", n(1.8))]);
effect_case("ec.distort.bulge", &[("height", n(-2.5)), ("taperRadius", n(60.0)), ("pinAllEdges", on()), ("center", Value::Vec2([30.0, 25.0]))]);
effect_case("ec.distort.ripple", &[("radius", n(80.0)), ("waveWidth", n(9.0)), ("waveHeight", n(4.0))]);
effect_case("ec.distort.ripple", &[("conversion", e(1)), ("phase", n(70.0)), ("speed", n(3.0)), ("center", Value::Vec2([20.0, 30.0]))]);
effect_case("ec.distort.wavewarp", &[("height", n(6.0)), ("width", n(25.0)), ("direction", n(30.0))]);
effect_case("ec.distort.wavewarp", &[("waveType", e(2)), ("pinning", e(1)), ("phase", n(400.0)), ("speed", n(-2.0))]);
effect_case("ec.distort.wavewarp", &[("waveType", e(8)), ("pinning", e(2)), ("height", n(-5.0)), ("direction", n(200.0))]);
effect_case("ec.distort.wavewarp", &[("waveType", e(4)), ("pinning", e(7)), ("width", n(33.0))]);
}
#[test]
fn turbulent_displace_and_cc_lens() {
effect_case("ec.distort.turbulentdisplace", &[("size", n(30.0)), ("amount", n(20.0))]);
effect_case(
"ec.distort.turbulentdisplace",
&[("displacement", e(1)), ("complexity", n(3.5)), ("evolution", n(90.0)), ("size", n(25.0)), ("pinning", e(2)), ("resizeLayer", on())],
);
effect_case(
"ec.distort.turbulentdisplace",
&[
("displacement", e(5)),
("evolutionOptions/cycleEvolution", on()),
("evolutionOptions/cycle", n(2.0)),
("evolution", n(500.0)),
("evolutionOptions/randomSeed", n(7.0)),
("pinning", e(0)),
("amount", n(-30.0)),
],
);
effect_case("ec.distort.turbulentdisplace", &[("displacement", e(8)), ("size", n(15.0)), ("pinning", e(6))]);
effect_case("ec.distort.cclens", &[("size", n(70.0))]);
effect_case("ec.distort.cclens", &[("size", n(45.0)), ("convergence", n(60.0)), ("center", Value::Vec2([30.0, 20.0]))]);
effect_case("ec.distort.cclens", &[("convergence", n(-70.0))]);
}
#[test]
fn mesh_warp() {
// 2 rows × 3 columns: 12 vertices.
let mesh = "0,0 0,0 0,0 0,0 0,0 6,-4 -5,3 0,0 0,0 0,0 0,0 0,0";
effect_case("ec.distort.meshwarp", &[("rows", n(2.0)), ("columns", n(3.0)), ("quality", n(3.0)), ("mesh", Value::Str(mesh.into()))]);
let mesh = "-4,-3 2,0 8,5 0,-2 9,9 1,0 -3,4 2,2 4,-6";
effect_case("ec.distort.meshwarp", &[("rows", n(2.0)), ("columns", n(2.0)), ("quality", n(5.0)), ("mesh", Value::Str(mesh.into()))]);
}
#[test]
fn mosaic_find_edges_emboss() {
effect_case("ec.stylize.mosaic", &[]);
effect_case("ec.stylize.mosaic", &[("horizontal", n(7.0)), ("vertical", n(3.0))]);
effect_case("ec.stylize.mosaic", &[("horizontal", n(13.0)), ("vertical", n(9.0)), ("sharpColors", on())]);
effect_case("ec.stylize.findedges", &[]);
effect_case("ec.stylize.findedges", &[("invert", on()), ("blend", n(30.0))]);
effect_case("ec.stylize.emboss", &[]);
effect_case("ec.stylize.emboss", &[("direction", n(160.0)), ("relief", n(4.0)), ("contrast", n(250.0)), ("blend", n(20.0))]);
}
#[test]
fn displacement_map_self() {
effect_case("ec.distort.displacementmap", &[("maxHorizontal", n(8.0)), ("maxVertical", n(-5.0))]);
effect_case("ec.distort.displacementmap", &[("useForHorizontal", e(4)), ("useForVertical", e(3)), ("wrapPixelsAround", on())]);
effect_case(
"ec.distort.displacementmap",
&[("useForHorizontal", e(10)), ("useForVertical", e(6)), ("maxVertical", n(12.0)), ("expandOutput", Value::Bool(false))],
);
}
/// Displacement Map reading another layer (Center / Stretch / Tile), composited.
#[test]
fn displacement_map_layer() {
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
for (behavior, wrap) in [(0, false), (1, false), (2, true), (2, false)] {
let mut s = Scene::new(depth);
let bg = s.solid([0.15, 0.1, 0.2], 97, 61);
s.push(bg);
let mut map = s.footage(50, 36);
map.switches.video = false;
let mid = s.push(map);
let mut l = s.footage(70, 44);
s.effect(
&mut l,
"ec.distort.displacementmap",
&[
("displacementMapLayer", Value::Layer(Some(mid.0))),
("useForHorizontal", e(0)),
("useForVertical", e(4)),
("maxHorizontal", n(9.0)),
("maxVertical", n(6.0)),
("displacementMapBehavior", e(behavior)),
("wrapPixelsAround", Value::Bool(wrap)),
],
);
set(&mut l, "transform/rotation", Value::Scalar(8.0));
s.push(l);
let label = format!("displacement map {behavior} {wrap} {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);
}
}
}