mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
M13.22: GPU time effects (Time Difference, Time Displacement, CC Force Motion Blur, CC Wide Time, Pixel Motion Blur, Timewarp's frame building; motion vectors and frame fetches on the CPU)
Timewarp without a Matte Layer is now planned once (time_fx::timewarp_plan: frames, shutter instants, motion fields) and built by the CPU or the GPU from the plan; with a Matte Layer it renders on the CPU (catalog::gpu_supported). Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
d0b92948ee
commit
a909de43b7
10 files changed
+662
-29
No files matched your search
@@ -94,12 +94,14 @@ const CPU_ONLY_CONTROLS: &[(&str, &[&str])] = &[
|
||||
];
|
||||
|
||||
/// Whether the GPU kernel of effect `id` covers the instance's current settings (see
|
||||
/// [`CPU_ONLY_CONTROLS`]; plus Cell Pattern's HQ variants and Turbulent Displace's locked
|
||||
/// pinning).
|
||||
/// [`CPU_ONLY_CONTROLS`]; plus Cell Pattern's HQ variants, Turbulent Displace's locked
|
||||
/// pinning, Warp's strong Fisheye / Twist and Timewarp with a Matte Layer).
|
||||
pub fn gpu_supported(id: &str, ctx: &crate::EffectCtx) -> bool {
|
||||
match id {
|
||||
"ec.generate.cellpattern" if (6..=10).contains(&ctx.params.e("cellPattern")) => return false,
|
||||
"ec.distort.turbulentdisplace" if ctx.params.e("pinning") >= 8 => return false,
|
||||
// Timewarp's Matte Layer (foreground and background on their own vectors).
|
||||
"ec.time.timewarp" if ctx.params.get("matteLayer").and_then(effectcraft_keyframe::Value::as_layer).is_some() => return false,
|
||||
// Fisheye / Twist (a crease at the unit circle) bent past 50 % or with Horizontal /
|
||||
// Vertical Distortion: Newton's inverse wanders chaotically before converging near the
|
||||
// fold, so f32 lands on other pixels than the CPU's f64.
|
||||
|
||||
@@ -95,7 +95,7 @@ pub use transition::place_layer;
|
||||
pub use noise::remove_grain_level;
|
||||
pub use noise3::{FractalGpu, fractal_gpu};
|
||||
pub use textfx::{TextLayer, text_layer};
|
||||
pub use time_fx::{posterized_time, time_frames};
|
||||
pub use time_fx::{PixelMotion, TimewarpPlan, TwStep, posterized_time, time_frames, timewarp_crop, timewarp_plan};
|
||||
// effectcraft-gpu fx_tone.
|
||||
pub use ocio::color_stabilizer_maps;
|
||||
|
||||
@@ -959,6 +959,13 @@ pub const GPU_EFFECTS: &[&str] = &[
|
||||
// effectcraft-gpu fx_text (glyph coverage rasterised on the CPU)
|
||||
"ec.text.numbers",
|
||||
"ec.text.timecode",
|
||||
// effectcraft-gpu fx_time (frames fetched by the host, combined on the GPU)
|
||||
"ec.time.timedifference",
|
||||
"ec.time.timedisplacement",
|
||||
"ec.time.ccforcemotionblur",
|
||||
"ec.time.ccwidetime",
|
||||
"ec.time.pixelmotionblur",
|
||||
"ec.time.timewarp",
|
||||
];
|
||||
|
||||
/// Effects whose output depends on [`EffectCtx::time`] directly (not only through animated
|
||||
|
||||
+122
-19
@@ -680,23 +680,56 @@ fn fetch_layer(ctx: &EffectCtx, id: &str, times: &[f64], grid: &Buf) -> Option<V
|
||||
)
|
||||
}
|
||||
|
||||
/// Timewarp. Whole Frames shows the nearest source frame, Frame Mix cross-fades the two
|
||||
/// neighbouring frames and Pixel Motion builds the in-between frame along estimated motion
|
||||
/// vectors (block-matching optical flow, [`effectcraft_raster::flow`]) tuned by the Tuning
|
||||
/// group. A Matte Layer splits foreground and background so each moves on its own vectors; a
|
||||
/// Warp Layer supplies the motion instead of the layer itself. Motion Blur averages Shutter
|
||||
/// Samples instants across the shutter.
|
||||
fn timewarp(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
if ctx.env.host.is_none() {
|
||||
return b;
|
||||
/// One output instant of a Timewarp without a Matte Layer: a fetched frame, a cross-fade of two,
|
||||
/// or the in-between frame built along a motion field ([`TimewarpPlan::flows`]).
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum TwStep {
|
||||
Whole(usize),
|
||||
Mix(usize, usize, f32),
|
||||
Motion { a: usize, b: usize, w: f32, flow: usize },
|
||||
}
|
||||
|
||||
/// Timewarp without a Matte Layer, prepared for the GPU compositor (effectcraft-gpu `fx_time`):
|
||||
/// the fetched frames on one grid, the instants across the shutter (with their weights) and the
|
||||
/// motion fields (block matching on the CPU). [`TimewarpPlan::render_cpu`] is the CPU effect.
|
||||
pub struct TimewarpPlan {
|
||||
pub grid: Buf,
|
||||
pub frames: Vec<Image>,
|
||||
pub steps: Vec<(TwStep, f32)>,
|
||||
pub flows: Vec<Flow>,
|
||||
pub pm: PixelMotion,
|
||||
}
|
||||
|
||||
impl TimewarpPlan {
|
||||
pub fn render_cpu(&self) -> Buf {
|
||||
let f = &self.frames;
|
||||
let imgs: Vec<Image> = self
|
||||
.steps
|
||||
.iter()
|
||||
.map(|(s, _)| match *s {
|
||||
TwStep::Whole(i) => f[i].clone(),
|
||||
TwStep::Mix(i, j, w) => effectcraft_raster::flow::mix(&f[i], &f[j], w),
|
||||
TwStep::Motion { a, b, w, flow } => timewarp_interpolate(&f[a], &f[b], w, &self.flows[flow], &self.pm),
|
||||
})
|
||||
.collect();
|
||||
let weights: Vec<f32> = self.steps.iter().map(|(_, w)| *w).collect();
|
||||
let img = if imgs.len() == 1 { imgs.into_iter().next().unwrap_or_default() } else { average(&imgs, &weights) };
|
||||
with_img(self.grid.clone(), img)
|
||||
}
|
||||
}
|
||||
|
||||
/// Source-crop the edges of a frame (Pixel Motion works on cropped frames).
|
||||
pub fn timewarp_crop(img: &Image, pm: &PixelMotion) -> Image {
|
||||
crop_edges(img, pm.crops)
|
||||
}
|
||||
|
||||
/// The source instants Timewarp shows at the frame (with their weights) and the layer times
|
||||
/// they need: (instants, frame times, frames per second).
|
||||
fn timewarp_instants(ctx: &EffectCtx) -> (Vec<(f64, f32)>, Vec<f64>, f64) {
|
||||
let fps = ctx.fps();
|
||||
let by_frame = ctx.params.e("adjustTimeBy") == 1;
|
||||
let speed = ctx.params.f("speed");
|
||||
let src = timewarp_source_time(ctx.time, by_frame, speed, ctx.params.f("sourceFrame"), fps);
|
||||
let method = ctx.params.e("method");
|
||||
let one = method > 0 && ctx.params.b("tuning/buildFromOneImage");
|
||||
// Instants of source time (with weights) across the shutter.
|
||||
let mut instants: Vec<(f64, f32)> = vec![];
|
||||
if ctx.params.b("motionBlur/enableMotionBlur") {
|
||||
let angle = if ctx.params.e("motionBlur/shutterControl") == 1 { ctx.params.f("motionBlur/shutterAngle") } else { 180.0 };
|
||||
@@ -709,18 +742,88 @@ fn timewarp(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
} else {
|
||||
instants.push((src, 1.0));
|
||||
}
|
||||
// Each instant needs the frame at or before it and the next one.
|
||||
let split = |s: f64| {
|
||||
let f = s * fps;
|
||||
let f0 = (f + 1e-6).floor();
|
||||
(f0, ((f - f0) as f32).max(0.0))
|
||||
};
|
||||
let mut times: Vec<f64> = vec![];
|
||||
for &(s, _) in &instants {
|
||||
let (f0, _) = split(s);
|
||||
let (f0, _) = tw_split(s, fps);
|
||||
times.push(f0 / fps);
|
||||
times.push((f0 + 1.0) / fps);
|
||||
}
|
||||
(instants, times, fps)
|
||||
}
|
||||
|
||||
/// The frame at or before source time `s` and the fraction to the next one.
|
||||
fn tw_split(s: f64, fps: f64) -> (f64, f32) {
|
||||
let f = s * fps;
|
||||
let f0 = (f + 1e-6).floor();
|
||||
(f0, ((f - f0) as f32).max(0.0))
|
||||
}
|
||||
|
||||
/// [`TimewarpPlan`] of a Timewarp instance without a Matte Layer. `None` without the effect host
|
||||
/// (the layer passes through) or with a Matte Layer.
|
||||
pub fn timewarp_plan(ctx: &EffectCtx) -> Option<TimewarpPlan> {
|
||||
ctx.env.host?;
|
||||
if ctx.params.get("matteLayer").and_then(Value::as_layer).is_some() {
|
||||
return None;
|
||||
}
|
||||
let (instants, times, fps) = timewarp_instants(ctx);
|
||||
let method = ctx.params.e("method");
|
||||
let one = method > 0 && ctx.params.b("tuning/buildFromOneImage");
|
||||
let fr = fetch(ctx, ×)?;
|
||||
let pm = PixelMotion::from(ctx, fr.grid.scale);
|
||||
let warps = fetch_layer(ctx, "warpLayer", ×, &fr.grid);
|
||||
let idx = |t: f64| times.iter().position(|q| (q - t).abs() < 1e-9).unwrap_or(0);
|
||||
let mut flows: Vec<(i64, Flow)> = vec![];
|
||||
let mut steps = vec![];
|
||||
for &(s, wgt) in &instants {
|
||||
let (f0, frac) = tw_split(s, fps);
|
||||
let (i0, i1) = (idx(f0 / fps), idx((f0 + 1.0) / fps));
|
||||
let step = if method == 0 || frac < 1e-4 || (one && method == 1) {
|
||||
let pick = if method == 0 || frac < 1e-4 { 0.0 } else { frac.round() };
|
||||
TwStep::Whole(if pick < 0.5 { i0 } else { i1 })
|
||||
} else if method == 1 {
|
||||
TwStep::Mix(i0, i1, frac)
|
||||
} else {
|
||||
let key = f0 as i64;
|
||||
let flow = match flows.iter().position(|(k, _)| *k == key) {
|
||||
Some(k) => k,
|
||||
None => {
|
||||
let f = match &warps {
|
||||
Some(w) => timewarp_flow(&w[i0], &w[i1], &pm),
|
||||
None => timewarp_flow(&fr.imgs[i0], &fr.imgs[i1], &pm),
|
||||
};
|
||||
flows.push((key, f));
|
||||
flows.len() - 1
|
||||
}
|
||||
};
|
||||
TwStep::Motion { a: i0, b: i1, w: frac, flow }
|
||||
};
|
||||
steps.push((step, wgt));
|
||||
}
|
||||
Some(TimewarpPlan { grid: fr.grid, frames: fr.imgs, steps, flows: flows.into_iter().map(|(_, f)| f).collect(), pm })
|
||||
}
|
||||
|
||||
/// Timewarp. Whole Frames shows the nearest source frame, Frame Mix cross-fades the two
|
||||
/// neighbouring frames and Pixel Motion builds the in-between frame along estimated motion
|
||||
/// vectors (block-matching optical flow, [`effectcraft_raster::flow`]) tuned by the Tuning
|
||||
/// group. A Matte Layer splits foreground and background so each moves on its own vectors; a
|
||||
/// Warp Layer supplies the motion instead of the layer itself. Motion Blur averages Shutter
|
||||
/// Samples instants across the shutter.
|
||||
fn timewarp(ctx: &EffectCtx, b: Buf) -> Buf {
|
||||
if ctx.env.host.is_none() {
|
||||
return b;
|
||||
}
|
||||
if ctx.params.get("matteLayer").and_then(Value::as_layer).is_none() {
|
||||
return match timewarp_plan(ctx) {
|
||||
Some(plan) => plan.render_cpu(),
|
||||
None => b,
|
||||
};
|
||||
}
|
||||
// Instants of source time (with weights) across the shutter; each needs the frame at or
|
||||
// before it and the next one.
|
||||
let (instants, times, fps) = timewarp_instants(ctx);
|
||||
let split = |s: f64| tw_split(s, fps);
|
||||
let method = ctx.params.e("method");
|
||||
let one = method > 0 && ctx.params.b("tuning/buildFromOneImage");
|
||||
let Some(fr) = fetch(ctx, ×) else { return b };
|
||||
let grid = fr.grid.clone();
|
||||
let frame = |t: f64| -> &Image {
|
||||
|
||||
@@ -317,6 +317,7 @@ impl GpuContext {
|
||||
include_str!("shaders/fx_light.wgsl"),
|
||||
include_str!("shaders/fx_transition.wgsl"),
|
||||
include_str!("shaders/fx_text.wgsl"),
|
||||
include_str!("shaders/fx_time.wgsl"),
|
||||
]
|
||||
.concat();
|
||||
let module =
|
||||
@@ -379,6 +380,7 @@ impl GpuContext {
|
||||
.chain(crate::fx_light::KERNELS)
|
||||
.chain(crate::fx_transition::KERNELS)
|
||||
.chain(crate::fx_text::KERNELS)
|
||||
.chain(crate::fx_time::KERNELS)
|
||||
.map(|e| (e, &layout))
|
||||
.chain(EXT_ENTRIES.iter().map(|e| (e, &layout_ext)))
|
||||
.map(|(e, layout)| {
|
||||
|
||||
@@ -136,6 +136,7 @@ fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
_ if crate::fx_light::IDS.contains(&id) => crate::fx_light::apply(e, id, ctx, b),
|
||||
_ if crate::fx_transition::IDS.contains(&id) => crate::fx_transition::apply(e, id, ctx, b),
|
||||
_ if crate::fx_text::IDS.contains(&id) => crate::fx_text::apply(e, id, ctx, b),
|
||||
_ if crate::fx_time::IDS.contains(&id) => crate::fx_time::apply(e, id, ctx, b),
|
||||
_ => pointwise(e, id, ctx, b),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,223 @@
|
||||
//! GPU effects, time (kernels in `shaders/fx_time.wgsl`, every entry point prefixed `ftm_`):
|
||||
//! Time Difference, Time Displacement, CC Force Motion Blur, CC Wide Time, Pixel Motion Blur and
|
||||
//! Timewarp. Like Echo (`fx_noise`), the other frames come from the effect host on the CPU
|
||||
//! (`effectcraft_effects::time_frames`: fetched at the CPU effect's layer times and placed on
|
||||
//! one grid) and are uploaded; the kernels combine them with the CPU effect's arithmetic in its
|
||||
//! order (weighted sums one frame per pass, so the float rounding matches).
|
||||
//!
|
||||
//! Time Displacement's per-pixel time (the map's luminance, quantised to the time resolution)
|
||||
//! is computed on the CPU, since it decides which frames to fetch; the GPU gathers each pixel
|
||||
//! from its frame. Timewarp's motion vectors (block matching and smoothing,
|
||||
//! `effectcraft_raster::flow`) are estimated on the CPU (`effectcraft_effects::timewarp_plan`);
|
||||
//! the frame building (Whole Frames, Frame Mix, Pixel Motion's warp along the vectors with its
|
||||
//! error fallback, the shutter samples' average) runs on the GPU. Timewarp with a Matte Layer
|
||||
//! renders on the CPU (`catalog::gpu_supported`).
|
||||
|
||||
use effectcraft_effects::util::{fit_layer, unpremul};
|
||||
use effectcraft_effects::{Buf, EffectCtx, TwStep};
|
||||
use effectcraft_keyframe::Value;
|
||||
|
||||
use crate::context::{Enc, GpuImage, Params};
|
||||
use crate::effects::GBuf;
|
||||
|
||||
/// Compute entry points in `fx_time.wgsl`.
|
||||
pub(crate) const KERNELS: &[&str] = &["ftm_acc", "ftm_mix", "ftm_difference", "ftm_displace", "ftm_motion"];
|
||||
|
||||
/// Effect ids implemented here.
|
||||
pub(crate) const IDS: &[&str] =
|
||||
&["ec.time.timedifference", "ec.time.timedisplacement", "ec.time.ccforcemotionblur", "ec.time.ccwidetime", "ec.time.pixelmotionblur", "ec.time.timewarp"];
|
||||
|
||||
/// 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> {
|
||||
match id {
|
||||
"ec.time.timedifference" => time_difference(e, ctx, b),
|
||||
"ec.time.timedisplacement" => time_displacement(e, ctx, b),
|
||||
"ec.time.ccforcemotionblur" => {
|
||||
let n = ctx.params.f("motionBlurLevels").round().clamp(2.0, 64.0) as usize;
|
||||
let angle = if ctx.params.b("overrideShutterAngle") { ctx.params.f("shutterAngle") } else { 180.0 };
|
||||
let span = angle.clamp(0.0, 3600.0) / 360.0 / ctx.fps();
|
||||
mean_of(e, ctx, b, &(0..n).map(|i| ctx.time + span * i as f64 / n as f64).collect::<Vec<_>>())
|
||||
}
|
||||
"ec.time.ccwidetime" => {
|
||||
let fwd = ctx.params.f("forwardSteps").round().clamp(0.0, 45.0) as i64;
|
||||
let back = ctx.params.f("backwardSteps").round().clamp(0.0, 45.0) as i64;
|
||||
let fd = 1.0 / ctx.fps();
|
||||
mean_of(e, ctx, b, &(-back..=fwd).map(|k| ctx.time + k as f64 * fd).collect::<Vec<_>>())
|
||||
}
|
||||
"ec.time.pixelmotionblur" => {
|
||||
let angle = if ctx.params.e("shutterControl") == 1 { ctx.params.f("shutterAngle") } else { 180.0 };
|
||||
let n = ctx.params.f("shutterSamples").round().clamp(1.0, 64.0) as usize;
|
||||
let span = angle.clamp(0.0, 720.0) / 360.0 / ctx.fps();
|
||||
let times: Vec<f64> = (0..n).map(|i| ctx.time - span * 0.5 + if n > 1 { span * i as f64 / (n - 1) as f64 } else { span * 0.5 }).collect();
|
||||
mean_of(e, ctx, b, ×)
|
||||
}
|
||||
"ec.time.timewarp" => timewarp(e, ctx, b),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn kernel(e: &mut Enc, entry: &str, p: &Params, src: &GpuImage, aux: Option<&GpuImage>, data: Option<&wgpu::Buffer>) -> GpuImage {
|
||||
let out = e.scratch(src.width, src.height);
|
||||
e.pixels(entry, p, src, aux, &out, data);
|
||||
out
|
||||
}
|
||||
|
||||
/// time_fx::average: Σ frame × weight, one frame per pass in the CPU's order.
|
||||
fn average(e: &mut Enc, frames: &[GpuImage], weights: &[f32]) -> Option<GpuImage> {
|
||||
let first = frames.first()?;
|
||||
let mut acc = e.zeros(first.width, first.height);
|
||||
for (f, &k) in frames.iter().zip(weights) {
|
||||
let mut p = Params::default();
|
||||
p.f[0][0] = k;
|
||||
acc = kernel(e, "ftm_acc", &p, &acc, Some(f), None);
|
||||
}
|
||||
Some(acc)
|
||||
}
|
||||
|
||||
fn on_grid(img: GpuImage, grid: &Buf) -> GBuf {
|
||||
GBuf { img, offset: grid.offset, scale: grid.scale }
|
||||
}
|
||||
|
||||
/// time_fx::mean_of: the plain average of the layer at `times`.
|
||||
fn mean_of(e: &mut Enc, ctx: &EffectCtx, b: GBuf, times: &[f64]) -> Option<GBuf> {
|
||||
if times.is_empty() {
|
||||
return Some(b);
|
||||
}
|
||||
let Some((grid, imgs)) = effectcraft_effects::time_frames(ctx, times) else { return Some(b) };
|
||||
let frames = imgs.iter().map(|i| e.g.upload_image(i)).collect::<Option<Vec<_>>>()?;
|
||||
let k = 1.0 / times.len() as f32;
|
||||
let img = average(e, &frames, &vec![k; times.len()])?;
|
||||
Some(on_grid(img, &grid))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Time Difference
|
||||
|
||||
fn time_difference(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
let Some(host) = ctx.env.host else { return Some(b) };
|
||||
let pr = ctx.params;
|
||||
let off = pr.f("timeOffset");
|
||||
let target_id = pr.get("targetLayer").and_then(Value::as_layer);
|
||||
let (grid, cur, target) = match target_id.and_then(|id| host.layer_at(id, ctx.env.comp_time + off, false)) {
|
||||
Some(other) => {
|
||||
let Some((grid, mut imgs)) = effectcraft_effects::time_frames(ctx, &[ctx.time]) else { return Some(b) };
|
||||
let cur = imgs.pop().unwrap_or_default();
|
||||
let tgt = fit_layer(ctx, &Buf { img: cur.clone(), ..grid.clone() }, &other, true);
|
||||
(grid, cur, tgt)
|
||||
}
|
||||
None => {
|
||||
let Some((grid, mut imgs)) = effectcraft_effects::time_frames(ctx, &[ctx.time, ctx.time + off]) else { return Some(b) };
|
||||
let tgt = imgs.pop().unwrap_or_default();
|
||||
let cur = imgs.pop().unwrap_or_default();
|
||||
(grid, cur, tgt)
|
||||
}
|
||||
};
|
||||
let (cur, target) = (e.g.upload_image(&cur)?, e.g.upload_image(&target)?);
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [pr.b("absoluteDifference") as u32, pr.e("alphaChannel"), 0, 0];
|
||||
p.f[0][0] = 1.0 + pr.f("contrast") as f32 / 25.0;
|
||||
let img = kernel(e, "ftm_difference", &p, &cur, Some(&target), None);
|
||||
Some(on_grid(img, &grid))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Time Displacement
|
||||
|
||||
fn time_displacement(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
if ctx.env.host.is_none() {
|
||||
return Some(b);
|
||||
}
|
||||
let pr = ctx.params;
|
||||
let max = pr.f("maxDisplacementTime");
|
||||
let res = pr.f("timeResolution").clamp(1.0, 999.0);
|
||||
let Some((grid, mut imgs)) = effectcraft_effects::time_frames(ctx, &[ctx.time]) else { return Some(b) };
|
||||
let grid = Buf { img: imgs.pop().unwrap_or_default(), ..grid };
|
||||
let map = match ctx.layer_param("displacementMapLayer", true) {
|
||||
Some(o) => fit_layer(ctx, &grid, &o, pr.b("stretchMap")),
|
||||
None => grid.img.clone(),
|
||||
};
|
||||
// The CPU effect's per-pixel time step (luminance → −max..+max, quantised).
|
||||
let span = 2.0 * max.abs() * res;
|
||||
let step = if span > 63.0 { 2.0 * max.abs() / 63.0 } else { 1.0 / res };
|
||||
let idx: Vec<i32> = map
|
||||
.data
|
||||
.iter()
|
||||
.map(|p| {
|
||||
let (c, a) = unpremul(*p);
|
||||
let l = if a > 0.0 { effectcraft_color::luminance(c[0], c[1], c[2]) } else { 0.5 };
|
||||
(((l as f64 - 0.5) * 2.0 * max) / step).round() as i32
|
||||
})
|
||||
.collect();
|
||||
let mut keys = idx.clone();
|
||||
keys.sort_unstable();
|
||||
keys.dedup();
|
||||
if keys == [0] {
|
||||
return Some(on_grid(e.g.upload_image(&grid.img)?, &grid));
|
||||
}
|
||||
let times: Vec<f64> = keys.iter().map(|k| ctx.time + *k as f64 * step).collect();
|
||||
let Some((fgrid, frames)) = effectcraft_effects::time_frames(ctx, ×) else { return Some(on_grid(e.g.upload_image(&grid.img)?, &grid)) };
|
||||
let dx = (fgrid.offset[0] - grid.offset[0]).round() as i32;
|
||||
let dy = (fgrid.offset[1] - grid.offset[1]).round() as i32;
|
||||
let which: Vec<u8> = idx.iter().flat_map(|k| (keys.binary_search(k).unwrap_or(0) as u32).to_le_bytes()).collect();
|
||||
let data = e.bytes(which);
|
||||
let (w, h) = (grid.img.width, grid.img.height);
|
||||
let mut acc = e.zeros(w, h);
|
||||
for (fi, f) in frames.iter().enumerate() {
|
||||
let frame = e.g.upload_image(f)?;
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [fi as u32, w, dx as u32, dy as u32];
|
||||
let out = e.scratch(w, h);
|
||||
e.pixels("ftm_displace", &p, &acc, Some(&frame), &out, Some(&data));
|
||||
acc = out;
|
||||
}
|
||||
Some(on_grid(acc, &grid))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Timewarp
|
||||
|
||||
fn timewarp(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
if ctx.env.host.is_none() {
|
||||
return Some(b);
|
||||
}
|
||||
// A Matte Layer splits foreground and background: the CPU's path.
|
||||
if ctx.params.get("matteLayer").and_then(Value::as_layer).is_some() {
|
||||
return None;
|
||||
}
|
||||
let Some(plan) = effectcraft_effects::timewarp_plan(ctx) else { return Some(b) };
|
||||
let mut up: Vec<Option<GpuImage>> = vec![None; plan.frames.len()];
|
||||
let mut frame = |e: &mut Enc, i: usize| -> Option<GpuImage> {
|
||||
if up[i].is_none() {
|
||||
up[i] = Some(e.g.upload_image(&plan.frames[i])?);
|
||||
}
|
||||
up[i].clone()
|
||||
};
|
||||
let mut imgs = vec![];
|
||||
for (step, _) in &plan.steps {
|
||||
let img = match *step {
|
||||
TwStep::Whole(i) => frame(e, i)?,
|
||||
TwStep::Mix(i, j, w) => {
|
||||
let (a, bb) = (frame(e, i)?, frame(e, j)?);
|
||||
let mut p = Params::default();
|
||||
p.f[0][0] = w;
|
||||
kernel(e, "ftm_mix", &p, &a, Some(&bb), None)
|
||||
}
|
||||
TwStep::Motion { a, b: bi, w, flow } => {
|
||||
let pa = e.g.upload_image(&effectcraft_effects::timewarp_crop(&plan.frames[a], &plan.pm))?;
|
||||
let pb = e.g.upload_image(&effectcraft_effects::timewarp_crop(&plan.frames[bi], &plan.pm))?;
|
||||
let f = &plan.flows[flow];
|
||||
let v: Vec<f32> = f.v.iter().flat_map(|q| [q[0], q[1]]).collect();
|
||||
let data = e.data(&v);
|
||||
let mut p = Params::default();
|
||||
p.u[0] = [if f.v.is_empty() { 0 } else { f.cols as u32 }, f.rows as u32, plan.pm.extreme as u32, plan.pm.build_from_one as u32];
|
||||
p.f[0] = [f.block as f32, w, plan.pm.error_threshold, 0.0];
|
||||
kernel(e, "ftm_motion", &p, &pa, Some(&pb), Some(&data))
|
||||
}
|
||||
};
|
||||
imgs.push(img);
|
||||
}
|
||||
let img = if imgs.len() == 1 {
|
||||
imgs.pop()?
|
||||
} else {
|
||||
let weights: Vec<f32> = plan.steps.iter().map(|(_, w)| *w).collect();
|
||||
average(e, &imgs, &weights)?
|
||||
};
|
||||
Some(on_grid(img, &plan.grid))
|
||||
}
|
||||
@@ -56,6 +56,7 @@ mod fx_light;
|
||||
mod fx_noise;
|
||||
mod fx_stylize;
|
||||
mod fx_text;
|
||||
mod fx_time;
|
||||
mod fx_tone;
|
||||
mod fx_transition;
|
||||
mod fx_warp;
|
||||
@@ -220,6 +221,8 @@ mod tests_fx_stylize;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_text;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_time;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_tone;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_transition;
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
// GPU effects, time: see src/fx_time.rs. The frames come from the effect host (fetched and
|
||||
// placed on one grid on the CPU, as Echo's); these kernels combine them as the CPU effects do.
|
||||
|
||||
fn ftm_inside(p: vec2<i32>) -> bool {
|
||||
let d = out_dims();
|
||||
return p.x < d.x && p.y < d.y;
|
||||
}
|
||||
|
||||
// util::unpremul: (straight colour, alpha).
|
||||
fn ftm_unpremul(p: vec4<f32>) -> vec4<f32> {
|
||||
if (p.w > 1e-6) {
|
||||
return vec4<f32>(p.xyz / p.w, p.w);
|
||||
}
|
||||
return vec4<f32>(0.0, 0.0, 0.0, max(p.w, 0.0));
|
||||
}
|
||||
|
||||
// Weighted sum, one frame per pass (time_fx::average): src + aux × f[0].x.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn ftm_acc(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!ftm_inside(p)) {
|
||||
return;
|
||||
}
|
||||
textureStore(out, p, textureLoad(src, p, 0) + textureLoad(aux, p, 0) * P.f[0].x);
|
||||
}
|
||||
|
||||
// Cross-fade (raster::flow::mix): src + (aux − src) × f[0].x.
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn ftm_mix(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!ftm_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let a = textureLoad(src, p, 0);
|
||||
textureStore(out, p, a + (textureLoad(aux, p, 0) - a) * P.f[0].x);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Time Difference (time_fx::time_difference)
|
||||
// src = the layer now, aux = the target. u[0] = (absolute difference, alpha channel);
|
||||
// f[0].x = contrast gain.
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn ftm_difference(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!ftm_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let cu = ftm_unpremul(textureLoad(src, p, 0));
|
||||
let tu = ftm_unpremul(textureLoad(aux, p, 0));
|
||||
let c = cu.xyz;
|
||||
let d = tu.xyz;
|
||||
let ca = cu.w;
|
||||
let da = tu.w;
|
||||
let gain = P.f[0].x;
|
||||
let diff = c - d;
|
||||
var rgb = clamp(0.5 + diff * 0.5 * gain, vec3<f32>(0.0), vec3<f32>(1.0));
|
||||
if (P.u[0].x != 0u) {
|
||||
rgb = clamp(abs(diff) * gain, vec3<f32>(0.0), vec3<f32>(1.0));
|
||||
}
|
||||
let mx = max(max(rgb.x, rgb.y), rgb.z);
|
||||
let light = (mx + min(min(rgb.x, rgb.y), rgb.z)) * 0.5;
|
||||
var a: f32;
|
||||
switch P.u[0].y {
|
||||
case 1u: {
|
||||
a = da;
|
||||
}
|
||||
case 2u: {
|
||||
a = (ca + da) * 0.5;
|
||||
}
|
||||
case 3u: {
|
||||
a = max(ca, da);
|
||||
}
|
||||
case 4u: {
|
||||
a = 1.0;
|
||||
}
|
||||
case 5u: {
|
||||
a = light;
|
||||
}
|
||||
case 6u: {
|
||||
a = mx;
|
||||
}
|
||||
case 7u: {
|
||||
a = abs(ca - da) * gain;
|
||||
}
|
||||
case 8u: {
|
||||
a = clamp(abs(ca - da) * gain, 0.0, 1.0);
|
||||
rgb = vec3<f32>(1.0);
|
||||
}
|
||||
default: {
|
||||
a = ca;
|
||||
}
|
||||
}
|
||||
a = clamp(a, 0.0, 1.0);
|
||||
textureStore(out, p, vec4<f32>(rgb * a, a));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Time Displacement (time_fx::time_displacement)
|
||||
// One pass per sampled time: pixels whose frame index (data, u32 per pixel, row length u[0].y)
|
||||
// is u[0].x take frame aux at (x + dx, y + dy) (u[0].zw, i32), the others keep src.
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn ftm_displace(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!ftm_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let k = bitcast<u32>(data[u32(p.y) * P.u[0].y + u32(p.x)]);
|
||||
if (k == P.u[0].x) {
|
||||
textureStore(out, p, tex_get(aux, p.x + bitcast<i32>(P.u[0].z), p.y + bitcast<i32>(P.u[0].w)));
|
||||
} else {
|
||||
textureStore(out, p, textureLoad(src, p, 0));
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- Timewarp Pixel Motion (time_fx::timewarp_interpolate)
|
||||
// src = frame a, aux = frame b (both source-cropped); data = the motion vectors (x, y pairs).
|
||||
// u[0] = (columns, rows, extreme filtering, build from one image); f[0] = (vector spacing,
|
||||
// fraction w, error threshold).
|
||||
|
||||
// raster::flow::Flow::at.
|
||||
fn ftm_flow_at(x: f32, y: f32) -> vec2<f32> {
|
||||
let cols = P.u[0].x;
|
||||
let rows = P.u[0].y;
|
||||
if (cols == 0u || rows == 0u) {
|
||||
return vec2<f32>(0.0);
|
||||
}
|
||||
let b = P.f[0].x;
|
||||
let fx = clamp(x / b - 0.5, 0.0, f32(cols - 1u));
|
||||
let fy = clamp(y / b - 0.5, 0.0, f32(rows - 1u));
|
||||
let x0 = u32(floor(fx));
|
||||
let y0 = u32(floor(fy));
|
||||
let x1 = min(x0 + 1u, cols - 1u);
|
||||
let y1 = min(y0 + 1u, rows - 1u);
|
||||
let tx = fx - f32(x0);
|
||||
let ty = fy - f32(y0);
|
||||
let g00 = vec2<f32>(data[(y0 * cols + x0) * 2u], data[(y0 * cols + x0) * 2u + 1u]);
|
||||
let g10 = vec2<f32>(data[(y0 * cols + x1) * 2u], data[(y0 * cols + x1) * 2u + 1u]);
|
||||
let g01 = vec2<f32>(data[(y1 * cols + x0) * 2u], data[(y1 * cols + x0) * 2u + 1u]);
|
||||
let g11 = vec2<f32>(data[(y1 * cols + x1) * 2u], data[(y1 * cols + x1) * 2u + 1u]);
|
||||
let top = g00 + (g10 - g00) * tx;
|
||||
let bot = g01 + (g11 - g01) * tx;
|
||||
return top + (bot - top) * ty;
|
||||
}
|
||||
|
||||
fn ftm_sample(t: texture_2d<f32>, x: f32, y: f32) -> vec4<f32> {
|
||||
if (P.u[0].z != 0u) {
|
||||
return sample_bicubic(t, x, y);
|
||||
}
|
||||
return sample_bilinear_clamped(t, x, y);
|
||||
}
|
||||
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn ftm_motion(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let p = vec2<i32>(gid.xy);
|
||||
if (!ftm_inside(p)) {
|
||||
return;
|
||||
}
|
||||
let cx = f32(p.x) + 0.5;
|
||||
let cy = f32(p.y) + 0.5;
|
||||
let f = ftm_flow_at(cx, cy);
|
||||
let w = P.f[0].y;
|
||||
var pa = ftm_sample(src, cx - w * f.x, cy - w * f.y);
|
||||
var pb = ftm_sample(aux, cx + (1.0 - w) * f.x, cy + (1.0 - w) * f.y);
|
||||
if (P.u[0].w != 0u) {
|
||||
textureStore(out, p, select(pb, pa, w < 0.5));
|
||||
return;
|
||||
}
|
||||
let d = abs(pa - pb);
|
||||
let err = max(max(d.x, d.y), max(d.z, d.w));
|
||||
let thr = P.f[0].z;
|
||||
if (thr > 0.0 && err > thr && thr < 1.0) {
|
||||
pa = textureLoad(src, p, 0);
|
||||
pb = textureLoad(aux, p, 0);
|
||||
}
|
||||
textureStore(out, p, pa + (pb - pa) * w);
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
//! GPU time effects (Time Difference, Time Displacement, CC Force Motion Blur, CC Wide Time,
|
||||
//! Pixel Motion Blur, Timewarp) vs the CPU effects (the oracle). They fetch other frames through
|
||||
//! the effect host: a precomp whose content moves, at a nonzero time, composited at 8 and
|
||||
//! 32 bpc (the direct cases have no host and pass the layer through).
|
||||
|
||||
use effectcraft_color::Label;
|
||||
use effectcraft_keyframe::{Keyframe, Value};
|
||||
use effectcraft_project::{BitDepth, Comp, ItemKind, LayerSource, build};
|
||||
use effectcraft_render::RenderOpts;
|
||||
use effectcraft_time::{FrameRate, Tick};
|
||||
|
||||
use crate::tests::{Scene, check, compare_at, effect_case, n, opts, set, v3};
|
||||
|
||||
fn e(v: u32) -> Value {
|
||||
Value::Enum(v)
|
||||
}
|
||||
|
||||
fn on() -> Value {
|
||||
Value::Bool(true)
|
||||
}
|
||||
|
||||
/// `id` with `vals` on a moving precomp; `map` adds a hidden footage layer and sets parameter
|
||||
/// `map` to it.
|
||||
fn moving(id: &str, vals: &[(&str, Value)], map: Option<&str>) {
|
||||
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 vals = vals.to_vec();
|
||||
if let Some(param) = map {
|
||||
let mut m = s.footage(50, 36);
|
||||
m.switches.video = false;
|
||||
let mid = s.push(m);
|
||||
vals.push((param, Value::Layer(Some(mid.0))));
|
||||
}
|
||||
let inner = Comp::new(60, 40, FrameRate::FPS_30, Tick::from_seconds_f64(2.0));
|
||||
let inner_id = s.p.add_item("Inner", Label::Sandstone, None, ItemKind::Comp(inner.into()));
|
||||
let mut a = s.footage(30, 24);
|
||||
let pos = a.props.prop_mut("transform/position").unwrap();
|
||||
pos.keys = vec![Keyframe::new(Tick::ZERO, v3(10.0, 12.0)), Keyframe::new(Tick::from_seconds_f64(1.0), v3(50.0, 30.0))];
|
||||
let rot = a.props.prop_mut("transform/rotation").unwrap();
|
||||
rot.keys = vec![Keyframe::new(Tick::ZERO, Value::Scalar(0.0)), Keyframe::new(Tick::from_seconds_f64(1.0), Value::Scalar(90.0))];
|
||||
s.p.comp_mut(inner_id).unwrap().layers = vec![a];
|
||||
let mut l = build::layer(&mut s.p, &s.comp, "Inner", LayerSource::Comp { item: inner_id }, (60, 40), None);
|
||||
s.effect(&mut l, id, &vals);
|
||||
set(&mut l, "transform/rotation", Value::Scalar(8.0));
|
||||
s.push(l);
|
||||
let t = Tick::from_seconds_f64(0.4);
|
||||
let label = format!("{id} {vals:?} {depth:?}");
|
||||
check(&label, compare_at(&s, opts(), t), 0.0);
|
||||
check(&format!("{label} half"), compare_at(&s, RenderOpts { scale: 0.5, ..opts() }, t), 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn without_a_host() {
|
||||
for id in
|
||||
["ec.time.timedifference", "ec.time.timedisplacement", "ec.time.ccforcemotionblur", "ec.time.ccwidetime", "ec.time.pixelmotionblur", "ec.time.timewarp"]
|
||||
{
|
||||
effect_case(id, &[]);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn frame_averages() {
|
||||
moving("ec.time.ccforcemotionblur", &[], None);
|
||||
moving("ec.time.ccforcemotionblur", &[("motionBlurLevels", n(5.0)), ("shutterAngle", n(400.0))], None);
|
||||
moving("ec.time.ccwidetime", &[], None);
|
||||
moving("ec.time.ccwidetime", &[("forwardSteps", n(1.0)), ("backwardSteps", n(4.0))], None);
|
||||
moving("ec.time.pixelmotionblur", &[], None);
|
||||
moving("ec.time.pixelmotionblur", &[("shutterControl", e(1)), ("shutterAngle", n(300.0)), ("shutterSamples", n(5.0))], None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn time_difference() {
|
||||
moving("ec.time.timedifference", &[("timeOffset", n(-0.1))], None);
|
||||
for alpha in 0..9 {
|
||||
moving(
|
||||
"ec.time.timedifference",
|
||||
&[("timeOffset", n(0.2)), ("contrast", n(40.0)), ("absoluteDifference", Value::Bool(alpha % 2 == 0)), ("alphaChannel", e(alpha))],
|
||||
None,
|
||||
);
|
||||
}
|
||||
moving("ec.time.timedifference", &[("timeOffset", n(0.1)), ("alphaChannel", e(2))], Some("targetLayer"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn time_displacement() {
|
||||
moving("ec.time.timedisplacement", &[("maxDisplacementTime", n(0.3)), ("timeResolution", n(10.0))], None);
|
||||
moving("ec.time.timedisplacement", &[("maxDisplacementTime", n(0.5)), ("timeResolution", n(30.0))], Some("displacementMapLayer"));
|
||||
moving("ec.time.timedisplacement", &[("maxDisplacementTime", n(2.0)), ("stretchMap", Value::Bool(false))], Some("displacementMapLayer"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn timewarp() {
|
||||
for method in 0..3 {
|
||||
moving("ec.time.timewarp", &[("method", e(method)), ("speed", n(37.0))], None);
|
||||
}
|
||||
moving("ec.time.timewarp", &[("method", e(1)), ("adjustTimeBy", e(1)), ("sourceFrame", n(7.4))], None);
|
||||
moving("ec.time.timewarp", &[("method", e(2)), ("speed", n(43.0)), ("motionBlur/enableMotionBlur", on()), ("motionBlur/shutterSamples", n(3.0))], None);
|
||||
moving(
|
||||
"ec.time.timewarp",
|
||||
&[
|
||||
("speed", n(61.0)),
|
||||
("tuning/filtering", e(1)),
|
||||
("tuning/errorThreshold", n(30.0)),
|
||||
("sourceCrops/leftCrop", n(4.0)),
|
||||
("sourceCrops/topCrop", n(3.0)),
|
||||
],
|
||||
None,
|
||||
);
|
||||
moving("ec.time.timewarp", &[("speed", n(55.0)), ("tuning/buildFromOneImage", on())], None);
|
||||
moving("ec.time.timewarp", &[("speed", n(47.0))], Some("warpLayer"));
|
||||
// A Matte Layer renders on the CPU (catalog::gpu_supported).
|
||||
moving("ec.time.timewarp", &[("speed", n(47.0)), ("show", e(0))], Some("matteLayer"));
|
||||
}
|
||||
+7
-7
@@ -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**: 190 effects also run on the GPU compositor with identical results.
|
||||
- **GPU**: 196 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).
|
||||
|
||||
@@ -377,14 +377,14 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
||||
|
||||
| Effect | Id | Params | GPU | 32 | Status |
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| CC Force Motion Blur | `ec.time.ccforcemotionblur` | 4 | | 32 | Implemented |
|
||||
| CC Wide Time | `ec.time.ccwidetime` | 3 | | 32 | Implemented |
|
||||
| CC Force Motion Blur | `ec.time.ccforcemotionblur` | 4 | GPU | 32 | Implemented |
|
||||
| CC Wide Time | `ec.time.ccwidetime` | 3 | GPU | 32 | Implemented |
|
||||
| Echo | `ec.time.echo` | 5 | GPU | 32 | Implemented |
|
||||
| Pixel Motion Blur | `ec.time.pixelmotionblur` | 4 | | 32 | Implemented |
|
||||
| Pixel Motion Blur | `ec.time.pixelmotionblur` | 4 | GPU | 32 | Implemented |
|
||||
| Posterize Time | `ec.time.posterizetime` | 1 | GPU | 32 | Implemented |
|
||||
| Time Difference | `ec.time.timedifference` | 5 | | 32 | Implemented |
|
||||
| Time Displacement | `ec.time.timedisplacement` | 4 | | 32 | Implemented |
|
||||
| Timewarp | `ec.time.timewarp` | 28 | | 32 | Implemented |
|
||||
| Time Difference | `ec.time.timedifference` | 5 | GPU | 32 | Implemented |
|
||||
| Time Displacement | `ec.time.timedisplacement` | 4 | GPU | 32 | Implemented |
|
||||
| Timewarp | `ec.time.timewarp` | 28 | GPU | 32 | Implemented |
|
||||
|
||||
## Transition
|
||||
|
||||
|
||||
Reference in new issue
Block a user