mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 21:16:52 -06:00
M13.9: gpu Refine Soft / Hard Matte (single-frame refinement)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
0fbde2f4ec
commit
05b229825e
@@ -89,6 +89,8 @@ const CPU_ONLY_CONTROLS: &[(&str, &[&str])] = &[
|
||||
("ec.blur.cameralens", &["irisProperties/diffractionFringe", "blurMap/blurMapLayer"]),
|
||||
("ec.noise.removegrain", &["temporalFiltering/enabled"]),
|
||||
("ec.distort.liquify", &["viewFreezeAreaMask", "viewMesh"]),
|
||||
("ec.matte.refinesoft", &["reduceChatter", "useMotionBlur", "invert"]),
|
||||
("ec.matte.refinehard", &["reduceChatter", "useMotionBlur", "invert"]),
|
||||
];
|
||||
|
||||
/// Whether the GPU kernel of effect `id` covers the instance's current settings (see
|
||||
|
||||
@@ -829,6 +829,8 @@ pub const GPU_EFFECTS: &[&str] = &[
|
||||
"ec.key.unmult",
|
||||
"ec.matte.simplechoker",
|
||||
"ec.matte.mattechoker",
|
||||
"ec.matte.refinesoft",
|
||||
"ec.matte.refinehard",
|
||||
"ec.channel.setmatte",
|
||||
"ec.channel.setchannels",
|
||||
"ec.channel.shiftchannels",
|
||||
|
||||
@@ -15,7 +15,7 @@ use effectcraft_effects::EffectCtx;
|
||||
use effectcraft_effects::util::{SRC_ORDER, Src};
|
||||
|
||||
use crate::context::{Enc, GpuImage, Params};
|
||||
use crate::effects::{GBuf, gaussian_blur};
|
||||
use crate::effects::{GBuf, box_passes, gaussian_blur};
|
||||
use crate::fx_color::{DILATE_X, ERODE_X, morph_frac};
|
||||
|
||||
/// Compute entry points in `fx_key.wgsl`.
|
||||
@@ -36,6 +36,8 @@ pub(crate) const IDS: &[&str] = &[
|
||||
"ec.key.unmult",
|
||||
"ec.matte.simplechoker",
|
||||
"ec.matte.mattechoker",
|
||||
"ec.matte.refinesoft",
|
||||
"ec.matte.refinehard",
|
||||
"ec.channel.setmatte",
|
||||
"ec.channel.setchannels",
|
||||
"ec.channel.shiftchannels",
|
||||
@@ -58,6 +60,8 @@ pub(crate) fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<G
|
||||
"ec.key.keycleaner" => key_cleaner(e, ctx, b),
|
||||
"ec.matte.simplechoker" => simple_choker(e, ctx, b),
|
||||
"ec.matte.mattechoker" => matte_choker(e, ctx, b),
|
||||
"ec.matte.refinesoft" => refine(e, ctx, b, false),
|
||||
"ec.matte.refinehard" => refine(e, ctx, b, true),
|
||||
"ec.channel.setchannels" => set_channels(e, ctx, b),
|
||||
_ => point(e, id, ctx, b),
|
||||
}
|
||||
@@ -355,6 +359,81 @@ fn matte_choker(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
||||
Some(b)
|
||||
}
|
||||
|
||||
/// matte::refine (Refine Soft / Hard Matte) for one frame: Reduce Chatter, the matte motion
|
||||
/// blur (they read neighbouring frames) and Invert run on the CPU.
|
||||
fn refine(e: &mut Enc, ctx: &EffectCtx, b: GBuf, hard: bool) -> Option<GBuf> {
|
||||
let pr = ctx.params;
|
||||
// Invert gives alpha to the empty areas, whose colour then comes from a blur of nearly
|
||||
// transparent pixels (util::set_alpha): ill-conditioned, so it renders on the CPU.
|
||||
if pr.f("reduceChatter") > 0.0 || pr.b("useMotionBlur") || pr.b("invert") {
|
||||
return None;
|
||||
}
|
||||
let dk = |id: &str| if hard { id.to_string() } else { format!("decontamination/{id}") };
|
||||
let r = (pr.f("radius") * b.scale).round().max(1.0) as usize;
|
||||
let smooth = pr.f("smooth") as f32 / 100.0;
|
||||
let feather = pr.f("feather") / 100.0 * r as f64 * 0.5;
|
||||
let choke = (pr.f("choke") - if hard { 0.0 } else { pr.f("shiftEdge") }).clamp(-100.0, 100.0) as f32 / 100.0 * 0.5;
|
||||
let decon = pr.b("decontaminate");
|
||||
let decon_amt = pr.f(&dk("decontaminationAmount")) as f32 / 100.0;
|
||||
let extend = pr.b(&dk("extendWhereSmoothed"));
|
||||
let decon_r = r as f64 + pr.f(&dk("increaseDecontaminationRadius")).max(0.0) * b.scale;
|
||||
let view_map = pr.b(&dk("viewDecontaminationMap"));
|
||||
let contrast = if hard { pr.f("alphaContrast") as f32 / 100.0 } else { 1.0 + pr.f("contrast").max(0.0) as f32 / 100.0 * 3.0 };
|
||||
let edge_details = hard || pr.b("calculateEdgeDetails");
|
||||
let view_edges = !hard && pr.b("viewEdgeRegion");
|
||||
let (w, h) = (b.img.width, b.img.height);
|
||||
|
||||
// (luminance guide, alpha, guide × alpha, guide²); the edge band from the alpha's range.
|
||||
let pack = run(e, &op(29), &b.img, None, None);
|
||||
let hi = morph_frac(e, &pack, r as f64, [2, 1, 2, 2]);
|
||||
let lo = morph_frac(e, &pack, r as f64, [2, 0, 2, 2]);
|
||||
let band = run(e, &op(32), &hi, Some(&lo), None);
|
||||
let mut na = if edge_details {
|
||||
let eps = 1e-4 + smooth * smooth * 0.05;
|
||||
let means = box_passes(e, &pack, &[r], &[r], true);
|
||||
let mut p = op(30);
|
||||
p.f[0][0] = eps;
|
||||
let coef = run(e, &p, &means, None, None);
|
||||
let coef = box_passes(e, &coef, &[r], &[r], true);
|
||||
let (rows, row) = e.image_rows(&band);
|
||||
let mut p = op(31);
|
||||
p.u[3][0] = row;
|
||||
run(e, &p, &coef, Some(&pack), Some(&rows))
|
||||
} else {
|
||||
let a = run(e, &op(24), &b.img, None, None);
|
||||
if smooth > 0.0 {
|
||||
let s = smooth as f64 * r as f64 * 0.5;
|
||||
gaussian_blur(e, &a, s, s, true)
|
||||
} else {
|
||||
a
|
||||
}
|
||||
};
|
||||
if feather > 0.0 {
|
||||
na = gaussian_blur(e, &na, feather, feather, true);
|
||||
}
|
||||
let mut p = op(33);
|
||||
p.u[0][1] = pr.b("invert") as u32;
|
||||
p.f[0] = [choke, contrast, 0.0, 0.0];
|
||||
let na = run(e, &p, &na, None, None);
|
||||
if view_edges {
|
||||
let img = run(e, &op(34), &na, Some(&band), None);
|
||||
return Some(GBuf { img, ..b });
|
||||
}
|
||||
let est = (decon && decon_amt > 0.0 && !view_map).then(|| gaussian_blur(e, &b.img, decon_r * 0.5, decon_r * 0.5, true));
|
||||
let rows = est.as_ref().map(|img| e.image_rows(img));
|
||||
let mut p = op(35);
|
||||
p.u[0] = [35, view_map as u32, (decon && (view_map || decon_amt > 0.0)) as u32, extend as u32];
|
||||
p.u[3] = [rows.as_ref().map_or(0, |r| r.1), rows.is_some() as u32, 0, 0];
|
||||
p.f[0][0] = decon_amt;
|
||||
let img = run(e, &p, &b.img, Some(&na), rows.as_ref().map(|r| &r.0));
|
||||
if view_map {
|
||||
return Some(GBuf { img, ..b });
|
||||
}
|
||||
debug_assert_eq!((img.width, img.height), (w, h));
|
||||
let img = set_alpha(e, &img, &na);
|
||||
Some(GBuf { img, ..b })
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- channel
|
||||
|
||||
const SET_CHANNEL_LAYERS: [&str; 4] = ["sourceLayer1", "sourceLayer2", "sourceLayer3", "sourceLayer4"];
|
||||
|
||||
@@ -163,6 +163,10 @@ fn fxk_overflow(op: u32, v: f32, how: u32) -> f32 {
|
||||
// 22 Screen Key matte 23 Screen Key output 24 alpha plane 25 Key Cleaner output
|
||||
// 26 Matte Choker ramp 27 util::set_alpha (aux.x = alpha, data = fill)
|
||||
// 28 matte view (alpha as opaque grey)
|
||||
// Refine Soft / Hard Matte: 29 guide pack (luminance, alpha, products); 30 guided-filter
|
||||
// coefficients from the box means (f[0].x = eps); 31 refined alpha (src = boxed coefficients,
|
||||
// aux = pack, data = edge band); 32 edge band (src.y − aux.y > 1e-3); 33 choke / contrast /
|
||||
// invert; 34 View Edge Region; 35 decontamination (aux.x = refined alpha, data = colour estimate)
|
||||
@compute @workgroup_size(16, 16)
|
||||
fn fxk_point(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let dims = out_dims();
|
||||
@@ -737,6 +741,73 @@ fn fxk_point(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
}
|
||||
o = fxk_premul(cc, na);
|
||||
}
|
||||
case 29u: {
|
||||
let g = luminance(px.xyz);
|
||||
o = vec4<f32>(g, px.w, g * px.w, g * g);
|
||||
}
|
||||
case 30u: {
|
||||
let var_g = px.w - px.x * px.x;
|
||||
let cov = px.z - px.x * px.y;
|
||||
let ca = cov / (var_g + f0.x);
|
||||
o = vec4<f32>(ca, px.y - ca * px.x, 0.0, 0.0);
|
||||
}
|
||||
case 31u: {
|
||||
let q = textureLoad(aux, p, 0);
|
||||
if (fxk_rows(p).x != 0.0) {
|
||||
o = vec4<f32>(clamp(px.x * q.x + px.y, 0.0, 1.0));
|
||||
} else {
|
||||
o = vec4<f32>(q.y);
|
||||
}
|
||||
}
|
||||
case 32u: {
|
||||
o = vec4<f32>(select(0.0, 1.0, px.y - textureLoad(aux, p, 0).y > 1e-3));
|
||||
}
|
||||
case 33u: {
|
||||
var v = px.x;
|
||||
let choke = f0.x;
|
||||
if (choke > 0.0) {
|
||||
v = (v - choke) / (1.0 - choke);
|
||||
} else if (choke < 0.0) {
|
||||
v = v / (1.0 + choke);
|
||||
}
|
||||
if (abs(f0.y - 1.0) > 1e-6) {
|
||||
v = (v - 0.5) * f0.y + 0.5;
|
||||
}
|
||||
v = clamp(v, 0.0, 1.0);
|
||||
if (u0.y != 0u) {
|
||||
v = 1.0 - v;
|
||||
}
|
||||
o = vec4<f32>(v);
|
||||
}
|
||||
case 34u: {
|
||||
let v = px.x;
|
||||
if (textureLoad(aux, p, 0).x != 0.0) {
|
||||
o = vec4<f32>(0.5 + 0.5 * v, 0.5 * v, 0.5 * v, 1.0);
|
||||
} else {
|
||||
o = vec4<f32>(v, v, v, 1.0);
|
||||
}
|
||||
}
|
||||
case 35u: {
|
||||
let n = textureLoad(aux, p, 0).x;
|
||||
let a0 = px.w;
|
||||
var t = 0.0;
|
||||
if (a0 > 1e-4 && a0 < 0.999) {
|
||||
t = 1.0 - a0;
|
||||
} else if (u0.w != 0u && n > 1e-4 && n < 0.999) {
|
||||
t = 1.0 - n;
|
||||
}
|
||||
let strength = f0.x * t;
|
||||
if (u0.y != 0u) {
|
||||
let v = select(0.0, strength, u0.z != 0u);
|
||||
o = vec4<f32>(v, v, v, 1.0);
|
||||
} else if (P.u[3].y != 0u && strength > 0.0 && a0 > 1e-4) {
|
||||
let q = fxk_rows(p);
|
||||
if (q.w > 1e-4) {
|
||||
let cc = c + (q.xyz / q.w - c) * strength;
|
||||
o = fxk_premul(cc, a0);
|
||||
}
|
||||
}
|
||||
}
|
||||
case 28u: {
|
||||
let v = clamp(px.w, 0.0, 1.0);
|
||||
o = vec4<f32>(v, v, v, 1.0);
|
||||
|
||||
@@ -139,6 +139,18 @@ fn chokers() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refine_mattes() {
|
||||
effect_case("ec.matte.refinesoft", &[]);
|
||||
effect_case("ec.matte.refinesoft", &[("radius", n(4.0)), ("smooth", n(40.0)), ("feather", n(30.0)), ("contrast", n(20.0)), ("shiftEdge", n(-25.0))]);
|
||||
effect_case("ec.matte.refinesoft", &[("calculateEdgeDetails", off()), ("smooth", n(50.0)), ("shiftEdge", n(30.0))]);
|
||||
effect_case("ec.matte.refinesoft", &[("radius", n(3.0)), ("viewEdgeRegion", on())]);
|
||||
effect_case("ec.matte.refinesoft", &[("decontamination/viewDecontaminationMap", on()), ("decontamination/extendWhereSmoothed", off())]);
|
||||
effect_case("ec.matte.refinesoft", &[("decontamination/increaseDecontaminationRadius", n(4.0)), ("decontamination/decontaminationAmount", n(60.0))]);
|
||||
effect_case("ec.matte.refinehard", &[]);
|
||||
effect_case("ec.matte.refinehard", &[("choke", n(20.0)), ("feather", n(20.0)), ("decontaminate", off())]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn channel_effects() {
|
||||
effect_case("ec.channel.setchannels", &[("setRedTo", e(2)), ("setGreenTo", e(4)), ("setBlueTo", e(5)), ("setAlphaTo", e(6))]);
|
||||
|
||||
+3
-3
@@ -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**: 150 effects also run on the GPU compositor with identical results.
|
||||
- **GPU**: 152 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).
|
||||
|
||||
@@ -255,8 +255,8 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
||||
|---|---|---:|:-:|:-:|---|
|
||||
| Matte Choker | `ec.matte.mattechoker` | 7 | GPU | 32 | Implemented |
|
||||
| Mocha shape | `ec.obsolete.mochashape` | 6 | | 32 | Implemented |
|
||||
| Refine Hard Matte | `ec.matte.refinehard` | 16 | | 32 | Implemented |
|
||||
| Refine Soft Matte | `ec.matte.refinesoft` | 18 | | 32 | Implemented |
|
||||
| Refine Hard Matte | `ec.matte.refinehard` | 16 | GPU | 32 | Implemented |
|
||||
| Refine Soft Matte | `ec.matte.refinesoft` | 18 | GPU | 32 | Implemented |
|
||||
| Roto Brush & Refine Edge | `ec.matte.rotobrush` | 24 | | 32 | Implemented |
|
||||
| Simple Choker | `ec.matte.simplechoker` | 2 | GPU | 32 | Implemented |
|
||||
|
||||
|
||||
Reference in New Issue
Block a user