From 3103e7e80f116edf7b3be5047cc443ad6d2a5519 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Sat, 3 Oct 2026 03:30:33 -0400 Subject: [PATCH] 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 --- crates/effects/src/catalog.rs | 3 - crates/effects/src/color_fx.rs | 89 ++++++++++++++++ crates/effects/src/lib.rs | 7 +- crates/effects/src/matte.rs | 188 +++++++++++++++++++++++++++------ crates/gpu/src/effects.rs | 4 + docs/effects.md | 10 +- 6 files changed, 260 insertions(+), 41 deletions(-) diff --git a/crates/effects/src/catalog.rs b/crates/effects/src/catalog.rs index 77f7f49f..b72242dd 100644 --- a/crates/effects/src/catalog.rs +++ b/crates/effects/src/catalog.rs @@ -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"), diff --git a/crates/effects/src/color_fx.rs b/crates/effects/src/color_fx.rs index 240dee1f..7427e9d2 100644 --- a/crates/effects/src/color_fx.rs +++ b/crates/effects/src/color_fx.rs @@ -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 { 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]); + } } diff --git a/crates/effects/src/lib.rs b/crates/effects/src/lib.rs index 49d98236..d465059d 100644 --- a/crates/effects/src/lib.rs +++ b/crates/effects/src/lib.rs @@ -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. diff --git a/crates/effects/src/matte.rs b/crates/effects/src/matte.rs index d7701158..4c42c9c3 100644 --- a/crates/effects/src/matte.rs +++ b/crates/effects/src/matte.rs @@ -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 { + 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 { 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 { 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 { 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 { + None + } + fn audio(&self, _: u64, _: f64, _: usize, _: u32) -> Option> { + None + } + fn self_at(&self, t: f64, _: usize) -> Option { + 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::(); + 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))); + } + } } diff --git a/crates/gpu/src/effects.rs b/crates/gpu/src/effects.rs index f84d4bb1..e14ff230 100644 --- a/crates/gpu/src/effects.rs +++ b/crates/gpu/src/effects.rs @@ -345,6 +345,10 @@ fn pointwise(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option { 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; diff --git a/docs/effects.md b/docs/effects.md index 840b8e5b..d615b817 100644 --- a/docs/effects.md +++ b/docs/effects.md @@ -3,11 +3,11 @@ -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..`) 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..`) 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..`) 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 |