mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
M9.13: Fill masks/feather, Refine mattes Reduce Chatter and motion blur
- Fill: Fill Mask, All Masks, Horizontal / Vertical Feather, Invert outside the mask (GPU falls back to the CPU when masks are used). - Refine Soft / Hard Matte: Reduce Chatter (temporal steadying of oscillating edges) and Use Motion Blur (samples, shutter angle, higher quality) from neighbouring frames. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
ba5e2061fe
commit
3103e7e80f
6 files changed
+260
-41
No files matched your search
@@ -19,7 +19,6 @@ pub const PARTIAL: &[(&str, &str)] = &[
|
||||
("ec.distort.transform", "no shutter-angle motion blur"),
|
||||
("ec.distort.reshape", "no correspondence points"),
|
||||
("ec.distort.rollingshutterrepair", "Pixel Motion Detail has no effect"),
|
||||
("ec.generate.fill", "no Fill Mask / All Masks / feather"),
|
||||
("ec.generate.advancedlightning", "no Alpha Obstacle; most Expert Settings missing"),
|
||||
("ec.generate.radiowaves", "no Image Contours or Mask wave types, Parameters Are Set At, reflection or stroke profile"),
|
||||
("ec.generate.scribble", "no caps, joins or Start/End Apply To"),
|
||||
@@ -27,8 +26,6 @@ pub const PARTIAL: &[(&str, &str)] = &[
|
||||
("ec.key.innerouter", "one additional mask per side; no cleanup strokes"),
|
||||
("ec.key.advancedspill", "no Tolerance, Desaturate or Spill Color Correction"),
|
||||
("ec.keying.keylight", "no Source Crops X/Y Method or Edge Colour; no colour suppression / balancing"),
|
||||
("ec.matte.refinesoft", "no Reduce Chatter or motion blur"),
|
||||
("ec.matte.refinehard", "no Reduce Chatter or motion blur"),
|
||||
("ec.obsolete.pathtext", "no kerning, shear/scale, line spacing, fade time or jitter"),
|
||||
("ec.noise.addgrain", "no preview region, presets, channel balance or temporal controls"),
|
||||
("ec.noise.matchgrain", "no preview region, presets, sampling or temporal controls"),
|
||||
|
||||
@@ -340,10 +340,66 @@ fn black_white(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
b
|
||||
}
|
||||
|
||||
/// Whether Fill uses masks (Fill Mask or All Masks): the GPU kernel fills alpha only.
|
||||
pub fn fill_uses_masks(ctx: &EffectCtx) -> bool {
|
||||
!ctx.env.masks.is_empty() && (ctx.params.b("allMasks") || ctx.params.f("fillMask") >= 1.0)
|
||||
}
|
||||
|
||||
/// Coverage (0..1, 2×2 supersampled) of the closed polygons `polys` (buffer px) on a `w`×`h`
|
||||
/// grid: the union of the shapes, each inverted when its mask is.
|
||||
pub(crate) fn polys_coverage(w: usize, h: usize, polys: &[(Vec<[f64; 2]>, bool)]) -> crate::util::Plane {
|
||||
use rayon::prelude::*;
|
||||
let mut out = crate::util::Plane::new(w, h);
|
||||
out.data.par_chunks_mut(w.max(1)).enumerate().for_each(|(y, row)| {
|
||||
for (x, v) in row.iter_mut().enumerate() {
|
||||
let mut hit = 0.0;
|
||||
for (sx, sy) in [(0.25, 0.25), (0.75, 0.25), (0.25, 0.75), (0.75, 0.75)] {
|
||||
let (px, py) = (x as f64 + sx, y as f64 + sy);
|
||||
if polys.iter().any(|(p, inv)| crate::util::point_in_poly(p, px, py) != *inv) {
|
||||
hit += 0.25;
|
||||
}
|
||||
}
|
||||
*v = hit;
|
||||
}
|
||||
});
|
||||
out
|
||||
}
|
||||
|
||||
/// Fill: paints the layer's pixels with Color. With Fill Mask (or All Masks) only the pixels
|
||||
/// inside that mask (or any mask) are painted, with the mask edge feathered horizontally /
|
||||
/// vertically; Invert paints outside instead. Without a mask, Invert fills the transparent
|
||||
/// areas.
|
||||
fn fill(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let c = ctx.params.color("color");
|
||||
let invert = ctx.params.b("invert");
|
||||
let opacity = ctx.params.f("opacity") as f32 / 100.0;
|
||||
if fill_uses_masks(ctx) {
|
||||
let all = ctx.params.b("allMasks");
|
||||
let idx = ctx.params.f("fillMask").round() as usize;
|
||||
let polys: Vec<(Vec<[f64; 2]>, bool)> = ctx
|
||||
.env
|
||||
.masks
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(i, m)| (all || i + 1 == idx) && m.points.len() >= 3)
|
||||
.map(|(_, m)| (m.points.iter().map(|q| [q[0] * b.scale + b.offset[0], q[1] * b.scale + b.offset[1]]).collect(), m.inverted))
|
||||
.collect();
|
||||
let (w, h) = (b.img.width as usize, b.img.height as usize);
|
||||
let mut cov = polys_coverage(w, h, &polys);
|
||||
let (fh, fv) = (ctx.params.f("horizontalFeather").max(0.0) * b.scale, ctx.params.f("verticalFeather").max(0.0) * b.scale);
|
||||
if fh > 0.0 || fv > 0.0 {
|
||||
cov = crate::util::gauss_plane(&cov, fh * 0.5, fv * 0.5);
|
||||
}
|
||||
use rayon::prelude::*;
|
||||
b.img.data.par_iter_mut().zip(cov.data.par_iter()).for_each(|(px, &m)| {
|
||||
let m = if invert { 1.0 - m } else { m } * opacity;
|
||||
let a = px[3];
|
||||
for i in 0..3 {
|
||||
px[i] += (c[i] * a - px[i]) * m;
|
||||
}
|
||||
});
|
||||
return b;
|
||||
}
|
||||
b.img.data.iter_mut().for_each(|px| {
|
||||
let a = if invert { 1.0 - px[3] } else { px[3] };
|
||||
let f = [c[0] * a, c[1] * a, c[2] * a];
|
||||
@@ -755,8 +811,12 @@ pub fn specs() -> Vec<EffectSpec> {
|
||||
name: "Fill",
|
||||
category: "Generate",
|
||||
params: vec![
|
||||
p("fillMask", "Fill Mask", num(0.0), ParamUi::Mask),
|
||||
p("allMasks", "All Masks", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("color", "Color", col(1.0, 0.0, 0.0), ParamUi::Color),
|
||||
p("invert", "Invert", Value::Bool(false), ParamUi::Checkbox),
|
||||
p("horizontalFeather", "Horizontal Feather", num(0.0), slider(0.0, 5000.0, 0.0, 100.0, 1)),
|
||||
p("verticalFeather", "Vertical Feather", num(0.0), slider(0.0, 5000.0, 0.0, 100.0, 1)),
|
||||
p("opacity", "Opacity", num(100.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
],
|
||||
render: fill,
|
||||
@@ -1069,4 +1129,33 @@ mod tests {
|
||||
let ctx = EffectCtx { params: ¶ms, time: 0.0, layer_size: [1.0, 1.0], seed: 0, adjustment: false, env: Default::default() };
|
||||
assert!(!huesat_ranges_identity(&ctx));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fill_mask_all_masks_feather_and_invert() {
|
||||
let sq = |x0: f64, x1: f64| crate::MaskShape {
|
||||
name: String::new(),
|
||||
points: vec![[x0, 0.0], [x1, 0.0], [x1, 10.0], [x0, 10.0]],
|
||||
closed: true,
|
||||
inverted: false,
|
||||
};
|
||||
let masks = [sq(0.0, 5.0), sq(15.0, 20.0)];
|
||||
let env = EffectEnv { masks: &masks, ..Default::default() };
|
||||
let grey = Image::filled(20, 10, [0.5, 0.5, 0.5, 1.0]);
|
||||
let run = |vals: &[(&str, Value)]| run_fx("ec.generate.fill", vals, grey.clone(), 0.0, env).img;
|
||||
let one = run(&[("fillMask", num(1.0))]);
|
||||
assert_eq!(one.get(2, 5), [1.0, 0.0, 0.0, 1.0]);
|
||||
assert_eq!(one.get(10, 5), [0.5, 0.5, 0.5, 1.0]);
|
||||
assert_eq!(one.get(17, 5), [0.5, 0.5, 0.5, 1.0]);
|
||||
let all = run(&[("allMasks", Value::Bool(true))]);
|
||||
assert_eq!(all.get(17, 5), [1.0, 0.0, 0.0, 1.0]);
|
||||
let inv = run(&[("fillMask", num(1.0)), ("invert", Value::Bool(true))]);
|
||||
assert_eq!(inv.get(2, 5), [0.5, 0.5, 0.5, 1.0]);
|
||||
assert_eq!(inv.get(10, 5), [1.0, 0.0, 0.0, 1.0]);
|
||||
let soft = run(&[("fillMask", num(1.0)), ("horizontalFeather", num(6.0))]);
|
||||
let v = soft.get(6, 5)[1];
|
||||
assert!(v > 0.05 && v < 0.5, "feathered edge {v}");
|
||||
// Without masks, Fill fills the layer as before.
|
||||
let plain = run_fx("ec.generate.fill", &[("fillMask", num(1.0))], grey.clone(), 0.0, EffectEnv::default()).img;
|
||||
assert_eq!(plain.get(10, 5), [1.0, 0.0, 0.0, 1.0]);
|
||||
}
|
||||
}
|
||||
@@ -62,7 +62,9 @@ use std::collections::HashMap;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
pub use card3d::{CompLight, CompScene};
|
||||
pub use color_fx::{HUESAT_CHANNELS, LEVELS_CHANNELS, exposure_settings, huesat_ranges_identity, levels_channel_ids, levels_channels_identity, levels_clip};
|
||||
pub use color_fx::{
|
||||
HUESAT_CHANNELS, LEVELS_CHANNELS, exposure_settings, fill_uses_masks, huesat_ranges_identity, levels_channel_ids, levels_channels_identity, levels_clip,
|
||||
};
|
||||
pub use color2::Curve;
|
||||
use effectcraft_keyframe::Value;
|
||||
use effectcraft_project::build::Ids;
|
||||
@@ -693,6 +695,9 @@ pub const TIME_DEPENDENT: &[&str] = &[
|
||||
"ec.time.ccforcemotionblur",
|
||||
"ec.time.ccwidetime",
|
||||
"ec.time.pixelmotionblur",
|
||||
// Refine mattes' Reduce Chatter / motion blur read neighbouring frames.
|
||||
"ec.matte.refinesoft",
|
||||
"ec.matte.refinehard",
|
||||
// Card Wipe's position / rotation jitter moves with time.
|
||||
"ec.transition.cardwipe",
|
||||
// Temporal Smoothing reads neighbouring frames.
|
||||
|
||||
+156
-32
@@ -93,38 +93,91 @@ fn refine(ctx: &EffectCtx, mut b: Buf, hard: bool) -> Buf {
|
||||
let hi = morph_plane(&a, r, r, true);
|
||||
let lo = morph_plane(&a, r, r, false);
|
||||
let band = |i: usize| hi.data[i] - lo.data[i] > 1e-3;
|
||||
let mut na = Plane::new(a.w, a.h);
|
||||
if edge_details {
|
||||
let guide = Plane::from_image(&b.img, |px| effectcraft_color::luminance(px[0], px[1], px[2]));
|
||||
let eps = 1e-4 + smooth * smooth * 0.05;
|
||||
let filtered = guided_filter(&guide, &a, r, eps);
|
||||
na.data.par_iter_mut().enumerate().for_each(|(i, v)| {
|
||||
*v = if band(i) { filtered.data[i].clamp(0.0, 1.0) } else { a.data[i] };
|
||||
});
|
||||
} else {
|
||||
na = a.clone();
|
||||
if smooth > 0.0 {
|
||||
let s = smooth as f64 * r as f64 * 0.5;
|
||||
na = gauss_plane(&na, s, s);
|
||||
}
|
||||
}
|
||||
if feather > 0.0 {
|
||||
na = gauss_plane(&na, feather, feather);
|
||||
}
|
||||
na = na.map(|v| {
|
||||
let mut v = if choke > 0.0 {
|
||||
(v - choke) / (1.0 - choke)
|
||||
} else if choke < 0.0 {
|
||||
v / (1.0 + choke)
|
||||
// The refined matte of a frame.
|
||||
let refine_alpha = |img: &effectcraft_raster::Image| -> Plane {
|
||||
let a = Plane::alpha(img);
|
||||
let hi = morph_plane(&a, r, r, true);
|
||||
let lo = morph_plane(&a, r, r, false);
|
||||
let mut na = Plane::new(a.w, a.h);
|
||||
if edge_details {
|
||||
let guide = Plane::from_image(img, |px| effectcraft_color::luminance(px[0], px[1], px[2]));
|
||||
let eps = 1e-4 + smooth * smooth * 0.05;
|
||||
let filtered = guided_filter(&guide, &a, r, eps);
|
||||
na.data.par_iter_mut().enumerate().for_each(|(i, v)| {
|
||||
*v = if hi.data[i] - lo.data[i] > 1e-3 { filtered.data[i].clamp(0.0, 1.0) } else { a.data[i] };
|
||||
});
|
||||
} else {
|
||||
v
|
||||
};
|
||||
if (contrast - 1.0).abs() > 1e-6 {
|
||||
v = (v - 0.5) * contrast + 0.5;
|
||||
na = a.clone();
|
||||
if smooth > 0.0 {
|
||||
let s = smooth as f64 * r as f64 * 0.5;
|
||||
na = gauss_plane(&na, s, s);
|
||||
}
|
||||
}
|
||||
let v = v.clamp(0.0, 1.0);
|
||||
if invert { 1.0 - v } else { v }
|
||||
});
|
||||
if feather > 0.0 {
|
||||
na = gauss_plane(&na, feather, feather);
|
||||
}
|
||||
na.map(|v| {
|
||||
let mut v = if choke > 0.0 {
|
||||
(v - choke) / (1.0 - choke)
|
||||
} else if choke < 0.0 {
|
||||
v / (1.0 + choke)
|
||||
} else {
|
||||
v
|
||||
};
|
||||
if (contrast - 1.0).abs() > 1e-6 {
|
||||
v = (v - 0.5) * contrast + 0.5;
|
||||
}
|
||||
let v = v.clamp(0.0, 1.0);
|
||||
if invert { 1.0 - v } else { v }
|
||||
})
|
||||
};
|
||||
let mut na = refine_alpha(&b.img);
|
||||
// Neighbouring frames of this effect's input (same pixel grid), refined the same way.
|
||||
let fps = ctx.fps();
|
||||
let frame_at = |t: f64| -> Option<Plane> {
|
||||
let o = ctx.env.host?.self_at(t, ctx.env.effect_index)?;
|
||||
(o.img.width == b.img.width && o.img.height == b.img.height && (o.offset[0] - b.offset[0]).abs() < 1e-6 && (o.offset[1] - b.offset[1]).abs() < 1e-6)
|
||||
.then(|| refine_alpha(&o.img))
|
||||
};
|
||||
// Reduce Chatter: where the previous and next frames agree but this one differs, the matte
|
||||
// is chattering; average the three there. Where they disagree (motion) it is left alone.
|
||||
let chatter = (pr.f("reduceChatter") / 100.0).clamp(0.0, 1.0) as f32;
|
||||
if chatter > 0.0 {
|
||||
let prev = frame_at(ctx.time - 1.0 / fps);
|
||||
let next = frame_at(ctx.time + 1.0 / fps);
|
||||
if let (Some(p), Some(n)) = (&prev, &next) {
|
||||
na.data.par_iter_mut().enumerate().for_each(|(i, v)| {
|
||||
let (c, a, z) = (*v, p.data[i], n.data[i]);
|
||||
let steady = (1.0 - (a - z).abs() / 0.5).clamp(0.0, 1.0);
|
||||
*v = c + ((a + c + z) / 3.0 - c) * chatter * steady;
|
||||
});
|
||||
}
|
||||
}
|
||||
// Motion blur: the matte averaged over the shutter (centred on the frame).
|
||||
if pr.b("useMotionBlur") {
|
||||
let g = |id: &str| pr.get(&format!("motionBlur/{id}")).map(Value::as_f64);
|
||||
let samples = g("motionBlurSamples").unwrap_or(16.0).clamp(1.0, 64.0) as usize;
|
||||
let samples = if pr.b("motionBlur/higherQuality") { samples * 2 } else { samples }.min(64);
|
||||
let span = g("shutterAngle").unwrap_or(180.0).clamp(0.0, 720.0) / 360.0 / fps;
|
||||
if samples > 1 && span > 0.0 {
|
||||
let mut acc = na.clone();
|
||||
let mut n = 1.0f32;
|
||||
for k in 0..samples {
|
||||
let t = ctx.time - span * 0.5 + span * k as f64 / (samples - 1) as f64;
|
||||
if (t - ctx.time).abs() < 1e-9 {
|
||||
continue;
|
||||
}
|
||||
if let Some(p) = frame_at(t) {
|
||||
acc.data.iter_mut().zip(&p.data).for_each(|(a, q)| *a += q);
|
||||
n += 1.0;
|
||||
}
|
||||
}
|
||||
if n > 1.0 {
|
||||
acc.data.iter_mut().for_each(|a| *a /= n);
|
||||
na = acc;
|
||||
}
|
||||
}
|
||||
}
|
||||
if view_edges {
|
||||
// The refined matte in grey with the analysed edge band tinted.
|
||||
b.img.data.par_iter_mut().enumerate().for_each(|(i, px)| {
|
||||
@@ -190,6 +243,16 @@ pub fn specs() -> Vec<EffectSpec> {
|
||||
check(pre[3], "View Decontamination Map", false),
|
||||
]
|
||||
};
|
||||
// Reduce Chatter and the motion-blur settings.
|
||||
let temporal = || {
|
||||
vec![
|
||||
p("reduceChatter", "Reduce Chatter", num(0.0), pct0()),
|
||||
check("useMotionBlur", "Use Motion Blur", false),
|
||||
p("motionBlur/motionBlurSamples", "Motion Blur Samples", num(16.0), slider(1.0, 64.0, 1.0, 64.0, 0)),
|
||||
p("motionBlur/shutterAngle", "Shutter Angle", num(180.0), slider(0.0, 720.0, 0.0, 360.0, 1)),
|
||||
check("motionBlur/higherQuality", "Higher Quality", false),
|
||||
]
|
||||
};
|
||||
let radius = |d: f64| p("radius", "Additional Edge Radius", num(d), slider(1.0, 200.0, 1.0, 50.0, 1));
|
||||
let mut soft_params = vec![
|
||||
check("calculateEdgeDetails", "Calculate Edge Details", true),
|
||||
@@ -199,8 +262,9 @@ pub fn specs() -> Vec<EffectSpec> {
|
||||
p("feather", "Feather", num(10.0), pct0()),
|
||||
p("contrast", "Contrast", num(0.0), pct0()),
|
||||
p("shiftEdge", "Shift Edge", num(0.0), slider(-100.0, 100.0, -100.0, 100.0, 0)),
|
||||
check("decontaminate", "Decontaminate Edge Colors", true),
|
||||
];
|
||||
soft_params.extend(temporal());
|
||||
soft_params.push(check("decontaminate", "Decontaminate Edge Colors", true));
|
||||
soft_params.extend(decon(&[
|
||||
"decontamination/decontaminationAmount",
|
||||
"decontamination/extendWhereSmoothed",
|
||||
@@ -214,8 +278,9 @@ pub fn specs() -> Vec<EffectSpec> {
|
||||
p("smooth", "Smooth", num(20.0), pct0()),
|
||||
p("feather", "Feather", num(0.0), pct0()),
|
||||
p("choke", "Choke", num(0.0), slider(-100.0, 100.0, -100.0, 100.0, 0)),
|
||||
check("decontaminate", "Decontaminate Edge Colors", true),
|
||||
];
|
||||
hard_params.extend(temporal());
|
||||
hard_params.push(check("decontaminate", "Decontaminate Edge Colors", true));
|
||||
hard_params.extend(decon(&["decontaminationAmount", "extendWhereSmoothed", "increaseDecontaminationRadius", "viewDecontaminationMap"]));
|
||||
// Our extras: the analysed edge band's radius, alpha contrast and inversion.
|
||||
hard_params.push(radius(3.0));
|
||||
@@ -345,4 +410,63 @@ mod tests {
|
||||
assert!(out.get(0, 0)[3] < 0.01);
|
||||
assert!(out.data.iter().all(|p| (0.0..=1.0).contains(&p[3])));
|
||||
}
|
||||
|
||||
/// A white square whose left edge sits at x = 10 + `jitter` × (frame parity) + `speed` × frame.
|
||||
struct Edge {
|
||||
jitter: i64,
|
||||
speed: i64,
|
||||
}
|
||||
fn edge(x0: i64) -> Image {
|
||||
let mut img = Image::new(40, 20);
|
||||
for y in 4..16 {
|
||||
for x in x0.max(0)..(x0 + 16).min(40) {
|
||||
img.set(x as u32, y, [1.0, 1.0, 1.0, 1.0]);
|
||||
}
|
||||
}
|
||||
img
|
||||
}
|
||||
impl crate::EffectHost for Edge {
|
||||
fn layer(&self, _: u64, _: bool) -> Option<crate::LayerPixels> {
|
||||
None
|
||||
}
|
||||
fn audio(&self, _: u64, _: f64, _: usize, _: u32) -> Option<Vec<f32>> {
|
||||
None
|
||||
}
|
||||
fn self_at(&self, t: f64, _: usize) -> Option<Buf> {
|
||||
let f = (t * 10.0).round() as i64;
|
||||
Some(Buf { img: edge(10 + self.jitter * (f & 1) + self.speed * f), offset: [0.0; 2], scale: 1.0 })
|
||||
}
|
||||
}
|
||||
|
||||
fn run_t(id: &str, over: &[(&str, Value)], t: f64, host: &Edge) -> Image {
|
||||
let f = (t * 10.0).round() as i64;
|
||||
let img = edge(10 + host.jitter * (f & 1) + host.speed * f);
|
||||
let env = crate::EffectEnv { host: Some(host), frame_rate: 10.0, ..Default::default() };
|
||||
crate::run_fx(id, over, img, t, env).img
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refine_mattes_reduce_chatter_and_motion_blur() {
|
||||
for id in ["ec.matte.refinesoft", "ec.matte.refinehard"] {
|
||||
let jitter = Edge { jitter: 1, speed: 0 };
|
||||
let base = [("feather", num(0.0)), ("smooth", num(0.0)), ("decontaminate", Value::Bool(false))];
|
||||
let with = |extra: &[(&str, Value)], t: f64, h: &Edge| {
|
||||
let mut v: Vec<(&str, Value)> = base.to_vec();
|
||||
v.extend_from_slice(extra);
|
||||
run_t(id, &v, t, h)
|
||||
};
|
||||
let diff = |a: &Image, b: &Image| a.data.iter().zip(&b.data).map(|(p, q)| (p[3] - q[3]).abs()).sum::<f32>();
|
||||
let (a0, a1) = (with(&[], 1.0, &jitter), with(&[], 1.1, &jitter));
|
||||
let (c0, c1) = (with(&[("reduceChatter", num(100.0))], 1.0, &jitter), with(&[("reduceChatter", num(100.0))], 1.1, &jitter));
|
||||
assert!(diff(&c0, &c1) < diff(&a0, &a1) * 0.7, "{id}: chatter {} vs {}", diff(&c0, &c1), diff(&a0, &a1));
|
||||
// Motion blur smears a moving matte edge along the motion.
|
||||
let moving = Edge { jitter: 0, speed: 4 };
|
||||
let mb = with(&[("useMotionBlur", Value::Bool(true)), ("motionBlur/shutterAngle", num(360.0))], 0.3, &moving);
|
||||
let sharp = with(&[], 0.3, &moving);
|
||||
let partial = |img: &Image| img.data.iter().filter(|p| p[3] > 0.05 && p[3] < 0.95).count();
|
||||
assert!(partial(&mb) > partial(&sharp) + 10, "{id}: {} vs {}", partial(&mb), partial(&sharp));
|
||||
// Without a host both are inert.
|
||||
assert_eq!(run(id, &[("reduceChatter", num(100.0)), ("useMotionBlur", Value::Bool(true))], edge(10)), run(id, &[], edge(10)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -345,6 +345,10 @@ fn pointwise(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
p.f[0][0] = 1.0 - f("blend") as f32 / 100.0;
|
||||
}
|
||||
"ec.generate.fill" => {
|
||||
// Fill Mask / All Masks render on the CPU.
|
||||
if effectcraft_effects::fill_uses_masks(ctx) {
|
||||
return None;
|
||||
}
|
||||
p.u[0] = [7, ctx.params.b("invert") as u32, 0, 0];
|
||||
p.f[0] = ctx.params.color("color");
|
||||
p.f[1][0] = f("opacity") as f32 / 100.0;
|
||||
|
||||
+5
-5
@@ -3,11 +3,11 @@
|
||||
<!-- Generated from the effect registry (crates/effects/src/catalog.rs); do not edit by hand.
|
||||
Regenerate: UPDATE_DOCS=1 cargo test -p effectcraft-effects --lib effects_doc_is_current -->
|
||||
|
||||
EffectCraft ships 303 effects with 2702 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.
|
||||
EffectCraft ships 303 effects with 2716 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**: 24 effects also run on the GPU compositor with identical results.
|
||||
- **32**: 303 effects process 32-bit float (HDR, overbright) pixels without clamping.
|
||||
- **Status**: 270 implemented in full, 33 partial (what is missing is listed).
|
||||
- **Status**: 273 implemented in full, 30 partial (what is missing is listed).
|
||||
|
||||
Effects are addressed by id (`ec.<category>.<name>`) or display name in commands, scripts and the control channel, and every parameter by its id (`effects/#1/blurriness`).
|
||||
|
||||
@@ -202,7 +202,7 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
||||
| Circle | `ec.generate.circle` | 11 | | 32 | Implemented |
|
||||
| Ellipse | `ec.generate.ellipse` | 8 | | 32 | Implemented |
|
||||
| Eyedropper Fill | `ec.generate.eyedropperfill` | 5 | | 32 | Implemented |
|
||||
| Fill | `ec.generate.fill` | 3 | GPU | 32 | Partial: no Fill Mask / All Masks / feather |
|
||||
| Fill | `ec.generate.fill` | 7 | GPU | 32 | Implemented |
|
||||
| Fractal | `ec.generate.fractal` | 21 | | 32 | Implemented |
|
||||
| Gradient Ramp | `ec.generate.gradientramp` | 7 | GPU | 32 | Implemented |
|
||||
| Grid | `ec.generate.grid` | 12 | | 32 | Implemented |
|
||||
@@ -254,8 +254,8 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| Matte Choker | `ec.matte.mattechoker` | 7 | | 32 | Implemented |
|
||||
| Mocha shape | `ec.obsolete.mochashape` | 6 | | 32 | Implemented |
|
||||
| Refine Hard Matte | `ec.matte.refinehard` | 11 | | 32 | Partial: no Reduce Chatter or motion blur |
|
||||
| Refine Soft Matte | `ec.matte.refinesoft` | 13 | | 32 | Partial: no Reduce Chatter or motion blur |
|
||||
| Refine Hard Matte | `ec.matte.refinehard` | 16 | | 32 | Implemented |
|
||||
| Refine Soft Matte | `ec.matte.refinesoft` | 18 | | 32 | Implemented |
|
||||
| Roto Brush & Refine Edge | `ec.matte.rotobrush` | 24 | | 32 | Implemented |
|
||||
| Simple Choker | `ec.matte.simplechoker` | 2 | | 32 | Implemented |
|
||||
|
||||
|
||||
Reference in new issue
Block a user