mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
Merge pull request #235 from storytold/fix/227-turbulent-displace
Effects: Turbulent Displace displaces the whole layer, not one quadrant (#227)
This commit is contained in:
9 files changed
+96
-21
No files matched your search
@@ -6,6 +6,7 @@ use effectcraft_project::ParamUi;
|
||||
use effectcraft_raster::Image;
|
||||
use rayon::prelude::*;
|
||||
|
||||
use crate::util::pin_rect;
|
||||
use crate::{Buf, EffectCtx, EffectSpec, num, p, popup, slider};
|
||||
|
||||
fn spec(id: &'static str, name: &'static str, params: Vec<crate::ParamSpec>, render: crate::RenderFn) -> EffectSpec {
|
||||
@@ -67,8 +68,7 @@ fn bulge(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
// Taper Radius: higher values make the sides of the bulge shallower (a more peaked profile).
|
||||
let taper = 1.0 + ctx.params.f("taperRadius").max(0.0) / 100.0 * 3.0;
|
||||
let pin = ctx.params.b("pinAllEdges");
|
||||
let (lx, ly) = (b.offset[0], b.offset[1]);
|
||||
let (lw, lh) = (b.layer_w(ctx) * b.scale, b.layer_h(ctx) * b.scale);
|
||||
let (lx, ly, lw, lh) = pin_rect(ctx, b.offset, b.scale);
|
||||
let edge = (lw.min(lh) * 0.1).max(1.0);
|
||||
b.img = remap(&b.img, false, |x, y| {
|
||||
let (dx, dy) = ((x - c.0) / rx, (y - c.1) / ry);
|
||||
@@ -168,7 +168,8 @@ fn wave_warp(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
if !ctx.adjustment {
|
||||
b.pad(h.abs().ceil() as u32 + 1);
|
||||
}
|
||||
let rect = (b.offset[0], b.offset[1], (b.layer_w(ctx) * b.scale).max(1.0), (b.layer_h(ctx) * b.scale).max(1.0));
|
||||
let (x0, y0, lw, lh) = pin_rect(ctx, b.offset, b.scale);
|
||||
let rect = (x0, y0, lw.max(1.0), lh.max(1.0));
|
||||
b.img = remap(&b.img, false, |x, y| {
|
||||
// Waves travel along `dir`; displacement is perpendicular to it.
|
||||
let along = x * dx + y * dy;
|
||||
|
||||
@@ -20,7 +20,7 @@ use effectcraft_raster::{Image, Px};
|
||||
use rayon::prelude::*;
|
||||
|
||||
use crate::generate::value_noise;
|
||||
use crate::util::{SRC_NAMES, Src, fit_layer, layer_rect, lerp4, map_xy, pick, premul, remap, smoothstep, src_at, unpremul};
|
||||
use crate::util::{SRC_NAMES, Src, fit_layer, layer_rect, lerp4, map_xy, pick, pin_rect, premul, remap, smoothstep, src_at, unpremul};
|
||||
use crate::{Buf, EffectCtx, EffectSpec, col, num, p, popup, slider};
|
||||
|
||||
fn spec(id: &'static str, name: &'static str, params: Vec<crate::ParamSpec>, render: crate::RenderFn) -> EffectSpec {
|
||||
@@ -103,7 +103,7 @@ fn turbulent_displace(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let locked = pin_raw >= 8;
|
||||
let pin = if locked { pin_raw - 7 } else { pin_raw };
|
||||
let falloff = if (3..=5).contains(&kind) { 0.3 } else { 0.5 };
|
||||
let (lx, ly, lw, lh) = layer_rect(ctx, &b);
|
||||
let (lx, ly, lw, lh) = pin_rect(ctx, b.offset, b.scale);
|
||||
let edge = (lw.min(lh) * if locked { 0.25 } else { 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 n = |u: f64, v: f64, s: u32| {
|
||||
|
||||
@@ -347,6 +347,11 @@ pub struct EffectEnv<'a> {
|
||||
/// comp's and the layer's motion blur switches are on (Transform's Use Composition's
|
||||
/// Shutter Angle).
|
||||
pub shutter: Option<(f64, f64, u32)>,
|
||||
/// Top-left corner of the layer bounds ([`EffectCtx::layer_size`]) in layer coordinates:
|
||||
/// (0, 0) for layers with a source rectangle; shape and text layers have comp-sized bounds
|
||||
/// centred on their origin (−w/2, −h/2). Edge pinning measures from these bounds
|
||||
/// ([`util::pin_rect`]).
|
||||
pub bounds_origin: [f64; 2],
|
||||
}
|
||||
|
||||
/// What an effect gets to render with.
|
||||
|
||||
@@ -427,6 +427,15 @@ pub fn layer_rect(ctx: &crate::EffectCtx, b: &crate::Buf) -> (f64, f64, f64, f64
|
||||
(b.offset[0], b.offset[1], ctx.layer_size[0] * b.scale, ctx.layer_size[1] * b.scale)
|
||||
}
|
||||
|
||||
/// The layer bounds edge pinning holds in place, in the pixels of a buffer with `offset` and
|
||||
/// `scale` (see [`crate::Buf`]): (x0, y0, width, height). Unlike [`layer_rect`] it starts at
|
||||
/// [`crate::EffectEnv::bounds_origin`], so a shape layer's content around its origin lies
|
||||
/// inside it.
|
||||
pub fn pin_rect(ctx: &crate::EffectCtx, offset: [f64; 2], scale: f64) -> (f64, f64, f64, f64) {
|
||||
let o = ctx.env.bounds_origin;
|
||||
(offset[0] + o[0] * scale, offset[1] + o[1] * scale, ctx.layer_size[0] * scale, ctx.layer_size[1] * scale)
|
||||
}
|
||||
|
||||
/// Resample another layer (a layer parameter) into `b`'s pixel grid. With `stretch` the other
|
||||
/// layer is scaled to this layer's size ("Stretch to Fit"); otherwise it is centred.
|
||||
pub fn fit_layer(ctx: &crate::EffectCtx, b: &crate::Buf, other: &crate::LayerPixels, stretch: bool) -> Image {
|
||||
|
||||
@@ -253,6 +253,8 @@ pub(crate) fn chain_key(chain: &[FxStep], buf: &Buf, levels: Option<f32>) -> u12
|
||||
k.debug(&e.working_space);
|
||||
k.u64(e.working_linear as u64);
|
||||
k.debug(&e.shutter);
|
||||
k.f64(e.bounds_origin[0]);
|
||||
k.f64(e.bounds_origin[1]);
|
||||
}
|
||||
k.finish()
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
use std::f64::consts::TAU;
|
||||
|
||||
use effectcraft_effects::EffectCtx;
|
||||
use effectcraft_effects::util::{Src, src_at};
|
||||
use effectcraft_effects::util::{self, Src, src_at};
|
||||
|
||||
use crate::context::{Enc, Params};
|
||||
use crate::effects::GBuf;
|
||||
@@ -77,11 +77,6 @@ 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> {
|
||||
@@ -101,13 +96,12 @@ fn bulge(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
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 (x0, y0, lw, lh) = util::pin_rect(ctx, b.offset, 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];
|
||||
p.f[2] = [x0 as f32, y0 as f32, lw as f32, lh as f32];
|
||||
run(e, "dst_bulge", &p, b, None)
|
||||
}
|
||||
|
||||
@@ -121,8 +115,8 @@ fn wave_warp(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
||||
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];
|
||||
let (x0, y0, lw, lh) = util::pin_rect(ctx, b.offset, b.scale);
|
||||
let rect = [x0 as f32, y0 as f32, lw.max(1.0) as f32, lh.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).
|
||||
@@ -189,8 +183,8 @@ fn turbulent(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
||||
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 (x0, y0, lw, lh) = util::pin_rect(ctx, b.offset, b.scale);
|
||||
let edge = (lw.min(lh) * 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) => {
|
||||
@@ -204,7 +198,7 @@ fn turbulent(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
||||
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[2] = [x0 as f32, y0 as f32, lw as f32, lh as f32];
|
||||
p.f[3] = [edge as f32, falloff, 0.0, 0.0];
|
||||
run(e, "dst_turbulent", &p, b, None)
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
//! Distort / stylize GPU effects vs the CPU effects (the oracle): see `fx_distort.rs`.
|
||||
|
||||
use effectcraft_keyframe::Value;
|
||||
use effectcraft_project::BitDepth;
|
||||
use effectcraft_project::build::{self, Ids};
|
||||
use effectcraft_project::{BitDepth, LayerSource};
|
||||
use effectcraft_render::RenderOpts;
|
||||
use effectcraft_time::Tick;
|
||||
|
||||
@@ -54,6 +55,30 @@ fn turbulent_displace_and_cc_lens() {
|
||||
effect_case("ec.distort.cclens", &[("convergence", n(-70.0))]);
|
||||
}
|
||||
|
||||
/// Edge pinning on a shape layer, whose comp-sized bounds are centred on its origin (#227).
|
||||
#[test]
|
||||
fn edge_pinning_on_a_shape_layer() {
|
||||
let cases: [(&str, &[(&str, Value)]); 3] = [
|
||||
("ec.distort.turbulentdisplace", &[("size", n(30.0)), ("amount", n(20.0))]),
|
||||
("ec.distort.wavewarp", &[("pinning", e(1)), ("height", n(6.0)), ("width", n(25.0))]),
|
||||
("ec.distort.bulge", &[("pinAllEdges", on()), ("height", n(2.0)), ("hradius", n(40.0)), ("vradius", n(30.0)), ("center", Value::Vec2([-20.0, -10.0]))]),
|
||||
];
|
||||
for (id, vals) in cases {
|
||||
let mut s = Scene::new(BitDepth::Bpc32);
|
||||
let mut l = build::layer(&mut s.p, &s.comp, "Shape", LayerSource::Shape, (97, 61), None);
|
||||
let mut next = s.p.next_id;
|
||||
let mut ids = Ids(&mut next);
|
||||
let rect = build::shape_rect(&mut ids, [90.0, 54.0], [0.0, 0.0], 0.0);
|
||||
let fill = build::shape_fill(&mut ids, [0.9, 0.3, 0.1, 1.0]);
|
||||
let g = build::shape_group(&mut ids, "Rectangle 1", vec![rect, fill]);
|
||||
s.p.next_id = next;
|
||||
l.props.sub_mut("contents").unwrap().children.push(g.into());
|
||||
s.effect(&mut l, id, vals);
|
||||
s.push(l);
|
||||
check(&format!("{id} on a shape layer"), compare_at(&s, opts(), Tick::ZERO), 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mesh_warp() {
|
||||
// 2 rows × 3 columns: 12 vertices.
|
||||
|
||||
@@ -895,7 +895,14 @@ impl<'a> Renderer<'a> {
|
||||
let Some(fx) = layer.effects() else { return };
|
||||
let lt = layer.layer_time(ctx.time);
|
||||
let size = source_size(self.project, layer);
|
||||
let layer_size = if size.0 == 0 { [ctx.comp.width as f64, ctx.comp.height as f64] } else { [size.0 as f64, size.1 as f64] };
|
||||
// Layers without a source rectangle (shape, text) have comp-sized bounds centred on
|
||||
// their origin, which their content surrounds.
|
||||
let (layer_size, bounds_origin) = if size.0 == 0 {
|
||||
let (w, h) = (ctx.comp.width as f64, ctx.comp.height as f64);
|
||||
([w, h], [-w / 2.0, -h / 2.0])
|
||||
} else {
|
||||
([size.0 as f64, size.1 as f64], [0.0; 2])
|
||||
};
|
||||
let mask_shapes = masks::shapes(ctx, layer);
|
||||
let host = FxHost { r: self, ctx, layer, index: Default::default() };
|
||||
let env = EffectEnv {
|
||||
@@ -907,6 +914,7 @@ impl<'a> Renderer<'a> {
|
||||
working_space: self.pipe.space,
|
||||
working_linear: self.pipe.linear,
|
||||
shutter: self.mb_on(ctx, layer).then_some((ctx.comp.shutter_angle, ctx.comp.shutter_phase, ctx.comp.motion_blur_samples)),
|
||||
bounds_origin,
|
||||
};
|
||||
// Video effects in stack order (index, group, spec); disabled and audio effects skipped.
|
||||
let stack: Vec<(usize, &effectcraft_project::PropGroup, &'static effectcraft_effects::EffectSpec)> = fx
|
||||
|
||||
@@ -425,3 +425,34 @@ fn auto_backend_sends_2d_and_3d_comps_to_the_gpu() {
|
||||
p.settings.gpu_acceleration = false;
|
||||
assert!(!asked(&p, crate::Backend::Auto));
|
||||
}
|
||||
|
||||
/// #227: a shape layer's content surrounds its origin, so edge pinning (Turbulent Displace's
|
||||
/// default Pin All, Wave Warp's All Edges) must measure from comp-sized bounds centred on it;
|
||||
/// bounds starting at the origin pinned all but the bottom-right quadrant.
|
||||
#[test]
|
||||
fn edge_pinning_displaces_all_of_a_centred_shape_layer() {
|
||||
for (id, vals) in [("ec.distort.turbulentdisplace", vec![]), ("ec.distort.wavewarp", vec![("pinning", Value::Enum(1))])] {
|
||||
let (mut p, cid, comp) = setup();
|
||||
let mut l = build::layer(&mut p, &comp, "Shape Layer 1", LayerSource::Shape, (200, 100), None);
|
||||
let mut next = p.next_id;
|
||||
let mut ids = Ids(&mut next);
|
||||
let rect = build::shape_rect(&mut ids, [180.0, 80.0], [0.0, 0.0], 0.0);
|
||||
let fill = build::shape_fill(&mut ids, [1.0, 0.0, 0.0, 1.0]);
|
||||
let g = build::shape_group(&mut ids, "Rectangle 1", vec![rect, fill]);
|
||||
p.next_id = next;
|
||||
l.props.sub_mut("contents").unwrap().children.push(g.into());
|
||||
p.comp_mut(cid).unwrap().layers.push(l.clone());
|
||||
let plain = render_frame(&p, cid, Tick::ZERO, 1.0);
|
||||
add_effect_200(&mut p, &mut l, id, &vals);
|
||||
p.comp_mut(cid).unwrap().layers = vec![l];
|
||||
let warped = render_frame(&p, cid, Tick::ZERO, 1.0);
|
||||
// Alpha change per comp quadrant (TL, TR, BL, BR): the rectangle's edges move in each.
|
||||
let mut q = [0.0f32; 4];
|
||||
for y in 0..100 {
|
||||
for x in 0..200 {
|
||||
q[(x >= 100) as usize + 2 * (y >= 50) as usize] += (plain.get(x, y)[3] - warped.get(x, y)[3]).abs();
|
||||
}
|
||||
}
|
||||
assert!(q.iter().all(|&d| d > 20.0), "{id}: per-quadrant change {q:?}");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user