Merge branch 'worktree-agent-a0fac3acf963642ce'

This commit is contained in:
Brandon Thomas 2026-10-03 08:58:21 -04:00
commit f5a136e346
31 changed files with 7254 additions and 167 deletions

View File

@ -87,6 +87,10 @@ const CPU_ONLY_CONTROLS: &[(&str, &[&str])] = &[
),
("ec.noise.addgrain", &["viewingMode", "preset", "color/redBalance", "color/greenBalance", "color/blueBalance", "previewRegion/"]),
("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

View File

@ -330,10 +330,21 @@ fn stretch(v: f32, lo: f32, hi: f32) -> f32 {
/// 0 = Auto Levels (per channel), 1 = Auto Contrast (shared), 2 = Auto Color (per channel + neutral mids).
fn auto_correct(ctx: &EffectCtx, mut b: Buf, kind: u32) -> Buf {
let blend = (ctx.params.f("blend") / 100.0).clamp(0.0, 1.0) as f32;
let (ranges, gam) = auto_correct_settings(ctx, &b.img, kind);
map_ca(&mut b.img, |c, a| {
let o = [0, 1, 2].map(|k| stretch(c[k], ranges[k].0, ranges[k].1).powf(gam[k]));
(lerp3(o, c, blend), a)
});
b
}
/// What Auto Levels (`kind` 0), Auto Contrast (1) and Auto Color (2) measure on `img`: each
/// channel's input range (low, high) and gamma. (The GPU effects apply them.)
pub fn auto_correct_settings(ctx: &EffectCtx, img: &Image, kind: u32) -> ([(f32, f32); 3], [f32; 3]) {
let black = ctx.params.f("blackClip").clamp(0.0, 49.0) / 100.0;
let white = ctx.params.f("whiteClip").clamp(0.0, 49.0) / 100.0;
let blend = (ctx.params.f("blend") / 100.0).clamp(0.0, 1.0) as f32;
let h = smoothed_histograms(ctx, &b.img);
let h = smoothed_histograms(ctx, img);
let ranges: [(f32, f32); 3] = if kind == 1 {
let mut combined = vec![0.0; BINS];
for k in 0..3 {
@ -350,7 +361,7 @@ fn auto_correct(ctx: &EffectCtx, mut b: Buf, kind: u32) -> Buf {
let mut gam = [1.0f32; 3];
if kind == 2 && ctx.params.b("snapNeutralMidtones") {
let mut sums = [0.0f64; 4];
for px in &b.img.data {
for px in &img.data {
let (c, a) = unpremul(*px);
if a > 1e-4 {
for k in 0..3 {
@ -369,11 +380,7 @@ fn auto_correct(ctx: &EffectCtx, mut b: Buf, kind: u32) -> Buf {
}
}
}
map_ca(&mut b.img, |c, a| {
let o = [0, 1, 2].map(|k| stretch(c[k], ranges[k].0, ranges[k].1).powf(gam[k]));
(lerp3(o, c, blend), a)
});
b
(ranges, gam)
}
fn auto_levels(ctx: &EffectCtx, b: Buf) -> Buf {
@ -397,24 +404,12 @@ fn cdf(h: &[f64]) -> Vec<f32> {
.collect()
}
fn equalize(ctx: &EffectCtx, mut b: Buf) -> Buf {
let amt = (ctx.params.f("amount") / 100.0).clamp(0.0, 1.0) as f32;
if amt <= 0.0 {
return b;
}
let style = ctx.params.e("style");
let h = histograms(&b.img);
let look = |c: &Vec<f32>, v: f32| c[((v.clamp(0.0, 1.0) * (BINS - 1) as f32).round() as usize).min(BINS - 1)];
/// Equalize's lookup tables (`BINS` entries each) for `img`: the R, G and B cumulative
/// histograms (Style RGB), the luminance one (Brightness) or the combined one (Photoshop Style).
pub fn equalize_tables(img: &Image, style: u32) -> Vec<Vec<f32>> {
let h = histograms(img);
match style {
1 => {
let c = cdf(&h[3]);
map_ca(&mut b.img, |col, a| {
let l = luminance(col[0], col[1], col[2]);
let nl = look(&c, l);
let o = if l > 1e-5 { col.map(|v| v * nl / l) } else { [nl; 3] };
(lerp3(col, o, amt), a)
});
}
1 => vec![cdf(&h[3])],
2 => {
let mut combined = vec![0.0; BINS];
for k in 0..3 {
@ -422,12 +417,34 @@ fn equalize(ctx: &EffectCtx, mut b: Buf) -> Buf {
combined[i] += h[k][i];
}
}
let c = cdf(&combined);
map_ca(&mut b.img, |col, a| (lerp3(col, col.map(|v| look(&c, v)), amt), a));
vec![cdf(&combined)]
}
_ => vec![cdf(&h[0]), cdf(&h[1]), cdf(&h[2])],
}
}
fn equalize(ctx: &EffectCtx, mut b: Buf) -> Buf {
let amt = (ctx.params.f("amount") / 100.0).clamp(0.0, 1.0) as f32;
if amt <= 0.0 {
return b;
}
let style = ctx.params.e("style");
let t = equalize_tables(&b.img, style);
let look = |c: &Vec<f32>, v: f32| c[((v.clamp(0.0, 1.0) * (BINS - 1) as f32).round() as usize).min(BINS - 1)];
match style {
1 => {
map_ca(&mut b.img, |col, a| {
let l = luminance(col[0], col[1], col[2]);
let nl = look(&t[0], l);
let o = if l > 1e-5 { col.map(|v| v * nl / l) } else { [nl; 3] };
(lerp3(col, o, amt), a)
});
}
2 => {
map_ca(&mut b.img, |col, a| (lerp3(col, col.map(|v| look(&t[0], v)), amt), a));
}
_ => {
let cs = [cdf(&h[0]), cdf(&h[1]), cdf(&h[2])];
map_ca(&mut b.img, |col, a| (lerp3(col, [0, 1, 2].map(|k| look(&cs[k], col[k])), amt), a));
map_ca(&mut b.img, |col, a| (lerp3(col, [0, 1, 2].map(|k| look(&t[k], col[k])), amt), a));
}
}
b
@ -436,17 +453,29 @@ fn equalize(ctx: &EffectCtx, mut b: Buf) -> Buf {
// ---------------------------------------------------------------------------------------------
// Shadow/Highlight
fn shadow_highlight(ctx: &EffectCtx, mut b: Buf) -> Buf {
/// Shadow/Highlight's shadow and highlight amounts for `img` (Auto Amounts measures them).
pub fn shadow_highlight_amounts(ctx: &EffectCtx, img: &Image) -> (f32, f32) {
let (mut s_amt, mut h_amt) = (ctx.params.f("shadowAmount") as f32 / 100.0, ctx.params.f("highlightAmount") as f32 / 100.0);
let luma = Plane::luma(&b.img);
let src_hist = histograms(&b.img);
if ctx.params.b("autoAmounts") {
// Temporal Smoothing / Scene Detect average the analysis over neighbouring frames.
let mean =
if ctx.params.f("temporalSmoothing") > 0.0 { hist_mean(&smoothed_histograms(ctx, &b.img)[3]) as f32 } else { hist_mean(&src_hist[3]) as f32 };
let mean = if ctx.params.f("temporalSmoothing") > 0.0 { hist_mean(&smoothed_histograms(ctx, img)[3]) } else { hist_mean(&histograms(img)[3]) } as f32;
s_amt = ((0.6 - mean) * 1.5).clamp(0.0, 1.0) * 0.8 + 0.1;
h_amt = ((mean - 0.6) * 1.5).clamp(0.0, 1.0) * 0.8;
}
(s_amt, h_amt)
}
/// The luminance values clipping `black` / `white` fractions of `img`'s alpha-weighted
/// luminance histogram (Shadow/Highlight's Black / White Clip).
pub fn luma_clip_points(img: &Image, black: f64, white: f64) -> (f32, f32) {
clip_points(&histograms(img)[3], black, white)
}
fn shadow_highlight(ctx: &EffectCtx, mut b: Buf) -> Buf {
let (s_amt, h_amt) = shadow_highlight_amounts(ctx, &b.img);
let luma = Plane::luma(&b.img);
let (bc, wc) = (ctx.params.f("moreOptions/blackClip").clamp(0.0, 49.0) / 100.0, ctx.params.f("moreOptions/whiteClip").clamp(0.0, 49.0) / 100.0);
let clip0 = (bc > 0.0 || wc > 0.0).then(|| luma_clip_points(&b.img, bc, wc));
let s_tw = (ctx.params.f("moreOptions/shadowTonalWidth") as f32 / 100.0).max(0.01);
let h_tw = (ctx.params.f("moreOptions/highlightTonalWidth") as f32 / 100.0).max(0.01);
let s_r = ctx.params.f("moreOptions/shadowRadius").max(0.0) * b.scale / 2.0;
@ -478,10 +507,8 @@ fn shadow_highlight(ctx: &EffectCtx, mut b: Buf) -> Buf {
// Black / White Clip: the adjusted image's extremes (ignoring those fractions of pixels)
// are mapped back onto the original's, so lifting shadows or recovering highlights doesn't
// flatten the ends of the tonal range.
let (bc, wc) = (ctx.params.f("moreOptions/blackClip").clamp(0.0, 49.0) / 100.0, ctx.params.f("moreOptions/whiteClip").clamp(0.0, 49.0) / 100.0);
if bc > 0.0 || wc > 0.0 {
let (lo0, hi0) = clip_points(&src_hist[3], bc, wc);
let (lo1, hi1) = clip_points(&histograms(&b.img)[3], bc, wc);
if let Some((lo0, hi0)) = clip0 {
let (lo1, hi1) = luma_clip_points(&b.img, bc, wc);
if (lo0, hi0) != (lo1, hi1) && hi1 - lo1 > 1e-3 {
let k = (hi0 - lo0) / (hi1 - lo1);
map_ca(&mut b.img, |c, a| (c.map(|v| (lo0 + (v - lo1) * k).max(0.0)), a));

View File

@ -354,7 +354,7 @@ fn magnify(ctx: &EffectCtx, mut b: Buf) -> Buf {
}
/// Magnify's Blending Mode choices after "None".
const MAGNIFY_MODES: [BlendMode; 17] = [
pub const MAGNIFY_MODES: [BlendMode; 17] = [
BlendMode::Normal,
BlendMode::Add,
BlendMode::Multiply,

View File

@ -325,7 +325,9 @@ pub fn build_mesh(strokes: &[LiquifyStroke], w: f64, h: f64, frozen: &(dyn Fn(f6
type MeshCache = Mutex<HashMap<u64, Arc<Mesh>>>;
fn cached_mesh(ctx: &EffectCtx) -> Arc<Mesh> {
/// Liquify's displacement mesh for the instance (replayed strokes, cached; also used by the
/// GPU kernels).
pub fn liquify_mesh(ctx: &EffectCtx) -> Arc<Mesh> {
use std::hash::{Hash, Hasher};
let text = ctx.params.s("distortionMesh");
let mask = ctx.params.f("freezeAreaMask").round() as usize;
@ -369,7 +371,7 @@ fn liquify(ctx: &EffectCtx, mut b: Buf) -> Buf {
if ctx.params.s("distortionMesh").trim().is_empty() && !show_freeze && !show_mesh {
return b;
}
let mesh = cached_mesh(ctx);
let mesh = liquify_mesh(ctx);
let off = ctx.params.v2("distortionMeshOffset");
let mesh_color = ctx.params.color("meshColor");
let src = b.img.clone();

View File

@ -67,11 +67,13 @@ use std::sync::OnceLock;
pub use card3d::{CompLight, CompScene};
pub use color_fx::{
HUESAT_CHANNELS, LEVELS_CHANNELS, exposure_settings, fill_uses_masks, huesat_ranges_identity, levels_channel_ids, levels_channels_identity, levels_clip,
HUESAT_CHANNELS, HUESAT_RANGES, LEVELS_CHANNELS, PHOTO_FILTER_CUSTOM, PHOTO_FILTERS, exposure_settings, fill_uses_masks, huesat_ranges_identity,
levels_channel_ids, levels_channel_settings, levels_channels_identity, levels_clip,
};
pub use color2::Curve;
pub use color2::{Curve, auto_correct_settings, equalize_tables, luma_clip_points, shadow_highlight_amounts};
pub use distort::transform_shutter;
pub use distort2::parse_mesh;
pub use distort2::{MAGNIFY_MODES, parse_mesh};
pub use distort4::liquify_mesh;
use effectcraft_keyframe::Value;
use effectcraft_project::build::Ids;
use effectcraft_project::{GroupKind, ParamUi, PropGroup, Property};
@ -79,12 +81,19 @@ pub use effectcraft_raster::{AuxChannels, Image};
pub use misc::{INVERT_ALPHA, INVERT_CHANNELS, glow_operation};
pub use sim::particle_state;
pub use sim3::playground_state;
pub use stylize2::strobe_on;
// CPU helpers the GPU kernels share (effectcraft-gpu).
pub use blur2::{camera_lens_plain, camera_lens_spans};
pub use generate::gen_mode;
pub use generate2::pattern_kind as cell_pattern_kind;
pub use noise::GrainLook;
pub use transition::place_layer;
// effectcraft-gpu fx_noise.
pub use noise::remove_grain_level;
pub use noise3::{FractalGpu, fractal_gpu};
pub use time_fx::{posterized_time, time_frames};
// effectcraft-gpu fx_tone.
pub use ocio::color_stabilizer_maps;
/// Effect categories in Effects & Presets order.
pub const CATEGORIES: &[&str] = &[
@ -806,6 +815,103 @@ pub const GPU_EFFECTS: &[&str] = &[
"ec.color.selectivecolor",
"ec.key.linearcolor",
"ec.keying.keylight",
// effectcraft-gpu fx_key (keying, matte, channel)
"ec.key.colorkey",
"ec.key.luma",
"ec.key.colorrange",
"ec.key.extract",
"ec.key.differencematte",
"ec.key.colordifference",
"ec.key.screen",
"ec.key.spill",
"ec.key.advancedspill",
"ec.key.keycleaner",
"ec.key.unmult",
"ec.matte.simplechoker",
"ec.matte.mattechoker",
"ec.matte.refinesoft",
"ec.matte.refinehard",
"ec.channel.setmatte",
"ec.channel.setchannels",
"ec.channel.shiftchannels",
"ec.channel.removecolormatting",
"ec.channel.arithmetic",
"ec.channel.solidcomposite",
"ec.channel.combiner",
"ec.channel.blend",
"ec.channel.calculations",
"ec.channel.compoundarithmetic",
// effectcraft-gpu fx_tone (colour correction)
"ec.color.levelsic",
"ec.color.gammapedestalgain",
"ec.color.photofilter",
"ec.color.changecolor",
"ec.color.changetocolor",
"ec.color.leavecolor",
"ec.color.broadcast",
"ec.color.colorbalancehls",
"ec.color.videolimiter",
"ec.color.psarbitrarymap",
"ec.color.cctoner",
"ec.color.cccoloroffset",
"ec.color.cckernel",
"ec.color.autolevels",
"ec.color.autocontrast",
"ec.color.autocolor",
"ec.color.equalize",
"ec.color.shadowhighlight",
"ec.color.cccolorneutralizer",
"ec.color.colorstabilizer",
// effectcraft-gpu fx_stylize (stylize, distort)
"ec.stylize.posterize",
"ec.stylize.threshold",
"ec.stylize.ccthreshold",
"ec.stylize.ccthresholdrgb",
"ec.stylize.strobe",
"ec.stylize.ccvignette",
"ec.stylize.scatter",
"ec.stylize.brushstrokes",
"ec.stylize.roughenedges",
"ec.stylize.texturize",
"ec.stylize.motiontile",
"ec.stylize.cckaleida",
"ec.stylize.ccrepetile",
"ec.distort.mirror",
"ec.distort.offset",
"ec.distort.polar",
"ec.distort.spherize",
"ec.distort.cornerpin",
"ec.distort.opticscompensation",
"ec.distort.magnify",
"ec.distort.ccslant",
"ec.distort.ccsmear",
"ec.distort.ccsplit",
"ec.distort.ccsplit2",
"ec.distort.cctiler",
"ec.distort.ccgriddler",
"ec.distort.liquify",
"ec.distort.twirllegacy",
"ec.distort.ccripplepulse",
"ec.distort.ccpowerpin",
"ec.distort.ccflomotion",
// effectcraft-gpu fx_noise (noise, blur, time)
"ec.noise.turbulent",
"ec.noise.median",
"ec.noise.medianlegacy",
"ec.noise.dustscratches",
"ec.noise.removegrain",
"ec.noise.noisealpha",
"ec.noise.noisehls",
"ec.noise.noisehlsauto",
"ec.blur.smart",
"ec.blur.bilateral",
"ec.blur.sharpen",
"ec.blur.unsharp",
"ec.blur.compound",
"ec.blur.channel",
"ec.channel.minimax",
"ec.time.echo",
"ec.time.posterizetime",
];
/// Effects whose output depends on [`EffectCtx::time`] directly (not only through animated

View File

@ -477,6 +477,28 @@ pub(crate) fn sampling_boxes(ctx: &EffectCtx, img: &Image, scale: f64) -> Vec<(u
sample_boxes(img, n, s)
}
/// The grain level (0.5..2) measured in the sample boxes of `img`.
fn grain_level(img: &Image, boxes: &[(usize, usize, usize)]) -> f32 {
let mut m = vec![false; img.data.len()];
let w = img.width as usize;
for &(x, y, s) in boxes {
for yy in y..y + s {
for xx in x..x + s {
m[yy * w + xx] = true;
}
}
}
let st = crate::noise2::grain_stats_masked(img, Some(&m));
let s = (st.std[0] + st.std[1] + st.std[2]) / 3.0;
if boxes.is_empty() { 1.0 } else { (s / 0.02).clamp(0.5, 2.0) }
}
/// Remove Grain's measured grain level for buffer `b` without Temporal Filtering (the GPU
/// kernels' spatial pass, effectcraft-gpu `fx_noise`).
pub fn remove_grain_level(ctx: &EffectCtx, b: &Buf) -> f32 {
grain_level(&b.img, &sampling_boxes(ctx, &b.img, b.scale))
}
fn remove_grain(ctx: &EffectCtx, mut b: Buf) -> Buf {
let pr = ctx.params;
let amt = pr.f("noiseReductionSettings/noiseReduction") as f32;
@ -535,20 +557,7 @@ fn remove_grain(ctx: &EffectCtx, mut b: Buf) -> Buf {
}
}
// The grain level measured in the sample boxes scales the smoothing.
let level = {
let mut m = vec![false; b.img.data.len()];
let w = b.img.width as usize;
for &(x, y, s) in &boxes {
for yy in y..y + s {
for xx in x..x + s {
m[yy * w + xx] = true;
}
}
}
let st = crate::noise2::grain_stats_masked(&b.img, Some(&m));
let s = (st.std[0] + st.std[1] + st.std[2]) / 3.0;
if boxes.is_empty() { 1.0 } else { (s / 0.02).clamp(0.5, 2.0) }
};
let level = grain_level(&b.img, &boxes);
let passes = pr.f("noiseReductionSettings/passes").round().clamp(1.0, 8.0) as usize;
let multichannel = pr.e("noiseReductionSettings/mode") == 0;
let texture = (pr.f("fineTuning/texture") as f32).clamp(0.0, 1.0);

View File

@ -326,6 +326,89 @@ impl Fractal {
}
}
/// Fractal / Turbulent Noise resolved for the GPU kernel (effectcraft-gpu `fx_noise`): the
/// per-pixel constants and, per octave, the terms [`Fractal::raw`] computes before touching the
/// pixel, in the same f32 order.
pub struct FractalGpu {
pub kind: u32,
pub noise_type: u32,
pub seed: u32,
pub invert: bool,
pub overflow: u32,
pub contrast: f32,
pub brightness: f32,
/// Rotation (sin, cos).
pub rot: (f32, f32),
pub size: [f32; 2],
/// Cycle Evolution: (t, cycle, t / cycle).
pub cycle: Option<[f32; 3]>,
pub sub_rotation: bool,
/// Σ amplitude × weight (the normaliser).
pub norm: f32,
/// Per octave: origin x, y, sub-rotation sin, cos, frequency, amplitude, weight, evolution
/// (turbulence applied), z offset, Dynamic Progressive warp factor.
pub octaves: Vec<[f32; 10]>,
/// Blending Mode (`None` = the noise alone), Opacity and Blend With Original.
pub mode: Option<BlendMode>,
pub opacity: f32,
pub blend: f32,
}
/// [`FractalGpu`] for a buffer with `offset` / `scale`.
pub fn fractal_gpu(ctx: &EffectCtx, offset: [f64; 2], scale: f64, turbulent: bool) -> FractalGpu {
let b = Buf { img: effectcraft_raster::Image::new(0, 0), offset, scale };
let fr = Fractal::from_params(ctx, &b, turbulent);
let n_oct = fr.octaves.ceil() as usize;
let frac = fr.octaves - fr.octaves.floor();
let (mut norm, mut amp, mut f, mut depth) = (0.0f32, 1.0f32, 1.0f32, 1.0f32);
let mut octaves = Vec::with_capacity(n_oct);
for o in 0..n_oct {
let w = if o + 1 == n_oct && frac > 0.0 { frac } else { 1.0 };
let of = o as f32;
let k = if fr.perspective { depth } else { 1.0 } * (1.0 + fr.turbulence * of);
let mut org = fr.origin;
if k != 1.0 {
org = [fr.center[0] + (fr.origin[0] - fr.center[0]) * k, fr.center[1] + (fr.origin[1] - fr.center[1]) * k];
}
if !fr.center_subscale {
org = [org[0] + fr.sub_offset[0] * of, org[1] + fr.sub_offset[1] * of];
}
let (s, c) = (fr.sub_rotation * of).sin_cos();
let evo = fr.evolution * (1.0 + fr.turbulence * of);
let dk = if fr.kind == 6 { 0.5 + of * 0.35 } else { 1.0 };
octaves.push([org[0], org[1], s, c, f, amp, w, evo, of * 7.31, dk]);
norm += amp * w;
amp *= fr.influence;
f /= fr.sub_scaling;
depth *= fr.sub_scaling;
}
let cycle = fr.cycle.map(|c| {
let t = fr.evolution.rem_euclid(c);
[t, c, t / c]
});
FractalGpu {
kind: fr.kind,
noise_type: fr.noise_type,
seed: fr.seed,
invert: fr.invert,
overflow: fr.overflow,
contrast: fr.contrast,
brightness: fr.brightness,
rot: fr.rot,
size: fr.size,
cycle,
sub_rotation: fr.sub_rotation != 0.0,
norm,
octaves,
mode: match ctx.params.get("blendingMode").map(Value::as_enum).unwrap_or(BLEND_NONE) {
BLEND_NONE => None,
m => Some(BLEND_MODES.get(m as usize).and_then(|l| BlendMode::from_name(l)).unwrap_or(BlendMode::Normal)),
},
opacity: (ctx.params.f("opacity") as f32 / 100.0).clamp(0.0, 1.0),
blend: ctx.params.f("blend") as f32 / 100.0,
}
}
/// Map a value by an [`OVERFLOWS`] option.
pub fn overflow(v: f32, mode: u32) -> f32 {
match mode {

View File

@ -905,6 +905,46 @@ fn curve3(xs: [f64; 3], ys: [f64; 3], v: f64) -> f64 {
a.1 + (c.1 - a.1) * (v - a.0) / (c.0 - a.0).max(1e-9)
}
/// Color Stabilizer's correction of buffer `b` as one piecewise-linear map per channel (points
/// with increasing x; the end segments extend), the same maps [`color_stabilizer`] applies.
/// `None` without a host or reference frame (the layer passes through). (The GPU effect.)
pub fn color_stabilizer_maps(ctx: &EffectCtx, b: &Buf) -> Option<[Vec<(f64, f64)>; 3]> {
let reference = ctx.env.host?.self_at(ctx.params.f("referenceFrame"), ctx.env.effect_index)?;
let r = ctx.params.f("sampleSize").max(0.5);
let pts = [ctx.params.v2("blackPoint"), ctx.params.v2("midPoint"), ctx.params.v2("whitePoint")];
let cur = pts.map(|pt| sample_mean(b, pt, r));
let refv = pts.map(|pt| sample_mean(&reference, pt, r));
let mode = ctx.params.e("stabilize");
Some([0, 1, 2].map(|k| {
let offset = |c0: f64, r0: f64| vec![(0.0, r0 - c0), (1.0, 1.0 + r0 - c0)];
match mode {
0 => offset(cur[0][k], refv[0][k]),
1 => {
let (c0, c1, r0, r1) = (cur[0][k], cur[2][k], refv[0][k], refv[2][k]);
if (c1 - c0).abs() < 1e-6 {
offset(c0, r0)
} else if c0 < c1 {
vec![(c0, r0), (c1, r1)]
} else {
vec![(c1, r1), (c0, r0)]
}
}
_ => {
let mut v = vec![(0.0, 0.0)];
let mut pairs: Vec<(f64, f64)> = (0..3).map(|i| (cur[i][k], refv[i][k])).collect();
pairs.sort_by(|a, b| a.0.total_cmp(&b.0));
for (x, y) in pairs {
if x > v.last().expect("non-empty").0 + 1e-4 && x < 1.0 - 1e-4 {
v.push((x, y));
}
}
v.push((1.0, 1.0));
v
}
}
}))
}
fn color_stabilizer(ctx: &EffectCtx, mut b: Buf) -> Buf {
let Some(host) = ctx.env.host else { return b };
let Some(reference) = host.self_at(ctx.params.f("referenceFrame"), ctx.env.effect_index) else { return b };

View File

@ -223,7 +223,8 @@ fn scatter(ctx: &EffectCtx, mut b: Buf) -> Buf {
// ---- Strobe Light ----
pub(crate) fn strobe_on(time: f64, duration: f64, period: f64, prob: f64, seed: u32) -> bool {
/// Whether Strobe Light flashes at `time` (also used by the GPU kernels).
pub fn strobe_on(time: f64, duration: f64, period: f64, prob: f64, seed: u32) -> bool {
if period <= 0.0 {
return false;
}

View File

@ -219,6 +219,12 @@ fn fetch(ctx: &EffectCtx, times: &[f64]) -> Option<Frames> {
Some(Frames { grid, imgs })
}
/// The layer's frames at layer times `times` placed on one grid (the GPU Echo / Posterize Time,
/// effectcraft-gpu `fx_noise`): (grid buffer, one image per time). `None` without a host.
pub fn time_frames(ctx: &EffectCtx, times: &[f64]) -> Option<(Buf, Vec<Image>)> {
fetch(ctx, times).map(|f| (f.grid, f.imgs))
}
fn with_img(grid: Buf, img: Image) -> Buf {
Buf { img, ..grid }
}

View File

@ -154,6 +154,10 @@ impl GpuContext {
include_str!("shaders/fx_color.wgsl"),
include_str!("shaders/fx_distort.wgsl"),
include_str!("shaders/fx_generate.wgsl"),
include_str!("shaders/fx_key.wgsl"),
include_str!("shaders/fx_stylize.wgsl"),
include_str!("shaders/fx_noise.wgsl"),
include_str!("shaders/fx_tone.wgsl"),
]
.concat();
let module =
@ -207,6 +211,10 @@ impl GpuContext {
.chain(crate::fx_color::KERNELS)
.chain(crate::fx_distort::KERNELS)
.chain(crate::fx_generate::KERNELS)
.chain(crate::fx_key::KERNELS)
.chain(crate::fx_stylize::KERNELS)
.chain(crate::fx_noise::KERNELS)
.chain(crate::fx_tone::KERNELS)
.map(|e| (e, &layout))
.chain(EXT_ENTRIES.iter().map(|e| (e, &layout_ext)))
.map(|(e, layout)| {

View File

@ -113,14 +113,24 @@ fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
"ec.blur.gaussian" => gaussian(e, ctx, b),
"ec.blur.fastbox" => box_blur(e, ctx, b),
"ec.blur.directional" => directional(e, ctx, b),
"ec.stylize.glow" if crate::fx_stylize::glow_extra(ctx) => crate::fx_stylize::glow(e, ctx, b),
"ec.distort.transform" if crate::fx_stylize::transform_blur(ctx) => crate::fx_stylize::transform(e, ctx, b),
"ec.stylize.glow" => glow(e, ctx, b),
"ec.perspective.dropshadow" => drop_shadow(e, ctx, b),
"ec.distort.transform" => transform(e, ctx, b),
"ec.color.curves" => curves(e, ctx, b),
"ec.color.huesaturation" if !effectcraft_effects::huesat_ranges_identity(ctx) => crate::fx_tone::hue_saturation(e, ctx, b),
"ec.color.levels" if !effectcraft_effects::levels_channels_identity(ctx) => crate::fx_tone::levels(e, ctx, b),
"ec.channel.invert" if (4..=11).contains(&ctx.params.e("channel")) => crate::fx_tone::invert(e, ctx, b),
"ec.noise.fractal" if crate::fx_noise::fractal_extra(ctx) => crate::fx_noise::fractal(e, ctx, b, false),
_ if id.starts_with("ec.control.") => Some(b),
_ if crate::fx_color::IDS.contains(&id) => crate::fx_color::apply(e, id, ctx, b),
_ if crate::fx_distort::IDS.contains(&id) => crate::fx_distort::apply(e, id, ctx, b),
_ if crate::fx_generate::IDS.contains(&id) => crate::fx_generate::apply(e, id, ctx, b),
_ if crate::fx_key::IDS.contains(&id) => crate::fx_key::apply(e, id, ctx, b),
_ if crate::fx_stylize::IDS.contains(&id) => crate::fx_stylize::apply(e, id, ctx, b),
_ if crate::fx_noise::IDS.contains(&id) => crate::fx_noise::apply(e, id, ctx, b),
_ if crate::fx_tone::IDS.contains(&id) => crate::fx_tone::apply(e, id, ctx, b),
_ => pointwise(e, id, ctx, b),
}
}

View File

@ -235,7 +235,7 @@ fn morph(e: &mut Enc, img: &GpuImage, r: u32, modes: [u32; 4]) -> GpuImage {
}
/// util::morph_frac (blend between the two nearest integer radii).
fn morph_frac(e: &mut Enc, img: &GpuImage, radius: f64, modes: [u32; 4]) -> GpuImage {
pub(crate) fn morph_frac(e: &mut Enc, img: &GpuImage, radius: f64, modes: [u32; 4]) -> GpuImage {
if radius <= 0.0 {
return img.clone();
}
@ -253,8 +253,8 @@ fn morph_frac(e: &mut Enc, img: &GpuImage, radius: f64, modes: [u32; 4]) -> GpuI
out
}
const ERODE_X: [u32; 4] = [0, 2, 2, 2];
const DILATE_X: [u32; 4] = [1, 2, 2, 2];
pub(crate) const ERODE_X: [u32; 4] = [0, 2, 2, 2];
pub(crate) const DILATE_X: [u32; 4] = [1, 2, 2, 2];
// ---------------------------------------------------------------- Key Light

630
crates/gpu/src/fx_key.rs Normal file
View File

@ -0,0 +1,630 @@
//! GPU effects, keying, matte and channel family (kernels in `shaders/fx_key.wgsl`, every entry
//! point prefixed `fxk_`): the CPU effects' exact steps with the pixel loops as compute kernels.
//!
//! The keyers (Color Key, Luma Key, Color Range, Extract, Difference Matte, Color Difference
//! Key, Screen Key) build their matte in a plane pass, run the CPU's edge thin (morphology) and
//! feather (Gaussian) on it, and apply it in a second pass; spill suppression, Unmult and the
//! channel effects are one per-pixel pass (`fxk_point`). Simple / Matte Choker and Key Cleaner
//! work on the alpha plane and put it back with `util::set_alpha` (straight colour kept, the
//! transparent pixels' colour from a small blur). Advanced Spill Suppressor's Standard method
//! finds the dominant key primary with a GPU sum reduction (`fxk_reduce`). Other layers (layer
//! parameters) are resampled into the buffer like `util::fit_layer` (`fxk_fit`).
use effectcraft_color::rgb_to_hsl;
use effectcraft_effects::EffectCtx;
use effectcraft_effects::util::{SRC_ORDER, Src};
use crate::context::{Enc, GpuImage, Params};
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`.
pub(crate) const KERNELS: &[&str] = &["fxk_point", "fxk_fit", "fxk_reduce"];
/// Effect ids implemented here.
pub(crate) const IDS: &[&str] = &[
"ec.key.colorkey",
"ec.key.luma",
"ec.key.colorrange",
"ec.key.extract",
"ec.key.differencematte",
"ec.key.colordifference",
"ec.key.screen",
"ec.key.spill",
"ec.key.advancedspill",
"ec.key.keycleaner",
"ec.key.unmult",
"ec.matte.simplechoker",
"ec.matte.mattechoker",
"ec.matte.refinesoft",
"ec.matte.refinehard",
"ec.channel.setmatte",
"ec.channel.setchannels",
"ec.channel.shiftchannels",
"ec.channel.removecolormatting",
"ec.channel.arithmetic",
"ec.channel.solidcomposite",
"ec.channel.combiner",
"ec.channel.blend",
"ec.channel.calculations",
"ec.channel.compoundarithmetic",
];
/// 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.key.colorkey" | "ec.key.luma" | "ec.key.colorrange" | "ec.key.extract" => matte_key(e, id, ctx, b),
"ec.key.differencematte" => difference_matte(e, ctx, b),
"ec.key.screen" => screen_key(e, ctx, b),
"ec.key.advancedspill" => advanced_spill(e, ctx, b),
"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),
}
}
// ---------------------------------------------------------------- helpers
/// One `fxk_point` pass over `src`'s size.
fn run(e: &mut Enc, p: &Params, src: &GpuImage, aux: Option<&GpuImage>, data: Option<&wgpu::Buffer>) -> GpuImage {
let out = e.image(src.width, src.height);
e.pixels("fxk_point", p, src, aux, &out, data);
out
}
fn op(code: u32) -> Params {
let mut p = Params::default();
p.u[0][0] = code;
p
}
fn src_index(s: Src) -> u32 {
SRC_ORDER.iter().position(|x| *x == s).unwrap_or(10) as u32
}
/// Layer parameter `id` resampled into the buffer's pixel grid (`util::fit_layer`):
/// `Some(None)` when no layer is chosen (the effect then uses its own layer).
fn fitted(e: &mut Enc, ctx: &EffectCtx, b: &GBuf, id: &str, masks_and_effects: bool, stretch: bool) -> Option<Option<GpuImage>> {
let Some(o) = ctx.layer_param(id, masks_and_effects) else { return Some(None) };
let (w, h) = (b.img.width, b.img.height);
if o.buf.img.is_empty() {
return Some(Some(e.image(w, h)));
}
let ls = ctx.layer_size;
let os = o.size;
let (sx, sy) = if stretch && ls[0] > 0.0 && ls[1] > 0.0 { (os[0] / ls[0], os[1] / ls[1]) } else { (1.0, 1.0) };
let (dx, dy) = if stretch { (0.0, 0.0) } else { ((os[0] - ls[0]) * 0.5, (os[1] - ls[1]) * 0.5) };
let inv = 1.0 / b.scale.max(1e-9);
// Per column / row: the other layer's buffer coordinate.
let mut data = Vec::with_capacity((w + h) as usize);
data.extend((0..w).map(|x| (((x as f64 + 0.5 - b.offset[0]) * inv * sx + dx) * o.buf.scale + o.buf.offset[0]) as f32));
data.extend((0..h).map(|y| (((y as f64 + 0.5 - b.offset[1]) * inv * sy + dy) * o.buf.scale + o.buf.offset[1]) as f32));
let src = e.g.upload_image(&o.buf.img)?;
let buf = e.data(&data);
let out = e.image(w, h);
e.pixels("fxk_fit", &Params::default(), &src, None, &out, Some(&buf));
Some(Some(out))
}
/// `util::layer_or_self`.
fn layer_or_self(e: &mut Enc, ctx: &EffectCtx, b: &GBuf, id: &str, stretch: bool) -> Option<GpuImage> {
Some(fitted(e, ctx, b, id, true, stretch)?.unwrap_or_else(|| b.img.clone()))
}
/// keying::thin_feather on a matte plane (x channel).
fn thin_feather(e: &mut Enc, m: GpuImage, thin: f64, feather: f64) -> GpuImage {
let m = if thin > 0.0 {
morph_frac(e, &m, thin, ERODE_X)
} else if thin < 0.0 {
morph_frac(e, &m, -thin, DILATE_X)
} else {
m
};
if feather > 0.0 { gaussian_blur(e, &m, feather * 0.5, feather * 0.5, true) } else { m }
}
/// keying::apply_matte (view 0 final, 1 source, 2 matte).
fn apply_matte(e: &mut Enc, b: GBuf, m: &GpuImage, view: u32) -> GBuf {
if view == 1 {
return b;
}
let mut p = op(5);
p.u[0][1] = view;
let img = run(e, &p, &b.img, Some(m), None);
GBuf { img, ..b }
}
/// keying::primary.
fn primary(c: [f32; 4]) -> [u32; 3] {
if c[1] >= c[0] && c[1] >= c[2] {
[1, 0, 2]
} else if c[2] >= c[0] && c[2] >= c[1] {
[2, 0, 1]
} else {
[0, 1, 2]
}
}
/// util::set_alpha with the new alpha in `na`'s x channel.
fn set_alpha(e: &mut Enc, img: &GpuImage, na: &GpuImage) -> GpuImage {
let fill = gaussian_blur(e, img, 3.0, 3.0, true);
let (rows, row) = e.image_rows(&fill);
let mut p = op(27);
p.u[3][0] = row;
run(e, &p, img, Some(na), Some(&rows))
}
// ---------------------------------------------------------------- keyers
fn matte_key(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let pr = ctx.params;
let f = |k: &str| pr.f(k) as f32;
let mut p = Params::default();
let mut thin = 0.0;
let mut feather = 0.0;
match id {
"ec.key.colorkey" => {
let k = pr.color("keyColor");
p.u[0][0] = 1;
p.f[0] = [k[0], k[1], k[2], f("colorTolerance") / 255.0];
thin = pr.f("edgeThin") * b.scale;
feather = pr.f("edgeFeather") * b.scale;
}
"ec.key.luma" => {
p.u[0] = [2, pr.e("keyType"), 0, 0];
p.f[0] = [f("threshold") / 255.0, f("tolerance") / 255.0, 0.0, 0.0];
thin = pr.f("edgeThin") * b.scale;
feather = pr.f("edgeFeather") * b.scale;
}
"ec.key.colorrange" => {
p.u[0] = [3, pr.e("colorSpace"), 0, 0];
p.f[0] = [f("minL"), f("minA"), f("minB"), 0.0];
p.f[1] = [f("maxL"), f("maxA"), f("maxB"), 0.0];
p.f[2][0] = f("fuzziness");
}
_ => {
p.u[0] = [4, pr.e("channel"), pr.b("invert") as u32, 0];
p.f[0] = [f("blackPoint"), f("whitePoint"), f("blackSoftness"), f("whiteSoftness")];
}
}
let m = run(e, &p, &b.img, None, None);
let m = thin_feather(e, m, thin, feather);
Some(apply_matte(e, b, &m, 0))
}
fn difference_matte(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let pr = ctx.params;
let view = pr.e("view");
if view == 1 {
return Some(b);
}
let blur = pr.f("blurBeforeDifference") * b.scale;
let other = layer_or_self(e, ctx, &b, "differenceLayer", pr.e("ifLayerSizesDiffer") == 1)?;
let (src, other) = if blur > 0.05 {
(gaussian_blur(e, &b.img, blur * 0.5, blur * 0.5, true), gaussian_blur(e, &other, blur * 0.5, blur * 0.5, true))
} else {
(b.img.clone(), other)
};
let mut p = op(6);
p.f[0] = [(pr.f("matchingTolerance") / 100.0) as f32, (pr.f("matchingSoftness") / 100.0) as f32, 0.0, 0.0];
let m = run(e, &p, &src, Some(&other), None);
Some(apply_matte(e, b, &m, if view == 2 { 3 } else { 4 }))
}
/// keying::secondary's screen-key matte and composite.
fn screen_key(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let pr = ctx.params;
let view = pr.e("view");
if view == 3 {
return Some(b);
}
let sc = pr.color("screenColor");
let ix = primary(sc);
let (pi, o1, o2) = (ix[0] as usize, ix[1] as usize, ix[2] as usize);
let gain = pr.f("screenGain") as f32 / 100.0;
let bal = pr.f("screenBalance") as f32 / 100.0;
let cb = pr.f("clipBlack") as f32 / 100.0;
let cw = pr.f("clipWhite") as f32 / 100.0;
let preblur = pr.f("screenPreblur") * b.scale;
let sg = pr.f("screenShrinkGrow") * b.scale;
let soft = pr.f("screenSoftness") * b.scale;
let (hi, lo) = if sc[o1] > sc[o2] { (sc[o1], sc[o2]) } else { (sc[o2], sc[o1]) };
let sd = (sc[pi] - (bal * hi + (1.0 - bal) * lo)).max(1e-3);
let key_src = if preblur > 0.0 { gaussian_blur(e, &b.img, preblur * 0.5, preblur * 0.5, true) } else { b.img.clone() };
let mut p = op(22);
p.u[1] = [ix[0], ix[1], ix[2], 0];
p.f[0] = [sd, gain, bal, cb];
p.f[1][0] = cw;
let mut m = run(e, &p, &key_src, None, None);
if sg < 0.0 {
m = morph_frac(e, &m, -sg, ERODE_X);
} else if sg > 0.0 {
m = morph_frac(e, &m, sg, DILATE_X);
}
if soft > 0.0 {
m = gaussian_blur(e, &m, soft * 0.5, soft * 0.5, true);
}
let mut p = op(23);
p.u[0] = [23, view, (pr.e("edgeColor") == 1) as u32, 0];
p.u[1] = [ix[0], ix[1], ix[2], 0];
p.f[0] = [sc[0], sc[1], sc[2], bal];
p.f[1][0] = pr.f("despill") as f32 / 100.0;
let img = run(e, &p, &b.img, Some(&m), None);
Some(GBuf { img, ..b })
}
fn advanced_spill(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let pr = ctx.params;
let ultra = pr.e("method") == 1;
let key = pr.color("ultraSettings/keyColor");
let mut p = op(8);
let sums = if ultra {
let ix = primary(key);
p.u[1] = [ix[0], ix[1], ix[2], 0];
None
} else {
// Standard: the dominant key primary over the whole buffer (sum reduction).
p.u[1][3] = 1;
let mut cur = b.img.clone();
let mut first = true;
while first || cur.width > 1 || cur.height > 1 {
let out = e.image(cur.width.div_ceil(16), cur.height.div_ceil(16));
let mut rp = Params::default();
rp.u[0][0] = first as u32;
e.pixels("fxk_reduce", &rp, &cur, None, &out, None);
cur = out;
first = false;
}
Some(cur)
};
let tol = if ultra { pr.get("ultraSettings/tolerance").map(|v| v.as_f64()).unwrap_or(100.0) as f32 / 100.0 } else { 1.0 };
let f = |k: &str| pr.f(k) as f32 / 100.0;
let (desat, neutral) = if ultra { (f("ultraSettings/desaturate"), f("ultraSettings/spillColorCorrection")) } else { (0.0, 0.0) };
p.f[0] = [f("suppression"), f("ultraSettings/spillRange"), f("ultraSettings/lumaCorrection"), tol];
p.f[1] = [desat, neutral, rgb_to_hsl(key[0], key[1], key[2]).0, 0.0];
let img = run(e, &p, &b.img, sums.as_ref(), None);
Some(GBuf { img, ..b })
}
fn key_cleaner(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let pr = ctx.params;
let r = (pr.f("additionalEdgeRadius") * b.scale).max(0.0);
let strength = pr.f("strength") as f32 / 100.0;
let chatter = pr.b("reduceChatter");
let mut a = run(e, &op(24), &b.img, None, None);
if chatter {
a = gaussian_blur(e, &a, 1.0, 1.0, true);
}
let est = (r > 0.0 && strength > 0.0).then(|| gaussian_blur(e, &b.img, r * 0.5, r * 0.5, true));
let rows = est.as_ref().map(|img| e.image_rows(img));
let mut p = op(25);
p.u[0][1] = chatter as u32;
p.u[3] = [rows.as_ref().map_or(0, |r| r.1), rows.is_some() as u32, 0, 0];
p.f[0] = [pr.f("alphaContrast") as f32 / 100.0, strength, 0.0, 0.0];
let img = run(e, &p, &b.img, Some(&a), rows.as_ref().map(|r| &r.0));
Some(GBuf { img, ..b })
}
// ---------------------------------------------------------------- mattes
fn simple_choker(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
let c = ctx.params.f("chokeMatte") * b.scale;
if c.abs() > 1e-6 {
if c < 0.0 && !ctx.adjustment {
b.pad(e, (-c).ceil() as u32 + 1)?;
}
let a = run(e, &op(24), &b.img, None, None);
let na = if c > 0.0 { morph_frac(e, &a, c, ERODE_X) } else { morph_frac(e, &a, -c, DILATE_X) };
b.img = set_alpha(e, &b.img, &na);
}
if ctx.params.e("view") == 1 {
b.img = run(e, &op(28), &b.img, None, None);
}
Some(b)
}
fn matte_choker(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
let pr = ctx.params;
let g1 = pr.f("geometricSoftness1") * b.scale;
let c1 = pr.f("choke1") as f32;
let s1 = pr.f("grayLevelSoftness1") as f32 / 100.0;
let g2 = pr.f("geometricSoftness2") * b.scale;
let c2 = pr.f("choke2") as f32;
let s2 = pr.f("grayLevelSoftness2") as f32 / 100.0;
let it = pr.f("iterations").round().clamp(1.0, 100.0) as usize;
if (c1 < 0.0 || c2 < 0.0 || g1 > 0.0 || g2 > 0.0) && !ctx.adjustment {
b.pad(e, ((g1 + g2) * 1.5 * it as f64).ceil() as u32 + 1)?;
}
let mut a = run(e, &op(24), &b.img, None, None);
let stage = |e: &mut Enc, a: GpuImage, geo: f64, choke: f32, gray: f32| -> GpuImage {
let a = if geo > 0.0 { gaussian_blur(e, &a, geo * 0.5, geo * 0.5, true) } else { a };
let t = 0.5 + choke / 255.0 * 0.5;
let w = gray.max(1e-3);
if (t - 0.5).abs() < 1e-6 && (w - 1.0).abs() < 1e-6 {
return a;
}
let mut p = op(26);
p.f[0] = [t, w, 0.0, 0.0];
run(e, &p, &a, None, None)
};
for _ in 0..it {
a = stage(e, a, g1, c1, s1);
a = stage(e, a, g2, c2, s2);
}
b.img = set_alpha(e, &b.img, &a);
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"];
const SHIFT_ORDER: [Src; 11] =
[Src::Alpha, Src::Red, Src::Green, Src::Blue, Src::Luminance, Src::Hue, Src::Lightness, Src::Saturation, Src::Full, Src::Half, Src::Off];
const MATTE_ORDER: [Src; 10] = [Src::Red, Src::Green, Src::Blue, Src::Alpha, Src::Luminance, Src::Hue, Src::Lightness, Src::Saturation, Src::Full, Src::Off];
fn set_channels(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let pr = ctx.params;
let s = ["setRedTo", "setGreenTo", "setBlueTo", "setAlphaTo"].map(|id| src_index(effectcraft_effects::util::src_at(pr.e(id))));
let stretch = pr.b("stretchLayersToFit");
let mut layers = vec![];
for id in SET_CHANNEL_LAYERS {
layers.push(fitted(e, ctx, &b, id, true, stretch)?);
}
if layers.iter().all(Option::is_none) {
return route(e, b, s);
}
let mut vals = e.image(b.img.width, b.img.height);
for (k, layer) in layers.iter().enumerate() {
let mut p = op(20);
p.u[0] = [20, k as u32, s[k], 0];
vals = run(e, &p, &vals, Some(layer.as_ref().unwrap_or(&b.img)), None);
}
let img = run(e, &op(21), &vals, None, None);
Some(GBuf { img, ..b })
}
/// channel::route.
fn route(e: &mut Enc, b: GBuf, s: [u32; 4]) -> Option<GBuf> {
if s == [0, 1, 2, 3] {
return Some(b);
}
let mut p = op(11);
p.u[1] = s;
let img = run(e, &p, &b.img, None, None);
Some(GBuf { img, ..b })
}
fn point(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let pr = ctx.params;
let f = |k: &str| pr.f(k) as f32;
let mut p = Params::default();
let mut aux = None;
match id {
"ec.key.spill" => {
let k = pr.color("colorToSuppress");
let amt = f("suppression") / 100.0;
if pr.e("colorAccuracy") == 1 {
let kg = (k[0] + k[1] + k[2]) / 3.0;
let kc = [k[0] - kg, k[1] - kg, k[2] - kg];
let kn = (kc[0] * kc[0] + kc[1] * kc[1] + kc[2] * kc[2]).sqrt();
if kn < 1e-5 {
return Some(b);
}
p.u[0] = [7, 1, 0, 0];
p.f[0] = [kc[0] / kn, kc[1] / kn, kc[2] / kn, amt];
} else {
let ix = primary(k);
p.u[0] = [7, 0, 0, 0];
p.u[1] = [ix[0], ix[1], ix[2], 0];
p.f[0][3] = amt;
}
}
"ec.key.colordifference" => {
let view = pr.e("view");
if view == 0 {
return Some(b);
}
let accurate = pr.e("colorMatchingAccuracy") == 1;
let lin = |c: [f32; 3]| if accurate { c.map(|v| effectcraft_color::srgb_to_linear(v.clamp(0.0, 1.0))) } else { c };
let k = pr.color("keyColor");
let key = lin([k[0], k[1], k[2]]);
let ix = primary([key[0], key[1], key[2], 1.0]);
let (pi, o1, o2) = (ix[0] as usize, ix[1] as usize, ix[2] as usize);
let lv = |pre: &str| ["InBlack", "InWhite", "Gamma", "OutBlack", "OutWhite"].map(|s| f(&format!("{pre}{s}")));
let (pa, pb) = (lv("partialA"), lv("partialB"));
p.u[0] = [9, view, accurate as u32, 0];
p.u[1] = [ix[0], ix[1], ix[2], 0];
p.f[0] = [(key[pi] - key[o1]).max(1e-3), (key[pi] - key[o2]).max(1e-3), f("blackLevel"), f("whiteLevel")];
p.f[1] = [pa[0], pa[1], pa[2], pa[3]];
p.f[2] = [pa[4], pb[4], f("gamma"), 0.0];
p.f[3] = [pb[0], pb[1], pb[2], pb[3]];
}
"ec.key.unmult" => {
let white = pr.e("backgroundColor") == 1;
let level = if white { pr.f("whiteLevel") } else { pr.f("blackLevel") }.clamp(0.0, 100.0) as f32 / 100.0;
p.u[0] = [10, white as u32, pr.b("removeColorMatting") as u32, pr.b("clipHdrResults") as u32];
p.f[0] = [level, pr.f("softness").clamp(0.0, 100.0) as f32 / 100.0, 0.0, 0.0];
}
"ec.channel.shiftchannels" => {
let s = ["takeRedFrom", "takeGreenFrom", "takeBlueFrom", "takeAlphaFrom"].map(|id| src_index(SHIFT_ORDER[(pr.e(id) as usize).min(10)]));
return route(e, b, s);
}
"ec.channel.removecolormatting" => {
p.u[0] = [12, pr.b("clipHdr") as u32, 0, 0];
p.f[0] = pr.color("backgroundColor");
}
"ec.channel.arithmetic" => {
p.u[0] = [13, pr.e("operator"), pr.b("clip") as u32, 0];
p.f[0] = [f("redValue") / 255.0, f("greenValue") / 255.0, f("blueValue") / 255.0, 0.0];
}
"ec.channel.solidcomposite" => {
const MODES: [effectcraft_color::BlendMode; 12] = {
use effectcraft_color::BlendMode::*;
[Normal, Add, Multiply, Screen, Overlay, SoftLight, HardLight, Darken, Lighten, Difference, Color, Luminosity]
};
let c = pr.color("color");
let opa = (f("opacity") / 100.0 * c[3]).clamp(0.0, 1.0);
p.u[0] = [14, crate::ops::mode_id(MODES[(pr.e("blendingMode") as usize).min(11)]), 0, 0];
p.f[0] = [c[0] * opa, c[1] * opa, c[2] * opa, opa];
p.f[1][0] = f("sourceOpacity") / 100.0;
}
"ec.channel.setmatte" => {
let src = MATTE_ORDER[(pr.e("takeMatteFrom") as usize).min(9)];
aux = fitted(e, ctx, &b, "takeMatteFromLayer", true, pr.b("stretchMatteToFit"))?;
let pre_ok = !matches!(src, Src::Alpha | Src::Full | Src::Off);
let flags = pr.b("invertMatte") as u32
| (pr.b("compositeMatteWithOriginal") as u32) << 1
| ((pr.b("premultiplyMatteLayer") && pre_ok) as u32) << 2
| (aux.is_none() as u32) << 3
| ((src == Src::Alpha) as u32) << 4;
p.u[0] = [15, src_index(src), flags, 0];
}
"ec.channel.combiner" => {
if pr.b("useSecondLayer") {
aux = Some(layer_or_self(e, ctx, &b, "sourceLayer", true)?);
}
let flags = pr.b("invert") as u32 | (pr.b("solidAlpha") as u32) << 1 | (aux.is_some() as u32) << 2;
p.u[0] = [16, pr.e("from"), pr.e("to"), flags];
}
"ec.channel.blend" => {
let orig = (f("blendWithOriginal") / 100.0).clamp(0.0, 1.0);
if orig >= 1.0 {
return Some(b);
}
aux = Some(layer_or_self(e, ctx, &b, "blendWithLayer", pr.e("ifLayerSizesDiffer") == 1)?);
p.u[0] = [17, pr.e("mode"), 0, 0];
p.f[0][0] = orig;
}
"ec.channel.calculations" => {
const MODES: [effectcraft_color::BlendMode; 22] = {
use effectcraft_color::BlendMode::*;
[
Normal,
Darken,
Multiply,
ColorBurn,
LinearBurn,
Add,
Lighten,
Screen,
ColorDodge,
LinearDodge,
Overlay,
SoftLight,
HardLight,
LinearLight,
VividLight,
PinLight,
HardMix,
Difference,
Exclusion,
Hue,
Saturation,
Luminosity,
]
};
if pr.get("secondLayer").and_then(|v| v.as_layer()).is_some() {
aux = Some(layer_or_self(e, ctx, &b, "secondLayer", pr.b("stretchSecondLayerToFit"))?);
}
let flags =
pr.b("invertInput") as u32 | (pr.b("invertSecondLayer") as u32) << 1 | (pr.b("preserveTransparency") as u32) << 2 | (aux.is_some() as u32) << 3;
let mode = MODES[(pr.e("blendingMode") as usize).min(MODES.len() - 1)];
p.u[0] = [18, pr.e("inputChannel"), pr.e("secondLayerChannel"), crate::ops::mode_id(mode)];
p.u[1][0] = flags;
p.f[0][0] = (f("secondLayerOpacity") / 100.0).clamp(0.0, 1.0);
}
"ec.channel.compoundarithmetic" => {
let orig = (f("blendWithOriginal") / 100.0).clamp(0.0, 1.0);
if orig >= 1.0 {
return Some(b);
}
aux = Some(layer_or_self(e, ctx, &b, "secondSource", pr.b("stretchSecondSourceToFit"))?);
p.u[0] = [19, pr.e("operator"), pr.e("operateOnChannels"), pr.e("overflowBehavior")];
p.f[0][0] = orig;
}
_ => return None,
}
let img = run(e, &p, &b.img, aux.as_ref(), None);
Some(GBuf { img, ..b })
}

581
crates/gpu/src/fx_noise.rs Normal file
View File

@ -0,0 +1,581 @@
//! GPU effects, noise, blur and time family (kernels in `shaders/fx_noise.wgsl`, every entry point prefixed
//! `fxn_`): the CPU effects' exact steps with the pixel loops as compute kernels.
//!
//! Median, Median (Legacy) and Dust & Scratches take the median of the 512-level quantised
//! channels by bisection over the window (radius ≤ [`MEDIAN_MAX_R`] on the GPU). Fractal and
//! Turbulent Noise resolve every per-octave term on the CPU in the CPU's f32 order
//! (`effectcraft_effects::fractal_gpu`) and evaluate the pixels on the GPU. Echo and Posterize
//! Time fetch the layer's other frames through the effect host like the CPU does, then
//! accumulate on the GPU. Remove Grain measures the grain level in its sample boxes on the CPU
//! (one readback) and runs the guided filters, Texture, Unsharp Mask and Preview on the GPU;
//! Temporal Filtering and the Noise Samples / Blending Matte views render on the CPU.
use effectcraft_color::BlendMode;
use effectcraft_effects::{Buf, EffectCtx};
use effectcraft_keyframe::Value;
use crate::context::{Enc, GpuImage, Params};
use crate::effects::{GBuf, box_passes, gaussian_blur};
/// Compute entry points in `fx_noise.wgsl`.
pub(crate) const KERNELS: &[&str] = &[
"fxn_quant",
"fxn_median",
"fxn_median_out",
"fxn_edge",
"fxn_smart_edges",
"fxn_unsharp",
"fxn_pick",
"fxn_cblur_out",
"fxn_straight",
"fxn_premul",
"fxn_morph",
"fxn_crop",
"fxn_cb_map",
"fxn_cb_level",
"fxn_echo",
"fxn_echo_fin",
"fxn_fractal",
"fxn_noise_alpha",
"fxn_noise_hls",
"fxn_gf_guide",
"fxn_gf",
"fxn_rg_texture",
"fxn_rg_unsharp",
"fxn_rg_out",
];
/// Effect ids implemented here (Fractal Noise's other modes come through [`fractal_extra`]).
pub(crate) const IDS: &[&str] = &[
"ec.noise.turbulent",
"ec.noise.median",
"ec.noise.medianlegacy",
"ec.noise.dustscratches",
"ec.noise.removegrain",
"ec.noise.noisealpha",
"ec.noise.noisehls",
"ec.noise.noisehlsauto",
"ec.blur.smart",
"ec.blur.bilateral",
"ec.blur.sharpen",
"ec.blur.unsharp",
"ec.blur.compound",
"ec.blur.channel",
"ec.channel.minimax",
"ec.time.echo",
"ec.time.posterizetime",
];
/// Largest median radius (buffer pixels) the GPU takes; larger ones render on the CPU.
pub(crate) const MEDIAN_MAX_R: usize = 16;
/// 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.noise.turbulent" => fractal(e, ctx, b, true),
"ec.noise.median" | "ec.noise.medianlegacy" | "ec.noise.dustscratches" => median(e, id, ctx, b),
"ec.noise.removegrain" => remove_grain(e, ctx, b),
"ec.noise.noisealpha" => noise_alpha(e, ctx, b),
"ec.noise.noisehls" => noise_hls(e, ctx, b, ctx.params.f("noisePhase") as f32 / 360.0),
"ec.noise.noisehlsauto" => noise_hls(e, ctx, b, ctx.time as f32 * ctx.params.f("noiseAnimationSpeed") as f32),
"ec.blur.smart" => smart_blur(e, ctx, b),
"ec.blur.bilateral" => bilateral(e, ctx, b),
"ec.blur.sharpen" => {
let s = b.scale;
unsharp(e, b, ctx.params.f("amount") as f32 / 50.0, s, 0.0)
}
"ec.blur.unsharp" => {
let s = (ctx.params.f("radius") * b.scale).max(0.1);
unsharp(e, b, ctx.params.f("amount") as f32 / 100.0, s, ctx.params.f("threshold") as f32)
}
"ec.blur.compound" => compound_blur(e, ctx, b),
"ec.blur.channel" => channel_blur(e, ctx, b),
"ec.channel.minimax" => minimax(e, ctx, b),
"ec.time.echo" => echo(e, ctx, b),
"ec.time.posterizetime" => posterize_time(e, ctx, b),
_ => None,
}
}
/// A same-size per-pixel kernel.
fn run(e: &mut Enc, entry: &str, p: &Params, src: &GpuImage, aux: Option<&GpuImage>, data: Option<&wgpu::Buffer>) -> GpuImage {
let out = e.image(src.width, src.height);
e.pixels(entry, p, src, aux, &out, data);
out
}
fn with_u0(u: [u32; 4]) -> Params {
let mut p = Params::default();
p.u[0] = u;
p
}
// ---------------------------------------------------------------- Fractal / Turbulent Noise (noise3.rs)
/// Fractal Noise settings the `pointwise` kernel (effects.rs) leaves to the CPU, which this
/// family's kernel covers: every Noise and Fractal Type, Cycle Evolution, Perspective Offset,
/// Sub Rotation / Offset and all Blending Modes.
pub(crate) fn fractal_extra(ctx: &EffectCtx) -> bool {
let pr = ctx.params;
pr.e("noiseType") != 2
|| pr.e("fractalType") > 2
|| pr.e("blendingMode") > 1
|| pr.b("evolutionOptions/cycleEvolution")
|| pr.b("transform/perspectiveOffset")
|| pr.f("subSettings/subRotation") != 0.0
|| (!pr.b("subSettings/centerSubscale") && pr.v2("subSettings/subOffset") != [0.0, 0.0])
}
/// noise3::render.
pub(crate) fn fractal(e: &mut Enc, ctx: &EffectCtx, b: GBuf, turbulent: bool) -> Option<GBuf> {
let fr = effectcraft_effects::fractal_gpu(ctx, b.offset, b.scale, turbulent);
let mode = match fr.mode {
None => 255,
Some(m) => BlendMode::ALL.iter().position(|&x| x == m).unwrap_or(0) as u32,
};
let data: Vec<f32> = fr.octaves.iter().flatten().copied().collect();
let cycle = fr.cycle.unwrap_or([0.0; 3]);
let mut p = Params::default();
p.u[0] = [fr.kind, fr.noise_type, fr.octaves.len() as u32, fr.seed];
p.u[1] = [fr.sub_rotation as u32, fr.cycle.is_some() as u32, fr.invert as u32, fr.overflow];
p.u[2][0] = mode;
p.f[0] = [fr.rot.0, fr.rot.1, fr.size[0], fr.size[1]];
p.f[1] = [fr.contrast, fr.brightness, fr.opacity, fr.blend];
p.f[2] = [cycle[0], cycle[1], cycle[2], fr.norm];
let buf = e.data(&data);
let img = run(e, "fxn_fractal", &p, &b.img, None, Some(&buf));
Some(GBuf { img, ..b })
}
// ---------------------------------------------------------------- Median family (noise.rs, noise2.rs)
/// noise::median_image (of the straight colour with alpha 1 when `straight`).
fn median_image(e: &mut Enc, img: &GpuImage, r: usize, straight: bool) -> GpuImage {
let q = run(e, "fxn_quant", &with_u0([straight as u32, 0, 0, 0]), img, None, None);
run(e, "fxn_median", &with_u0([r as u32, 0, 0, 0]), &q, None, None)
}
fn median(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let r = (ctx.params.f("radius") * b.scale).round().max(0.0) as usize;
if r == 0 {
return Some(b);
}
if r > MEDIAN_MAX_R {
return None;
}
let (mode, on_alpha, straight) = match id {
"ec.noise.medianlegacy" => {
let on = ctx.params.b("operateOnAlphaChannel");
(if on { 2 } else { 3 }, on, !on)
}
"ec.noise.dustscratches" => (1, ctx.params.b("operateOnAlpha"), false),
_ => (0, ctx.params.b("operateOnAlpha"), false),
};
let m = median_image(e, &b.img, r, straight);
let mut p = with_u0([mode, on_alpha as u32, 0, 0]);
if mode == 1 {
p.f[0][0] = ctx.params.f("threshold") as f32 / 255.0;
}
let img = run(e, "fxn_median_out", &p, &b.img, Some(&m), None);
Some(GBuf { img, ..b })
}
// ---------------------------------------------------------------- Remove Grain (noise.rs)
fn remove_grain(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let pr = ctx.params;
let amt = pr.f("noiseReductionSettings/noiseReduction") as f32;
let um_amount = pr.f("unsharpMask/amount") as f32 / 100.0;
let view = pr.get("viewingMode").map(Value::as_enum).unwrap_or(3);
// Noise Samples / Blending Matte views and Temporal Filtering render on the CPU.
if view == 1 || view == 2 || pr.b("temporalFiltering/enabled") {
return None;
}
if amt <= 0.0 && um_amount <= 0.0 {
return Some(b);
}
// The grain level measured in the sample boxes (CPU; only the smoothing uses it).
let level = if amt > 0.0 {
let img = e.download(&b.img)?;
effectcraft_effects::remove_grain_level(ctx, &Buf { img, offset: b.offset, scale: b.scale })
} else {
1.0
};
let passes = pr.f("noiseReductionSettings/passes").round().clamp(1.0, 8.0) as usize;
let multichannel = pr.e("noiseReductionSettings/mode") == 0;
let texture = (pr.f("fineTuning/texture") as f32).clamp(0.0, 1.0);
let r = (2.0 * b.scale).round().max(1.0) as usize;
let eps = (0.02 * amt * level).powi(2);
let orig = b.img.clone();
let mut ch = orig.clone();
if amt > 0.0 {
for _ in 0..passes {
let g = run(e, "fxn_gf_guide", &with_u0([!multichannel as u32, 0, 0, 0]), &ch, None, None);
ch = guided_filter(e, &g, &ch, r, eps);
}
if texture > 0.0 {
let mut p = Params::default();
p.f[0][0] = texture;
ch = run(e, "fxn_rg_texture", &p, &ch, Some(&orig), None);
}
}
if um_amount > 0.0 {
let radius = (pr.f("unsharpMask/radius") * b.scale).max(0.1);
let low = gaussian_blur(e, &ch, radius, radius, true);
let mut p = Params::default();
p.f[0] = [um_amount, pr.f("unsharpMask/threshold") as f32 / 255.0, 0.0, 0.0];
ch = run(e, "fxn_rg_unsharp", &p, &ch, Some(&low), None);
}
let mut p = Params::default();
if view == 0 {
let c = b.to_px(pr.v2("previewRegion/center"));
let (hw, hh) = (pr.f("previewRegion/width") * b.scale * 0.5, pr.f("previewRegion/height") * b.scale * 0.5);
let col = pr.color("previewRegion/boxColor");
p.u[0] = [1, pr.b("previewRegion/showBox") as u32, 0, 0];
p.f[0] = [(c.0 - hw) as f32, (c.0 + hw) as f32, (c.1 - hh) as f32, (c.1 + hh) as f32];
p.f[1] = [col[0], col[1], col[2], 1.0];
}
let img = run(e, "fxn_rg_out", &p, &ch, Some(&orig), None);
Some(GBuf { img, ..b })
}
/// One guided-filter arithmetic pass (`fxn_gf`), `d` = an image read through `data`.
fn gf(e: &mut Enc, mode: u32, s: &GpuImage, a: &GpuImage, d: Option<&GpuImage>, eps: f32) -> GpuImage {
let mut p = with_u0([mode, 0, 0, 0]);
p.f[0][0] = eps;
let rows = d.map(|d| e.image_rows(d));
if let Some((_, row)) = &rows {
p.u[0][1] = *row;
}
run(e, "fxn_gf", &p, s, Some(a), rows.as_ref().map(|r| &r.0))
}
/// util::guided_filter on every channel at once (guide `g`, input `p`).
fn guided_filter(e: &mut Enc, g: &GpuImage, p: &GpuImage, r: usize, eps: f32) -> GpuImage {
let bx = |e: &mut Enc, img: &GpuImage| box_passes(e, img, &[r], &[r], true);
let mean_g = bx(e, g);
let mean_p = bx(e, p);
let gp = gf(e, 5, g, p, None, 0.0);
let gg = gf(e, 6, g, g, None, 0.0);
let corr_gp = bx(e, &gp);
let corr_gg = bx(e, &gg);
let cov = gf(e, 0, &corr_gp, &mean_p, Some(&mean_g), 0.0);
let var = gf(e, 1, &corr_gg, &mean_g, None, 0.0);
let a = gf(e, 2, &cov, &var, None, eps);
let bb = gf(e, 3, &a, &mean_p, Some(&mean_g), 0.0);
let ma = bx(e, &a);
let mb = bx(e, &bb);
gf(e, 4, &ma, &mb, Some(g), 0.0)
}
// ---------------------------------------------------------------- Noise Alpha / Noise HLS (noise.rs)
/// noise::phased_cycle's two seeds and smoothed fraction for `phase`.
fn phase_terms(seed: u32, phase: f32, cycle: i64) -> (u32, u32, f32) {
let k = phase.floor();
let t = phase - k;
let k = k as i64;
let wrap = |k: i64| if cycle > 0 { k.rem_euclid(cycle) } else { k } as u32;
(seed.wrapping_add(wrap(k).wrapping_mul(0x632b)), seed.wrapping_add(wrap(k + 1).wrapping_mul(0x632b)), t * t * (3.0 - 2.0 * t))
}
fn noise_alpha(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let k = ctx.params.f("amount") as f32 / 100.0;
if k <= 0.0 {
return Some(b);
}
let kind = ctx.params.e("noise");
let seed = (ctx.params.f("randomSeed") as i64 as u32) ^ ctx.seed.wrapping_mul(0x2f1);
let phase = ctx.params.f("noisePhase") as f32 / 360.0 + if kind >= 2 { ctx.time as f32 } else { 0.0 };
let cycle = if ctx.params.b("noiseOptions/cycleNoise") { ctx.params.f("noiseOptions/cycle").round().max(1.0) as i64 } else { 0 };
let ph = if kind >= 2 { phase } else { phase.floor() };
let (sa, sb, t) = phase_terms(seed, ph, cycle);
let mut p = with_u0([kind % 2, ctx.params.e("originalAlpha"), ctx.params.e("overflow"), sa]);
p.u[1][0] = sb;
p.f[0] = [k, t, 0.0, 0.0];
let img = run(e, "fxn_noise_alpha", &p, &b.img, None, None);
Some(GBuf { img, ..b })
}
fn noise_hls(e: &mut Enc, ctx: &EffectCtx, b: GBuf, phase: f32) -> Option<GBuf> {
let hue = ctx.params.f("hue") as f32 / 100.0;
let light = ctx.params.f("lightness") as f32 / 100.0;
let satv = ctx.params.f("saturation") as f32 / 100.0;
if hue == 0.0 && light == 0.0 && satv == 0.0 {
return Some(b);
}
let gs = (ctx.params.f("grainSize") * b.scale).max(0.1) as f32;
let seed = ctx.seed.wrapping_mul(0x51f3);
let mut seeds = [0u32; 6];
let mut t = 0.0;
for k in 0..3u32 {
let (sa, sb, tt) = phase_terms(seed.wrapping_add(k * 7919), phase, 0);
seeds[2 * k as usize] = sa;
seeds[2 * k as usize + 1] = sb;
t = tt;
}
let mut p = with_u0([ctx.params.e("noise"), seed, 0, 0]);
p.u[1] = [seeds[0], seeds[1], seeds[2], seeds[3]];
p.u[2] = [seeds[4], seeds[5], 0, 0];
p.f[0] = [hue, light, satv, gs];
p.f[1] = [phase, t, 0.0, 0.0];
let img = run(e, "fxn_noise_hls", &p, &b.img, None, None);
Some(GBuf { img, ..b })
}
// ---------------------------------------------------------------- blurs (blur2.rs, misc.rs)
/// blur2::edge_preserving (+ Bilateral's grey output with `grey`).
fn edge_preserving(e: &mut Enc, img: &GpuImage, r: f64, range: f32, hard: bool, grey: bool) -> GpuImage {
let ri = r.ceil() as u32;
let stride = ((r / 6.0).ceil() as u32).max(1);
let sig_s2 = 2.0 * (r / 2.0).max(0.5).powi(2);
let sig_r2 = 2.0 * range.max(1e-3).powi(2);
let d2max = (r * r + 0.5).floor() as u32;
let mut p = with_u0([ri, stride, hard as u32, d2max]);
p.u[1][0] = grey as u32;
p.f[0] = [sig_s2 as f32, sig_r2, range, 0.0];
run(e, "fxn_edge", &p, img, None, None)
}
fn bilateral(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let r = ctx.params.f("radius") * b.scale;
if r < 0.5 {
return Some(b);
}
let range = ctx.params.f("threshold") as f32 / 100.0;
let img = edge_preserving(e, &b.img, r, range, false, !ctx.params.b("colorize"));
Some(GBuf { img, ..b })
}
fn smart_blur(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let r = ctx.params.f("radius") * b.scale;
let thr = ctx.params.f("threshold") as f32 / 100.0;
let mode = ctx.params.e("mode");
let blurred = if r >= 0.5 { edge_preserving(e, &b.img, r, thr, true, false) } else { b.img.clone() };
if mode == 0 {
return Some(GBuf { img: blurred, ..b });
}
let mut p = with_u0([mode, 0, 0, 0]);
p.f[0][0] = thr.max(0.01) * 0.5;
let img = run(e, "fxn_smart_edges", &p, &blurred, None, None);
Some(GBuf { img, ..b })
}
/// misc::unsharp_core (Sharpen, Unsharp Mask).
fn unsharp(e: &mut Enc, b: GBuf, amount: f32, sigma: f64, threshold: f32) -> Option<GBuf> {
let blurred = gaussian_blur(e, &b.img, sigma, sigma, true);
let mut p = Params::default();
p.f[0] = [amount, threshold, 0.0, 0.0];
let img = run(e, "fxn_unsharp", &p, &b.img, Some(&blurred), None);
Some(GBuf { img, ..b })
}
fn dims_xy(dim: u32) -> (f64, f64) {
match dim {
1 => (1.0, 0.0),
2 => (0.0, 1.0),
_ => (1.0, 1.0),
}
}
fn pick(e: &mut Enc, acc: &GpuImage, from: &GpuImage, mask: [bool; 4], alpha: bool) -> GpuImage {
let mut p = with_u0([alpha as u32, 0, 0, 0]);
p.u[1] = mask.map(|m| m as u32);
run(e, "fxn_pick", &p, acc, Some(from), None)
}
fn channel_blur(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
let sig = ["redBlurriness", "greenBlurriness", "blueBlurriness", "alphaBlurriness"].map(|id| ctx.params.f(id).max(0.0) * 0.5 * b.scale);
if sig.iter().all(|&s| s < 0.05) {
return Some(b);
}
let (kx, ky) = dims_xy(ctx.params.e("dimensions"));
let repeat = ctx.params.b("repeatEdge") || ctx.adjustment;
let maxs = sig.iter().cloned().fold(0.0, f64::max);
if !repeat {
b.pad(e, (maxs * 3.0).ceil() as u32 + 1)?;
}
// n: each colour channel blurred by its own amount; a: the alpha blurred by the same amount
// (its divisor), alpha = the Alpha Blurriness result.
let mut n = b.img.clone();
let mut a = pick(e, &b.img, &b.img, [true; 4], true);
let mut done: Vec<u64> = Vec::new();
for &s in &sig {
if s < 0.05 || done.contains(&s.to_bits()) {
continue;
}
done.push(s.to_bits());
let bl = gaussian_blur(e, &b.img, s * kx, s * ky, true);
let on = |c: usize| sig[c] >= 0.05 && sig[c].to_bits() == s.to_bits();
n = pick(e, &n, &bl, [on(0), on(1), on(2), false], false);
a = pick(e, &a, &bl, [on(0), on(1), on(2), on(3)], true);
}
b.img = run(e, "fxn_cblur_out", &Params::default(), &n, Some(&a), None);
Some(b)
}
fn compound_blur(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
let max_sigma = ctx.params.f("maximumBlur").max(0.0) * 0.5 * b.scale;
if max_sigma < 0.05 {
return Some(b);
}
let invert = ctx.params.b("invertBlur");
if !ctx.adjustment {
b.pad(e, (max_sigma * 3.0).ceil() as u32 + 1)?;
}
let (w, h) = (b.img.width as usize, b.img.height as usize);
// The blur map: the Blur Layer fitted to the buffer (util::fit_layer), else the layer itself.
let map = match ctx.layer_param("blurLayer", true) {
Some(o) => {
if o.buf.img.is_empty() {
return None;
}
let stretch = ctx.params.b("stretchMapToFit");
let (ls, os) = (ctx.layer_size, o.size);
let (sx, sy) = if stretch && ls[0] > 0.0 && ls[1] > 0.0 { (os[0] / ls[0], os[1] / ls[1]) } else { (1.0, 1.0) };
let (dx, dy) = if stretch { (0.0, 0.0) } else { ((os[0] - ls[0]) * 0.5, (os[1] - ls[1]) * 0.5) };
let inv = 1.0 / b.scale.max(1e-9);
let col = |x: usize| (((x as f64 + 0.5 - b.offset[0]) * inv) * sx + dx) * o.buf.scale + o.buf.offset[0];
let row = |y: usize| (((y as f64 + 0.5 - b.offset[1]) * inv) * sy + dy) * o.buf.scale + o.buf.offset[1];
let data: Vec<f32> = (0..w).map(|x| col(x) as f32).chain((0..h).map(|y| row(y) as f32)).collect();
let layer = e.g.upload_image(&o.buf.img)?;
let buf = e.data(&data);
let mut p = with_u0([1, invert as u32, 0, 0]);
p.u[1][0] = w as u32;
let out = e.image(b.img.width, b.img.height);
e.pixels("fxn_cb_map", &p, &layer, None, &out, Some(&buf));
out
}
None => run(e, "fxn_cb_map", &with_u0([0, invert as u32, 0, 0]), &b.img, None, None),
};
const N: usize = 5;
let levels: Vec<GpuImage> = (0..N)
.map(|k| {
if k == 0 {
b.img.clone()
} else {
let s = max_sigma * k as f64 / (N - 1) as f64;
gaussian_blur(e, &b.img, s, s, ctx.adjustment)
}
})
.collect();
let (rows, row) = e.image_rows(&map);
let out = e.image(b.img.width, b.img.height);
for k in 0..N - 1 {
e.pixels("fxn_cb_level", &with_u0([k as u32, row, 0, 0]), &levels[k], Some(&levels[k + 1]), &out, Some(&rows));
}
b.img = out;
Some(b)
}
// ---------------------------------------------------------------- Minimax (channel.rs)
/// util::morph_plane per channel (`modes`: 0 min, 1 max, 2 keep) with radii (`rx`, `ry`).
fn morph(e: &mut Enc, img: &GpuImage, rx: usize, ry: usize, modes: [u32; 4]) -> GpuImage {
let mut cur = img.clone();
for (vertical, r) in [(0, rx), (1, ry)] {
if r == 0 {
continue;
}
let mut p = with_u0([r as u32, vertical, 0, 0]);
p.u[1] = modes;
cur = run(e, "fxn_morph", &p, &cur, None, None);
}
cur
}
fn minimax(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
let r = (ctx.params.f("radius") * b.scale).round().max(0.0) as usize;
if r == 0 {
return Some(b);
}
let op = ctx.params.e("operation");
let dont_shrink = ctx.params.b("dontShrinkEdges");
let (rx, ry) = match ctx.params.e("direction") {
1 => (r, 0),
2 => (0, r),
_ => (r, r),
};
let sel: [bool; 4] = match ctx.params.e("channel") {
1 => [false, false, false, true],
2 => [true, true, true, true],
3 => [true, false, false, false],
4 => [false, true, false, false],
5 => [false, false, true, false],
_ => [true, true, true, false],
};
if sel[3] && op != 0 && !ctx.adjustment {
b.pad(e, rx.max(ry) as u32)?;
}
let (w, h) = (b.img.width, b.img.height);
let mut planes = run(e, "fxn_straight", &Params::default(), &b.img, None, None);
// Beyond the layer edge is empty (0) unless Don't Shrink Edges repeats the edge pixels.
if !dont_shrink {
let (pw, ph) = (w + 2 * rx as u32, h + 2 * ry as u32);
if !e.g.fits(pw, ph) {
return None;
}
let padded = e.image(pw, ph);
e.copy_into(&planes, &padded, rx as u32, ry as u32);
planes = padded;
}
let modes = |max: bool| sel.map(|s| if s { max as u32 } else { 2 });
let stages: &[bool] = match op {
0 => &[false],
1 => &[true],
2 => &[false, true],
_ => &[true, false],
};
for &max in stages {
planes = morph(e, &planes, rx, ry, modes(max));
}
if !dont_shrink {
let out = e.image(w, h);
e.pixels("fxn_crop", &with_u0([rx as u32, ry as u32, 0, 0]), &planes, None, &out, None);
planes = out;
}
b.img = run(e, "fxn_premul", &Params::default(), &planes, None, None);
Some(b)
}
// ---------------------------------------------------------------- Echo / Posterize Time (time_fx.rs)
fn echo(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let n = ctx.params.f("numberOfEchoes").round().clamp(0.0, 255.0) as usize;
let dt = ctx.params.f("echoTime");
let start = ctx.params.f("startingIntensity").clamp(0.0, 1.0) as f32;
let decay = ctx.params.f("decay").clamp(0.0, 1.0) as f32;
let op = ctx.params.e("echoOperator");
let times: Vec<f64> = (0..=n).map(|i| ctx.time + i as f64 * dt).collect();
// Without the effect host (or the frames) the layer passes through, as on the CPU.
let Some((grid, imgs)) = effectcraft_effects::time_frames(ctx, &times) else { return Some(b) };
let (w, h) = (grid.img.width, grid.img.height);
let mut acc = [e.image(w, h), e.image(w, h)];
for (i, img) in imgs.iter().enumerate() {
let frame = e.g.upload_image(img)?;
let mut p = with_u0([op, (i == 0) as u32, 0, 0]);
p.f[0][0] = start * decay.powi(i as i32);
let [a, o] = &acc;
e.pixels("fxn_echo", &p, a, Some(&frame), o, None);
acc.swap(0, 1);
}
let mut p = with_u0([op, 0, 0, 0]);
p.f[0][0] = (n + 1) as f32;
let img = run(e, "fxn_echo_fin", &p, &acc[0], None, None);
Some(GBuf { img, offset: grid.offset, scale: grid.scale })
}
fn posterize_time(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
if ctx.env.host.is_none() {
return Some(b);
}
let tt = effectcraft_effects::posterized_time(ctx.time, ctx.params.f("frameRate"));
match effectcraft_effects::time_frames(ctx, &[tt]) {
Some((grid, mut imgs)) => {
let img = e.g.upload_image(&imgs.pop().unwrap_or_default())?;
Some(GBuf { img, offset: grid.offset, scale: grid.scale })
}
None => Some(b),
}
}

1002
crates/gpu/src/fx_stylize.rs Normal file

File diff suppressed because it is too large Load Diff

381
crates/gpu/src/fx_tone.rs Normal file
View File

@ -0,0 +1,381 @@
//! GPU effects, tonal and colour correction family (kernels in `shaders/fx_tone.wgsl`, every
//! entry point prefixed `fxt_`): one per-pixel pass each (`fxt_point`) with the CPU effect's
//! exact maths.
//!
//! Levels (Individual Controls), Gamma/Pedestal/Gain, Photo Filter, Change Color, Change to
//! Color, Leave Color, Broadcast Colors, Color Balance (HLS), Video Limiter, PS Arbitrary Map,
//! CC Toner, CC Color Offset and CC Kernel, plus Hue/Saturation's colour ranges and Levels'
//! per-channel controls (the parts the base kernels in `kernels.wgsl` leave to the CPU). Auto
//! Levels / Contrast / Color and Equalize measure the frame's histograms on the CPU (the image
//! is read back for that) and apply the correction on the GPU.
use effectcraft_color::rgb_to_hsl;
use effectcraft_effects::EffectCtx;
use crate::context::{Enc, Params};
use crate::effects::{GBuf, gaussian_blur};
/// Compute entry points in `fx_tone.wgsl`.
pub(crate) const KERNELS: &[&str] = &["fxt_point"];
/// Effect ids implemented here.
pub(crate) const IDS: &[&str] = &[
"ec.color.levelsic",
"ec.color.gammapedestalgain",
"ec.color.photofilter",
"ec.color.changecolor",
"ec.color.changetocolor",
"ec.color.leavecolor",
"ec.color.broadcast",
"ec.color.colorbalancehls",
"ec.color.videolimiter",
"ec.color.psarbitrarymap",
"ec.color.cctoner",
"ec.color.cccoloroffset",
"ec.color.cckernel",
"ec.color.autolevels",
"ec.color.autocontrast",
"ec.color.autocolor",
"ec.color.equalize",
"ec.color.shadowhighlight",
"ec.color.cccolorneutralizer",
"ec.color.colorstabilizer",
];
/// 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.color.shadowhighlight" => shadow_highlight(e, ctx, b),
_ => point(e, id, ctx, b),
}
}
/// Invert in the HLS and YIQ channels (the base `pointwise` kernel does RGB and Alpha).
pub(crate) fn invert(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let mut p = op(18);
p.u[0][1] = ctx.params.e("channel");
p.f[0][0] = 1.0 - ctx.params.f("blend") as f32 / 100.0;
run(e, &p, b, None)
}
/// Shadow/Highlight: the amounts (Auto Amounts) and the Black / White Clip ranges are measured
/// on the CPU; the luminance bases are blurred and the image adjusted on the GPU.
fn shadow_highlight(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let pr = ctx.params;
let src = e.download(&b.img)?;
let (s_amt, h_amt) = effectcraft_effects::shadow_highlight_amounts(ctx, &src);
let (bc, wc) = (pr.f("moreOptions/blackClip").clamp(0.0, 49.0) / 100.0, pr.f("moreOptions/whiteClip").clamp(0.0, 49.0) / 100.0);
let clip0 = (bc > 0.0 || wc > 0.0).then(|| effectcraft_effects::luma_clip_points(&src, bc, wc));
let s_r = pr.f("moreOptions/shadowRadius").max(0.0) * b.scale / 2.0;
let h_r = pr.f("moreOptions/highlightRadius").max(0.0) * b.scale / 2.0;
let (w, h) = (b.img.width, b.img.height);
let luma = e.image(w, h);
e.pixels("fxt_point", &op(19), &b.img, None, &luma, None);
let base_s = gaussian_blur(e, &luma, s_r, s_r, true);
let base_h = if (h_r - s_r).abs() < 1e-9 { base_s.clone() } else { gaussian_blur(e, &luma, h_r, h_r, true) };
let bases = e.image(w, h);
e.pixels("fxt_point", &op(21), &base_s, Some(&base_h), &bases, None);
let mut p = op(20);
p.f[0] =
[s_amt, h_amt, (pr.f("moreOptions/shadowTonalWidth") as f32 / 100.0).max(0.01), (pr.f("moreOptions/highlightTonalWidth") as f32 / 100.0).max(0.01)];
p.f[1] = [
pr.f("moreOptions/colorCorrection") as f32 / 100.0,
pr.f("moreOptions/midtoneContrast") as f32 / 100.0,
(pr.f("blend") / 100.0).clamp(0.0, 1.0) as f32,
0.0,
];
let out = e.image(w, h);
e.pixels("fxt_point", &p, &b.img, Some(&bases), &out, None);
let Some((lo0, hi0)) = clip0 else { return Some(GBuf { img: out, ..b }) };
let adjusted = e.download(&out)?;
let (lo1, hi1) = effectcraft_effects::luma_clip_points(&adjusted, bc, wc);
if (lo0, hi0) == (lo1, hi1) || hi1 - lo1 <= 1e-3 {
return Some(GBuf { img: out, ..b });
}
let mut p = op(22);
p.f[0] = [lo0, lo1, (hi0 - lo0) / (hi1 - lo1), 0.0];
run(e, &p, GBuf { img: out, ..b }, None)
}
fn rgb(c: [f32; 4]) -> [f32; 4] {
[c[0], c[1], c[2], 0.0]
}
/// Hue/Saturation with colour ranges (Channel Control); Master and Colorize alone run in the
/// base `pointwise` kernel.
pub(crate) fn hue_saturation(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let pr = ctx.params;
let f = |id: String, d: f64| pr.get(&id).map(|v| v.as_f64()).unwrap_or(d) as f32;
let mut data = vec![];
for (r, _, c) in effectcraft_effects::HUESAT_RANGES {
let range = [
f(format!("{r}RangeStart"), c - 15.0),
f(format!("{r}RangeEnd"), c + 15.0),
f(format!("{r}StartFalloff"), 30.0).max(0.0),
f(format!("{r}EndFalloff"), 30.0).max(0.0),
f(format!("{r}Hue"), 0.0) / 360.0,
f(format!("{r}Saturation"), 0.0) / 100.0,
f(format!("{r}Lightness"), 0.0) / 100.0,
];
if range[4] != 0.0 || range[5] != 0.0 || range[6] != 0.0 {
data.extend(range);
}
}
let g = |k: &str| pr.f(k) as f32;
let mut p = Params::default();
p.u[0] = [14, pr.b("colorize") as u32, (data.len() / 7) as u32, 0];
p.f[0] = [g("hue") / 360.0, g("saturation") / 100.0, g("lightness") / 100.0, 0.0];
p.f[1] = [g("colorizeHue") / 360.0, g("colorizeSaturation") / 100.0, g("colorizeLightness") / 100.0, 0.0];
run(e, &p, b, Some(data))
}
/// Levels with Red / Green / Blue / Alpha controls (the RGB controls alone run in the base
/// `pointwise` kernel).
pub(crate) fn levels(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let pr = ctx.params;
let (cb, cw) = effectcraft_effects::levels_clip(ctx);
let mut data = vec![
pr.f("inBlack") as f32,
pr.f("inWhite") as f32,
pr.f("gamma").max(0.01) as f32,
pr.f("outBlack") as f32,
pr.f("outWhite") as f32,
cb as u32 as f32,
cw as u32 as f32,
];
for (v, (cb, cw)) in effectcraft_effects::levels_channel_settings(ctx) {
data.extend(v);
data.extend([cb as u32 as f32, cw as u32 as f32]);
}
run(e, &op(15), b, Some(data))
}
fn op(code: u32) -> Params {
let mut p = Params::default();
p.u[0][0] = code;
p
}
fn run(e: &mut Enc, p: &Params, b: GBuf, data: Option<Vec<f32>>) -> Option<GBuf> {
let buf = data.map(|d| e.data(&d));
let out = e.image(b.img.width, b.img.height);
e.pixels("fxt_point", p, &b.img, None, &out, buf.as_ref());
Some(GBuf { img: out, ..b })
}
/// color3::parse_map.
fn parse_map(s: &str) -> Vec<f32> {
let vals: Vec<f32> = s.split(|ch: char| ch == ',' || ch.is_whitespace()).filter(|t| !t.is_empty()).filter_map(|t| t.parse::<f32>().ok()).collect();
if vals.len() < 2 {
return (0..256).map(|i| i as f32 / 255.0).collect();
}
let max = vals.iter().cloned().fold(0.0f32, f32::max);
let k = if max > 1.0 { 1.0 / 255.0 } else { 1.0 };
vals.iter().map(|v| (v * k).clamp(0.0, 1.0)).collect()
}
fn point(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let pr = ctx.params;
let f = |k: &str| pr.f(k) as f32;
let mut p = Params::default();
let mut data = None;
match id {
"ec.color.levelsic" => {
const IDS: [[&str; 5]; 5] = [
["rgb/rgbInBlack", "rgb/rgbInWhite", "rgb/rgbGamma", "rgb/rgbOutBlack", "rgb/rgbOutWhite"],
["red/redInBlack", "red/redInWhite", "red/redGamma", "red/redOutBlack", "red/redOutWhite"],
["green/greenInBlack", "green/greenInWhite", "green/greenGamma", "green/greenOutBlack", "green/greenOutWhite"],
["blue/blueInBlack", "blue/blueInWhite", "blue/blueGamma", "blue/blueOutBlack", "blue/blueOutWhite"],
["alpha/alphaInBlack", "alpha/alphaInWhite", "alpha/alphaGamma", "alpha/alphaOutBlack", "alpha/alphaOutWhite"],
];
let lv: Vec<[f32; 5]> = IDS.iter().map(|ids| ids.map(f)).collect();
let ident = |l: &[f32; 5]| *l == [0.0, 1.0, 1.0, 0.0, 1.0];
if lv.iter().all(ident) {
return Some(b);
}
let (cb, cw) = effectcraft_effects::levels_clip(ctx);
let mut d = vec![];
for l in &lv {
d.extend(l);
d.push(ident(l) as u32 as f32);
}
p.u[0] = [1, cb as u32, cw as u32, 0];
data = Some(d);
}
"ec.color.gammapedestalgain" => {
let ch = |n: &str| [f(&format!("{n}Gamma")).max(0.01), f(&format!("{n}Pedestal")), f(&format!("{n}Gain")), 0.0];
p.u[0][0] = 2;
p.f[0] = ch("red");
p.f[1] = ch("green");
p.f[2] = ch("blue");
p.f[3][0] = f("blackStretch").max(0.0);
}
"ec.color.photofilter" => {
let fi = pr.e("filter");
let c = match effectcraft_effects::PHOTO_FILTERS.get(fi as usize) {
Some((_, rgb)) if fi != effectcraft_effects::PHOTO_FILTER_CUSTOM => rgb.map(|v| v as f32 / 255.0),
_ => {
let c = pr.color("color");
[c[0], c[1], c[2]]
}
};
p.u[0] = [3, pr.b("preserveLuminosity") as u32, 0, 0];
p.f[0] = [c[0], c[1], c[2], f("density") / 100.0];
}
"ec.color.changetocolor" => {
let from = pr.color("from");
let to = pr.color("to");
let (fh, fs, fl) = rgb_to_hsl(from[0], from[1], from[2]);
let (th, ts, tl) = rgb_to_hsl(to[0], to[1], to[2]);
let change = pr.e("change");
let flags = matches!(change, 1 | 3) as u32
| (matches!(change, 2 | 3) as u32) << 1
| ((pr.e("changeBy") == 1) as u32) << 2
| (pr.b("viewCorrectionMatte") as u32) << 3;
p.u[0] = [4, flags, 0, 0];
p.f[0] = [fh, fs, fl, f("softness") / 100.0];
p.f[1] = [th, ts, tl, 0.0];
p.f[2] = [f("toleranceGroup/hue") / 100.0, f("toleranceGroup/lightness") / 100.0, f("toleranceGroup/saturation") / 100.0, 0.0];
}
"ec.color.leavecolor" => {
let amt = f("amount") / 100.0;
if amt <= 0.0 {
return Some(b);
}
let k = pr.color("color");
p.u[0] = [5, (pr.e("matchColors") == 1) as u32, 0, 0];
p.f[0] = [k[0], k[1], k[2], rgb_to_hsl(k[0], k[1], k[2]).0];
p.f[1] = [amt, f("tolerance") / 100.0, f("edgeSoftness") / 100.0, 0.0];
}
"ec.color.changecolor" => {
let view_mask = pr.e("view") == 1;
let (dh, dl, ds) = (f("hueTransform") / 360.0, f("lightnessTransform") / 100.0, f("saturationTransform") / 100.0);
if !view_mask && dh == 0.0 && dl == 0.0 && ds == 0.0 {
return Some(b);
}
let k = pr.color("colorToChange");
p.u[0] = [6, pr.e("matchColors"), view_mask as u32 | (pr.b("invertColorCorrectionMask") as u32) << 1, 0];
p.f[0] = [k[0], k[1], k[2], rgb_to_hsl(k[0], k[1], k[2]).0];
p.f[1] = [dh, dl, ds, 0.0];
p.f[2] = [f("matchingTolerance") / 100.0, f("matchingSoftness") / 100.0, 0.0, 0.0];
}
"ec.color.broadcast" => {
let pal = pr.e("locale") == 1;
let (setup, span) = if pal { (0.0, 100.0) } else { (7.5, 92.5) };
p.u[0] = [7, pal as u32, pr.e("howToMakeSafe"), 0];
p.f[0][0] = (f("maxSignal") - setup) / span;
}
"ec.color.colorbalancehls" => {
let (dh, dl, ds) = (f("hue") / 360.0, f("lightness") / 100.0, f("saturation") / 100.0);
if dh == 0.0 && dl == 0.0 && ds == 0.0 {
return Some(b);
}
p.u[0][0] = 8;
p.f[0] = [dh, dl, ds, 0.0];
}
"ec.color.cctoner" => {
let g = |id: &str| rgb(pr.color(id));
let stops = match pr.e("tones") {
0 => vec![g("shadows"), g("highlights")],
2 => vec![g("shadows"), g("darktones"), g("midtones"), g("brights"), g("highlights")],
_ => vec![g("shadows"), g("midtones"), g("highlights")],
};
p.u[0] = [9, stops.len() as u32 - 1, 0, 0];
p.f[0][0] = (f("blend") / 100.0).clamp(0.0, 1.0);
for (i, s) in stops.iter().enumerate() {
p.f[1 + i] = *s;
}
}
"ec.color.cccoloroffset" => {
let offs = ["redPhase", "greenPhase", "bluePhase"].map(|id| (pr.f(id) / 360.0).rem_euclid(1.0) as f32);
if offs.iter().all(|o| *o == 0.0) {
return Some(b);
}
p.u[0] = [10, pr.e("overflow"), 0, 0];
p.f[0] = [offs[0], offs[1], offs[2], 0.0];
}
"ec.color.cckernel" => {
let k = ["k1", "k2", "k3", "k4", "k5", "k6", "k7", "k8", "k9"].map(f);
let (scale, offset) = (f("scale"), f("offset"));
let identity = k.iter().enumerate().all(|(i, v)| if i == 4 { *v == 1.0 } else { *v == 0.0 });
if identity && scale == 1.0 && offset == 0.0 {
return Some(b);
}
p.u[0] = [11, pr.b("absolute") as u32, 0, 0];
p.f[0] = [k[0], k[1], k[2], k[3]];
p.f[1] = [k[4], k[5], k[6], k[7]];
p.f[2] = [k[8], scale, offset, 0.0];
}
"ec.color.psarbitrarymap" => {
let map = parse_map(pr.s("map"));
p.u[0] = [12, pr.b("applyPhaseMapToAlpha") as u32, map.len() as u32, 0];
p.f[0][0] = (pr.f("phase") * (map.len() - 1) as f64 / 255.0) as f32;
data = Some(map);
}
"ec.color.videolimiter" => {
const CLIP_LEVELS: [f32; 5] = [0.9, 0.95, 1.0, 1.05, 1.09];
let level = CLIP_LEVELS[(pr.e("clipLevel") as usize).min(4)];
let comp = [0.0, 0.03, 0.05, 0.1, 0.2][(pr.e("compressionBeforeClipping") as usize).min(4)];
p.u[0] = [13, pr.e("clipMethod"), pr.b("gamutWarning") as u32, 0];
p.f[0] = [level, level * (1.0 - comp), 0.0, 0.0];
p.f[1] = rgb(pr.color("gamutWarningColor"));
}
"ec.color.colorstabilizer" => {
// The sample means are measured on the CPU (the frame is read back).
if ctx.env.host.is_none() {
return Some(b);
}
let img = e.download(&b.img)?;
let cpu = effectcraft_effects::Buf { img, offset: b.offset, scale: b.scale };
let Some(maps) = effectcraft_effects::color_stabilizer_maps(ctx, &cpu) else { return Some(b) };
let mut d = vec![];
for m in &maps {
d.push(m.len() as f32);
for i in 0..5 {
let (x, y) = m.get(i).copied().unwrap_or((0.0, 0.0));
d.extend([x as f32, y as f32]);
}
}
p.u[0][0] = 24;
data = Some(d);
}
"ec.color.cccolorneutralizer" => {
let pairs = [("shadowsUnbalance", "shadowsBalance"), ("midtonesUnbalance", "midtonesBalance"), ("highlightsUnbalance", "highlightsBalance")];
for (i, (u, bal)) in pairs.iter().enumerate() {
let (u, v) = (pr.color(u), pr.color(bal));
p.f[i] = [v[0] - u[0], v[1] - u[1], v[2] - u[2], 0.0];
}
p.u[0][0] = 23;
p.f[3] = [f("pinning") / 100.0, f("blendWOriginal") / 100.0, f("contrast") / 100.0, 0.0];
p.f[4] = [f("darks") / 100.0, f("brights") / 100.0, 0.0, 0.0];
}
"ec.color.autolevels" | "ec.color.autocontrast" | "ec.color.autocolor" | "ec.color.equalize" => {
// The histograms are measured on the CPU.
let img = e.download(&b.img)?;
if id == "ec.color.equalize" {
let amt = (pr.f("amount") / 100.0).clamp(0.0, 1.0) as f32;
if amt <= 0.0 {
return Some(b);
}
let style = pr.e("style");
let tables = effectcraft_effects::equalize_tables(&img, style);
p.u[0] = [17, style, tables.first().map_or(0, |t| t.len()) as u32, 0];
p.f[0][0] = amt;
data = Some(tables.concat());
} else {
let kind = match id {
"ec.color.autolevels" => 0,
"ec.color.autocontrast" => 1,
_ => 2,
};
let (ranges, gam) = effectcraft_effects::auto_correct_settings(ctx, &img, kind);
p.u[0][0] = 16;
p.f[0] = [ranges[0].0, ranges[1].0, ranges[2].0, 0.0];
p.f[1] = [ranges[0].1, ranges[1].1, ranges[2].1, 0.0];
p.f[2] = [gam[0], gam[1], gam[2], (pr.f("blend") / 100.0).clamp(0.0, 1.0) as f32];
}
}
_ => return None,
}
run(e, &p, b, data)
}

View File

@ -16,7 +16,10 @@
//!
//! GPU effects ([`effectcraft_effects::GPU_EFFECTS`]) run as compute kernels with the CPU
//! effect's exact steps (padding, box-blur radii, parameter conversions); chains of them are
//! uploaded and read back once.
//! uploaded and read back once. Each family lives in its own module with its own WGSL file
//! (`fx_color`, `fx_distort`, `fx_generate`, `fx_key`, `fx_noise`, `fx_stylize`, `fx_tone`);
//! settings a kernel cannot match fall back to the CPU (`catalog::gpu_supported`, or `None`
//! from the family's `apply`).
//!
//! GPU particles (`particles`): the stepped particle effects hand their simulation to
//! [`effectcraft_effects::psim::ParticleSim`], implemented here with one invocation per particle
@ -35,6 +38,10 @@ mod effects;
mod fx_color;
mod fx_distort;
mod fx_generate;
mod fx_key;
mod fx_noise;
mod fx_stylize;
mod fx_tone;
mod ops;
mod particles;
mod walk;
@ -174,4 +181,12 @@ mod tests_fx_distort;
#[cfg(test)]
mod tests_fx_generate;
#[cfg(test)]
mod tests_fx_key;
#[cfg(test)]
mod tests_fx_noise;
#[cfg(test)]
mod tests_fx_stylize;
#[cfg(test)]
mod tests_fx_tone;
#[cfg(test)]
mod tests_particles;

View File

@ -0,0 +1,867 @@
// GPU effects (keying, matte and channel family): see src/fx_key.rs. Each entry point mirrors
// the CPU effect in effectcraft-effects operation for operation. Every name here is prefixed
// `fxk_` (the family files share one module).
// util::unpremul: (straight colour, alpha); colour 0 when alpha ≤ 1e-6.
fn fxk_straight(px: vec4<f32>) -> vec3<f32> {
if (px.w > 1e-6) {
return px.xyz / px.w;
}
return vec3<f32>(0.0);
}
fn fxk_premul(c: vec3<f32>, a: f32) -> vec4<f32> {
return vec4<f32>(c * a, a);
}
// Pixel `p` of the third image, copied into `data` as RGBA f32 rows of P.u[3].x pixels.
fn fxk_rows(p: vec2<i32>) -> vec4<f32> {
let i = (u32(p.y) * P.u[3].x + u32(p.x)) * 4u;
return vec4<f32>(data[i], data[i + 1u], data[i + 2u], data[i + 3u]);
}
// keying::to_lab.
fn fxk_cbrt(t: f32) -> f32 {
return sign(t) * pow(abs(t), 1.0 / 3.0);
}
fn fxk_lab_f(t: f32) -> f32 {
if (t > 0.008856) {
return fxk_cbrt(t);
}
return 7.787 * t + 16.0 / 116.0;
}
// keying::range_components.
fn fxk_range_components(space: u32, c: vec3<f32>) -> vec3<f32> {
if (space == 1u) {
let y = 0.299 * c.x + 0.587 * c.y + 0.114 * c.z;
let u = -0.14713 * c.x - 0.28886 * c.y + 0.436 * c.z;
let v = 0.615 * c.x - 0.51499 * c.y - 0.10001 * c.z;
return vec3<f32>(y * 255.0, u * 255.0 + 128.0, v * 255.0 + 128.0);
}
if (space == 2u) {
return c * 255.0;
}
let cl = clamp(c, vec3<f32>(0.0), vec3<f32>(1.0));
let l = vec3<f32>(srgb_to_linear(cl.x), srgb_to_linear(cl.y), srgb_to_linear(cl.z));
let x = (0.4124 * l.x + 0.3576 * l.y + 0.1805 * l.z) / 0.95047;
let yy = 0.2126 * l.x + 0.7152 * l.y + 0.0722 * l.z;
let z = (0.0193 * l.x + 0.1192 * l.y + 0.9505 * l.z) / 1.08883;
let fx = fxk_lab_f(x);
let fy = fxk_lab_f(yy);
let fz = fxk_lab_f(z);
return vec3<f32>((116.0 * fy - 16.0) * 2.55, 500.0 * (fx - fy) + 128.0, 200.0 * (fy - fz) + 128.0);
}
// keying::matte_levels.
fn fxk_matte_levels(v: f32, in_b: f32, in_w: f32, gamma: f32, out_b: f32, out_w: f32) -> f32 {
var t = clamp((v - in_b / 255.0) / max((in_w - in_b) / 255.0, 1e-4), 0.0, 1.0);
t = powz(t, 1.0 / max(gamma, 0.01));
return clamp(out_b / 255.0 + t * (out_w - out_b) / 255.0, 0.0, 1.0);
}
// keying::secondary.
fn fxk_secondary(c: vec3<f32>, o1: u32, o2: u32, bal: f32) -> f32 {
var hi = c[o2];
var lo = c[o1];
if (c[o1] > c[o2]) {
hi = c[o1];
lo = c[o2];
}
return bal * hi + (1.0 - bal) * lo;
}
// channel::arithmetic's 8-bit quantisation.
fn fxk_q(v: f32) -> u32 {
return u32(round_away(clamp(v, 0.0, 1.0) * 255.0));
}
// channel2::extract.
fn fxk_extract(px: vec4<f32>, ch: u32, inv: bool) -> vec4<f32> {
let c = fxk_straight(px);
let a = max(px.w, 0.0);
var g: f32;
switch ch {
case 0u: {
if (inv) {
return fxk_premul(1.0 - c, a);
}
return px;
}
case 1u: { g = luminance(c) * a; }
case 2u: { g = c.x * a; }
case 3u: { g = c.y * a; }
case 4u: { g = c.z * a; }
default: { g = a; }
}
if (inv) {
g = 1.0 - g;
}
return vec4<f32>(g, g, g, 1.0);
}
// channel2::arith.
fn fxk_arith(op: u32, a: f32, b: f32) -> f32 {
switch op {
case 0u: { return b; }
case 1u: { return a + b; }
case 2u: { return a - b; }
case 3u: { return a * b; }
case 4u: { return abs(a - b); }
case 5u: { return f32(fxk_q(a) & fxk_q(b)) / 255.0; }
case 6u: { return f32(fxk_q(a) | fxk_q(b)) / 255.0; }
case 7u: { return f32(fxk_q(a) ^ fxk_q(b)) / 255.0; }
case 8u, 11u: { return max(a, b); }
case 9u, 10u: { return min(a, b); }
case 12u: { return a + b - a * b; }
case 13u: {
if (a < 0.5) {
return 2.0 * a * b;
}
return 1.0 - 2.0 * (1.0 - a) * (1.0 - b);
}
default: {
if (b < 0.5) {
return 2.0 * a * b;
}
return 1.0 - 2.0 * (1.0 - a) * (1.0 - b);
}
}
}
// channel2::overflow.
fn fxk_overflow(op: u32, v: f32, how: u32) -> f32 {
if (how == 0u) {
return clamp(v, 0.0, 1.0);
}
if (how == 1u) {
let m = 1.0001;
let r = v - m * floor(v / m);
return min(select(r, 0.0, r >= m || r < 0.0), 1.0);
}
if (op == 1u) {
return v * 0.5;
}
if (op == 2u) {
return (v + 1.0) * 0.5;
}
return clamp(v, 0.0, 1.0);
}
// Per-pixel keying / matte / channel operations. u[0].x = op (see src/fx_key.rs for each
// effect's parameter layout):
// 1 Color Key matte 2 Luma Key matte 3 Color Range matte 4 Extract matte
// 5 apply a matte (aux.x; u[0].y = 0 final, 2 matte view, 3 Difference Matte's matte view,
// 4 Difference Matte's output)
// 6 Difference Matte matte (src and aux blurred)
// 7 Spill Suppressor 8 Advanced Spill Suppressor (aux = 1×1 excess sums when u[1].w)
// 9 Color Difference Key 10 Unmult 11 route channels (u[1] = sources)
// 12 Remove Color Matting 13 Arithmetic 14 Solid Composite 15 Set Matte
// 16 Channel Combiner 17 Blend 18 Calculations 19 Compound Arithmetic
// 20 Set Channels: channel u[0].y of the value image from aux; 21 Set Channels: premultiply
// 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();
let p = vec2<i32>(gid.xy);
if (p.x >= dims.x || p.y >= dims.y) {
return;
}
let px = textureLoad(src, p, 0);
let c = fxk_straight(px);
let a = max(px.w, 0.0);
let u0 = P.u[0];
let u1 = P.u[1];
let f0 = P.f[0];
var o = px;
switch u0.x {
case 1u: {
let k = f0.xyz;
let d = max(max(abs(c.x - k.x), abs(c.y - k.y)), abs(c.z - k.z));
o = vec4<f32>(select(1.0, 0.0, d <= f0.w));
}
case 2u: {
let l = luminance(c);
let thr = f0.x;
let tol = f0.y;
var m: f32;
switch u0.y {
case 0u: {
if (tol > 0.0) {
m = clamp((thr + tol - l) / tol, 0.0, 1.0);
} else {
m = select(1.0, 0.0, l > thr);
}
}
case 2u: { m = select(1.0, 0.0, abs(l - thr) <= tol); }
case 3u: { m = select(0.0, 1.0, abs(l - thr) <= tol); }
default: {
if (tol > 0.0) {
m = clamp((l - (thr - tol)) / tol, 0.0, 1.0);
} else {
m = select(1.0, 0.0, l < thr);
}
}
}
o = vec4<f32>(m);
}
case 3u: {
let v = fxk_range_components(u0.y, c);
var out_d = 0.0;
for (var i = 0u; i < 3u; i++) {
out_d = max(max(out_d, P.f[0][i] - v[i]), v[i] - P.f[1][i]);
}
let fuzz = P.f[2].x;
var m = 1.0;
if (out_d <= 0.0) {
m = 0.0;
} else if (fuzz > 0.0) {
m = clamp(out_d / fuzz, 0.0, 1.0);
}
o = vec4<f32>(m);
}
case 4u: {
var v: f32;
switch u0.y {
case 1u: { v = c.x; }
case 2u: { v = c.y; }
case 3u: { v = c.z; }
case 4u: { v = a; }
default: { v = luminance(c); }
}
v = 255.0 * v;
let bp = f0.x;
let wp = f0.y;
let bs = f0.z;
let ws = f0.w;
var k = 1.0;
if (v < bp) {
k = select(0.0, clamp(1.0 - (bp - v) / bs, 0.0, 1.0), bs > 0.0);
} else if (v > wp) {
k = select(0.0, clamp(1.0 - (v - wp) / ws, 0.0, 1.0), ws > 0.0);
}
if (u0.z != 0u) {
k = 1.0 - k;
}
o = vec4<f32>(k);
}
case 5u: {
let m = textureLoad(aux, p, 0).x;
switch u0.y {
case 2u: {
let k = clamp(m, 0.0, 1.0);
let v = px.w * k;
o = vec4<f32>(v, v, v, 1.0);
}
case 3u: {
let v = m * px.w;
o = vec4<f32>(v, v, v, px.w);
}
case 4u: { o = px * m; }
default: { o = px * clamp(m, 0.0, 1.0); }
}
}
case 6u: {
let q = textureLoad(aux, p, 0);
let oc = fxk_straight(q);
let oa = max(q.w, 0.0);
let d = max(max(max(abs(c.x - oc.x), abs(c.y - oc.y)), abs(c.z - oc.z)), abs(a - oa));
var m: f32;
if (d <= f0.x) {
m = 0.0;
} else if (f0.y <= 1e-6) {
m = 1.0;
} else {
m = clamp((d - f0.x) / f0.y, 0.0, 1.0);
}
o = vec4<f32>(m);
}
case 7u: {
if (px.w <= 0.0) {
textureStore(out, p, px);
return;
}
var s = px.xyz / px.w;
if (u0.y == 1u) {
let g = (s.x + s.y + s.z) / 3.0;
let dir = f0.xyz;
let proj = (s.x - g) * dir.x + (s.y - g) * dir.y + (s.z - g) * dir.z;
if (proj > 0.0) {
s = s - proj * dir * f0.w;
}
} else {
let spill = max(s[u1.x] - max(s[u1.y], s[u1.z]), 0.0);
s[u1.x] = s[u1.x] - spill * f0.w;
}
o = vec4<f32>(s * px.w, px.w);
}
case 8u: {
if (px.w <= 0.0) {
textureStore(out, p, px);
return;
}
var pi = u1.x;
var o1 = u1.y;
var o2 = u1.z;
if (u1.w != 0u) {
let ex = textureLoad(aux, vec2<i32>(0, 0), 0);
if (ex.y >= ex.x && ex.y >= ex.z) {
pi = 1u;
o1 = 0u;
o2 = 2u;
} else if (ex.z >= ex.x) {
pi = 2u;
o1 = 0u;
o2 = 1u;
} else {
pi = 0u;
o1 = 1u;
o2 = 2u;
}
}
var s = px.xyz / px.w;
let amt = f0.x;
let spill_range = f0.y;
let luma = f0.z;
let tol = f0.w;
let desat = P.f[1].x;
let neutral = P.f[1].y;
let key_hue = P.f[1].z;
var w = 1.0;
if (tol < 1.0) {
let h = rgb_to_hsl(s).x;
let d0 = abs(h - key_hue);
let d = min(d0, 1.0 - d0);
w = clamp(1.0 - d / max(tol * 0.5, 1e-4), 0.0, 1.0);
}
let mx = max(s[o1], s[o2]);
let avg = 0.5 * (s[o1] + s[o2]);
let lim = mx + (avg - mx) * spill_range;
let spill = max(s[pi] - lim, 0.0) * amt * w;
s[pi] = s[pi] - spill;
let lw = vec3<f32>(0.2126, 0.7152, 0.0722);
let back = spill * lw[pi] * luma + spill * neutral / 3.0;
s = s + back;
if (desat > 0.0 && spill > 0.0) {
let l = luminance(s);
let k = min(desat * min(spill * 4.0, 1.0), 1.0);
s = s + (vec3<f32>(l) - s) * k;
}
o = vec4<f32>(s * px.w, px.w);
}
case 9u: {
var cc = c;
if (u0.z != 0u) {
let cl = clamp(cc, vec3<f32>(0.0), vec3<f32>(1.0));
cc = vec3<f32>(srgb_to_linear(cl.x), srgb_to_linear(cl.y), srgb_to_linear(cl.z));
}
let pa = P.f[1];
let pb = P.f[3];
let ua = 1.0 - clamp((cc[u1.x] - cc[u1.y]) / f0.x, 0.0, 1.0);
let ub = 1.0 - clamp((cc[u1.x] - cc[u1.z]) / f0.y, 0.0, 1.0);
let ca = fxk_matte_levels(ua, pa.x, pa.y, pa.z, pa.w, P.f[2].x);
let cb = fxk_matte_levels(ub, pb.x, pb.y, pb.z, pb.w, P.f[2].y);
let um = max(ua, ub);
let cm = fxk_matte_levels(max(ca, cb), f0.z, f0.w, P.f[2].z, 0.0, 255.0);
var view = u0.y;
if (view == 8u) {
let right = p.x * 2 >= dims.x;
let bottom = p.y * 2 >= dims.y;
if (!right && !bottom) {
view = 2u;
} else if (right && !bottom) {
view = 4u;
} else if (!right && bottom) {
view = 6u;
} else {
view = 7u;
}
}
var g = -1.0;
switch view {
case 1u: { g = ua; }
case 2u: { g = ca; }
case 3u: { g = ub; }
case 4u: { g = cb; }
case 5u: { g = um; }
case 6u: { g = cm; }
default: {}
}
if (g >= 0.0) {
let m = g * a;
o = vec4<f32>(m, m, m, 1.0);
} else {
o = px * cm;
}
}
case 10u: {
let white = u0.y != 0u;
var d: f32;
var start: f32;
if (white) {
d = 1.0 - min(min(c.x, c.y), c.z);
start = 1.0 - f0.x;
} else {
d = max(max(c.x, c.y), c.z);
start = f0.x;
}
d = clamp(d, 0.0, 1.0);
let width = f0.y * (1.0 - start);
var m: f32;
if (width > 1e-6) {
m = clamp((d - start) / width, 0.0, 1.0);
} else {
m = select(0.0, 1.0, d > start);
}
if (m <= 1e-6) {
o = vec4<f32>(0.0);
} else {
let bg = select(0.0, 1.0, white);
var oc = c;
if (u0.z != 0u) {
oc = (c - bg * (1.0 - m)) / m;
}
if (u0.w != 0u) {
oc = clamp(oc, vec3<f32>(0.0), vec3<f32>(1.0));
}
o = fxk_premul(oc, a * m);
}
}
case 11u: {
let v = vec4<f32>(dst_pick(u1.x, c, a), dst_pick(u1.y, c, a), dst_pick(u1.z, c, a), dst_pick(u1.w, c, a));
o = fxk_premul(v.xyz, clamp(v.w, 0.0, 1.0));
}
case 12u: {
if (px.w <= 1e-6) {
o = vec4<f32>(0.0);
} else {
for (var i = 0; i < 3; i++) {
let s = px[i] / px.w;
var v = max(s - f0[i] * (1.0 - px.w), 0.0);
if (u0.y != 0u) {
v = min(v, px.w);
}
o[i] = v;
}
}
}
case 13u: {
if (px.w <= 0.0) {
textureStore(out, p, px);
return;
}
let s = px.xyz / px.w;
var r = vec3<f32>(0.0);
for (var i = 0u; i < 3u; i++) {
let v = s[i];
let kv = f0[i];
let iv = fxk_q(v);
let ik = u32(round_away(kv * 255.0));
var x: f32;
switch u0.y {
case 0u: { x = f32(iv & ik) / 255.0; }
case 1u: { x = f32(iv | ik) / 255.0; }
case 2u: { x = f32(iv ^ ik) / 255.0; }
case 3u: { x = v + kv; }
case 4u: { x = v - kv; }
case 5u: { x = abs(v - kv); }
case 6u: { x = min(v, kv); }
case 7u: { x = max(v, kv); }
case 8u: { x = select(v, 0.0, v > kv); }
case 9u: { x = select(v, 0.0, v < kv); }
case 10u: { x = select(0.0, 1.0, v >= kv); }
case 11u: { x = v * kv; }
default: { x = 1.0 - (1.0 - v) * (1.0 - kv); }
}
if (u0.z != 0u) {
r[i] = clamp(x, 0.0, 1.0);
} else {
r[i] = max(x, 0.0);
}
}
o = vec4<f32>(r * px.w, px.w);
}
case 14u: {
var b = blend_pixel(u0.y, f0, px * P.f[1].x, 0.5);
b.w = clamp(b.w, 0.0, 1.0);
o = vec4<f32>(max(b.xyz, vec3<f32>(0.0)), b.w);
}
case 15u: {
var mc = c;
var ma = a;
if ((u0.z & 8u) == 0u) {
let q = textureLoad(aux, p, 0);
mc = fxk_straight(q);
ma = max(q.w, 0.0);
}
var m = dst_pick(u0.y, mc, ma);
if ((u0.z & 4u) != 0u) {
m = m * ma;
}
m = clamp(m, 0.0, 1.0);
let inv = (u0.z & 1u) != 0u;
if (inv) {
m = 1.0 - m;
}
let own_m = (u0.z & 8u) != 0u;
let is_alpha = (u0.z & 16u) != 0u;
var na = m;
if ((u0.z & 2u) != 0u && (!own_m || !is_alpha || inv)) {
na = a * m;
}
o = fxk_premul(c, clamp(na, 0.0, 1.0));
}
case 16u: {
let ch_from = u0.y;
let ch_to = u0.z;
let inv = (u0.w & 1u) != 0u;
let solid = (u0.w & 2u) != 0u;
let second = (u0.w & 4u) != 0u;
var sc = c;
var sa = a;
if (second) {
let q = textureLoad(aux, p, 0);
sc = fxk_straight(q);
sa = max(q.w, 0.0);
}
if (sa <= 0.0 && ch_from != 7u && !second) {
if (solid) {
o = fxk_premul(c, 1.0);
}
textureStore(out, p, o);
return;
}
var na = a;
var r: vec3<f32>;
if (ch_from <= 3u) {
switch ch_from {
case 0u: {
let hsl = rgb_to_hsl(sc);
r = vec3<f32>(hsl.x, hsl.z, hsl.y);
}
case 1u: { r = hsl_to_rgb(sc.x, sc.z, sc.y); }
case 2u: {
let y = 0.299 * sc.x + 0.587 * sc.y + 0.114 * sc.z;
r = vec3<f32>(y, 0.492 * (sc.z - y) + 0.5, 0.877 * (sc.x - y) + 0.5);
}
default: {
let y = sc.x;
let uu = sc.y - 0.5;
let vv = sc.z - 0.5;
let rr = y + vv / 0.877;
let bb = y + uu / 0.492;
let gg = (y - 0.299 * rr - 0.114 * bb) / 0.587;
r = vec3<f32>(rr, gg, bb);
}
}
if (inv) {
r = 1.0 - r;
}
} else {
var v: f32;
switch ch_from {
case 4u: { v = sc.x; }
case 5u: { v = sc.y; }
case 6u: { v = sc.z; }
case 7u: { v = sa; }
case 8u: { v = rgb_to_hsl(sc).z; }
case 9u: { v = rgb_to_hsl(sc).x; }
case 10u: { v = rgb_to_hsl(sc).y; }
case 11u: { v = luminance(sc); }
case 12u: { v = max(max(sc.x, sc.y), sc.z); }
default: { v = min(min(sc.x, sc.y), sc.z); }
}
if (inv) {
v = 1.0 - v;
}
switch ch_to {
case 0u: { r = vec3<f32>(v, 0.0, 0.0); }
case 1u: { r = vec3<f32>(0.0, v, 0.0); }
case 2u: { r = vec3<f32>(0.0, 0.0, v); }
case 3u: {
na = clamp(v, 0.0, 1.0);
r = vec3<f32>(1.0);
}
case 5u: { r = hsl_to_rgb(v, 1.0, 0.5); }
case 6u: { r = hsl_to_rgb(0.0, clamp(v, 0.0, 1.0), 0.5); }
case 8u: {
let hsl = rgb_to_hsl(c);
r = hsl_to_rgb(hsl.x, clamp(hsl.y * v, 0.0, 1.0), hsl.z);
}
default: { r = vec3<f32>(v); }
}
}
if (solid) {
na = 1.0;
}
o = fxk_premul(max(r, vec3<f32>(0.0)), na);
}
case 17u: {
let q = textureLoad(aux, p, 0);
var bl: vec4<f32>;
switch u0.y {
case 0u: { bl = q; }
case 1u, 2u: {
let oa = q.w;
if (px.w <= 0.0 || oa <= 0.0) {
bl = px;
} else {
let hsl = rgb_to_hsl(c);
let ohsl = rgb_to_hsl(fxk_straight(q));
var h2 = ohsl.x;
var s2 = ohsl.y;
if (u0.y == 2u) {
if (hsl.y > 1e-4) {
s2 = hsl.y;
} else {
h2 = 0.0;
s2 = 0.0;
}
}
let rgb = hsl_to_rgb(h2, s2, hsl.z);
bl = px + (fxk_premul(rgb, a) - px) * oa;
}
}
case 3u: { bl = vec4<f32>(min(px.xyz, q.xyz), px.w); }
default: { bl = max(px, q); }
}
o = bl + (px - bl) * f0.x;
}
case 18u: {
let flags = u1.x;
var r = fxk_extract(px, u0.y, (flags & 1u) != 0u);
if ((flags & 8u) != 0u) {
let s = fxk_extract(textureLoad(aux, p, 0), u0.z, (flags & 2u) != 0u) * f0.x;
r = blend_pixel(u0.w, r, s, 0.5);
}
if ((flags & 4u) != 0u && r.w > 1e-6) {
r = fxk_premul(fxk_straight(r), px.w);
}
r.w = clamp(r.w, 0.0, 1.0);
o = r;
}
case 19u: {
let q = textureLoad(aux, p, 0);
let oc = fxk_straight(q);
let oa = max(q.w, 0.0);
var rc = c;
var ra = a;
if (u0.z != 2u) {
for (var i = 0u; i < 3u; i++) {
rc[i] = fxk_overflow(u0.y, fxk_arith(u0.y, c[i], oc[i]), u0.w);
}
}
if (u0.z != 0u) {
ra = fxk_overflow(u0.y, fxk_arith(u0.y, a, oa), u0.w);
}
let res = fxk_premul(rc, ra);
o = res + (px - res) * f0.x;
}
case 20u: {
let q = textureLoad(aux, p, 0);
o = px;
o[u0.y] = dst_pick(u0.z, fxk_straight(q), max(q.w, 0.0));
}
case 21u: {
o = fxk_premul(px.xyz, clamp(px.w, 0.0, 1.0));
}
case 22u: {
let d = c[u1.x] - fxk_secondary(c, u1.y, u1.z, f0.z);
let t = clamp(d / f0.x * f0.y, 0.0, 1.0);
o = vec4<f32>(clamp((1.0 - t - f0.w) / max(P.f[1].x - f0.w, 1e-3), 0.0, 1.0));
}
case 23u: {
let k = clamp(textureLoad(aux, p, 0).x, 0.0, 1.0);
let alpha = a * k;
if (u0.y == 1u) {
o = vec4<f32>(alpha, alpha, alpha, 1.0);
} else if (u0.y == 2u) {
var v = 0.5;
if (alpha <= 1e-3) {
v = 0.0;
} else if (alpha >= 0.999) {
v = 1.0;
}
o = vec4<f32>(v, v, v, 1.0);
} else {
let screen = f0.xyz;
var fg = c;
if (!(u0.z != 0u || k <= 1e-4)) {
let s = 1.0 - k;
for (var i = 0; i < 3; i++) {
fg[i] = min(max(c[i] - s * screen[i], 0.0) / k, max(c[i], 1.0));
}
}
let spill = max(fg[u1.x] - fxk_secondary(fg, u1.y, u1.z, f0.w), 0.0);
fg[u1.x] = fg[u1.x] - spill * P.f[1].x;
o = fxk_premul(fg, alpha);
}
}
case 24u: {
o = vec4<f32>(px.w);
}
case 25u: {
var na = textureLoad(aux, p, 0).x;
if (u0.y != 0u) {
na = clamp((na - 0.5) * 1.5 + 0.5, 0.0, 1.0);
}
var cc = c;
let a0 = a;
if (P.u[3].y != 0u && a0 > 1e-4 && a0 < 0.999) {
let q = fxk_rows(p);
if (q.w > 1e-6) {
let ea = q.w;
if (ea > 1e-4) {
let t = f0.y * (1.0 - a0);
cc = cc + (q.xyz / ea - cc) * t;
}
}
}
na = clamp((na - 0.5) * f0.x + 0.5, 0.0, 1.0);
if (a0 <= 1e-6) {
na = 0.0;
}
o = fxk_premul(cc, na);
}
case 26u: {
o = vec4<f32>(clamp((px.x - f0.x) / f0.y + 0.5, 0.0, 1.0));
}
case 27u: {
let na = clamp(textureLoad(aux, p, 0).x, 0.0, 1.0);
var cc = c;
if (px.w <= 1e-4) {
cc = fxk_straight(fxk_rows(p));
}
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);
}
default: {}
}
textureStore(out, p, o);
}
// util::fit_layer: bilinear sample of the other layer (src) at the buffer coordinates in data
// (one per column, then one per row).
@compute @workgroup_size(16, 16)
fn fxk_fit(@builtin(global_invocation_id) gid: vec3<u32>) {
let dims = out_dims();
let p = vec2<i32>(gid.xy);
if (p.x >= dims.x || p.y >= dims.y) {
return;
}
let x = data[u32(p.x)];
let y = data[u32(dims.x) + u32(p.y)];
textureStore(out, p, sample_bilinear(src, x, y));
}
// Sum of 16×16 blocks; u[0].x = first pass: sum Advanced Spill Suppressor's per-channel excess
// of each primary over the other two (straight colour × alpha), else sum the values.
@compute @workgroup_size(16, 16)
fn fxk_reduce(@builtin(global_invocation_id) gid: vec3<u32>) {
let dims = out_dims();
let p = vec2<i32>(gid.xy);
if (p.x >= dims.x || p.y >= dims.y) {
return;
}
let sd = vec2<i32>(textureDimensions(src));
var acc = vec4<f32>(0.0);
for (var j = 0; j < 16; j++) {
for (var i = 0; i < 16; i++) {
let q = vec2<i32>(p.x * 16 + i, p.y * 16 + j);
if (q.x >= sd.x || q.y >= sd.y) {
continue;
}
let v = textureLoad(src, q, 0);
if (P.u[0].x != 0u) {
let c = fxk_straight(v);
let a = max(v.w, 0.0);
acc += vec4<f32>(
max(c.x - max(c.y, c.z), 0.0) * a,
max(c.y - max(c.x, c.z), 0.0) * a,
max(c.z - max(c.x, c.y), 0.0) * a,
0.0,
);
} else {
acc += v;
}
}
}
textureStore(out, p, acc);
}

View File

@ -0,0 +1,825 @@
// GPU effects (noise, blur and time family): see src/fx_noise.rs. Each entry point mirrors the CPU effect in
// effectcraft-effects operation for operation. Every name here is prefixed `fxn_` (the
// family files share one module).
const FXN_MAX: f32 = 3.40282347e38;
fn fxn_pixel(gid: vec3<u32>) -> vec2<i32> {
let dims = out_dims();
let p = vec2<i32>(gid.xy);
if (p.x >= dims.x || p.y >= dims.y) {
return vec2<i32>(-1);
}
return p;
}
// a / b rounded like the CPU's IEEE division (GPU division may be approximate): one
// residual correction with fma. Used where a threshold follows the quotient.
fn fxn_div4(a: vec4<f32>, b: vec4<f32>) -> vec4<f32> {
let q = a / b;
let r = fma(-q, b, a);
return fma(r, 1.0 / b, q);
}
// util::unpremul: (straight colour, alpha).
fn fxn_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));
}
// ---------------------------------------------------------------- Median family (noise::median_image)
// Quantise to the median's 512 levels (0..511). u[0].x = 1: straight colour with alpha 1
// (Median (Legacy) colour only).
@compute @workgroup_size(16, 16)
fn fxn_quant(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = fxn_pixel(gid);
if (p.x < 0) {
return;
}
var v = textureLoad(src, p, 0);
if (P.u[0].x == 1u) {
v = vec4<f32>(fxn_unpremul(v).xyz, 1.0);
}
let q = clamp(v, vec4<f32>(0.0), vec4<f32>(1.0)) * 511.0;
textureStore(out, p, vec4<f32>(round_away(q.x), round_away(q.y), round_away(q.z), round_away(q.w)));
}
// Per-channel median of the quantised levels over a (2r+1)² window (edges repeated): the
// smallest level m with more than half the window at or below m. u[0].x = r.
@compute @workgroup_size(16, 16)
fn fxn_median(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = fxn_pixel(gid);
if (p.x < 0) {
return;
}
let r = i32(P.u[0].x);
let n = (2 * r + 1) * (2 * r + 1);
let half = f32(n / 2);
var lo = vec4<f32>(0.0);
var hi = vec4<f32>(511.0);
for (var it = 0; it < 9; it++) {
let mid = floor((lo + hi) * 0.5);
var cnt = vec4<f32>(0.0);
for (var dy = -r; dy <= r; dy++) {
for (var dx = -r; dx <= r; dx++) {
let q = tex_get_clamped(src, p.x + dx, p.y + dy);
cnt += select(vec4<f32>(0.0), vec4<f32>(1.0), q <= mid);
}
}
let above = cnt > vec4<f32>(half);
hi = select(hi, mid, above);
lo = select(mid + 1.0, lo, above);
}
textureStore(out, p, fxn_div4(lo, vec4<f32>(511.0)));
}
// noise::median_px.
fn fxn_median_px(o: vec4<f32>, m: vec4<f32>, on_alpha: bool) -> vec4<f32> {
if (on_alpha) {
let a = clamp(m.w, 0.0, 1.0);
return vec4<f32>(min(m.xyz, vec3<f32>(a)), a);
}
if (m.w <= 1e-4) {
return o;
}
return vec4<f32>(min(fxn_div4(m, vec4<f32>(m.w)).xyz, vec3<f32>(1.0)) * o.w, o.w);
}
// src = the layer, aux = the median. u[0] = (mode, operate on alpha): 0 Median, 1 Dust &
// Scratches (f[0].x = threshold), 2 Median (Legacy) with alpha, 3 Median (Legacy) colour only.
@compute @workgroup_size(16, 16)
fn fxn_median_out(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = fxn_pixel(gid);
if (p.x < 0) {
return;
}
let o = textureLoad(src, p, 0);
let m = textureLoad(aux, p, 0);
let on_alpha = P.u[0].y != 0u;
var r = o;
switch P.u[0].x {
case 0u: {
r = fxn_median_px(o, m, on_alpha);
}
case 1u: {
let cand = fxn_median_px(o, m, on_alpha);
let d = abs(cand - o);
if (max(max(d.x, d.y), max(d.z, d.w)) > P.f[0].x) {
r = cand;
}
}
case 2u: {
r = m;
}
default: {
r = vec4<f32>(m.xyz * o.w, o.w);
}
}
textureStore(out, p, r);
}
// ---------------------------------------------------------------- Bilateral / Smart Blur (blur2.rs)
// blur2::edge_preserving. u[0] = (ceil r, stride, hard, largest d²); u[1].x = grey (Bilateral
// with Colorize off); f[0] = (2σs², 2σr², range).
@compute @workgroup_size(16, 16)
fn fxn_edge(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = fxn_pixel(gid);
if (p.x < 0) {
return;
}
let ri = i32(P.u[0].x);
let stride = i32(P.u[0].y);
let hard = P.u[0].z != 0u;
let d2max = i32(P.u[0].w);
let sig_s2 = P.f[0].x;
let sig_r2 = P.f[0].y;
let range = P.f[0].z;
let c = textureLoad(src, p, 0);
let cl = luminance(c.xyz);
var acc = vec4<f32>(0.0);
var wsum = 0.0;
for (var dy = -ri; dy <= ri; dy += stride) {
for (var dx = -ri; dx <= ri; dx += stride) {
let d2 = dx * dx + dy * dy;
if (d2 > d2max) {
continue;
}
let q = tex_get_clamped(src, p.x + dx, p.y + dy);
var w = 0.0;
if (hard) {
w = select(0.0, 1.0, abs(luminance(q.xyz) - cl) <= range);
} else {
let d = q - c;
let dc = d.x * d.x + d.y * d.y + d.z * d.z + d.w * d.w;
w = exp(-f32(d2) / sig_s2 - dc / sig_r2);
}
if (w > 0.0) {
acc += q * w;
wsum += w;
}
}
}
var o = c;
if (wsum > 0.0) {
o = acc / wsum;
}
if (P.u[1].x != 0u && o.w > 0.0) {
let l = luminance(o.xyz / o.w);
o = vec4<f32>(vec3<f32>(l * o.w), o.w);
}
textureStore(out, p, o);
}
// Smart Blur's Edge Only / Overlay Edge (blur2::sobel on the blurred layer's premultiplied
// luminance). u[0].x = mode; f[0].x = edge threshold.
@compute @workgroup_size(16, 16)
fn fxn_smart_edges(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = fxn_pixel(gid);
if (p.x < 0) {
return;
}
var l: array<f32, 9>;
for (var j = 0; j < 3; j++) {
for (var i = 0; i < 3; i++) {
l[j * 3 + i] = luminance(tex_get_clamped(src, p.x + i - 1, p.y + j - 1).xyz);
}
}
let gx = l[2] + 2.0 * l[5] + l[8] - l[0] - 2.0 * l[3] - l[6];
let gy = l[6] + 2.0 * l[7] + l[8] - l[0] - 2.0 * l[1] - l[2];
let e = sqrt(gx * gx + gy * gy) * 0.25;
let px = textureLoad(src, p, 0);
let on = e > P.f[0].x;
var o = px;
if (P.u[0].x == 1u) {
o = vec4<f32>(vec3<f32>(select(0.0, 1.0, on) * px.w), px.w);
} else if (on) {
o = vec4<f32>(vec3<f32>(px.w), px.w);
}
textureStore(out, p, o);
}
// ---------------------------------------------------------------- Sharpen / Unsharp Mask (misc::unsharp_core)
// src = the layer, aux = its blur. f[0] = (amount, threshold in 0..255).
@compute @workgroup_size(16, 16)
fn fxn_unsharp(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = fxn_pixel(gid);
if (p.x < 0) {
return;
}
var o = textureLoad(src, p, 0);
let bl = textureLoad(aux, p, 0);
for (var c = 0; c < 3; c++) {
let d = o[c] - bl[c];
if (abs(d) * 255.0 >= P.f[0].y) {
o[c] = max(o[c] + d * P.f[0].x, 0.0);
}
}
textureStore(out, p, o);
}
// ---------------------------------------------------------------- planes
// Channel select: channel c of `src` becomes aux[c] (u[0].x = 0) or aux's alpha (1) where
// u[1][c] != 0.
@compute @workgroup_size(16, 16)
fn fxn_pick(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = fxn_pixel(gid);
if (p.x < 0) {
return;
}
var o = textureLoad(src, p, 0);
let a = textureLoad(aux, p, 0);
for (var c = 0; c < 4; c++) {
if (P.u[1][c] != 0u) {
o[c] = select(a[c], a.w, P.u[0].x == 1u);
}
}
textureStore(out, p, o);
}
// Channel Blur's result: src = blurred premultiplied channels, aux = the alpha blurred like
// each channel (alpha = the Alpha Blurriness result).
@compute @workgroup_size(16, 16)
fn fxn_cblur_out(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = fxn_pixel(gid);
if (p.x < 0) {
return;
}
let v = textureLoad(src, p, 0);
let ba = textureLoad(aux, p, 0);
var s = vec3<f32>(0.0);
for (var c = 0; c < 3; c++) {
if (ba[c] > 1e-6) {
s[c] = v[c] / ba[c];
}
}
let a = clamp(ba.w, 0.0, 1.0);
textureStore(out, p, vec4<f32>(s * a, a));
}
// Straight colour planes with the raw alpha (Minimax).
@compute @workgroup_size(16, 16)
fn fxn_straight(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = fxn_pixel(gid);
if (p.x < 0) {
return;
}
let v = textureLoad(src, p, 0);
textureStore(out, p, vec4<f32>(fxn_unpremul(v).xyz, v.w));
}
// Premultiply straight planes by the clamped alpha.
@compute @workgroup_size(16, 16)
fn fxn_premul(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = fxn_pixel(gid);
if (p.x < 0) {
return;
}
let v = textureLoad(src, p, 0);
let a = clamp(v.w, 0.0, 1.0);
textureStore(out, p, vec4<f32>(v.xyz * a, a));
}
// util::morph_plane along one axis (edges repeated). u[0] = (r, vertical); u[1] = per-channel
// operation (0 min, 1 max, 2 keep).
@compute @workgroup_size(16, 16)
fn fxn_morph(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = fxn_pixel(gid);
if (p.x < 0) {
return;
}
let r = i32(P.u[0].x);
let vertical = P.u[0].y != 0u;
let centre = textureLoad(src, p, 0);
var lo = centre;
var hi = centre;
for (var k = -r; k <= r; k++) {
var v: vec4<f32>;
if (vertical) {
v = tex_get_clamped(src, p.x, p.y + k);
} else {
v = tex_get_clamped(src, p.x + k, p.y);
}
lo = min(lo, v);
hi = max(hi, v);
}
var o = centre;
for (var c = 0; c < 4; c++) {
if (P.u[1][c] == 0u) {
o[c] = lo[c];
} else if (P.u[1][c] == 1u) {
o[c] = hi[c];
}
}
textureStore(out, p, o);
}
// Crop: out(p) = src(p + u[0].xy).
@compute @workgroup_size(16, 16)
fn fxn_crop(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = fxn_pixel(gid);
if (p.x < 0) {
return;
}
textureStore(out, p, textureLoad(src, p + vec2<i32>(P.u[0].xy), 0));
}
// ---------------------------------------------------------------- Compound Blur (blur2::compound_blur)
// The blur map (straight luminance × alpha, inverted with u[0].y) in every channel. u[0].x = 1:
// `src` is the Blur Layer sampled at the per-column / per-row coordinates in `data` (u[1].x =
// width); 0: `src` is the layer itself.
@compute @workgroup_size(16, 16)
fn fxn_cb_map(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = fxn_pixel(gid);
if (p.x < 0) {
return;
}
var v: vec4<f32>;
if (P.u[0].x == 1u) {
let w = P.u[1].x;
v = sample_bilinear(src, data[u32(p.x)], data[w + u32(p.y)]);
} else {
v = textureLoad(src, p, 0);
}
let s = fxn_unpremul(v);
var l = luminance(s.xyz) * s.w;
if (P.u[0].y != 0u) {
l = 1.0 - l;
}
textureStore(out, p, vec4<f32>(l));
}
// Writes the pixels whose blur level falls between levels k (src) and k + 1 (aux). The map is
// `data` (RGBA f32 rows of u[0].y pixels); u[0].x = k.
@compute @workgroup_size(16, 16)
fn fxn_cb_level(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = fxn_pixel(gid);
if (p.x < 0) {
return;
}
let m = data[(u32(p.y) * P.u[0].y + u32(p.x)) * 4u];
let t = clamp(m, 0.0, 1.0) * 4.0;
let k = min(u32(floor(t)), 3u);
if (k != P.u[0].x) {
return;
}
let f = t - f32(k);
let a = textureLoad(src, p, 0);
let c = textureLoad(aux, p, 0);
textureStore(out, p, a + (c - a) * f);
}
// ---------------------------------------------------------------- Echo (time_fx::echo)
// acc (src) ⊕ frame (aux) × f[0].x. u[0] = (operator, first frame).
@compute @workgroup_size(16, 16)
fn fxn_echo(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = fxn_pixel(gid);
if (p.x < 0) {
return;
}
let op = P.u[0].x;
var acc = textureLoad(src, p, 0);
if (P.u[0].y != 0u) {
acc = select(vec4<f32>(0.0), vec4<f32>(FXN_MAX), op == 2u);
}
let k = P.f[0].x;
let q = textureLoad(aux, p, 0) * k;
switch op {
case 1u: {
acc = max(acc, q);
}
case 2u: {
acc = min(acc, q);
}
case 3u: {
acc = acc + q - acc * q;
}
case 4u: {
acc += q * (1.0 - acc.w);
}
case 5u: {
acc = q + acc * (1.0 - q.w);
}
default: {
acc += q;
}
}
textureStore(out, p, acc);
}
// Echo's finish. u[0].x = operator; f[0].x = number of frames.
@compute @workgroup_size(16, 16)
fn fxn_echo_fin(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = fxn_pixel(gid);
if (p.x < 0) {
return;
}
let op = P.u[0].x;
var acc = textureLoad(src, p, 0);
if (op == 6u) {
acc = acc / P.f[0].x;
}
if (op == 2u && acc.x == FXN_MAX) {
acc = vec4<f32>(0.0);
}
textureStore(out, p, vec4<f32>(max(acc.xyz, vec3<f32>(0.0)), clamp(acc.w, 0.0, 1.0)));
}
// ---------------------------------------------------------------- Fractal / Turbulent Noise (noise3.rs)
fn fxn_catmull(p0: f32, p1: f32, p2: f32, p3: f32, t: f32) -> f32 {
return 0.5 * (2.0 * p1 + (p2 - p0) * t + (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t * t + (3.0 * p1 - p0 - 3.0 * p2 + p3) * t * t * t);
}
fn fxn_slice(ix: i32, iy: i32, iz: i32, tx: f32, ty: f32, seed: u32, nt: u32) -> f32 {
if (nt == 0u) {
return lattice(ix, iy, iz, seed);
}
if (nt == 1u) {
let a = lattice(ix, iy, iz, seed);
let b = lattice(ix + 1, iy, iz, seed);
let c = lattice(ix, iy + 1, iz, seed);
let d = lattice(ix + 1, iy + 1, iz, seed);
let top = a + (b - a) * tx;
let bot = c + (d - c) * tx;
return top + (bot - top) * ty;
}
var rows: array<f32, 4>;
for (var j = 0; j < 4; j++) {
let y = iy + j - 1;
rows[j] = fxn_catmull(lattice(ix - 1, y, iz, seed), lattice(ix, y, iz, seed), lattice(ix + 1, y, iz, seed), lattice(ix + 2, y, iz, seed), tx);
}
return fxn_catmull(rows[0], rows[1], rows[2], rows[3], ty);
}
// noise3::typed_noise.
fn fxn_typed_noise(x: f32, y: f32, z: f32, seed: u32, nt: u32) -> f32 {
if (nt == 2u) {
return value_noise(x, y, z, seed);
}
let x0 = floor(x);
let y0 = floor(y);
let z0 = floor(z);
let tx = x - x0;
let ty = y - y0;
let fz = fade(z - z0);
let a = fxn_slice(i32(x0), i32(y0), i32(z0), tx, ty, seed, nt);
let b = fxn_slice(i32(x0), i32(y0), i32(z0) + 1, tx, ty, seed, nt);
return a + (b - a) * fz;
}
// noise3::shape.
fn fxn_shape(kind: u32, n: f32) -> f32 {
let s = n * 2.0 - 1.0;
switch kind {
case 1u: {
return 1.0 - s * s;
}
case 2u: {
return 1.0 - abs(s);
}
case 3u: {
let t = 1.0 - abs(s);
return t * t;
}
case 8u: {
let t = clamp(abs(s) / 0.12, 0.0, 1.0);
return 1.0 - t * t;
}
default: {
return n;
}
}
}
// Fractal::layer. `o` = octave data base index.
fn fxn_layer(u: f32, v: f32, o: u32, z: f32, seed: u32) -> f32 {
let nt = P.u[0].y;
if (P.u[1].y != 0u) {
let t = P.f[2].x;
let c = P.f[2].y;
let w = P.f[2].z;
let a = fxn_typed_noise(u, v, t + z, seed, nt);
let b = fxn_typed_noise(u, v, t - c + z, seed, nt);
return a * (1.0 - w) + b * w;
}
return fxn_typed_noise(u, v, data[o + 7u] + z, seed, nt);
}
// u[0] = (kind, noise type, octaves, seed); u[1] = (sub rotation, cycle, invert, overflow);
// u[2].x = blend mode (BlendMode::ALL index, 255 = None); f[0] = (sin, cos, size x, size y);
// f[1] = (contrast, brightness, opacity, blend with original); f[2] = (cycle t, cycle, t / cycle,
// norm); data = 10 floats per octave (noise3::FractalGpu).
@compute @workgroup_size(16, 16)
fn fxn_fractal(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = fxn_pixel(gid);
if (p.x < 0) {
return;
}
let kind = P.u[0].x;
let n_oct = P.u[0].z;
let sr = P.f[0].x;
let cr = P.f[0].y;
let x = f32(p.x) + 0.5;
let y = f32(p.y) + 0.5;
var sum = 0.0;
var best = 0.0;
for (var oi = 0u; oi < n_oct; oi++) {
let o = oi * 10u;
let dx = x - data[o];
let dy = y - data[o + 1u];
var u = (dx * cr + dy * sr) / P.f[0].z;
var v = (-dx * sr + dy * cr) / P.f[0].w;
if (P.u[1].x != 0u) {
let s = data[o + 2u];
let c = data[o + 3u];
let u2 = u * c + v * s;
v = -u * s + v * c;
u = u2;
}
let f = data[o + 4u];
u = u * f;
v = v * f;
let z = data[o + 8u];
let seed = P.u[0].w + oi;
if (kind >= 4u && kind <= 6u) {
let k = data[o + 9u];
let a = fxn_layer(u * 0.5, v * 0.5, o, z + 3.1, seed ^ 0x55u) - 0.5;
let bb = fxn_layer(u * 0.5, v * 0.5, o, z + 5.7, seed ^ 0xaau) - 0.5;
if (kind == 5u) {
let s = sin(a * 3.0);
let c = cos(a * 3.0);
let u2 = u * c - v * s;
v = u * s + v * c;
u = u2;
} else {
u += a * 1.5 * k;
v += bb * 1.5 * k;
}
}
let n = fxn_shape(kind, fxn_layer(u, v, o, z, seed));
let amp = data[o + 5u];
if (kind == 7u) {
best = max(best, n * amp);
}
sum += n * amp * data[o + 6u];
}
var raw = sum / max(P.f[2].w, 1e-6);
if (kind == 7u) {
raw = best;
}
var val = (raw - 0.5) * P.f[1].x + 0.5 + P.f[1].y;
let ov = P.u[1].w;
if (ov == 0u) {
val = clamp(val, 0.0, 1.0);
} else if (ov == 1u) {
val = clamp(0.5 + 0.5 * tanh(2.0 * (val - 0.5)) / tanh(1.0), 0.0, 1.0);
} else if (ov == 2u) {
let t = val - 2.0 * floor(val / 2.0);
val = select(t, 2.0 - t, t > 1.0);
}
if (P.u[1].z != 0u) {
val = 1.0 - val;
}
let op = P.f[1].z;
let s = vec4<f32>(vec3<f32>(val * op), op);
let orig = textureLoad(src, p, 0);
var r = s;
if (P.u[2].x != 255u) {
r = blend_pixel(P.u[2].x, orig, s, 0.5);
}
let bo = P.f[1].w;
textureStore(out, p, orig * bo + r * (1.0 - bo));
}
// ---------------------------------------------------------------- Noise Alpha / Noise HLS (noise.rs)
// noise::phased_cycle with the phase's two seeds and smoothed fraction resolved on the CPU.
fn fxn_phased(x: u32, y: u32, sa: u32, sb: u32, t: f32) -> f32 {
let a = hash_noise(x, y, sa);
let b = hash_noise(x, y, sb);
return a + (b - a) * t;
}
// u[0] = (squared, original alpha mode, overflow, seed a); u[1].x = seed b; f[0] = (amount, t).
@compute @workgroup_size(16, 16)
fn fxn_noise_alpha(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = fxn_pixel(gid);
if (p.x < 0) {
return;
}
let k = P.f[0].x;
var n = fxn_phased(u32(p.x), u32(p.y), P.u[0].w, P.u[1].x, P.f[0].y);
if (P.u[0].x != 0u) {
n = n * n;
}
let s = fxn_unpremul(textureLoad(src, p, 0));
let a = s.w;
var na: f32;
switch P.u[0].y {
case 0u: {
na = a + n * k;
}
case 1u: {
na = select(0.0, a + n * k, a > 0.0);
}
case 2u: {
na = a * (1.0 + (2.0 * n - 1.0) * k);
}
default: {
na = a + (2.0 * n - 1.0) * k * 4.0 * a * (1.0 - a);
}
}
if (P.u[0].z == 1u) {
let t = na - 2.0 * floor(na / 2.0);
na = select(t, 2.0 - t, t > 1.0);
} else if (P.u[0].z == 2u && !(na >= 0.0 && na <= 1.0)) {
na = na - floor(na);
}
na = clamp(na, 0.0, 1.0);
textureStore(out, p, vec4<f32>(s.xyz * na, na));
}
// Noise HLS: u[0] = (noise kind, seed); u[1], u[2].xy = per channel (hue, lightness,
// saturation) the phase's two seeds; f[0] = (hue, lightness, saturation, grain size); f[1].x =
// phase (Grain), f[1].y = smoothed fraction.
fn fxn_hls_seed(i: u32) -> u32 {
if (i < 4u) {
return P.u[1][i];
}
return P.u[2][i - 4u];
}
fn fxn_hls_n(x: u32, y: u32, k: u32) -> f32 {
let s = P.u[0].y + k * 7919u;
switch P.u[0].x {
case 2u: {
let gs = P.f[0].w;
return clamp((value_noise(f32(x) / gs, f32(y) / gs, P.f[1].x, s) - 0.5) * 3.0, -1.0, 1.0);
}
case 1u: {
let v = fxn_phased(x, y, fxn_hls_seed(2u * k), fxn_hls_seed(2u * k + 1u), P.f[1].y) * 2.0 - 1.0;
return v * abs(v);
}
default: {
return fxn_phased(x, y, fxn_hls_seed(2u * k), fxn_hls_seed(2u * k + 1u), P.f[1].y) * 2.0 - 1.0;
}
}
}
@compute @workgroup_size(16, 16)
fn fxn_noise_hls(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = fxn_pixel(gid);
if (p.x < 0) {
return;
}
let px = textureLoad(src, p, 0);
let s0 = fxn_unpremul(px);
let a = s0.w;
if (a <= 0.0) {
textureStore(out, p, px);
return;
}
let x = u32(p.x);
let y = u32(p.y);
var hsl = rgb_to_hsl(s0.xyz);
if (P.f[0].x != 0.0) {
hsl.x = fract_euclid(hsl.x + fxn_hls_n(x, y, 0u) * P.f[0].x * 0.5);
}
if (P.f[0].y != 0.0) {
hsl.z = clamp(hsl.z + fxn_hls_n(x, y, 1u) * P.f[0].y, 0.0, 1.0);
}
if (P.f[0].z != 0.0) {
hsl.y = clamp(hsl.y + fxn_hls_n(x, y, 2u) * P.f[0].z, 0.0, 1.0);
}
let c = hsl_to_rgb(hsl.x, hsl.y, hsl.z);
textureStore(out, p, vec4<f32>(c * a, a));
}
// ---------------------------------------------------------------- Remove Grain (noise::remove_grain)
// The guide (util::guided_filter's g): the luminance of the premultiplied channels in every
// channel (Multichannel), or the channels themselves (u[0].x, Single Channel).
@compute @workgroup_size(16, 16)
fn fxn_gf_guide(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = fxn_pixel(gid);
if (p.x < 0) {
return;
}
let v = textureLoad(src, p, 0);
if (P.u[0].x != 0u) {
textureStore(out, p, v);
} else {
textureStore(out, p, vec4<f32>(luminance(v.xyz)));
}
}
// Guided filter arithmetic, S = src, A = aux, D = `data` (RGBA f32 rows of u[0].y pixels):
// u[0].x = 0: S − A·D, 1: S − A·A, 2: S / (A + f[0].x), 3: A − S·D, 4: S·D + A, 5: S·A, 6: S·S.
@compute @workgroup_size(16, 16)
fn fxn_gf(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = fxn_pixel(gid);
if (p.x < 0) {
return;
}
let s = textureLoad(src, p, 0);
let a = textureLoad(aux, p, 0);
var d = vec4<f32>(0.0);
let mode = P.u[0].x;
if (mode == 0u || mode == 3u || mode == 4u) {
let i = (u32(p.y) * P.u[0].y + u32(p.x)) * 4u;
d = vec4<f32>(data[i], data[i + 1u], data[i + 2u], data[i + 3u]);
}
var o: vec4<f32>;
switch mode {
case 0u: {
o = s - a * d;
}
case 1u: {
o = s - a * a;
}
case 2u: {
o = s / (a + P.f[0].x);
}
case 3u: {
o = a - s * d;
}
case 4u: {
o = s * d + a;
}
case 5u: {
o = s * a;
}
default: {
o = s * s;
}
}
textureStore(out, p, o);
}
// Texture: f + (o − f) × f[0].x on colour (src = filtered, aux = original).
@compute @workgroup_size(16, 16)
fn fxn_rg_texture(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = fxn_pixel(gid);
if (p.x < 0) {
return;
}
let f = textureLoad(src, p, 0);
let o = textureLoad(aux, p, 0);
textureStore(out, p, vec4<f32>(f.xyz + (o.xyz - f.xyz) * P.f[0].x, f.w));
}
// Unsharp Mask on the planes: src = channels, aux = their blur; f[0] = (amount, threshold).
@compute @workgroup_size(16, 16)
fn fxn_rg_unsharp(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = fxn_pixel(gid);
if (p.x < 0) {
return;
}
var v = textureLoad(src, p, 0);
let l = textureLoad(aux, p, 0);
for (var c = 0; c < 3; c++) {
if (abs(v[c] - l[c]) > P.f[0].y) {
v[c] = v[c] + (v[c] - l[c]) * P.f[0].x;
}
}
textureStore(out, p, v);
}
// Remove Grain's result: colour clamped to 0..alpha (alpha from aux = the original); with
// u[0].x the Preview viewing mode (noise::preview_compose): the original (aux) outside the
// region f[0] = (x0, x1, y0, y1), the box outline in f[1] where u[0].y.
@compute @workgroup_size(16, 16)
fn fxn_rg_out(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = fxn_pixel(gid);
if (p.x < 0) {
return;
}
let o = textureLoad(aux, p, 0);
let a = o.w;
var v = vec4<f32>(clamp(textureLoad(src, p, 0).xyz, vec3<f32>(0.0), vec3<f32>(max(a, 0.0))), a);
if (P.u[0].x != 0u) {
let x = f32(p.x) + 0.5;
let y = f32(p.y) + 0.5;
let r = P.f[0];
let inside = x >= r.x && x < r.y && y >= r.z && y < r.w;
if (!inside) {
v = o;
}
if (P.u[0].y != 0u) {
let nx = abs(x - r.x) < 0.75 || abs(x - r.y) < 0.75;
let ny = abs(y - r.z) < 0.75 || abs(y - r.w) < 0.75;
if ((nx && y >= r.z - 0.75 && y < r.w + 0.75) || (ny && x >= r.x - 0.75 && x < r.y + 0.75)) {
v = vec4<f32>(P.f[1].xyz, 1.0);
}
}
}
textureStore(out, p, v);
}

View File

@ -0,0 +1,925 @@
// GPU effects (stylize and distort family): see src/fx_stylize.rs. Each entry point mirrors the CPU effect in
// effectcraft-effects operation for operation. Every name here is prefixed `fxs_` (the
// family files share one module). Coordinates are pixel centres (x + 0.5, y + 0.5) in buffer
// pixels, as in util::remap. Per-column / per-row tables in `data` hold coordinates the CPU
// computes in f64 (anything with a floor or a wrap), so tile and wrap boundaries match exactly.
const FXS_TAU: f32 = 6.2831855;
const FXS_PI: f32 = 3.1415927;
const FXS_HALF_PI: f32 = 1.5707964;
fn fxs_inside(p: vec2<i32>) -> bool {
let d = out_dims();
return p.x < d.x && p.y < d.y;
}
fn fxs_centre(p: vec2<i32>) -> vec2<f32> {
return vec2<f32>(f32(p.x) + 0.5, f32(p.y) + 0.5);
}
// util::unpremul: straight colour (zero below alpha 1e-6) and alpha (≥ 0).
fn fxs_unpremul(px: vec4<f32>) -> vec4<f32> {
let a = px.w;
if (a > 1e-6) {
return vec4<f32>(fxs_qdiv(px.xyz, a), a);
}
return vec4<f32>(0.0, 0.0, 0.0, max(a, 0.0));
}
// util::smoothstep.
fn fxs_smoothstep(e0: f32, e1: f32, x: f32) -> f32 {
if (abs(e1 - e0) < 1e-9) {
return select(1.0, 0.0, x < e0);
}
let t = clamp((x - e0) / (e1 - e0), 0.0, 1.0);
return t * t * (3.0 - 2.0 * t);
}
// x / a rounded like the CPU's IEEE division (GPU division may be off by an ulp, which flips
// thresholds on 8 / 16 bpc colours that sit exactly on them): one residual correction.
fn fxs_qdiv(x: vec3<f32>, a: f32) -> vec3<f32> {
let q = x / a;
let r = fma(-q, vec3<f32>(a), x);
return q + r / a;
}
fn fxs_rem2(i: i32) -> i32 {
return ((i % 2) + 2) % 2;
}
// ---------------------------------------------------------------- per-pixel colour effects
// u[0].x = mode:
// 1 Posterize (misc::posterize): f[0].x = levels − 1
// 2 Threshold (misc::threshold): f[0].x = level
// 3 CC Threshold: u[0].yz = (channel, invert); f[0].xy = (threshold, blend)
// 4 CC Threshold RGB: u[0].yzw = invert R, G, B; f[0] = (thresholds, blend)
// 5 Strobe Light on colour: u[0].y = operator; f[0] = (strobe colour, blend)
// 6 Strobe Light transparent (Image::scale_alpha): f[0].x = factor
// 7 CC Vignette: f[0] = (cx, cy, distance, amount); f[1].x = pin highlights
fn fxs_bin(v: f32, t: f32, inv: bool) -> f32 {
return select(0.0, 1.0, (v >= t) != inv);
}
fn fxs_strobe_op(op: u32, v: f32, k: f32) -> f32 {
switch op {
case 1u: { return v + k; }
case 2u: { return max(v - k, 0.0); }
case 3u: { return v * k; }
case 4u: { return abs(v - k); }
case 5u: { return 1.0 - (1.0 - v) * (1.0 - k); }
case 6u: { return max(v, k); }
case 7u: { return min(v, k); }
default: { return k; }
}
}
@compute @workgroup_size(16, 16)
fn fxs_point(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = vec2<i32>(gid.xy);
if (!fxs_inside(p)) {
return;
}
let px = textureLoad(src, p, 0);
let mode = P.u[0].x;
let a = px.w;
if (mode == 6u) {
textureStore(out, p, px * P.f[0].x);
return;
}
if (a <= 0.0) {
textureStore(out, p, px);
return;
}
switch mode {
case 1u: {
let lv = P.f[0].x;
let c = clamp(fxs_qdiv(px.xyz, a), vec3<f32>(0.0), vec3<f32>(1.0)) * lv;
let o = vec3<f32>(round_away(c.x), round_away(c.y), round_away(c.z)) / lv;
textureStore(out, p, vec4<f32>(o * a, a));
}
case 2u: {
let v = select(0.0, 1.0, luminance(fxs_qdiv(px.xyz, a)) >= P.f[0].x);
textureStore(out, p, vec4<f32>(vec3<f32>(v) * a, a));
}
case 3u: {
let u = fxs_unpremul(px);
let c = u.xyz;
let t = P.f[0].x;
let blend = P.f[0].y;
let inv = P.u[0].z != 0u;
var o = c;
var na = u.w;
switch P.u[0].y {
case 1u: {
o = vec3<f32>(fxs_bin(c.x, t, inv), fxs_bin(c.y, t, inv), fxs_bin(c.z, t, inv));
}
case 2u: {
o = vec3<f32>(fxs_bin(rgb_to_hsl(c).y, t, inv));
}
case 3u: {
na = fxs_bin(u.w, t, inv);
}
default: {
o = vec3<f32>(fxs_bin(luminance(c), t, inv));
}
}
o = o + (c - o) * blend;
na = na + (u.w - na) * blend;
textureStore(out, p, vec4<f32>(o * na, na));
}
case 4u: {
let c = fxs_qdiv(px.xyz, a);
let t = P.f[0].xyz;
let o = vec3<f32>(fxs_bin(c.x, t.x, P.u[0].y != 0u), fxs_bin(c.y, t.y, P.u[0].z != 0u), fxs_bin(c.z, t.z, P.u[0].w != 0u));
let r = o + (c - o) * P.f[0].w;
textureStore(out, p, vec4<f32>(r * a, a));
}
case 5u: {
let c = px.xyz / a;
let k = P.f[0].xyz;
let op = P.u[0].y;
let o = vec3<f32>(fxs_strobe_op(op, c.x, k.x), fxs_strobe_op(op, c.y, k.y), fxs_strobe_op(op, c.z, k.z));
let r = o + (c - o) * P.f[0].w;
textureStore(out, p, vec4<f32>(r * a, a));
}
case 7u: {
let u = fxs_unpremul(px);
let q = fxs_centre(p);
let dx = q.x - P.f[0].x;
let dy = q.y - P.f[0].y;
let r = sqrt(dx * dx + dy * dy) / P.f[0].z;
let f0 = 1.0 / (1.0 + r * r);
let f = f0 * f0;
var k = 1.0 - P.f[0].w * (1.0 - f);
let pin = P.f[1].x;
if (pin > 0.0) {
let l = clamp(luminance(u.xyz), 0.0, 1.0);
k += (1.0 - k) * pin * l;
}
textureStore(out, p, vec4<f32>(max(u.xyz * k, vec3<f32>(0.0)) * u.w, u.w));
}
default: {
textureStore(out, p, px);
}
}
}
// ---------------------------------------------------------------- inverse-mapped warps
// u[0].x = mode (all sample bilinear, transparent outside, unless noted):
// 1 Mirror (distort::mirror): f[0] = (cx, cy, nx, ny)
// 2 Spherize (distort::spherize): f[0] = (cx, cy, radius)
// 3 Polar Coordinates (distort::polar, edges repeated): u[0].y = to polar;
// f[0] = (w, h, cx, cy); f[1] = (rmax, interpolation)
// 4 CC Slant (distort3::cc_slant): f[0] = (floor x, floor y, 1 / height, slant); f[1].x = x scale
// 5 CC Smear (distort3::cc_smear): f[0] = (from x, from y, dx, dy); f[1] = (len², reach, radius)
// 6 CC Split (distort3::split_impl): f[0] = (ax, ay, ux, uy); f[1] = (len, split 1, split 2, reach)
// 7 Optics Compensation (distort2::optics_compensation): u[0].y = reverse;
// f[0] = (cx, cy, fit, f); f[1] = (reference radius, half FOV)
// 8 CC Kaleida (stylize2::kaleida, edges repeated): u[0].y = mirror;
// f[0] = (cx, cy, size, rotation); f[1].x = segment angle
// 9 Liquify (distort4::liquify): u[0].xy = (_, mesh nx, mesh ny); f[0] = (cell, mesh offset x, y,
// percentage); f[1] = (scale, buffer offset x, y); data = mesh offsets (x, y) per node
// 10 Twirl (Legacy) (distort3::twirl_legacy): f[0] = (cx, cy, radius, angle)
// 11 CC Ripple Pulse (distort3::cc_ripple_pulse): u[0].y = render bump; f[0] = (cx, cy, front,
// speed); f[1] = (time span, wavelength, height)
// 12 CC Power Pin, full perspective (distort3::cc_power_pin): f[2..5] = inverse of the
// square → quad matrix (rows); f[5] = source rect in layer pixels (x0, y0, w, h);
// f[6] = (scale, buffer offset x, y)
fn fxs_mesh_d(i: u32, j: u32) -> vec2<f32> {
let k = (j * P.u[0].y + i) * 2u;
return vec2<f32>(data[k], data[k + 1u]);
}
// distort3::cc_ripple_pulse's wave.
fn fxs_pulse(r: f32) -> f32 {
let front = P.f[0].z;
let speed = P.f[0].w;
let span = P.f[1].x;
let age = (front - r) / max(speed, 1e-9);
if (age < 0.0 || age > span) {
return 0.0;
}
let fade = 1.0 - age / span;
return sin(FXS_TAU * (r - front) / P.f[1].y) * fade;
}
// distort4::Mesh::at.
fn fxs_mesh_at(x: f32, y: f32) -> vec2<f32> {
let cell = P.f[0].x;
let nx = P.u[0].y;
let ny = P.u[0].z;
let fx = x / cell;
let fy = y / cell;
if (fx < 0.0 || fy < 0.0 || fx > f32(nx - 1u) || fy > f32(ny - 1u)) {
return vec2<f32>(0.0);
}
let x0 = min(u32(floor(fx)), nx - 2u);
let y0 = min(u32(floor(fy)), ny - 2u);
let x1 = min(x0 + 1u, nx - 1u);
let y1 = min(y0 + 1u, ny - 1u);
let tx = fx - f32(x0);
let ty = fy - f32(y0);
let a = fxs_mesh_d(x0, y0);
let b = fxs_mesh_d(x1, y0);
let c = fxs_mesh_d(x0, y1);
let d = fxs_mesh_d(x1, y1);
let top = a + (b - a) * tx;
let bot = c + (d - c) * tx;
return top + (bot - top) * ty;
}
@compute @workgroup_size(16, 16)
fn fxs_warp(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = vec2<i32>(gid.xy);
if (!fxs_inside(p)) {
return;
}
let q = fxs_centre(p);
let x = q.x;
let y = q.y;
var s = q;
var clamped = false;
switch P.u[0].x {
case 1u: {
let n = P.f[0].zw;
let d = (x - P.f[0].x) * n.x + (y - P.f[0].y) * n.y;
if (d > 0.0) {
s = vec2<f32>(x - 2.0 * d * n.x, y - 2.0 * d * n.y);
}
}
case 2u: {
let c = P.f[0].xy;
let r = P.f[0].z;
let dx = x - c.x;
let dy = y - c.y;
let d = sqrt(dx * dx + dy * dy);
if (d < r && d != 0.0) {
let nd = d / r;
let k = (asin(nd) / FXS_HALF_PI) / nd;
s = vec2<f32>(c.x + dx * k, c.y + dy * k);
}
}
case 3u: {
clamped = true;
let w = P.f[0].x;
let h = P.f[0].y;
let cx = P.f[0].z;
let cy = P.f[0].w;
let rmax = P.f[1].x;
var pp: vec2<f32>;
if (P.u[0].y != 0u) {
let dx = x - cx;
let dy = y - cy;
let a0 = atan2(dx, -dy);
let a = a0 - floor(a0 / FXS_TAU) * FXS_TAU;
let r = sqrt(dx * dx + dy * dy) / rmax;
pp = vec2<f32>(a / FXS_TAU * w, h - r * h);
} else {
let a = x / w * FXS_TAU;
let r = (h - y) / h * rmax;
pp = vec2<f32>(cx + r * sin(a), cy - r * cos(a));
}
let amt = P.f[1].y;
s = vec2<f32>(x + (pp.x - x) * amt, y + (pp.y - y) * amt);
}
case 4u: {
let fl = P.f[0].xy;
s = vec2<f32>(fl.x + (x - fl.x - (fl.y - y) * P.f[0].w) / P.f[1].x, fl.y + (y - fl.y) * P.f[0].z);
}
case 5u: {
let fr = P.f[0].xy;
let dx = P.f[0].z;
let dy = P.f[0].w;
let t = clamp(((x - fr.x) * dx + (y - fr.y) * dy) / P.f[1].x, 0.0, 1.0);
let px = fr.x + dx * t;
let py = fr.y + dy * t;
let d = sqrt((x - px) * (x - px) + (y - py) * (y - py));
let w0 = max(1.0 - d / P.f[1].z, 0.0);
let w = w0 * w0 * (3.0 - 2.0 * w0);
let k = P.f[1].y * w * t;
s = vec2<f32>(x - dx * k, y - dy * k);
}
case 6u: {
let a = P.f[0].xy;
let ux = P.f[0].z;
let uy = P.f[0].w;
let nx = -uy;
let ny = ux;
let len = P.f[1].x;
let reach = P.f[1].w;
let rx = x - a.x;
let ry = y - a.y;
let t = (rx * ux + ry * uy) / len;
let sd = rx * nx + ry * ny;
if (t >= 0.0 && t <= 1.0) {
let amt = select(P.f[1].z, P.f[1].y, sd >= 0.0);
let g = amt * reach * sin(FXS_PI * t);
let ad = abs(sd);
if (ad < g) {
textureStore(out, p, vec4<f32>(0.0));
return;
}
let push = g * max(1.0 - (ad - g) / reach, 0.0);
let ns = select(-1.0, 1.0, sd >= 0.0) * (ad - push);
s = vec2<f32>(x + nx * (ns - sd), y + ny * (ns - sd));
}
}
case 7u: {
let c = P.f[0].xy;
let fit = P.f[0].z;
let f = P.f[0].w;
let r_ref = P.f[1].x;
let half = P.f[1].y;
let dx = x - c.x;
let dy = y - c.y;
let r = sqrt(dx * dx + dy * dy) * fit;
if (r >= 1e-9) {
var rs: f32;
if (P.u[0].y != 0u) {
rs = atan(r / f) / half * r_ref;
} else {
let th = r / r_ref * half;
if (th >= FXS_HALF_PI - 1e-4) {
textureStore(out, p, vec4<f32>(0.0));
return;
}
rs = f * tan(th);
}
let k = rs / r * fit;
s = vec2<f32>(c.x + dx * k, c.y + dy * k);
}
}
case 8u: {
clamped = true;
let c = P.f[0].xy;
let rot = P.f[0].w;
let seg = P.f[1].x;
let dx = x - c.x;
let dy = y - c.y;
let r = sqrt(dx * dx + dy * dy) / P.f[0].z;
let a0 = atan2(dy, dx) - rot;
var a = a0 - floor(a0 / seg) * seg;
if (P.u[0].y != 0u && a > seg * 0.5) {
a = seg - a;
}
s = vec2<f32>(c.x + r * cos(a + rot), c.y + r * sin(a + rot));
}
case 9u: {
let scale = P.f[1].x;
let o = P.f[1].yz;
let lx = (x - o.x) / scale;
let ly = (y - o.y) / scale;
let d = fxs_mesh_at(lx - P.f[0].y, ly - P.f[0].z);
if (d.x == 0.0 && d.y == 0.0) {
textureStore(out, p, textureLoad(src, p, 0));
return;
}
let pct = P.f[0].w;
s = vec2<f32>((lx + d.x * pct) * scale + o.x, (ly + d.y * pct) * scale + o.y);
}
case 10u: {
let c = P.f[0].xy;
let r = P.f[0].z;
let dx = x - c.x;
let dy = y - c.y;
let d = sqrt(dx * dx + dy * dy);
if (d < r) {
let a = -P.f[0].w * (1.0 - d / r);
let sn = sin(a);
let co = cos(a);
s = vec2<f32>(c.x + dx * co - dy * sn, c.y + dx * sn + dy * co);
}
}
case 11u: {
let c = P.f[0].xy;
let dx = x - c.x;
let dy = y - c.y;
let r = sqrt(dx * dx + dy * dy);
let height = P.f[1].z;
let d = fxs_pulse(r) * height;
var u = vec2<f32>(0.0);
if (r > 1e-9) {
u = vec2<f32>(dx / r, dy / r);
}
var o = sample_bilinear(src, x - u.x * d, y - u.y * d);
if (P.u[0].y != 0u && d != 0.0) {
let slope = (fxs_pulse(r + 0.5) - fxs_pulse(r - 0.5)) * height;
let k = clamp(1.0 - slope * 0.5, 0.0, 2.0);
o = vec4<f32>(o.xyz * k, o.w);
}
textureStore(out, p, o);
return;
}
case 12u: {
let r0 = P.f[2];
let r1 = P.f[3];
let r2 = P.f[4];
var u = r0.x * x + r0.y * y + r0.z;
var v = r1.x * x + r1.y * y + r1.z;
let w = r2.x * x + r2.y * y + r2.z;
if (w != 1.0 && w != 0.0) {
u = u / w;
v = v / w;
}
if (u < -1e-9 || u > 1.0 + 1e-9 || v < -1e-9 || v > 1.0 + 1e-9) {
textureStore(out, p, vec4<f32>(0.0));
return;
}
let rc = P.f[5];
let lx = rc.x + u * rc.z;
let ly = rc.y + v * rc.w;
let o = P.f[6];
s = vec2<f32>(lx * o.x + o.y, ly * o.x + o.z);
}
default: {}
}
if (clamped) {
textureStore(out, p, sample_bilinear_clamped(src, s.x, s.y));
} else {
textureStore(out, p, sample_bilinear(src, s.x, s.y));
}
}
// ---------------------------------------------------------------- CC Flo Motion (distort4::flo_motion)
// One knot's pull at layer point q: f[k] = (knot x, knot y, amount, _).
fn fxs_flo_knot(q: vec2<f32>, k: vec4<f32>) -> vec2<f32> {
if (k.z == 0.0) {
return q;
}
let d = q - k.xy;
let r = sqrt(d.x * d.x + d.y * d.y) / P.f[2].y;
let w = powz(1.0 - min(r, 1.0), P.f[2].x);
let s = max(1.0 - clamp(k.z, -4.0, 0.95) * w, 0.02);
return k.xy + d * s;
}
// u[0] = (supersampling, tile edges); f[0], f[1] = knots; f[2] = (falloff, radius, layer w,
// layer h); f[3] = (scale, buffer offset x, y)
@compute @workgroup_size(16, 16)
fn fxs_flomotion(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = vec2<i32>(gid.xy);
if (!fxs_inside(p)) {
return;
}
let ss = P.u[0].x;
let scale = P.f[3].x;
let off = P.f[3].yz;
let lw = P.f[2].z;
let lh = P.f[2].w;
var acc = vec4<f32>(0.0);
for (var sy = 0u; sy < ss; sy++) {
for (var sx = 0u; sx < ss; sx++) {
let px = f32(p.x) + (f32(sx) + 0.5) / f32(ss);
let py = f32(p.y) + (f32(sy) + 0.5) / f32(ss);
var q = vec2<f32>((px - off.x) / scale, (py - off.y) / scale);
q = fxs_flo_knot(q, P.f[0]);
q = fxs_flo_knot(q, P.f[1]);
if (P.u[0].y != 0u) {
q = vec2<f32>(q.x - floor(q.x / lw) * lw, q.y - floor(q.y / lh) * lh);
}
acc += sample_bilinear(src, q.x * scale + off.x, q.y * scale + off.y);
}
}
textureStore(out, p, acc / f32(ss * ss));
}
// ---------------------------------------------------------------- separable table warps
// The source point is (data[x], data[w + y]) (computed in f64 per column and per row), then
// mixed with the original: out = sample + (orig − sample) · blend (Offset, CC Tiler).
// u[0] = (buffer width, repeat edges); f[0].x = blend
@compute @workgroup_size(16, 16)
fn fxs_table(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = vec2<i32>(gid.xy);
if (!fxs_inside(p)) {
return;
}
let w = P.u[0].x;
let sx = data[u32(p.x)];
let sy = data[w + u32(p.y)];
var s: vec4<f32>;
if (P.u[0].y != 0u) {
s = sample_bilinear_clamped(src, sx, sy);
} else {
s = sample_bilinear(src, sx, sy);
}
let o = textureLoad(src, p, 0);
textureStore(out, p, s + (o - s) * P.f[0].x);
}
// CC Griddler (distort2::cc_griddler). data: per column (tile centre x, local x), per row
// (tile centre y, local y). u[0] = (buffer width, cut tiles); f[0] = (cos, sin, h scale, v scale);
// f[1].x = cut half size (0.45 · tile)
@compute @workgroup_size(16, 16)
fn fxs_griddler(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = vec2<i32>(gid.xy);
if (!fxs_inside(p)) {
return;
}
let w = P.u[0].x;
let x = u32(p.x);
let y = u32(p.y);
let tcx = data[2u * x];
let lx = data[2u * x + 1u];
let tcy = data[2u * w + 2u * y];
let ly = data[2u * w + 2u * y + 1u];
if (P.u[0].y != 0u && max(abs(lx), abs(ly)) > P.f[1].x) {
textureStore(out, p, vec4<f32>(0.0));
return;
}
let cr = P.f[0].x;
let sr = P.f[0].y;
let rx = lx * cr + ly * sr;
let ry = -lx * sr + ly * cr;
textureStore(out, p, sample_bilinear(src, tcx + rx / P.f[0].z, tcy + ry / P.f[0].w));
}
// Motion Tile (stylize2::motion_tile) into a new output size. data: per output column (tile
// index, fraction) of tx, then per row of ty. u[0] = (output width, horizontal phase shift,
// mirror edges); f[0] = (phase, layer w · scale, layer h · scale); f[1].xy = source buffer offset
@compute @workgroup_size(16, 16)
fn fxs_motiontile(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = vec2<i32>(gid.xy);
if (!fxs_inside(p)) {
return;
}
let w = P.u[0].x;
let x = u32(p.x);
let y = u32(p.y);
let ixc = data[2u * x];
let fxc = data[2u * x + 1u];
let iyr = data[2u * w + 2u * y];
let fyr = data[2u * w + 2u * y + 1u];
let phase = P.f[0].x;
var i = ixc;
var fx = fxc;
var j = iyr;
var fy = fyr;
if (P.u[0].y != 0u) {
let t = fxc + phase * iyr;
i = ixc + floor(t);
fx = t - floor(t);
} else {
let t = fyr + phase * ixc;
j = iyr + floor(t);
fy = t - floor(t);
}
if (P.u[0].z != 0u) {
if (fxs_rem2(i32(i)) == 1) {
fx = 1.0 - fx;
}
if (fxs_rem2(i32(j)) == 1) {
fy = 1.0 - fy;
}
}
textureStore(out, p, sample_bilinear(src, fx * P.f[0].y + P.f[1].x, fy * P.f[0].z + P.f[1].y));
}
// CC RepeTile (stylize2::repetile) into a new output size. data: per output column (tile index,
// position in the tile), then per row. u[0] = (output width, tiling); f[0] = layer rect (x0, y0, w, h)
@compute @workgroup_size(16, 16)
fn fxs_repetile(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = vec2<i32>(gid.xy);
if (!fxs_inside(p)) {
return;
}
let w = P.u[0].x;
let x = u32(p.x);
let y = u32(p.y);
let ix = i32(data[2u * x]);
var fx = data[2u * x + 1u];
let iy = i32(data[2u * w + 2u * y]);
var fy = data[2u * w + 2u * y + 1u];
let odd = fxs_rem2(ix + iy) == 1;
var flip_x = false;
var flip_y = false;
switch P.u[0].y {
case 1u: {
flip_x = fxs_rem2(ix) == 1;
flip_y = fxs_rem2(iy) == 1;
}
case 2u: {
flip_x = odd;
}
case 3u: {
flip_y = odd;
}
case 4u: {
flip_x = odd;
flip_y = odd;
}
default: {}
}
let r = P.f[0];
if (flip_x) {
fx = r.z - fx;
}
if (flip_y) {
fy = r.w - fy;
}
textureStore(out, p, sample_bilinear(src, r.x + fx, r.y + fy));
}
// ---------------------------------------------------------------- Magnify (distort2::magnify)
// data: per column (dx, sx, floor(sx) + 0.5), then per row (dy, sy, floor(sy) + 0.5).
// u[0] = (buffer width, square, scaling, blend: 0 none, 1 normal, 2 blend mode);
// u[1] = (blend mode id, seed); f[0] = (size, feather, opacity, magnification); f[1].x = size − feather
@compute @workgroup_size(16, 16)
fn fxs_magnify(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = vec2<i32>(gid.xy);
if (!fxs_inside(p)) {
return;
}
let bw = P.u[0].x;
let x = u32(p.x);
let y = u32(p.y);
let dx = data[3u * x];
let sx = data[3u * x + 1u];
let nx = data[3u * x + 2u];
let dy = data[3u * bw + 3u * y];
let sy = data[3u * bw + 3u * y + 1u];
let ny = data[3u * bw + 3u * y + 2u];
var d: f32;
if (P.u[0].y != 0u) {
d = max(abs(dx), abs(dy));
} else {
d = sqrt(dx * dx + dy * dy);
}
let size = P.f[0].x;
let feather = P.f[0].y;
var w: f32;
if (feather > 0.01) {
w = 1.0 - fxs_smoothstep(P.f[1].x, size, d);
} else {
w = clamp(size - d + 0.5, 0.0, 1.0);
}
w = w * P.f[0].z;
let px = textureLoad(src, p, 0);
let blend = P.u[0].w;
if (w <= 0.0) {
if (blend == 0u) {
textureStore(out, p, vec4<f32>(0.0));
} else {
textureStore(out, p, px);
}
return;
}
var m: vec4<f32>;
switch P.u[0].z {
case 1u: {
m = sample_bicubic(src, sx, sy);
}
case 2u: {
let seed = P.u[1].y;
let k = max(1.0 - 1.0 / P.f[0].w, 0.0);
let j1 = (hash_noise(x, y ^ (1u << 20u), seed) - 0.5) * k;
let j2 = (hash_noise(x, y ^ (2u << 20u), seed) - 0.5) * k;
m = sample_bilinear(src, nx + j1, ny + j2);
}
default: {
m = sample_bilinear(src, nx, ny);
}
}
if (blend == 0u) {
textureStore(out, p, m * w);
} else if (blend == 1u) {
textureStore(out, p, px + (m - px) * w);
} else {
textureStore(out, p, blend_pixel(P.u[1].x, px, m * w, 0.5));
}
}
// ---------------------------------------------------------------- Scatter (stylize2::scatter)
// u[0] = (grain, seed); f[0].x = amount
@compute @workgroup_size(16, 16)
fn fxs_scatter(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = vec2<i32>(gid.xy);
if (!fxs_inside(p)) {
return;
}
let grain = P.u[0].x;
let seed = P.u[0].y;
let amt = P.f[0].x;
let x = u32(p.x);
let y = u32(p.y);
var dx = 0.0;
var dy = 0.0;
if (grain != 2u) {
dx = (hash_noise(x, y, seed) - 0.5) * 2.0 * amt;
}
if (grain != 1u) {
dy = (hash_noise(x, y, seed ^ 0x5bd1u) - 0.5) * 2.0 * amt;
}
textureStore(out, p, tex_get(src, i32(round_away(f32(p.x) + dx)), i32(round_away(f32(p.y) + dy))));
}
// ---------------------------------------------------------------- Brush Strokes (stylize2::brush_strokes)
// data: cell column per x (w), cell row per y (h), then per cell (dx, dy, length, jitter, samples,
// painted). u[0] = (buffer width, cell columns, paint surface, buffer height); f[0].x = blend
@compute @workgroup_size(16, 16)
fn fxs_brush(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = vec2<i32>(gid.xy);
if (!fxs_inside(p)) {
return;
}
let w = P.u[0].x;
let h = P.u[0].w;
let cx = u32(data[u32(p.x)]);
let cy = u32(data[w + u32(p.y)]);
let base = w + h + (cy * P.u[0].y + cx) * 6u;
let dx = data[base];
let dy = data[base + 1u];
let l = data[base + 2u];
let jit = data[base + 3u];
let n = u32(data[base + 4u]);
let m = data[base + 5u];
let q = fxs_centre(p);
let bx = q.x - dy * jit;
let by = q.y + dx * jit;
var acc = vec4<f32>(0.0);
for (var i = 0u; i < n; i++) {
var t = 0.0;
if (n != 1u) {
t = (f32(i) / f32(n - 1u) - 0.5) * l;
}
acc += sample_bilinear_clamped(src, bx + dx * t, by + dy * t);
}
let stroke = acc / f32(n);
let orig = textureLoad(src, p, 0);
let surface = P.u[0].z;
var base_px = orig;
if (surface == 1u) {
base_px = vec4<f32>(0.0);
} else if (surface == 2u) {
base_px = vec4<f32>(1.0);
} else if (surface == 3u) {
base_px = vec4<f32>(0.0, 0.0, 0.0, 1.0);
}
var painted: vec4<f32>;
if (surface == 2u || surface == 3u) {
let k = 1.0 - stroke.w * m;
painted = stroke * m + base_px * k;
} else {
painted = base_px + (stroke - base_px) * m;
}
textureStore(out, p, painted + (orig - painted) * P.f[0].x);
}
// ---------------------------------------------------------------- Roughen Edges (stylize2::roughen_edges)
// noise::fbm.
fn fxs_fbm(u: f32, v: f32, z: f32, seed: u32, octaves0: f32) -> f32 {
let octaves = clamp(octaves0, 1.0, 20.0);
let n = u32(ceil(octaves));
let frac = octaves - floor(octaves);
var sum = 0.0;
var norm = 0.0;
var amp = 1.0;
var f = 1.0;
for (var o = 0u; o < n; o++) {
var w = 1.0;
if (o + 1u == n && frac > 0.0) {
w = frac;
}
sum += value_noise(u * f, v * f, z + f32(o) * 7.31, seed + o) * amp * w;
norm += amp * w;
amp *= 0.5;
f *= 2.0;
}
return sum / max(norm, 1e-6);
}
// aux = the (blurred) alpha in .w. u[0] = (seed, cycle evolution, coloured);
// f[0] = (offset x, offset y, scale · stretch x, scale · stretch y); f[1] = (frequency, evolution,
// cycle, cycle weight); f[2] = (octaves, influence, influence multiplier, sharpness);
// f[3] = (edge colour, sharpness multiplier)
@compute @workgroup_size(16, 16)
fn fxs_roughen(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = vec2<i32>(gid.xy);
if (!fxs_inside(p)) {
return;
}
let px = textureLoad(src, p, 0);
if (px.w <= 0.0) {
textureStore(out, p, px);
return;
}
let dv = textureLoad(aux, p, 0).w;
let fm = P.f[1].x;
let nx = (f32(p.x) + 0.5 - P.f[0].x) / P.f[0].z * fm;
let ny = (f32(p.y) + 0.5 - P.f[0].y) / P.f[0].w * fm;
let seed = P.u[0].x;
let evo = P.f[1].y;
let oct = P.f[2].x;
var n = fxs_fbm(nx, ny, evo, seed, oct);
if (P.u[0].y != 0u) {
n += (fxs_fbm(nx, ny, evo - P.f[1].z, seed, oct) - n) * P.f[1].w;
}
let infl = P.f[2].y;
let s = dv - 0.5 + (n - 0.5) * infl * P.f[2].z * 0.5;
let m = clamp(s * P.f[2].w * P.f[3].w * 4.0 + 0.5, 0.0, 1.0);
let u = fxs_unpremul(px);
var c = u.xyz;
if (P.u[0].z != 0u) {
let band = 1.0 - fxs_smoothstep(0.5, 0.9, dv + (n - 0.5) * infl * 0.5);
c = c + (P.f[3].xyz - c) * band;
}
let a = u.w * m;
textureStore(out, p, vec4<f32>(c * a, a));
}
// ---------------------------------------------------------------- Texturize (stylize2::texturize)
// The texture layer placed into the buffer (transition::place_layer) as luminance
// (Plane::luma). aux = the texture layer's buffer; data: per column x, then per row y in its
// pixels. u[0].x = buffer width
@compute @workgroup_size(16, 16)
fn fxs_tex_luma(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = vec2<i32>(gid.xy);
if (!fxs_inside(p)) {
return;
}
let w = P.u[0].x;
let t = sample_bilinear(aux, data[u32(p.x)], data[w + u32(p.y)]);
let l = luminance(fxs_unpremul(t).xyz);
textureStore(out, p, vec4<f32>(l));
}
// Relief shading. aux = the blurred texture luminance; f[0] = (light x, light y, contrast)
@compute @workgroup_size(16, 16)
fn fxs_texturize(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = vec2<i32>(gid.xy);
if (!fxs_inside(p)) {
return;
}
let gx = (tex_get_clamped(aux, p.x + 1, p.y).x - tex_get_clamped(aux, p.x - 1, p.y).x) * 0.5;
let gy = (tex_get_clamped(aux, p.x, p.y + 1).x - tex_get_clamped(aux, p.x, p.y - 1).x) * 0.5;
let shade = max(1.0 - P.f[0].z * 4.0 * (gx * P.f[0].x + gy * P.f[0].y), 0.0);
let px = textureLoad(src, p, 0);
textureStore(out, p, vec4<f32>(px.xyz * shade, px.w));
}
// ---------------------------------------------------------------- Glow (misc::glow) extras
// Bright pass with the Arbitrary Map: as `glow_bright` (same f[0] / u[0] layout, see
// kernels.wgsl) with the red, green and blue curves at data offsets f[1].xyz (−1 = identity).
@compute @workgroup_size(16, 16)
fn fxs_glow_map(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = vec2<i32>(gid.xy);
if (!fxs_inside(p)) {
return;
}
let px = textureLoad(src, p, 0);
let a = px.w;
if (a <= 0.0) {
textureStore(out, p, vec4<f32>(0.0));
return;
}
let thr = P.f[0].x;
var l = luminance(px.xyz / a);
if (P.u[0].y != 0u) {
l = a;
}
let k = clamp((l - thr) / max(1.0 - thr, 1e-3), 0.0, 1.0);
let t = glow_ab_t(l);
let c = vec3<f32>(curve_eval(P.f[1].x, t), curve_eval(P.f[1].y, t), curve_eval(P.f[1].z, t));
textureStore(out, p, vec4<f32>(c * k * a, k * a));
}
// On Top with another Glow Operation: blend_pixel(mode, original, glow · intensity, 0).
// aux = the blurred glow; u[0].x = blend mode id; f[0].x = intensity
@compute @workgroup_size(16, 16)
fn fxs_glow_op(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = vec2<i32>(gid.xy);
if (!fxs_inside(p)) {
return;
}
let g = textureLoad(aux, p, 0) * P.f[0].x;
var o = blend_pixel(P.u[0].x, textureLoad(src, p, 0), g, 0.0);
o.w = clamp(o.w, 0.0, 1.0);
textureStore(out, p, o);
}
// ---------------------------------------------------------------- accumulation (Transform motion blur)
// out = src + aux
@compute @workgroup_size(16, 16)
fn fxs_add(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = vec2<i32>(gid.xy);
if (!fxs_inside(p)) {
return;
}
textureStore(out, p, textureLoad(src, p, 0) + textureLoad(aux, p, 0));
}
// out = src / f[0].x
@compute @workgroup_size(16, 16)
fn fxs_div(@builtin(global_invocation_id) gid: vec3<u32>) {
let p = vec2<i32>(gid.xy);
if (!fxs_inside(p)) {
return;
}
textureStore(out, p, textureLoad(src, p, 0) / P.f[0].x);
}

View File

@ -0,0 +1,729 @@
// GPU effects (tonal and colour correction family): see src/fx_tone.rs. Each operation mirrors
// the CPU effect in effectcraft-effects operation for operation. Every name here is prefixed
// `fxt_` (the family files share one module).
fn fxt_straight(px: vec4<f32>) -> vec3<f32> {
if (px.w > 1e-6) {
return px.xyz / px.w;
}
return vec3<f32>(0.0);
}
fn fxt_mix(a: vec3<f32>, b: vec3<f32>, t: f32) -> vec3<f32> {
return a + (b - a) * t;
}
// f32::rem_euclid(360.0).
fn fxt_rem360(x: f32) -> f32 {
let r = x - 360.0 * floor(x / 360.0);
return select(r, 0.0, r >= 360.0);
}
// Levels (Individual Controls) channel `i` (data: 5 × (ib, iw, g, ob, ow, identity)).
fn fxt_lv(i: u32, v: f32, clip_b: bool, clip_w: bool) -> f32 {
let o = i * 6u;
if (data[o + 5u] != 0.0) {
return v;
}
var t = (v - data[o]) / max(data[o + 1u] - data[o], 1e-6);
if (clip_b) {
t = max(t, 0.0);
}
if (clip_w) {
t = min(t, 1.0);
}
return data[o + 3u] + (data[o + 4u] - data[o + 3u]) * powz(max(t, 0.0), 1.0 / max(data[o + 2u], 0.01));
}
// color_fx::level (data offset o: ib, iw, g, ob, ow, clip black, clip white).
fn fxt_level(o: u32, v: f32) -> f32 {
var t = (v - data[o]) / max(data[o + 1u] - data[o], 1e-6);
if (data[o + 5u] != 0.0) {
t = max(t, 0.0);
}
if (data[o + 6u] != 0.0) {
t = min(t, 1.0);
}
t = powz(max(t, 0.0), 1.0 / data[o + 2u]);
return data[o + 3u] + (data[o + 4u] - data[o + 3u]) * t;
}
fn fxt_level_ident(o: u32) -> bool {
return data[o] == 0.0 && data[o + 1u] == 1.0 && data[o + 2u] == 1.0 && data[o + 3u] == 0.0 && data[o + 4u] == 1.0;
}
fn fxt_adjust_sat(s: f32, sat: f32) -> f32 {
if (sat >= 0.0) {
return s + (1.0 - s) * sat * min(s, 1.0);
}
return s * (1.0 + sat);
}
fn fxt_adjust_light(l: f32, light: f32) -> f32 {
if (light >= 0.0) {
return l + (1.0 - l) * light;
}
return l * (1.0 + light);
}
// HueRange::weight for range `r` (data: start, end, fall start, fall end, hue, sat, light).
fn fxt_range_weight(r: u32, h: f32) -> f32 {
let o = r * 7u;
let width = fxt_rem360(data[o + 1u] - data[o]);
let d = fxt_rem360(h - data[o]);
if (d <= width) {
return 1.0;
}
let after = d - width;
let before = 360.0 - d;
var wa = 0.0;
if (data[o + 3u] > 0.0) {
wa = 1.0 - after / data[o + 3u];
}
var wb = 0.0;
if (data[o + 2u] > 0.0) {
wb = 1.0 - before / data[o + 2u];
}
return clamp(max(wa, wb), 0.0, 1.0);
}
// color3::colour_match.
fn fxt_colour_match(c: vec3<f32>, key: vec3<f32>, key_hue: f32, mode: u32, tol: f32, soft: f32) -> f32 {
var d: f32;
if (mode == 0u) {
d = sqrt(dot(c - key, c - key)) / sqrt(3.0);
} else if (mode == 1u) {
let hs = rgb_to_hsl(c);
let dh0 = abs(hs.x - key_hue);
let dh = min(dh0, 1.0 - dh0) * 2.0;
d = select(dh, 1.0, hs.y < 0.02);
} else {
let sa = max(c.x + c.y + c.z, 1e-6);
let sk = max(key.x + key.y + key.z, 1e-6);
let a = vec2<f32>(c.x / sa, c.y / sa);
let k = vec2<f32>(key.x / sk, key.y / sk);
d = sqrt(dot(a - k, a - k)) * 1.5;
}
if (d <= tol) {
return 1.0;
}
if (soft <= 1e-6) {
return 0.0;
}
return clamp(1.0 - (d - tol) / soft, 0.0, 1.0);
}
// color3::knee.
fn fxt_knee(v: f32, start: f32, limit: f32) -> f32 {
if (v <= start || limit <= start) {
return min(v, limit);
}
let r = limit - start;
return start + r * (1.0 - exp(-(v - start) / r));
}
// PS Arbitrary Map lookup (map in data, n entries; f[0].x = phase offset in entries).
fn fxt_lookup(v: f32, n: u32) -> f32 {
let nf = f32(n);
var x = clamp(v, 0.0, 1.0) * (nf - 1.0) + P.f[0].x;
x = x - nf * floor(x / nf);
let fl = floor(x);
let i0 = u32(fl) % n;
let i1 = select(i0, i0 + 1u, i0 + 1u < n);
let t = x - fl;
return data[i0] + (data[i1] - data[i0]) * t;
}
fn fxt_eq_look(table: u32, n: u32, v: f32) -> f32 {
let i = min(u32(round_away(clamp(v, 0.0, 1.0) * f32(n - 1u))), n - 1u);
return data[table * n + i];
}
// Per-pixel colour corrections. u[0].x = op (layouts in src/fx_tone.rs):
// 1 Levels (Individual Controls) 2 Gamma/Pedestal/Gain 3 Photo Filter 4 Change to Color
// 5 Leave Color 6 Change Color 7 Broadcast Colors 8 Color Balance (HLS) 9 CC Toner
// 10 CC Color Offset 11 CC Kernel 12 PS Arbitrary Map 13 Video Limiter
// 14 Hue/Saturation with colour ranges 15 Levels with channel controls
// 16 Auto Levels / Contrast / Color apply 17 Equalize apply 18 Invert (HLS / YIQ channels)
// 19 luminance plane 20 Shadow/Highlight (aux = (shadow base, highlight base))
// 21 pack src.x, aux.x 22 Shadow/Highlight clip remap 23 CC Color Neutralizer
// 24 Color Stabilizer (data: per channel n, then 5 × (x, y))
@compute @workgroup_size(16, 16)
fn fxt_point(@builtin(global_invocation_id) gid: vec3<u32>) {
let dims = out_dims();
let p = vec2<i32>(gid.xy);
if (p.x >= dims.x || p.y >= dims.y) {
return;
}
let px = textureLoad(src, p, 0);
let a = px.w;
let u0 = P.u[0];
let f0 = P.f[0];
// Image::map_straight: transparent pixels stay as they are.
let straight_skip = a <= 0.0;
let c = fxt_straight(px);
var o = px;
switch u0.x {
case 1u: {
let cb = u0.y != 0u;
let cw = u0.z != 0u;
// color2::map_ca.
if (a <= 0.0 && px.x == 0.0 && px.y == 0.0 && px.z == 0.0) {
let na = clamp(fxt_lv(4u, 0.0, cb, cw), 0.0, 1.0);
if (na > 0.0) {
o = vec4<f32>(0.0, 0.0, 0.0, clamp(na, 0.0, 1.0));
}
} else {
let aa = max(a, 0.0);
var nc: vec3<f32>;
for (var i = 0u; i < 3u; i++) {
nc[i] = fxt_lv(i + 1u, fxt_lv(0u, c[i], cb, cw), cb, cw);
}
let na = clamp(clamp(fxt_lv(4u, aa, cb, cw), 0.0, 1.0), 0.0, 1.0);
o = vec4<f32>(nc * na, na);
}
}
case 2u: {
if (straight_skip) {
break;
}
let stretch = P.f[3].x;
var r: vec3<f32>;
for (var i = 0; i < 3; i++) {
var v = c[i];
if (stretch > 0.0 && v > 0.0 && v < 1.0) {
let q = 1.0 - v;
v = v + stretch * 0.25 * v * (q * q * q * q);
}
let g = P.f[i];
r[i] = max(g.y + (g.z - g.y) * powz(max(v, 0.0), g.x), 0.0);
}
o = vec4<f32>(r * a, a);
}
case 3u: {
if (straight_skip) {
break;
}
var r = fxt_mix(c, c * f0.xyz, f0.w);
if (u0.y != 0u) {
let l0 = luminance(c);
let l1 = max(luminance(r), 1e-6);
r = r * l0 / l1;
}
o = vec4<f32>(r * a, a);
}
case 4u: {
if (straight_skip) {
break;
}
let fr = f0.xyz;
let tc = P.f[1].xyz;
let tol = P.f[2].xyz;
let soft = f0.w;
let hsl = rgb_to_hsl(c);
let d = vec3<f32>(abs(fract_euclid(hsl.x - fr.x + 0.5) - 0.5) * 2.0, abs(hsl.z - fr.z), abs(hsl.y - fr.y));
var k = 1.0;
for (var i = 0; i < 3; i++) {
let over = d[i] - tol[i];
if (over > 0.0) {
k = min(k, select(clamp(1.0 - over / soft, 0.0, 1.0), 0.0, soft <= 1e-6));
}
}
let ch_l = (u0.y & 1u) != 0u;
let ch_s = (u0.y & 2u) != 0u;
var r = c;
if ((u0.y & 8u) != 0u) {
r = vec3<f32>(k);
} else if (k > 0.0) {
var nh: f32;
var ns = hsl.y;
var nl = hsl.z;
if ((u0.y & 4u) != 0u) {
let dh = (fract_euclid(tc.x - fr.x + 0.5) - 0.5) * k;
nh = fract_euclid(hsl.x + dh);
if (ch_s) {
ns = hsl.y + (tc.y - fr.y) * k;
}
if (ch_l) {
nl = hsl.z + (tc.z - fr.z) * k;
}
} else {
let dh = (fract_euclid(tc.x - hsl.x + 0.5) - 0.5) * k;
nh = fract_euclid(hsl.x + dh);
if (ch_s) {
ns = hsl.y + (tc.y - hsl.y) * k;
}
if (ch_l) {
nl = hsl.z + (tc.z - hsl.z) * k;
}
}
r = hsl_to_rgb(nh, clamp(ns, 0.0, 1.0), clamp(nl, 0.0, 1.0));
}
o = vec4<f32>(r * a, a);
}
case 5u: {
if (straight_skip) {
break;
}
let keep = f0.xyz;
let amt = P.f[1].x;
let tol = P.f[1].y;
let soft = P.f[1].z;
var d: f32;
if (u0.y != 0u) {
let hsl = rgb_to_hsl(c);
d = select(abs(fract_euclid(hsl.x - f0.w + 0.5) - 0.5) * 2.0, 1.0, hsl.y < 0.02);
} else {
d = sqrt(dot(c - keep, c - keep)) / sqrt(3.0);
}
var keepk: f32;
if (tol >= 1.0 || d <= tol) {
keepk = 1.0;
} else if (soft <= 1e-6) {
keepk = 0.0;
} else {
keepk = clamp(1.0 - (d - tol) / soft, 0.0, 1.0);
}
let g = luminance(c);
let r = fxt_mix(c, vec3<f32>(g), amt * (1.0 - keepk));
o = vec4<f32>(r * a, a);
}
case 6u: {
if (a <= 0.0) {
break;
}
var m = fxt_colour_match(c, f0.xyz, f0.w, u0.y, P.f[2].x, P.f[2].y);
if ((u0.z & 2u) != 0u) {
m = 1.0 - m;
}
var r = c;
if ((u0.z & 1u) != 0u) {
r = vec3<f32>(m);
} else if (m > 0.0) {
let hsl = rgb_to_hsl(c);
let dh = P.f[1].x;
let dl = P.f[1].y;
let ds = P.f[1].z;
let h2 = fract_euclid(hsl.x + dh);
let s2 = clamp(hsl.y + ds * select(hsl.y, 1.0 - hsl.y, ds > 0.0), 0.0, 1.0);
let l2 = clamp(hsl.z + dl * select(hsl.z, 1.0 - hsl.z, dl > 0.0), 0.0, 1.0);
r = fxt_mix(c, hsl_to_rgb(h2, s2, l2), m);
}
o = vec4<f32>(r * a, a);
}
case 7u: {
if (a <= 0.0) {
break;
}
let pal = u0.y != 0u;
let y = 0.299 * c.x + 0.587 * c.y + 0.114 * c.z;
var ch: f32;
if (pal) {
let uu = 0.492 * (c.z - y);
let vv = 0.877 * (c.x - y);
ch = sqrt(uu * uu + vv * vv);
} else {
let ii = 0.596 * c.x - 0.274 * c.y - 0.322 * c.z;
let qq = 0.211 * c.x - 0.523 * c.y + 0.312 * c.z;
ch = sqrt(ii * ii + qq * qq);
}
let max_norm = f0.x;
let unsafe_px = y + ch > max_norm + 1e-5;
switch u0.z {
case 2u: {
if (unsafe_px) {
o = vec4<f32>(0.0);
}
}
case 3u: {
if (!unsafe_px) {
o = vec4<f32>(0.0);
}
}
case 1u: {
if (unsafe_px) {
let target_v = max(max_norm - y, 0.0);
let s = select(1.0, target_v / ch, ch > 1e-6);
var r = y + (c - y) * s;
if (y > max_norm) {
r = r * max_norm / y;
}
o = vec4<f32>(r * a, a);
}
}
default: {
if (unsafe_px) {
let k = max_norm / (y + ch);
o = vec4<f32>(c * k * a, a);
}
}
}
}
case 8u: {
if (straight_skip) {
break;
}
let hsl = rgb_to_hsl(c);
let dh = f0.x;
let dl = f0.y;
let ds = f0.z;
var l = hsl.z;
if (dl > 0.0) {
l = l + (1.0 - l) * dl;
} else {
l = l + l * dl;
}
var s = hsl.y;
if (ds > 0.0) {
s = s + (1.0 - s) * ds;
} else {
s = s + s * ds;
}
let r = hsl_to_rgb(hsl.x + dh, clamp(s, 0.0, 1.0), clamp(l, 0.0, 1.0));
o = vec4<f32>(r * a, a);
}
case 9u: {
if (straight_skip) {
break;
}
let n = u0.y;
let t = clamp(luminance(c), 0.0, 1.0) * f32(n);
let k = min(u32(floor(t)), n - 1u);
let tone = fxt_mix(P.f[1u + k].xyz, P.f[2u + k].xyz, t - f32(k));
let r = fxt_mix(tone, c, f0.x);
o = vec4<f32>(r * a, a);
}
case 10u: {
if (straight_skip) {
break;
}
var r = c;
for (var k = 0; k < 3; k++) {
if (f0[k] == 0.0) {
continue;
}
let x = c[k] + f0[k];
if (x <= 1.0) {
r[k] = x;
} else if (u0.y == 1u) {
r[k] = max(2.0 - x, 0.0);
} else if (u0.y == 2u) {
r[k] = select(1.0, 0.0, x - 1.0 < 0.5);
} else {
r[k] = fract_euclid(x);
}
}
o = vec4<f32>(r * a, a);
}
case 11u: {
if (a <= 0.0) {
break;
}
var acc = vec3<f32>(0.0);
for (var j = 0; j < 3; j++) {
for (var i = 0; i < 3; i++) {
let idx = j * 3 + i;
let w = P.f[idx / 4][idx % 4];
if (w == 0.0) {
continue;
}
let q = tex_get_clamped(src, p.x + i - 1, p.y + j - 1);
acc += fxt_straight(q) * w;
}
}
var v = acc * P.f[2].y + P.f[2].z;
if (u0.y != 0u) {
v = abs(v);
}
o = vec4<f32>(max(v, vec3<f32>(0.0)) * a, a);
}
case 12u: {
let n = u0.z;
let aa = max(a, 0.0);
let r = vec3<f32>(fxt_lookup(c.x, n), fxt_lookup(c.y, n), fxt_lookup(c.z, n));
var na = aa;
if (u0.y != 0u) {
na = fxt_lookup(aa, n);
}
o = vec4<f32>(r * na, na);
}
case 13u: {
if (a <= 0.0) {
break;
}
let level = f0.x;
let start = f0.y;
let y = luminance(c);
var r = c;
if (u0.y != 1u) {
let y2 = fxt_knee(max(y, 0.0), start, level);
r = r - y + y2;
}
if (u0.y != 0u) {
let yl = luminance(r);
var t = 1.0;
for (var i = 0; i < 3; i++) {
let v = r[i];
if (v > level && v - yl > 1e-6) {
t = min(t, (level - yl) / (v - yl));
}
if (v < 0.0 && yl - v > 1e-6) {
t = min(t, yl / (yl - v));
}
}
t = clamp(t, 0.0, 1.0);
r = yl + (r - yl) * t;
}
r = clamp(r, vec3<f32>(0.0), vec3<f32>(level));
if (u0.z != 0u && any(abs(c - r) > vec3<f32>(1e-4))) {
r = P.f[1].xyz;
}
o = vec4<f32>(r * a, a);
}
case 14u: {
if (straight_skip) {
break;
}
let hsl = rgb_to_hsl(c);
var h = hsl.x;
var s = hsl.y;
var l = hsl.z;
if (u0.y != 0u) {
let cl = P.f[1].z;
h = P.f[1].x;
s = P.f[1].y;
l = clamp(l + cl * select(l, 1.0 - l, cl > 0.0), 0.0, 1.0);
} else {
var s2 = fxt_adjust_sat(s, f0.y);
var l2 = fxt_adjust_light(l, f0.z);
var h2 = h + f0.x;
for (var r = 0u; r < u0.z; r++) {
let w = fxt_range_weight(r, hsl.x * 360.0) * min(hsl.y * 20.0, 1.0);
if (w > 0.0) {
let ro = r * 7u;
h2 += data[ro + 4u] * w;
s2 = fxt_adjust_sat(clamp(s2, 0.0, 1.0), data[ro + 5u] * w);
l2 = fxt_adjust_light(l2, data[ro + 6u] * w);
}
}
h = fract_euclid(h2);
s = clamp(s2, 0.0, 1.0);
l = l2;
}
o = vec4<f32>(hsl_to_rgb(h, s, l) * a, a);
}
case 15u: {
var r = c;
if (!straight_skip) {
for (var i = 0u; i < 3u; i++) {
r[i] = fxt_level(0u, c[i]);
}
}
// The master pass writes premultiplied values; the channel pass reads them back.
var q = px;
if (!straight_skip) {
q = vec4<f32>(r * a, a);
}
let qc = fxt_straight(q);
let qa = max(q.w, 0.0);
var any_ch = false;
for (var k = 0u; k < 4u; k++) {
if (!fxt_level_ident(7u + 7u * k)) {
any_ch = true;
}
}
if (!any_ch) {
o = q;
break;
}
var oc = qc;
for (var i = 0u; i < 3u; i++) {
let off = 7u + 7u * i;
if (!fxt_level_ident(off)) {
oc[i] = fxt_level(off, qc[i]);
}
}
var na = qa;
if (!fxt_level_ident(28u)) {
na = clamp(fxt_level(28u, qa), 0.0, 1.0);
}
o = vec4<f32>(oc * na, na);
}
case 16u: {
if (a <= 0.0 && px.x == 0.0 && px.y == 0.0 && px.z == 0.0) {
break;
}
let lo = f0.xyz;
let hi = P.f[1].xyz;
let g = P.f[2];
var r: vec3<f32>;
for (var k = 0; k < 3; k++) {
let st = clamp((c[k] - lo[k]) / (hi[k] - lo[k]), 0.0, 1.0);
r[k] = powz(st, g[k]);
}
r = fxt_mix(r, c, g.w);
let na = clamp(max(a, 0.0), 0.0, 1.0);
o = vec4<f32>(r * na, na);
}
case 17u: {
if (a <= 0.0 && px.x == 0.0 && px.y == 0.0 && px.z == 0.0) {
break;
}
let n = u0.z;
let amt = f0.x;
var r: vec3<f32>;
if (u0.y == 1u) {
let l = luminance(c);
let nl = fxt_eq_look(0u, n, l);
if (l > 1e-5) {
r = c * nl / l;
} else {
r = vec3<f32>(nl);
}
} else if (u0.y == 2u) {
r = vec3<f32>(fxt_eq_look(0u, n, c.x), fxt_eq_look(0u, n, c.y), fxt_eq_look(0u, n, c.z));
} else {
r = vec3<f32>(fxt_eq_look(0u, n, c.x), fxt_eq_look(1u, n, c.y), fxt_eq_look(2u, n, c.z));
}
r = fxt_mix(c, r, amt);
let na = clamp(max(a, 0.0), 0.0, 1.0);
o = vec4<f32>(r * na, na);
}
case 18u: {
if (a <= 0.0) {
break;
}
let ch = u0.y;
var r: vec3<f32>;
if (ch <= 7u) {
var hsl = rgb_to_hsl(c);
if (ch == 4u || ch == 5u) {
hsl.x = 1.0 - hsl.x;
}
if (ch == 4u || ch == 6u) {
hsl.z = 1.0 - hsl.z;
}
if (ch == 4u || ch == 7u) {
hsl.y = 1.0 - hsl.y;
}
r = hsl_to_rgb(hsl.x, hsl.y, hsl.z);
} else {
var q = vec3<f32>(
0.299 * c.x + 0.587 * c.y + 0.114 * c.z,
0.596 * c.x - 0.274 * c.y - 0.322 * c.z,
0.211 * c.x - 0.523 * c.y + 0.312 * c.z,
);
if (ch == 8u || ch == 9u) {
q.x = 1.0 - q.x;
}
if (ch == 8u || ch == 10u) {
q.y = -q.y;
}
if (ch == 8u || ch == 11u) {
q.z = -q.z;
}
r = max(
vec3<f32>(q.x + 0.956 * q.y + 0.621 * q.z, q.x - 0.272 * q.y - 0.647 * q.z, q.x - 1.106 * q.y + 1.703 * q.z),
vec3<f32>(0.0),
);
}
o = vec4<f32>((c + (r - c) * f0.x) * a, a);
}
case 19u: {
o = vec4<f32>(luminance(c));
}
case 20u: {
if (a <= 0.0) {
break;
}
let bases = textureLoad(aux, p, 0);
let s_amt = f0.x;
let h_amt = f0.y;
let ws0 = clamp(1.0 - bases.x / f0.z, 0.0, 1.0);
let wh0 = clamp((bases.y - (1.0 - f0.w)) / f0.w, 0.0, 1.0);
let ws = ws0 * ws0;
let wh = wh0 * wh0;
let gain = (1.0 + s_amt * ws * 2.0) * (1.0 - h_amt * wh * 0.5);
let l = luminance(c);
let nl = l * gain;
let sat = pow(max(gain, 1e-3), P.f[1].x);
var r = nl + (c - l) * sat;
let mc = P.f[1].y;
if (mc != 0.0) {
let shift = (0.5 + (nl - 0.5) * (1.0 + mc)) - nl;
r = r + shift;
}
r = fxt_mix(max(r, vec3<f32>(0.0)), c, P.f[1].z);
o = vec4<f32>(r * a, a);
}
case 21u: {
o = vec4<f32>(px.x, textureLoad(aux, p, 0).x, 0.0, 0.0);
}
case 22u: {
if (a <= 0.0 && px.x == 0.0 && px.y == 0.0 && px.z == 0.0) {
break;
}
let r = max(f0.x + (c - f0.y) * f0.z, vec3<f32>(0.0));
let na = clamp(max(a, 0.0), 0.0, 1.0);
o = vec4<f32>(r * na, na);
}
case 23u: {
if (a <= 0.0) {
break;
}
let l = clamp(luminance(c), 0.0, 1.0);
let ws = 1.0 - fxt_smoothstep(0.0, 0.5, l);
let wh = fxt_smoothstep(0.5, 1.0, l);
let wm = 1.0 - ws - wh;
let pin = P.f[3].x;
let keep = 1.0 - pin * clamp(1.0 - 4.0 * l * (1.0 - l), 0.0, 1.0);
let contrast = P.f[3].z;
let darks = P.f[4].x;
let brights = P.f[4].y;
var r: vec3<f32>;
for (var k = 0; k < 3; k++) {
let d = P.f[0][k] * ws + P.f[1][k] * wm + P.f[2][k] * wh;
var v = c[k] + d * keep;
v = 0.5 + (v - 0.5) * (1.0 + contrast);
v += darks * 0.25 * (1.0 - l) * (1.0 - l) + brights * 0.25 * l * l;
r[k] = max(v, 0.0);
}
r = fxt_mix(r, c, P.f[3].y);
o = vec4<f32>(r * a, a);
}
case 24u: {
if (a <= 0.0) {
break;
}
var r: vec3<f32>;
for (var k = 0u; k < 3u; k++) {
let base = k * 11u;
let n = u32(data[base]);
let v = c[k];
// slice::partition_point(x < v), clamped to 1..n-1.
var i = 0u;
while (i < n && data[base + 1u + 2u * i] < v) {
i++;
}
i = clamp(i, 1u, n - 1u);
let ax = data[base + 1u + 2u * (i - 1u)];
let ay = data[base + 2u + 2u * (i - 1u)];
let cx = data[base + 1u + 2u * i];
let cy = data[base + 2u + 2u * i];
r[k] = ay + (cy - ay) * (v - ax) / max(cx - ax, 1e-9);
}
o = vec4<f32>(r * a, a);
}
default: {}
}
textureStore(out, p, o);
}
// util::smoothstep.
fn fxt_smoothstep(e0: f32, e1: f32, x: f32) -> f32 {
if (abs(e1 - e0) < 1e-9) {
return select(1.0, 0.0, x < e0);
}
let t = clamp((x - e0) / (e1 - e0), 0.0, 1.0);
return t * t * (3.0 - 2.0 * t);
}

View File

@ -511,7 +511,7 @@ fn effect_chains_and_mixed_stacks() {
let mut l = s.footage(80, 50);
s.effect(&mut l, "ec.color.exposure", &[("master/exposure", n(0.7))]);
s.effect(&mut l, "ec.blur.gaussian", &[("blurriness", n(5.0))]);
s.effect(&mut l, "ec.stylize.posterize", &[]);
s.effect(&mut l, "ec.stylize.coloremboss", &[]);
s.effect(&mut l, "ec.stylize.glow", &[]);
s.effect(&mut l, "ec.channel.invert", &[("blend", n(50.0))]);
s.push(l);

View File

@ -57,7 +57,7 @@ fn gpu_adjustment_chains_match_the_cpu() {
#[test]
fn mixed_adjustment_chains_fall_back_per_effect() {
let mut stack = gpu_stack();
stack.insert(1, ("ec.stylize.posterize", vec![]));
stack.insert(1, ("ec.stylize.coloremboss", vec![]));
stack.push(("ec.stylize.glow", vec![("threshold", n(30.0))]));
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
let s = adjustment_scene(depth, &stack, true);
@ -93,7 +93,7 @@ impl FxTarget for Probe {
fn adjustment_stacks_split_into_gpu_runs_and_cpu_steps() {
let Some(g) = gpu() else { return };
let mut stack = gpu_stack();
stack.insert(1, ("ec.stylize.posterize", vec![]));
stack.insert(1, ("ec.stylize.coloremboss", vec![]));
let s = adjustment_scene(BitDepth::Bpc32, &stack, false);
let mut r = Renderer::new(&s.p, &Pattern, RenderOpts { backend: Backend::Gpu, ..opts() });
r.accel = Some(g);

View File

@ -0,0 +1,230 @@
//! GPU keying, matte and channel family effects vs the CPU effects (the oracle): direct on a
//! buffer at full and half resolution, as adjustment, and composited at 8 and 32 bpc.
use effectcraft_keyframe::Value;
use effectcraft_project::BitDepth;
use effectcraft_render::RenderOpts;
use effectcraft_time::Tick;
use crate::tests::{Scene, c, check, compare_at, effect_case, n, opts, set};
fn e(v: u32) -> Value {
Value::Enum(v)
}
fn on() -> Value {
Value::Bool(true)
}
fn off() -> Value {
Value::Bool(false)
}
/// [`effect_case`] for hard thresholds (8-bit quantisation, bitwise operators): the composited
/// renders may flip a few pixels sitting on the threshold after the layer transform's float
/// rounding, so up to 0.1 % of them may differ by any amount.
fn discontinuous_case(id: &str, vals: &[(&str, Value)]) {
crate::tests::effect_direct(id, vals, false);
crate::tests::effect_direct(id, vals, true);
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 l = s.footage(70, 44);
s.effect(&mut l, id, vals);
set(&mut l, "transform/rotation", Value::Scalar(8.0));
s.push(l);
check(&format!("{id} {vals:?} {depth:?}"), compare_at(&s, opts(), Tick::ZERO), 0.001);
check(&format!("{id} {vals:?} {depth:?} half"), compare_at(&s, RenderOpts { scale: 0.5, ..opts() }, Tick::ZERO), 0.001);
}
}
#[test]
fn colour_luma_range_extract_keys() {
effect_case("ec.key.colorkey", &[("keyColor", c(0.7, 0.6, 0.5)), ("colorTolerance", n(60.0))]);
effect_case("ec.key.colorkey", &[("keyColor", c(0.5, 0.6, 0.5)), ("colorTolerance", n(70.0)), ("edgeThin", n(1.5)), ("edgeFeather", n(4.0))]);
effect_case("ec.key.colorkey", &[("keyColor", c(0.5, 0.6, 0.5)), ("colorTolerance", n(70.0)), ("edgeThin", n(-2.0))]);
effect_case("ec.key.luma", &[("threshold", n(120.0)), ("tolerance", n(40.0))]);
effect_case("ec.key.luma", &[("keyType", e(0)), ("threshold", n(140.0)), ("tolerance", n(30.0)), ("edgeFeather", n(3.0))]);
effect_case("ec.key.luma", &[("keyType", e(2)), ("threshold", n(128.0)), ("tolerance", n(25.0)), ("edgeThin", n(1.0))]);
effect_case("ec.key.luma", &[("keyType", e(3)), ("threshold", n(100.0)), ("tolerance", n(50.0))]);
effect_case("ec.key.colorrange", &[("fuzziness", n(20.0))]);
effect_case("ec.key.colorrange", &[("colorSpace", e(1)), ("minL", n(60.0)), ("maxA", n(150.0)), ("minB", n(90.0)), ("fuzziness", n(15.0))]);
effect_case("ec.key.colorrange", &[("colorSpace", e(2)), ("minL", n(80.0)), ("maxA", n(180.0)), ("minB", n(40.0)), ("fuzziness", n(30.0))]);
effect_case("ec.key.extract", &[("blackPoint", n(60.0)), ("whitePoint", n(200.0)), ("blackSoftness", n(30.0)), ("whiteSoftness", n(20.0))]);
discontinuous_case("ec.key.extract", &[("channel", e(1)), ("blackPoint", n(90.0)), ("invert", on())]);
effect_case("ec.key.extract", &[("channel", e(4)), ("whitePoint", n(200.0)), ("whiteSoftness", n(40.0))]);
}
#[test]
fn difference_colour_difference_and_screen_keys() {
effect_case("ec.key.differencematte", &[]);
effect_case("ec.key.differencematte", &[("view", e(2)), ("matchingTolerance", n(0.0)), ("blurBeforeDifference", n(3.0))]);
effect_case("ec.key.colordifference", &[("keyColor", c(0.2, 0.3, 0.8))]);
effect_case(
"ec.key.colordifference",
&[
("keyColor", c(0.3, 0.8, 0.2)),
("colorMatchingAccuracy", e(1)),
("partialAInBlack", n(20.0)),
("partialAGamma", n(1.4)),
("partialBOutWhite", n(200.0)),
("blackLevel", n(10.0)),
("gamma", n(0.8)),
],
);
for view in [1, 2, 3, 4, 5, 6, 8] {
effect_case("ec.key.colordifference", &[("keyColor", c(0.2, 0.3, 0.8)), ("view", e(view)), ("partialBInWhite", n(180.0))]);
}
effect_case("ec.key.screen", &[("screenColor", c(0.4, 0.75, 0.3))]);
effect_case(
"ec.key.screen",
&[
("screenColor", c(0.3, 0.4, 0.8)),
("screenGain", n(130.0)),
("screenBalance", n(30.0)),
("despill", n(60.0)),
("clipBlack", n(10.0)),
("clipWhite", n(85.0)),
("screenPreblur", n(2.0)),
("screenShrinkGrow", n(-1.5)),
("screenSoftness", n(3.0)),
],
);
effect_case("ec.key.screen", &[("screenColor", c(0.8, 0.4, 0.3)), ("screenShrinkGrow", n(2.0)), ("edgeColor", e(1))]);
for view in [1, 2] {
effect_case("ec.key.screen", &[("screenColor", c(0.4, 0.75, 0.3)), ("view", e(view))]);
}
}
#[test]
fn spill_unmult_and_key_cleaner() {
effect_case("ec.key.spill", &[("colorToSuppress", c(0.2, 0.9, 0.1))]);
effect_case("ec.key.spill", &[("colorToSuppress", c(0.9, 0.8, 0.1)), ("colorAccuracy", e(1)), ("suppression", n(150.0))]);
effect_case("ec.key.advancedspill", &[]);
effect_case("ec.key.advancedspill", &[("suppression", n(80.0)), ("ultraSettings/spillRange", n(20.0)), ("ultraSettings/lumaCorrection", n(50.0))]);
effect_case(
"ec.key.advancedspill",
&[
("method", e(1)),
("ultraSettings/keyColor", c(0.9, 0.2, 0.3)),
("ultraSettings/tolerance", n(40.0)),
("ultraSettings/desaturate", n(50.0)),
("ultraSettings/spillColorCorrection", n(30.0)),
],
);
effect_case("ec.key.unmult", &[]);
effect_case("ec.key.unmult", &[("backgroundColor", e(1)), ("whiteLevel", n(90.0)), ("softness", n(40.0)), ("clipHdrResults", off())]);
effect_case("ec.key.unmult", &[("blackLevel", n(30.0)), ("softness", n(0.0)), ("removeColorMatting", off())]);
effect_case("ec.key.keycleaner", &[]);
effect_case("ec.key.keycleaner", &[("additionalEdgeRadius", n(6.0)), ("reduceChatter", on()), ("alphaContrast", n(250.0)), ("strength", n(60.0))]);
}
#[test]
fn chokers() {
effect_case("ec.matte.simplechoker", &[("chokeMatte", n(1.5))]);
effect_case("ec.matte.simplechoker", &[("chokeMatte", n(-2.5))]);
effect_case("ec.matte.simplechoker", &[("chokeMatte", n(-1.0)), ("view", e(1))]);
effect_case("ec.matte.mattechoker", &[]);
effect_case(
"ec.matte.mattechoker",
&[
("geometricSoftness1", n(2.0)),
("choke1", n(-40.0)),
("geometricSoftness2", n(3.0)),
("choke2", n(30.0)),
("grayLevelSoftness2", n(40.0)),
("iterations", n(2.0)),
],
);
}
#[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))]);
effect_case("ec.channel.shiftchannels", &[("takeRedFrom", e(3)), ("takeGreenFrom", e(7)), ("takeAlphaFrom", e(4))]);
effect_case("ec.channel.removecolormatting", &[("backgroundColor", c(0.3, 0.5, 0.7))]);
effect_case("ec.channel.removecolormatting", &[("backgroundColor", c(0.8, 0.2, 0.1)), ("clipHdr", off())]);
for op in [0, 2, 3, 4, 8, 10, 12] {
discontinuous_case("ec.channel.arithmetic", &[("operator", e(op)), ("redValue", n(120.0)), ("greenValue", n(60.0)), ("blueValue", n(200.0))]);
}
effect_case("ec.channel.arithmetic", &[("operator", e(3)), ("redValue", n(200.0)), ("clip", off())]);
for mode in [0, 2, 4, 9, 11] {
effect_case("ec.channel.solidcomposite", &[("blendingMode", e(mode)), ("color", c(0.8, 0.3, 0.2)), ("opacity", n(70.0)), ("sourceOpacity", n(80.0))]);
}
effect_case("ec.channel.setmatte", &[("takeMatteFrom", e(4))]);
effect_case("ec.channel.setmatte", &[("takeMatteFrom", e(3)), ("invertMatte", on())]);
effect_case("ec.channel.setmatte", &[("takeMatteFrom", e(1)), ("compositeMatteWithOriginal", off()), ("premultiplyMatteLayer", off())]);
for (from, to) in [(0, 0), (1, 0), (2, 0), (3, 0), (8, 4), (9, 5), (10, 6), (11, 3), (12, 8), (13, 7), (7, 1)] {
effect_case("ec.channel.combiner", &[("from", e(from)), ("to", e(to))]);
}
effect_case("ec.channel.combiner", &[("from", e(4)), ("to", e(2)), ("invert", on()), ("solidAlpha", on())]);
effect_case("ec.channel.blend", &[("mode", e(1)), ("blendWithOriginal", n(30.0))]);
effect_case("ec.channel.calculations", &[("inputChannel", e(1))]);
effect_case("ec.channel.calculations", &[("inputChannel", e(0)), ("invertInput", on())]);
effect_case("ec.channel.compoundarithmetic", &[("operator", e(3)), ("operateOnChannels", e(1))]);
}
/// The channel and keying effects that read another layer, composited.
#[test]
fn layer_inputs() {
let cases: Vec<(&str, Vec<(&str, Value)>)> = vec![
("ec.key.differencematte", vec![("matchingTolerance", n(10.0)), ("matchingSoftness", n(20.0))]),
("ec.key.differencematte", vec![("ifLayerSizesDiffer", e(1)), ("blurBeforeDifference", n(2.0)), ("view", e(2))]),
("ec.channel.setmatte", vec![("takeMatteFrom", e(4)), ("stretchMatteToFit", off())]),
("ec.channel.setmatte", vec![("takeMatteFrom", e(3))]),
("ec.channel.setchannels", vec![("setGreenTo", e(0)), ("setAlphaTo", e(4))]),
("ec.channel.combiner", vec![("useSecondLayer", on()), ("from", e(11)), ("to", e(7))]),
("ec.channel.blend", vec![("mode", e(0)), ("blendWithOriginal", n(40.0))]),
("ec.channel.blend", vec![("mode", e(2))]),
("ec.channel.blend", vec![("mode", e(3)), ("ifLayerSizesDiffer", e(1))]),
("ec.channel.blend", vec![("mode", e(4))]),
("ec.channel.calculations", vec![("secondLayerChannel", e(2)), ("blendingMode", e(2)), ("secondLayerOpacity", n(70.0))]),
("ec.channel.calculations", vec![("inputChannel", e(4)), ("blendingMode", e(10)), ("preserveTransparency", on()), ("invertSecondLayer", on())]),
("ec.channel.compoundarithmetic", vec![("operator", e(1)), ("overflowBehavior", e(2))]),
("ec.channel.compoundarithmetic", vec![("operator", e(4)), ("operateOnChannels", e(1)), ("blendWithOriginal", n(25.0))]),
("ec.channel.compoundarithmetic", vec![("operator", e(13)), ("operateOnChannels", e(2))]),
];
let layer_param = |id: &str| match id {
"ec.key.differencematte" => vec!["differenceLayer"],
"ec.channel.setmatte" => vec!["takeMatteFromLayer"],
"ec.channel.setchannels" => vec!["sourceLayer2", "sourceLayer4"],
"ec.channel.combiner" => vec!["sourceLayer"],
"ec.channel.blend" => vec!["blendWithLayer"],
"ec.channel.calculations" => vec!["secondLayer"],
_ => vec!["secondSource"],
};
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
for (id, vals) in &cases {
let mut s = Scene::new(depth);
let bg = s.solid([0.15, 0.1, 0.2], 97, 61);
s.push(bg);
let mut other = s.footage(50, 36);
other.switches.video = false;
let oid = s.push(other);
let mut l = s.footage(70, 44);
let mut vals = vals.clone();
for p in layer_param(id) {
vals.push((p, Value::Layer(Some(oid.0))));
}
s.effect(&mut l, id, &vals);
set(&mut l, "transform/rotation", Value::Scalar(8.0));
s.push(l);
let label = format!("{id} {vals:?} {depth:?}");
check(&label, compare_at(&s, opts(), Tick::ZERO), 0.0);
check(&format!("{label} half"), compare_at(&s, RenderOpts { scale: 0.5, ..opts() }, Tick::ZERO), 0.0);
}
}
}

View File

@ -0,0 +1,193 @@
//! GPU noise, blur and time family effects vs the CPU effects (the oracle): direct on a buffer at full and half
//! resolution, as adjustment, and composited at 8 and 32 bpc.
use effectcraft_color::Label;
use effectcraft_keyframe::{Keyframe, Value};
use effectcraft_project::build;
use effectcraft_project::{BitDepth, Comp, ItemKind, LayerSource};
use effectcraft_render::RenderOpts;
use effectcraft_time::{FrameRate, Tick};
use crate::tests::{Scene, c, check, compare_at, effect_case, n, opts, set};
fn e(v: u32) -> Value {
Value::Enum(v)
}
fn on() -> Value {
Value::Bool(true)
}
fn pt(x: f64, y: f64) -> Value {
Value::Vec2([x, y])
}
#[test]
fn medians() {
effect_case("ec.noise.median", &[("radius", n(2.0))]);
effect_case("ec.noise.median", &[("radius", n(5.0)), ("operateOnAlpha", on())]);
effect_case("ec.noise.medianlegacy", &[("radius", n(3.0))]);
effect_case("ec.noise.medianlegacy", &[("radius", n(2.0)), ("operateOnAlphaChannel", on())]);
effect_case("ec.noise.dustscratches", &[("radius", n(3.0)), ("threshold", n(10.0))]);
effect_case("ec.noise.dustscratches", &[("radius", n(2.0)), ("threshold", n(0.0)), ("operateOnAlpha", on())]);
}
#[test]
fn edge_preserving_blurs() {
effect_case("ec.blur.bilateral", &[]);
effect_case("ec.blur.bilateral", &[("radius", n(9.0)), ("threshold", n(30.0)), ("colorize", Value::Bool(false))]);
effect_case("ec.blur.bilateral", &[("radius", n(14.0)), ("threshold", n(5.0))]);
effect_case("ec.blur.smart", &[]);
effect_case("ec.blur.smart", &[("radius", n(6.0)), ("threshold", n(15.0)), ("mode", e(1))]);
effect_case("ec.blur.smart", &[("radius", n(4.0)), ("threshold", n(40.0)), ("mode", e(2))]);
}
#[test]
fn sharpen_and_unsharp_mask() {
effect_case("ec.blur.sharpen", &[("amount", n(60.0))]);
effect_case("ec.blur.sharpen", &[("amount", n(250.0))]);
effect_case("ec.blur.unsharp", &[("amount", n(120.0)), ("radius", n(4.0))]);
effect_case("ec.blur.unsharp", &[("amount", n(80.0)), ("radius", n(2.5)), ("threshold", n(12.0))]);
}
#[test]
fn channel_and_compound_blur() {
effect_case("ec.blur.channel", &[("redBlurriness", n(8.0)), ("blueBlurriness", n(3.0))]);
effect_case("ec.blur.channel", &[("greenBlurriness", n(6.0)), ("alphaBlurriness", n(6.0)), ("repeatEdge", on())]);
effect_case("ec.blur.channel", &[("redBlurriness", n(5.0)), ("alphaBlurriness", n(10.0)), ("dimensions", e(1))]);
effect_case("ec.blur.compound", &[]);
effect_case("ec.blur.compound", &[("maximumBlur", n(12.0)), ("invertBlur", on())]);
}
/// Compound Blur reading another layer (stretched and centred), composited.
#[test]
fn compound_blur_layer() {
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
for stretch in [true, false] {
let mut s = Scene::new(depth);
let bg = s.solid([0.15, 0.1, 0.2], 97, 61);
s.push(bg);
let mut map = s.footage(50, 36);
map.switches.video = false;
let mid = s.push(map);
let mut l = s.footage(70, 44);
s.effect(
&mut l,
"ec.blur.compound",
&[("blurLayer", Value::Layer(Some(mid.0))), ("maximumBlur", n(10.0)), ("stretchMapToFit", Value::Bool(stretch))],
);
set(&mut l, "transform/rotation", Value::Scalar(8.0));
s.push(l);
let label = format!("compound blur layer {stretch} {depth:?}");
check(&label, compare_at(&s, opts(), Tick::ZERO), 0.0);
check(&format!("{label} half"), compare_at(&s, RenderOpts { scale: 0.5, ..opts() }, Tick::ZERO), 0.0);
}
}
}
#[test]
fn minimax() {
effect_case("ec.channel.minimax", &[("radius", n(3.0))]);
effect_case("ec.channel.minimax", &[("radius", n(2.0)), ("operation", e(0)), ("channel", e(2))]);
effect_case("ec.channel.minimax", &[("radius", n(4.0)), ("operation", e(2)), ("channel", e(1)), ("direction", e(1))]);
effect_case("ec.channel.minimax", &[("radius", n(3.0)), ("operation", e(3)), ("channel", e(4)), ("dontShrinkEdges", on()), ("direction", e(2))]);
}
#[test]
fn turbulent_noise() {
effect_case("ec.noise.turbulent", &[]);
effect_case(
"ec.noise.turbulent",
&[("fractalType", e(3)), ("noiseType", e(3)), ("evolution", n(120.0)), ("evolutionOptions/turbulenceFactor", n(1.5)), ("blendingMode", e(5))],
);
effect_case(
"ec.noise.turbulent",
&[("fractalType", e(5)), ("noiseType", e(1)), ("complexity", n(3.5)), ("transform/scale", n(40.0)), ("overflow", e(1)), ("blendingMode", e(0))],
);
effect_case("ec.noise.turbulent", &[("fractalType", e(7)), ("noiseType", e(0)), ("transform/perspectiveOffset", on()), ("opacity", n(70.0))]);
}
#[test]
fn fractal_noise_other_modes() {
effect_case("ec.noise.fractal", &[("noiseType", e(3)), ("fractalType", e(4)), ("blendingMode", e(3))]);
effect_case(
"ec.noise.fractal",
&[
("fractalType", e(6)),
("evolutionOptions/cycleEvolution", on()),
("evolutionOptions/cycle", n(2.0)),
("evolution", n(500.0)),
("subSettings/subRotation", n(25.0)),
("subSettings/subOffset", pt(4.0, -3.0)),
],
);
effect_case("ec.noise.fractal", &[("fractalType", e(8)), ("noiseType", e(1)), ("blendingMode", e(12)), ("contrast", n(160.0)), ("invert", on())]);
}
#[test]
fn noise_alpha_and_hls() {
effect_case("ec.noise.noisealpha", &[("amount", n(40.0))]);
effect_case("ec.noise.noisealpha", &[("amount", n(70.0)), ("noise", e(3)), ("originalAlpha", e(2)), ("overflow", e(1)), ("noisePhase", n(100.0))]);
effect_case(
"ec.noise.noisealpha",
&[("amount", n(60.0)), ("noise", e(2)), ("originalAlpha", e(3)), ("overflow", e(2)), ("noiseOptions/cycleNoise", on()), ("randomSeed", n(9.0))],
);
effect_case("ec.noise.noisehls", &[("hue", n(30.0)), ("lightness", n(20.0)), ("saturation", n(40.0))]);
effect_case("ec.noise.noisehls", &[("noise", e(1)), ("lightness", n(50.0)), ("noisePhase", n(200.0))]);
effect_case("ec.noise.noisehls", &[("noise", e(2)), ("hue", n(60.0)), ("saturation", n(25.0)), ("grainSize", n(3.0))]);
effect_case("ec.noise.noisehlsauto", &[("noise", e(0)), ("hue", n(20.0)), ("lightness", n(30.0)), ("noiseAnimationSpeed", n(7.0))]);
}
#[test]
fn remove_grain() {
effect_case("ec.noise.removegrain", &[]);
effect_case(
"ec.noise.removegrain",
&[
("noiseReductionSettings/noiseReduction", n(2.5)),
("noiseReductionSettings/passes", n(2.0)),
("noiseReductionSettings/mode", e(1)),
("fineTuning/texture", n(0.3)),
],
);
effect_case("ec.noise.removegrain", &[("unsharpMask/amount", n(80.0)), ("unsharpMask/radius", n(2.0)), ("unsharpMask/threshold", n(4.0))]);
effect_case("ec.noise.removegrain", &[("viewingMode", e(0)), ("previewRegion/showBox", on()), ("previewRegion/boxColor", c(1.0, 0.2, 0.1))]);
}
/// Echo and Posterize Time fetch other frames through the effect host: a layer whose rotation
/// is keyframed, at a nonzero time (the direct cases have no host and pass the layer through).
#[test]
fn echo_and_posterize_time() {
effect_case("ec.time.echo", &[]);
effect_case("ec.time.posterizetime", &[]);
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
for (id, vals) in [
("ec.time.echo", vec![("numberOfEchoes", n(3.0)), ("decay", n(0.7)), ("echoTime", n(-0.1))]),
("ec.time.echo", vec![("numberOfEchoes", n(4.0)), ("echoOperator", e(2)), ("echoTime", n(0.05))]),
("ec.time.echo", vec![("numberOfEchoes", n(2.0)), ("echoOperator", e(4)), ("startingIntensity", n(0.8))]),
("ec.time.echo", vec![("numberOfEchoes", n(5.0)), ("echoOperator", e(6))]),
("ec.time.posterizetime", vec![("frameRate", n(4.0))]),
] {
let mut s = Scene::new(depth);
let bg = s.solid([0.15, 0.1, 0.2], 97, 61);
s.push(bg);
// A precomp whose content moves, so the frames differ.
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, crate::tests::v3(10.0, 12.0)), Keyframe::new(Tick::from_seconds_f64(1.0), crate::tests::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);
}
}
}

View File

@ -0,0 +1,223 @@
//! GPU stylize and distort family effects vs the CPU effects (the oracle): direct on a buffer at full and half
//! resolution, as adjustment, and composited at 8 and 32 bpc.
use effectcraft_keyframe::{Keyframe, Value};
use effectcraft_project::BitDepth;
use effectcraft_render::RenderOpts;
use effectcraft_time::Tick;
use crate::tests::{Scene, c, check, compare_at, effect_case, n, opts, set};
fn e(v: u32) -> Value {
Value::Enum(v)
}
fn on() -> Value {
Value::Bool(true)
}
fn pt(x: f64, y: f64) -> Value {
Value::Vec2([x, y])
}
#[test]
fn posterize_thresholds_strobe_vignette() {
effect_case("ec.stylize.posterize", &[("level", n(4.0))]);
effect_case("ec.stylize.posterize", &[("level", n(9.0))]);
effect_case("ec.stylize.threshold", &[("level", n(100.0))]);
effect_case("ec.stylize.threshold", &[("level", n(181.0))]);
effect_case("ec.stylize.ccthreshold", &[]);
effect_case("ec.stylize.ccthreshold", &[("channel", e(1)), ("invert", on()), ("threshold", n(90.0))]);
effect_case("ec.stylize.ccthreshold", &[("channel", e(2)), ("blend", n(30.0))]);
effect_case("ec.stylize.ccthreshold", &[("channel", e(3)), ("threshold", n(60.0))]);
effect_case(
"ec.stylize.ccthresholdrgb",
&[("redThreshold", n(70.0)), ("greenThreshold", n(150.0)), ("blueThreshold", n(110.0)), ("invertGreen", on()), ("blend", n(25.0))],
);
effect_case(
"ec.stylize.strobe",
&[("strobeDuration", n(0.5)), ("strobeOperator", e(1)), ("strobeColor", c(0.2, 0.5, 0.9)), ("blendWithOriginal", n(30.0))],
);
effect_case("ec.stylize.strobe", &[("strobeDuration", n(0.5)), ("strobeOperator", e(4)), ("strobeColor", c(0.6, 0.1, 0.3))]);
effect_case("ec.stylize.strobe", &[("strobeDuration", n(0.5)), ("strobe", e(1)), ("blendWithOriginal", n(40.0))]);
effect_case("ec.stylize.strobe", &[("strobeDuration", n(0.1)), ("strobePeriod", n(0.2)), ("randomStrobeProbability", n(50.0))]);
effect_case("ec.stylize.ccvignette", &[("amount", n(80.0))]);
effect_case("ec.stylize.ccvignette", &[("amount", n(-60.0)), ("pinHighlights", n(50.0)), ("angleOfView", n(120.0)), ("center", pt(20.0, 15.0))]);
}
#[test]
fn scatter_brush_strokes() {
effect_case("ec.stylize.scatter", &[("amount", n(5.0))]);
effect_case("ec.stylize.scatter", &[("amount", n(3.0)), ("grain", e(1))]);
effect_case("ec.stylize.scatter", &[("amount", n(7.0)), ("grain", e(2)), ("randomizeEveryFrame", on())]);
effect_case("ec.stylize.brushstrokes", &[]);
effect_case(
"ec.stylize.brushstrokes",
&[("strokeAngle", n(40.0)), ("brushSize", n(3.0)), ("strokeLength", n(10.0)), ("paintSurface", e(2)), ("blendWithOriginal", n(20.0))],
);
effect_case("ec.stylize.brushstrokes", &[("paintSurface", e(1)), ("strokeDensity", n(0.6)), ("strokeRandomness", n(2.0)), ("brushSize", n(1.3))]);
effect_case("ec.stylize.brushstrokes", &[("paintSurface", e(3)), ("strokeLength", n(25.0))]);
}
#[test]
fn roughen_edges() {
effect_case("ec.stylize.roughenedges", &[]);
effect_case("ec.stylize.roughenedges", &[("edgeType", e(1)), ("border", n(4.0)), ("edgeColor", c(0.1, 0.6, 0.9))]);
effect_case(
"ec.stylize.roughenedges",
&[
("edgeType", e(5)),
("evolutionOptions/cycleEvolution", on()),
("evolutionOptions/cycle", n(2.0)),
("evolution", n(500.0)),
("stretch", n(2.0)),
("complexity", n(3.0)),
],
);
effect_case("ec.stylize.roughenedges", &[("edgeType", e(7)), ("border", n(0.0)), ("scale", n(60.0)), ("evolutionOptions/randomSeed", n(9.0))]);
}
#[test]
fn tiles_kaleida_repetile() {
effect_case("ec.stylize.motiontile", &[("tileWidth", n(50.0)), ("tileHeight", n(40.0))]);
effect_case(
"ec.stylize.motiontile",
&[("outputWidth", n(150.0)), ("outputHeight", n(130.0)), ("mirrorEdges", on()), ("phase", n(70.0)), ("tileWidth", n(45.0))],
);
effect_case(
"ec.stylize.motiontile",
&[("horizontalPhaseShift", on()), ("phase", n(33.0)), ("tileCenter", pt(30.0, 17.0)), ("tileHeight", n(55.0)), ("outputWidth", n(80.0))],
);
effect_case("ec.stylize.cckaleida", &[]);
effect_case("ec.stylize.cckaleida", &[("mirroring", e(2)), ("rotation", n(20.0)), ("size", n(150.0))]);
effect_case("ec.stylize.ccrepetile", &[("expandRight", n(10.0)), ("expandUp", n(4.0))]);
effect_case("ec.stylize.ccrepetile", &[("expandLeft", n(12.0)), ("expandDown", n(9.0)), ("tiling", e(1))]);
effect_case("ec.stylize.ccrepetile", &[("expandLeft", n(30.0)), ("expandRight", n(30.0)), ("expandUp", n(20.0)), ("tiling", e(4))]);
effect_case("ec.distort.cctiler", &[]);
effect_case("ec.distort.cctiler", &[("scale", n(70.0)), ("blendWithOriginal", n(30.0)), ("center", pt(30.0, 18.0))]);
effect_case("ec.distort.ccgriddler", &[("rotation", n(20.0))]);
effect_case("ec.distort.ccgriddler", &[("cutTiles", on()), ("horizontalScale", n(120.0)), ("verticalScale", n(80.0)), ("tileSize", n(17.0))]);
}
#[test]
fn mirror_offset_polar_spherize_corner_pin() {
effect_case("ec.distort.mirror", &[("angle", n(30.0))]);
effect_case("ec.distort.mirror", &[("angle", n(200.0)), ("center", pt(27.0, 19.0))]);
effect_case("ec.distort.offset", &[("shift", pt(10.0, 5.0))]);
effect_case("ec.distort.offset", &[("shift", pt(50.3, 20.7)), ("blend", n(40.0))]);
effect_case("ec.distort.polar", &[("interpolation", n(100.0))]);
effect_case("ec.distort.polar", &[("interpolation", n(60.0)), ("conversion", e(1))]);
effect_case("ec.distort.spherize", &[("radius", n(20.0))]);
effect_case("ec.distort.spherize", &[("radius", n(35.0)), ("center", pt(30.0, 25.0))]);
effect_case("ec.distort.cornerpin", &[("ul", pt(5.0, 3.0)), ("ur", pt(60.0, 8.0)), ("lr", pt(66.0, 40.0)), ("ll", pt(2.0, 44.0))]);
effect_case("ec.distort.cornerpin", &[("ul", pt(-8.0, 6.0)), ("ur", pt(75.0, -4.0)), ("lr", pt(50.0, 30.0)), ("ll", pt(12.0, 50.0))]);
}
#[test]
fn optics_magnify() {
effect_case("ec.distort.opticscompensation", &[("fieldOfView", n(60.0))]);
effect_case("ec.distort.opticscompensation", &[("fieldOfView", n(90.0)), ("reverseLensDistortion", on()), ("fovOrientation", e(2))]);
effect_case(
"ec.distort.opticscompensation",
&[("fieldOfView", n(110.0)), ("reverseLensDistortion", on()), ("resize", e(1)), ("viewCenter", pt(30.0, 20.0))],
);
effect_case("ec.distort.opticscompensation", &[("fieldOfView", n(120.0)), ("optimalPixels", on()), ("fovOrientation", e(1))]);
effect_case("ec.distort.magnify", &[("size", n(15.0))]);
effect_case(
"ec.distort.magnify",
&[("shape", e(1)), ("feather", n(6.0)), ("size", n(18.0)), ("scaling", e(1)), ("blendingMode", e(3)), ("magnification", n(270.0))],
);
effect_case("ec.distort.magnify", &[("scaling", e(2)), ("blendingMode", e(0)), ("size", n(20.0)), ("opacity", n(70.0))]);
effect_case("ec.distort.magnify", &[("size", n(25.0)), ("center", pt(60.0, 10.0)), ("resizeLayer", on()), ("blendingMode", e(5))]);
}
#[test]
fn cc_slant_smear_split() {
effect_case("ec.distort.ccslant", &[("slant", n(30.0))]);
effect_case("ec.distort.ccslant", &[("slant", n(-20.0)), ("stretching", on()), ("height", n(120.0))]);
effect_case("ec.distort.ccsmear", &[]);
effect_case("ec.distort.ccsmear", &[("reach", n(-40.0)), ("radius", n(20.0)), ("to", pt(40.0, 10.0))]);
effect_case("ec.distort.ccsplit", &[]);
effect_case("ec.distort.ccsplit2", &[("split1", n(30.0)), ("split2", n(80.0)), ("pointB", pt(60.0, 30.0))]);
}
#[test]
fn twirl_legacy_ripple_pulse_power_pin() {
effect_case("ec.distort.twirllegacy", &[("angle", n(90.0))]);
effect_case("ec.distort.twirllegacy", &[("angle", n(-250.0)), ("radius", n(80.0)), ("center", pt(30.0, 18.0))]);
effect_case("ec.distort.ccripplepulse", &[("pulseLevel", n(80.0)), ("timeSpan", n(0.5))]);
effect_case("ec.distort.ccripplepulse", &[("pulseLevel", n(-60.0)), ("amplitude", n(150.0)), ("renderBump", on()), ("center", pt(25.0, 20.0))]);
effect_case("ec.distort.ccpowerpin", &[("topLeft", pt(5.0, 3.0)), ("bottomRight", pt(62.0, 38.0))]);
effect_case("ec.distort.ccpowerpin", &[("topRight", pt(80.0, -6.0)), ("bottomLeft", pt(-4.0, 50.0)), ("expandLeft", n(20.0)), ("expandBottom", n(-10.0))]);
}
#[test]
fn flo_motion() {
effect_case("ec.distort.ccflomotion", &[]);
effect_case("ec.distort.ccflomotion", &[("antialiasing", e(2)), ("tileEdges", on()), ("amount1", n(-150.0)), ("falloff", n(0.7))]);
effect_case("ec.distort.ccflomotion", &[("antialiasing", e(0)), ("amount2", n(0.0)), ("knot1", pt(20.0, 30.0))]);
}
#[test]
fn liquify_render() {
let mesh = "warp 20 50 0 0 0 10,10 20,15 30,20\ntwirlClockwise 24 80 0 0 0 40,25 42,27";
effect_case("ec.distort.liquify", &[("distortionMesh", Value::Str(mesh.into()))]);
effect_case(
"ec.distort.liquify",
&[("distortionMesh", Value::Str(mesh.into())), ("distortionPercentage", n(60.0)), ("distortionMeshOffset", pt(3.0, -2.0))],
);
}
#[test]
fn glow_operations_and_arbitrary_map() {
effect_case("ec.stylize.glow", &[("glowOperation", e(2)), ("threshold", n(30.0)), ("radius", n(8.0))]);
effect_case("ec.stylize.glow", &[("glowOperation", e(3)), ("colors", e(1)), ("intensity", n(2.0))]);
effect_case("ec.stylize.glow", &[("colors", e(2)), ("arbitraryMap", Value::Str("0,0 0.5,0.8 1,1|0,0.2 1,0.6|".into())), ("threshold", n(20.0))]);
effect_case("ec.stylize.glow", &[("colors", e(2)), ("arbitraryMap", Value::Str("0,1 1,0||0,0 1,1".into())), ("operation", e(1)), ("glowOperation", e(4))]);
}
/// Texturize with a texture layer (Tile / Center / Stretch), composited.
#[test]
fn texturize_layer() {
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
for placement in 0..3 {
let mut s = Scene::new(depth);
let bg = s.solid([0.15, 0.1, 0.2], 97, 61);
s.push(bg);
let mut tex = s.footage(50, 36);
tex.switches.video = false;
let tid = s.push(tex);
let mut l = s.footage(70, 44);
s.effect(
&mut l,
"ec.stylize.texturize",
&[("textureLayer", Value::Layer(Some(tid.0))), ("texturePlacement", e(placement)), ("textureContrast", n(1.5)), ("lightDirection", n(60.0))],
);
set(&mut l, "transform/rotation", Value::Scalar(8.0));
s.push(l);
let label = format!("texturize {placement} {depth:?}");
check(&label, compare_at(&s, opts(), Tick::ZERO), 0.0);
check(&format!("{label} half"), compare_at(&s, RenderOpts { scale: 0.5, ..opts() }, Tick::ZERO), 0.0);
}
}
}
/// Transform's own shutter angle: the effect's rotation and position animate.
#[test]
fn transform_motion_blur() {
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 l = s.footage(70, 44);
s.effect(&mut l, "ec.distort.transform", &[("useCompositionShutterAngle", Value::Bool(false)), ("shutterAngle", n(180.0))]);
let rot = l.props.prop_mut("effects/#1/rotation").expect("transform rotation");
rot.keys = vec![Keyframe::new(Tick::ZERO, Value::Scalar(0.0)), Keyframe::new(Tick::from_seconds_f64(1.0), Value::Scalar(120.0))];
let pos = l.props.prop_mut("effects/#1/position").expect("transform position");
pos.keys = vec![Keyframe::new(Tick::ZERO, pt(35.0, 22.0)), Keyframe::new(Tick::from_seconds_f64(1.0), pt(95.0, 40.0))];
s.push(l);
let label = format!("transform motion blur {depth:?}");
check(&label, compare_at(&s, opts(), Tick::from_seconds_f64(0.3)), 0.0);
check(&format!("{label} half"), compare_at(&s, RenderOpts { scale: 0.5, ..opts() }, Tick::from_seconds_f64(0.3)), 0.0);
}
}

View File

@ -0,0 +1,185 @@
//! GPU colour-correction family effects vs the CPU effects (the oracle): direct on a buffer at
//! full and half resolution, as adjustment, and composited at 8 and 32 bpc.
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, c, check, compare_at, effect_case, n, opts, set};
fn e(v: u32) -> Value {
Value::Enum(v)
}
fn on() -> Value {
Value::Bool(true)
}
fn off() -> Value {
Value::Bool(false)
}
#[test]
fn levels_and_gamma() {
effect_case(
"ec.color.levelsic",
&[("rgb/rgbInBlack", n(0.1)), ("rgb/rgbGamma", n(1.3)), ("red/redOutWhite", n(0.8)), ("blue/blueInWhite", n(0.7)), ("alpha/alphaGamma", n(0.7))],
);
effect_case("ec.color.levelsic", &[("green/greenInBlack", n(0.2)), ("green/greenInWhite", n(0.6)), ("clipToOutputWhite", e(0))]);
effect_case("ec.color.levelsic", &[("alpha/alphaOutBlack", n(0.3))]);
effect_case("ec.color.levels", &[("redInBlack", n(0.1)), ("greenGamma", n(1.5)), ("blueOutWhite", n(0.7)), ("inWhite", n(0.9))]);
effect_case("ec.color.levels", &[("alphaInWhite", n(0.6)), ("redInWhite", n(0.5)), ("redClipToOutputWhite", e(0))]);
effect_case("ec.color.gammapedestalgain", &[("redGamma", n(1.4)), ("greenPedestal", n(0.1)), ("blueGain", n(0.8))]);
effect_case("ec.color.gammapedestalgain", &[("blackStretch", n(2.0)), ("redGain", n(1.2))]);
}
#[test]
fn hue_saturation_ranges() {
effect_case("ec.color.huesaturation", &[("redsHue", n(40.0)), ("bluesSaturation", n(-60.0)), ("greensLightness", n(30.0))]);
effect_case(
"ec.color.huesaturation",
&[
("hue", n(20.0)),
("saturation", n(15.0)),
("yellowsSaturation", n(50.0)),
("yellowsRangeStart", n(30.0)),
("yellowsStartFalloff", n(8.0)),
("cyansLightness", n(-40.0)),
],
);
}
#[test]
fn colour_replacement() {
effect_case("ec.color.photofilter", &[]);
effect_case(
"ec.color.photofilter",
&[("filter", e(effectcraft_effects::PHOTO_FILTER_CUSTOM)), ("color", c(0.2, 0.7, 0.4)), ("density", n(60.0)), ("preserveLuminosity", off())],
);
effect_case("ec.color.changetocolor", &[("from", c(0.8, 0.4, 0.3)), ("to", c(0.2, 0.5, 0.9)), ("toleranceGroup/hue", n(15.0))]);
effect_case(
"ec.color.changetocolor",
&[("from", c(0.6, 0.6, 0.3)), ("to", c(0.9, 0.2, 0.6)), ("change", e(3)), ("changeBy", e(1)), ("toleranceGroup/hue", n(25.0)), ("softness", n(30.0))],
);
effect_case("ec.color.changetocolor", &[("from", c(0.6, 0.6, 0.3)), ("change", e(2)), ("toleranceGroup/hue", n(25.0)), ("viewCorrectionMatte", on())]);
effect_case("ec.color.leavecolor", &[("amount", n(80.0)), ("color", c(0.7, 0.5, 0.4)), ("tolerance", n(10.0)), ("edgeSoftness", n(20.0))]);
effect_case(
"ec.color.leavecolor",
&[("amount", n(100.0)), ("color", c(0.8, 0.4, 0.3)), ("matchColors", e(1)), ("tolerance", n(5.0)), ("edgeSoftness", n(30.0))],
);
effect_case("ec.color.changecolor", &[("hueTransform", n(90.0)), ("colorToChange", c(0.7, 0.5, 0.4)), ("matchingSoftness", n(20.0))]);
effect_case(
"ec.color.changecolor",
&[
("lightnessTransform", n(-30.0)),
("saturationTransform", n(40.0)),
("matchColors", e(0)),
("matchingTolerance", n(20.0)),
("matchingSoftness", n(15.0)),
],
);
effect_case("ec.color.changecolor", &[("view", e(1)), ("matchColors", e(2)), ("matchingSoftness", n(25.0)), ("invertColorCorrectionMask", on())]);
}
#[test]
fn broadcast_balance_limiter() {
effect_case("ec.color.broadcast", &[("maxSignal", n(95.0))]);
effect_case("ec.color.broadcast", &[("locale", e(1)), ("howToMakeSafe", e(1)), ("maxSignal", n(90.0))]);
effect_case("ec.color.colorbalancehls", &[("hue", n(40.0)), ("lightness", n(-20.0)), ("saturation", n(30.0))]);
effect_case("ec.color.colorbalancehls", &[("lightness", n(25.0)), ("saturation", n(-50.0))]);
effect_case("ec.color.videolimiter", &[("clipLevel", e(0))]);
effect_case("ec.color.videolimiter", &[("clipLevel", e(1)), ("clipMethod", e(1)), ("compressionBeforeClipping", e(4))]);
effect_case("ec.color.videolimiter", &[("clipLevel", e(0)), ("clipMethod", e(0)), ("gamutWarning", on())]);
}
#[test]
fn cc_colour_and_arbitrary_map() {
effect_case("ec.color.cctoner", &[]);
effect_case("ec.color.cctoner", &[("tones", e(2)), ("blend", n(30.0))]);
effect_case("ec.color.cctoner", &[("tones", e(0)), ("shadows", c(0.1, 0.0, 0.3)), ("highlights", c(1.0, 0.9, 0.6))]);
effect_case("ec.color.cccoloroffset", &[("redPhase", n(90.0)), ("bluePhase", n(200.0))]);
effect_case("ec.color.cccoloroffset", &[("greenPhase", n(120.0)), ("overflow", e(1))]);
effect_case("ec.color.cckernel", &[("k1", n(-1.0)), ("k5", n(5.0)), ("k9", n(-1.0)), ("scale", n(0.5)), ("offset", n(0.05))]);
effect_case("ec.color.cckernel", &[("k2", n(1.0)), ("k5", n(0.0)), ("k8", n(-1.0)), ("absolute", on())]);
let map: String = (0..256).map(|i| format!("{}", ((i as f64 / 255.0).powf(0.6) * 255.0).round())).collect::<Vec<_>>().join(",");
effect_case("ec.color.psarbitrarymap", &[("map", Value::Str(map.clone()))]);
effect_case("ec.color.psarbitrarymap", &[("map", Value::Str(map)), ("phase", n(40.0)), ("applyPhaseMapToAlpha", on())]);
}
#[test]
fn automatic_corrections() {
effect_case("ec.color.autolevels", &[]);
effect_case("ec.color.autolevels", &[("blackClip", n(2.0)), ("whiteClip", n(3.0)), ("blend", n(20.0))]);
effect_case("ec.color.autocontrast", &[("blackClip", n(1.0))]);
effect_case("ec.color.autocolor", &[]);
effect_case("ec.color.autocolor", &[("snapNeutralMidtones", on()), ("whiteClip", n(2.0))]);
effect_case("ec.color.equalize", &[]);
effect_case("ec.color.equalize", &[("style", e(1)), ("amount", n(70.0))]);
effect_case("ec.color.equalize", &[("style", e(2))]);
}
#[test]
fn shadow_highlight_neutralizer_invert() {
effect_case("ec.color.shadowhighlight", &[]);
effect_case(
"ec.color.shadowhighlight",
&[
("autoAmounts", off()),
("shadowAmount", n(70.0)),
("highlightAmount", n(40.0)),
("moreOptions/shadowRadius", n(8.0)),
("moreOptions/highlightRadius", n(15.0)),
("moreOptions/midtoneContrast", n(30.0)),
("moreOptions/blackClip", n(2.0)),
("blend", n(20.0)),
],
);
effect_case("ec.color.shadowhighlight", &[("moreOptions/blackClip", n(0.0)), ("moreOptions/whiteClip", n(0.0)), ("moreOptions/colorCorrection", n(-40.0))]);
effect_case(
"ec.color.cccolorneutralizer",
&[("shadowsBalance", c(0.1, 0.0, 0.05)), ("midtonesUnbalance", c(0.6, 0.5, 0.4)), ("pinning", n(40.0)), ("contrast", n(20.0)), ("darks", n(-30.0))],
);
effect_case("ec.color.cccolorneutralizer", &[("highlightsBalance", c(0.9, 1.0, 0.8)), ("brights", n(40.0)), ("blendWOriginal", n(25.0))]);
for ch in 4..=11 {
effect_case("ec.channel.invert", &[("channel", e(ch)), ("blend", n(20.0))]);
}
}
/// Color Stabilizer reads its reference frame through the effect host: a precomp whose content
/// moves, at a nonzero time (the direct cases have no host and pass the layer through).
#[test]
fn color_stabilizer() {
effect_case("ec.color.colorstabilizer", &[]);
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
for mode in [0, 1, 2] {
let mut s = Scene::new(depth);
let bg = s.solid([0.15, 0.1, 0.2], 97, 61);
s.push(bg);
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(50, 34);
let pos = a.props.prop_mut("transform/position").unwrap();
pos.keys = vec![Keyframe::new(Tick::ZERO, crate::tests::v3(22.0, 18.0)), Keyframe::new(Tick::from_seconds_f64(1.0), crate::tests::v3(38.0, 24.0))];
let op = a.props.prop_mut("transform/opacity").unwrap();
op.keys = vec![Keyframe::new(Tick::ZERO, Value::Scalar(100.0)), Keyframe::new(Tick::from_seconds_f64(1.0), Value::Scalar(40.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);
let vals = [
("stabilize", e(mode)),
("blackPoint", Value::Vec2([18.0, 20.0])),
("midPoint", Value::Vec2([30.0, 18.0])),
("whitePoint", Value::Vec2([42.0, 22.0])),
("sampleSize", n(4.0)),
];
s.effect(&mut l, "ec.color.colorstabilizer", &vals);
set(&mut l, "transform/rotation", Value::Scalar(8.0));
s.push(l);
let t = Tick::from_seconds_f64(0.5);
let label = format!("color stabilizer {mode} {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);
}
}
}

View File

@ -174,10 +174,15 @@ against the caster planes (Shadow Diffusion, Light Transmission) and blending. A
run their effect stacks on the GPU-resident comp (`Renderer::run_effects_on` with an `FxTarget`):
runs of GPU effects stay on the device and only non-GPU effects read back and upload. Advanced 3D
compositing and wireframes still run on the CPU between GPU steps (read back, draw, upload). GPU
effects (`effects::GPU_EFFECTS`, 58 of them: blurs, colour, keying incl. Key Light, distortion,
transitions, generators, noise and grain; see [effects.md](effects.md)) repeat the CPU effect's
steps (padding, box radii, parameters, hashes) as kernels; consecutive GPU effects run as one
chain with one upload and one readback. Tests render scenes on both paths and compare them
effects (`effects::GPU_EFFECTS`, 152 of them: blurs, colour correction, keying incl. Key Light,
mattes, channel, stylize, distortion, transitions, generators, noise, grain and time; see
[effects.md](effects.md)) repeat the CPU effect's steps (padding, box radii, parameters, hashes)
as kernels, in one module per family (`gpu::fx_*` with `shaders/fx_*.wgsl`); consecutive GPU
effects run as one chain with one upload and one readback. Statistics that need the whole frame
(Auto Levels / Contrast / Color, Equalize, Shadow/Highlight, Color Stabilizer, Remove Grain's
noise level) are measured on the CPU from one readback and applied on the GPU; effects reading
other frames (Echo, Posterize Time) upload the frames the host renders. Settings a kernel cannot
match render on the CPU (`effects::catalog::gpu_supported`). Tests render scenes on both paths and compare them
(≤ 1/255 at 8 bpc, ≤ 1e-3 at 32 bpc); they skip without an adapter. The desktop viewer builds the
`Gpu` on egui-wgpu's device and shows frames from GPU textures without reading them back
(`ui-egui::frames`); headless renders, the CLI (unless `--gpu`) and CI use the CPU. On the web

View File

@ -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**: 59 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).
@ -46,85 +46,85 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
| Effect | Id | Params | GPU | 32 | Status |
|---|---|---:|:-:|:-:|---|
| Bilateral Blur | `ec.blur.bilateral` | 3 | | 32 | Implemented |
| Bilateral Blur | `ec.blur.bilateral` | 3 | GPU | 32 | Implemented |
| CC Cross Blur | `ec.blur.cccross` | 3 | | 32 | Implemented |
| CC Radial Blur | `ec.blur.ccradial` | 4 | | 32 | Implemented |
| CC Radial Fast Blur | `ec.blur.ccradialfast` | 3 | GPU | 32 | Implemented |
| CC Vector Blur | `ec.blur.ccvector` | 4 | | 32 | Implemented |
| Camera Lens Blur | `ec.blur.cameralens` | 16 | GPU | 32 | Implemented |
| Camera-Shake Deblur | `ec.blur.camerashakedeblur` | 8 | | 32 | Implemented |
| Channel Blur | `ec.blur.channel` | 6 | | 32 | Implemented |
| Compound Blur | `ec.blur.compound` | 4 | | 32 | Implemented |
| Channel Blur | `ec.blur.channel` | 6 | GPU | 32 | Implemented |
| Compound Blur | `ec.blur.compound` | 4 | GPU | 32 | Implemented |
| Directional Blur | `ec.blur.directional` | 2 | GPU | 32 | Implemented |
| Fast Box Blur | `ec.blur.fastbox` | 4 | GPU | 32 | Implemented |
| Gaussian Blur | `ec.blur.gaussian` | 3 | GPU | 32 | Implemented |
| Radial Blur | `ec.blur.radial` | 4 | GPU | 32 | Implemented |
| Sharpen | `ec.blur.sharpen` | 1 | | 32 | Implemented |
| Smart Blur | `ec.blur.smart` | 3 | | 32 | Implemented |
| Unsharp Mask | `ec.blur.unsharp` | 3 | | 32 | Implemented |
| Sharpen | `ec.blur.sharpen` | 1 | GPU | 32 | Implemented |
| Smart Blur | `ec.blur.smart` | 3 | GPU | 32 | Implemented |
| Unsharp Mask | `ec.blur.unsharp` | 3 | GPU | 32 | Implemented |
## Channel
| Effect | Id | Params | GPU | 32 | Status |
|---|---|---:|:-:|:-:|---|
| Arithmetic | `ec.channel.arithmetic` | 5 | | 32 | Implemented |
| Blend | `ec.channel.blend` | 4 | | 32 | Implemented |
| Arithmetic | `ec.channel.arithmetic` | 5 | GPU | 32 | Implemented |
| Blend | `ec.channel.blend` | 4 | GPU | 32 | Implemented |
| CC Composite | `ec.channel.cccomposite` | 3 | | 32 | Implemented |
| Calculations | `ec.channel.calculations` | 9 | | 32 | Implemented |
| Channel Combiner | `ec.channel.combiner` | 6 | | 32 | Implemented |
| Compound Arithmetic | `ec.channel.compoundarithmetic` | 6 | | 32 | Implemented |
| Calculations | `ec.channel.calculations` | 9 | GPU | 32 | Implemented |
| Channel Combiner | `ec.channel.combiner` | 6 | GPU | 32 | Implemented |
| Compound Arithmetic | `ec.channel.compoundarithmetic` | 6 | GPU | 32 | Implemented |
| Invert | `ec.channel.invert` | 2 | GPU | 32 | Implemented |
| Minimax | `ec.channel.minimax` | 5 | | 32 | Implemented |
| Remove Color Matting | `ec.channel.removecolormatting` | 2 | | 32 | Implemented |
| Set Channels | `ec.channel.setchannels` | 9 | | 32 | Implemented |
| Set Matte | `ec.channel.setmatte` | 6 | | 32 | Implemented |
| Shift Channels | `ec.channel.shiftchannels` | 4 | | 32 | Implemented |
| Solid Composite | `ec.channel.solidcomposite` | 4 | | 32 | Implemented |
| Minimax | `ec.channel.minimax` | 5 | GPU | 32 | Implemented |
| Remove Color Matting | `ec.channel.removecolormatting` | 2 | GPU | 32 | Implemented |
| Set Channels | `ec.channel.setchannels` | 9 | GPU | 32 | Implemented |
| Set Matte | `ec.channel.setmatte` | 6 | GPU | 32 | Implemented |
| Shift Channels | `ec.channel.shiftchannels` | 4 | GPU | 32 | Implemented |
| Solid Composite | `ec.channel.solidcomposite` | 4 | GPU | 32 | Implemented |
## Color Correction
| Effect | Id | Params | GPU | 32 | Status |
|---|---|---:|:-:|:-:|---|
| Auto Color | `ec.color.autocolor` | 6 | | 32 | Implemented |
| Auto Contrast | `ec.color.autocontrast` | 5 | | 32 | Implemented |
| Auto Levels | `ec.color.autolevels` | 5 | | 32 | Implemented |
| Auto Color | `ec.color.autocolor` | 6 | GPU | 32 | Implemented |
| Auto Contrast | `ec.color.autocontrast` | 5 | GPU | 32 | Implemented |
| Auto Levels | `ec.color.autolevels` | 5 | GPU | 32 | Implemented |
| Black & White | `ec.color.blackwhite` | 8 | GPU | 32 | Implemented |
| Brightness & Contrast | `ec.color.brightnesscontrast` | 3 | GPU | 32 | Implemented |
| Broadcast Colors | `ec.color.broadcast` | 3 | | 32 | Implemented |
| CC Color Neutralizer | `ec.color.cccolorneutralizer` | 11 | | 32 | Implemented |
| CC Color Offset | `ec.color.cccoloroffset` | 4 | | 32 | Implemented |
| CC Kernel | `ec.color.cckernel` | 12 | | 32 | Implemented |
| CC Toner | `ec.color.cctoner` | 7 | | 32 | Implemented |
| Change Color | `ec.color.changecolor` | 9 | | 32 | Implemented |
| Change to Color | `ec.color.changetocolor` | 9 | | 32 | Implemented |
| Broadcast Colors | `ec.color.broadcast` | 3 | GPU | 32 | Implemented |
| CC Color Neutralizer | `ec.color.cccolorneutralizer` | 11 | GPU | 32 | Implemented |
| CC Color Offset | `ec.color.cccoloroffset` | 4 | GPU | 32 | Implemented |
| CC Kernel | `ec.color.cckernel` | 12 | GPU | 32 | Implemented |
| CC Toner | `ec.color.cctoner` | 7 | GPU | 32 | Implemented |
| Change Color | `ec.color.changecolor` | 9 | GPU | 32 | Implemented |
| Change to Color | `ec.color.changetocolor` | 9 | GPU | 32 | Implemented |
| Channel Mixer | `ec.color.channelmixer` | 13 | GPU | 32 | Implemented |
| Color Balance | `ec.color.colorbalance` | 10 | GPU | 32 | Implemented |
| Color Balance (HLS) | `ec.color.colorbalancehls` | 3 | | 32 | Implemented |
| Color Balance (HLS) | `ec.color.colorbalancehls` | 3 | GPU | 32 | Implemented |
| Color Link | `ec.color.colorlink` | 6 | | 32 | Implemented |
| Color Stabilizer | `ec.color.colorstabilizer` | 6 | | 32 | Implemented |
| Color Stabilizer | `ec.color.colorstabilizer` | 6 | GPU | 32 | Implemented |
| Colorama | `ec.color.colorama` | 20 | GPU | 32 | Implemented |
| Curves | `ec.color.curves` | 0 | GPU | 32 | Implemented |
| Equalize | `ec.color.equalize` | 2 | | 32 | Implemented |
| Equalize | `ec.color.equalize` | 2 | GPU | 32 | Implemented |
| Exposure | `ec.color.exposure` | 14 | GPU | 32 | Implemented |
| Gamma/Pedestal/Gain | `ec.color.gammapedestalgain` | 10 | | 32 | Implemented |
| Gamma/Pedestal/Gain | `ec.color.gammapedestalgain` | 10 | GPU | 32 | Implemented |
| Hue/Saturation | `ec.color.huesaturation` | 50 | GPU | 32 | Implemented |
| Leave Color | `ec.color.leavecolor` | 5 | | 32 | Implemented |
| Leave Color | `ec.color.leavecolor` | 5 | GPU | 32 | Implemented |
| Levels | `ec.color.levels` | 36 | GPU | 32 | Implemented |
| Levels (Individual Controls) | `ec.color.levelsic` | 27 | | 32 | Implemented |
| Levels (Individual Controls) | `ec.color.levelsic` | 27 | GPU | 32 | Implemented |
| Lumetri Color | `ec.color.lumetri` | 66 | GPU | 32 | Implemented |
| OCIO CDL Transform | `ec.color.ociocdl` | 12 | | 32 | Implemented |
| OCIO Color Space Transform | `ec.color.ociocolorspace` | 7 | | 32 | Implemented |
| OCIO Display Transform | `ec.color.ociodisplay` | 9 | | 32 | Implemented |
| OCIO File Transform | `ec.color.ociofile` | 4 | | 32 | Implemented |
| OCIO Look Transform | `ec.color.ociolook` | 6 | | 32 | Implemented |
| PS Arbitrary Map | `ec.color.psarbitrarymap` | 2 | | 32 | Implemented |
| Photo Filter | `ec.color.photofilter` | 4 | | 32 | Implemented |
| PS Arbitrary Map | `ec.color.psarbitrarymap` | 2 | GPU | 32 | Implemented |
| Photo Filter | `ec.color.photofilter` | 4 | GPU | 32 | Implemented |
| Selective Color | `ec.color.selectivecolor` | 38 | GPU | 32 | Implemented |
| Shadow/Highlight | `ec.color.shadowhighlight` | 14 | | 32 | Implemented |
| Shadow/Highlight | `ec.color.shadowhighlight` | 14 | GPU | 32 | Implemented |
| Tint | `ec.color.tint` | 3 | GPU | 32 | Implemented |
| Tritone | `ec.color.tritone` | 4 | GPU | 32 | Implemented |
| Vibrance | `ec.color.vibrance` | 2 | GPU | 32 | Implemented |
| Video Limiter | `ec.color.videolimiter` | 5 | | 32 | Implemented |
| Video Limiter | `ec.color.videolimiter` | 5 | GPU | 32 | Implemented |
## Distort
@ -135,37 +135,37 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
| CC Bend It | `ec.distort.ccbendit` | 4 | | 32 | Implemented |
| CC Bender | `ec.distort.ccbender` | 5 | | 32 | Implemented |
| CC Blobbylize | `ec.distort.ccblobbylize` | 15 | | 32 | Implemented |
| CC Flo Motion | `ec.distort.ccflomotion` | 7 | | 32 | Implemented |
| CC Griddler | `ec.distort.ccgriddler` | 5 | | 32 | Implemented |
| CC Flo Motion | `ec.distort.ccflomotion` | 7 | GPU | 32 | Implemented |
| CC Griddler | `ec.distort.ccgriddler` | 5 | GPU | 32 | Implemented |
| CC Lens | `ec.distort.cclens` | 3 | GPU | 32 | Implemented |
| CC Page Turn | `ec.distort.ccpageturn` | 7 | | 32 | Implemented |
| CC Power Pin | `ec.distort.ccpowerpin` | 10 | | 32 | Implemented |
| CC Ripple Pulse | `ec.distort.ccripplepulse` | 5 | | 32 | Implemented |
| CC Slant | `ec.distort.ccslant` | 4 | | 32 | Implemented |
| CC Smear | `ec.distort.ccsmear` | 4 | | 32 | Implemented |
| CC Split | `ec.distort.ccsplit` | 3 | | 32 | Implemented |
| CC Split 2 | `ec.distort.ccsplit2` | 4 | | 32 | Implemented |
| CC Tiler | `ec.distort.cctiler` | 3 | | 32 | Implemented |
| Corner Pin | `ec.distort.cornerpin` | 4 | | 32 | Implemented |
| CC Power Pin | `ec.distort.ccpowerpin` | 10 | GPU | 32 | Implemented |
| CC Ripple Pulse | `ec.distort.ccripplepulse` | 5 | GPU | 32 | Implemented |
| CC Slant | `ec.distort.ccslant` | 4 | GPU | 32 | Implemented |
| CC Smear | `ec.distort.ccsmear` | 4 | GPU | 32 | Implemented |
| CC Split | `ec.distort.ccsplit` | 3 | GPU | 32 | Implemented |
| CC Split 2 | `ec.distort.ccsplit2` | 4 | GPU | 32 | Implemented |
| CC Tiler | `ec.distort.cctiler` | 3 | GPU | 32 | Implemented |
| Corner Pin | `ec.distort.cornerpin` | 4 | GPU | 32 | Implemented |
| Detail-preserving Upscale | `ec.distort.upscale` | 4 | | 32 | Implemented |
| Displacement Map | `ec.distort.displacementmap` | 8 | GPU | 32 | Implemented |
| Liquify | `ec.distort.liquify` | 12 | | 32 | Implemented |
| Magnify | `ec.distort.magnify` | 10 | | 32 | Implemented |
| Liquify | `ec.distort.liquify` | 12 | GPU | 32 | Implemented |
| Magnify | `ec.distort.magnify` | 10 | GPU | 32 | Implemented |
| Mesh Warp | `ec.distort.meshwarp` | 3 | GPU | 32 | Implemented |
| Mirror | `ec.distort.mirror` | 2 | | 32 | Implemented |
| Offset | `ec.distort.offset` | 2 | | 32 | Implemented |
| Optics Compensation | `ec.distort.opticscompensation` | 6 | | 32 | Implemented |
| Polar Coordinates | `ec.distort.polar` | 2 | | 32 | Implemented |
| Mirror | `ec.distort.mirror` | 2 | GPU | 32 | Implemented |
| Offset | `ec.distort.offset` | 2 | GPU | 32 | Implemented |
| Optics Compensation | `ec.distort.opticscompensation` | 6 | GPU | 32 | Implemented |
| Polar Coordinates | `ec.distort.polar` | 2 | GPU | 32 | Implemented |
| Puppet | `ec.distort.puppet` | 1 | | 32 | Implemented |
| Reshape | `ec.distort.reshape` | 6 | | 32 | Implemented |
| Ripple | `ec.distort.ripple` | 7 | GPU | 32 | Implemented |
| Rolling Shutter Repair | `ec.distort.rollingshutterrepair` | 5 | | 32 | Implemented |
| Smear | `ec.distort.smear` | 8 | | 32 | Implemented |
| Spherize | `ec.distort.spherize` | 2 | | 32 | Implemented |
| Spherize | `ec.distort.spherize` | 2 | GPU | 32 | Implemented |
| Transform | `ec.distort.transform` | 12 | GPU | 32 | Implemented |
| Turbulent Displace | `ec.distort.turbulentdisplace` | 11 | GPU | 32 | Implemented |
| Twirl | `ec.distort.twirl` | 3 | GPU | 32 | Implemented |
| Twirl (Legacy) | `ec.distort.twirllegacy` | 3 | | 32 | Implemented |
| Twirl (Legacy) | `ec.distort.twirllegacy` | 3 | GPU | 32 | Implemented |
| Warp | `ec.distort.warp` | 5 | | 32 | Implemented |
| Warp Stabilizer | `ec.distort.warpstabilizer` | 20 | | 32 | Implemented |
| Wave Warp | `ec.distort.wavewarp` | 7 | GPU | 32 | Implemented |
@ -236,29 +236,29 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
| Effect | Id | Params | GPU | 32 | Status |
|---|---|---:|:-:|:-:|---|
| Advanced Spill Suppressor | `ec.key.advancedspill` | 8 | | 32 | Implemented |
| Advanced Spill Suppressor | `ec.key.advancedspill` | 8 | GPU | 32 | Implemented |
| CC Simple Wire Removal | `ec.key.ccsimplewireremoval` | 7 | | 32 | Implemented |
| Color Difference Key | `ec.key.colordifference` | 16 | | 32 | Implemented |
| Color Range | `ec.key.colorrange` | 8 | | 32 | Implemented |
| Difference Matte | `ec.key.differencematte` | 6 | | 32 | Implemented |
| Extract | `ec.key.extract` | 6 | | 32 | Implemented |
| Color Difference Key | `ec.key.colordifference` | 16 | GPU | 32 | Implemented |
| Color Range | `ec.key.colorrange` | 8 | GPU | 32 | Implemented |
| Difference Matte | `ec.key.differencematte` | 6 | GPU | 32 | Implemented |
| Extract | `ec.key.extract` | 6 | GPU | 32 | Implemented |
| Inner/Outer Key | `ec.key.innerouter` | 88 | | 32 | Implemented |
| Key Cleaner | `ec.key.keycleaner` | 4 | | 32 | Implemented |
| Key Cleaner | `ec.key.keycleaner` | 4 | GPU | 32 | Implemented |
| Key Light | `ec.keying.keylight` | 56 | GPU | 32 | Implemented |
| Linear Color Key | `ec.key.linearcolor` | 6 | GPU | 32 | Implemented |
| Screen Key | `ec.key.screen` | 11 | | 32 | Implemented |
| Unmult | `ec.key.unmult` | 6 | | 32 | Implemented |
| Screen Key | `ec.key.screen` | 11 | GPU | 32 | Implemented |
| Unmult | `ec.key.unmult` | 6 | GPU | 32 | Implemented |
## Matte
| Effect | Id | Params | GPU | 32 | Status |
|---|---|---:|:-:|:-:|---|
| Matte Choker | `ec.matte.mattechoker` | 7 | | 32 | Implemented |
| 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 | | 32 | Implemented |
| Simple Choker | `ec.matte.simplechoker` | 2 | GPU | 32 | Implemented |
## Noise & Grain
@ -266,17 +266,17 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|---|---|---:|:-:|:-:|---|
| Add Grain | `ec.noise.addgrain` | 32 | GPU | 32 | Implemented |
| Curl Noise | `ec.noise.curlnoise` | 10 | | 32 | Implemented |
| Dust & Scratches | `ec.noise.dustscratches` | 3 | | 32 | Implemented |
| Dust & Scratches | `ec.noise.dustscratches` | 3 | GPU | 32 | Implemented |
| Fractal Noise | `ec.noise.fractal` | 26 | GPU | 32 | Implemented |
| Match Grain | `ec.noise.matchgrain` | 37 | | 32 | Implemented |
| Median | `ec.noise.median` | 2 | | 32 | Implemented |
| Median (Legacy) | `ec.noise.medianlegacy` | 2 | | 32 | Implemented |
| Median | `ec.noise.median` | 2 | GPU | 32 | Implemented |
| Median (Legacy) | `ec.noise.medianlegacy` | 2 | GPU | 32 | Implemented |
| Noise | `ec.noise.noise` | 3 | GPU | 32 | Implemented |
| Noise Alpha | `ec.noise.noisealpha` | 8 | | 32 | Implemented |
| Noise HLS | `ec.noise.noisehls` | 6 | | 32 | Implemented |
| Noise HLS Auto | `ec.noise.noisehlsauto` | 6 | | 32 | Implemented |
| Remove Grain | `ec.noise.removegrain` | 20 | | 32 | Implemented |
| Turbulent Noise | `ec.noise.turbulent` | 22 | | 32 | Implemented |
| Noise Alpha | `ec.noise.noisealpha` | 8 | GPU | 32 | Implemented |
| Noise HLS | `ec.noise.noisehls` | 6 | GPU | 32 | Implemented |
| Noise HLS Auto | `ec.noise.noisehlsauto` | 6 | GPU | 32 | Implemented |
| Remove Grain | `ec.noise.removegrain` | 20 | GPU | 32 | Implemented |
| Turbulent Noise | `ec.noise.turbulent` | 22 | GPU | 32 | Implemented |
## Obsolete
@ -284,13 +284,13 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|---|---|---:|:-:|:-:|---|
| Basic 3D | `ec.obsolete.basic3d` | 5 | | 32 | Implemented |
| Basic Text | `ec.obsolete.basictext` | 11 | | 32 | Implemented |
| Color Key | `ec.key.colorkey` | 4 | | 32 | Implemented |
| Color Key | `ec.key.colorkey` | 4 | GPU | 32 | Implemented |
| Gaussian Blur (Legacy) | `ec.obsolete.gaussianlegacy` | 2 | | 32 | Implemented |
| Lightning | `ec.obsolete.lightning` | 25 | | 32 | Implemented |
| Luma Key | `ec.key.luma` | 5 | | 32 | Implemented |
| Luma Key | `ec.key.luma` | 5 | GPU | 32 | Implemented |
| Path Text | `ec.obsolete.pathtext` | 32 | | 32 | Implemented |
| Reduce Interlace Flicker | `ec.blur.reduceflicker` | 1 | | 32 | Implemented |
| Spill Suppressor | `ec.key.spill` | 3 | | 32 | Implemented |
| Spill Suppressor | `ec.key.spill` | 3 | GPU | 32 | Implemented |
## Paint
@ -340,31 +340,31 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
| Effect | Id | Params | GPU | 32 | Status |
|---|---|---:|:-:|:-:|---|
| Brush Strokes | `ec.stylize.brushstrokes` | 7 | | 32 | Implemented |
| Brush Strokes | `ec.stylize.brushstrokes` | 7 | GPU | 32 | Implemented |
| CC Block Load | `ec.stylize.ccblockload` | 5 | | 32 | Implemented |
| CC Burn Film | `ec.stylize.ccburnfilm` | 3 | | 32 | Implemented |
| CC Glass | `ec.stylize.ccglass` | 17 | | 32 | Implemented |
| CC HexTile | `ec.stylize.cchextile` | 5 | | 32 | Implemented |
| CC Kaleida | `ec.stylize.cckaleida` | 4 | | 32 | Implemented |
| CC Kaleida | `ec.stylize.cckaleida` | 4 | GPU | 32 | Implemented |
| CC Mr. Smoothie | `ec.stylize.ccmrsmoothie` | 7 | | 32 | Implemented |
| CC Plastic | `ec.stylize.ccplastic` | 18 | | 32 | Implemented |
| CC RepeTile | `ec.stylize.ccrepetile` | 5 | | 32 | Implemented |
| CC Threshold | `ec.stylize.ccthreshold` | 4 | | 32 | Implemented |
| CC Threshold RGB | `ec.stylize.ccthresholdrgb` | 7 | | 32 | Implemented |
| CC Vignette | `ec.stylize.ccvignette` | 4 | | 32 | Implemented |
| CC RepeTile | `ec.stylize.ccrepetile` | 5 | GPU | 32 | Implemented |
| CC Threshold | `ec.stylize.ccthreshold` | 4 | GPU | 32 | Implemented |
| CC Threshold RGB | `ec.stylize.ccthresholdrgb` | 7 | GPU | 32 | Implemented |
| CC Vignette | `ec.stylize.ccvignette` | 4 | GPU | 32 | Implemented |
| Cartoon | `ec.stylize.cartoon` | 12 | | 32 | Implemented |
| Color Emboss | `ec.stylize.coloremboss` | 4 | | 32 | Implemented |
| Emboss | `ec.stylize.emboss` | 4 | GPU | 32 | Implemented |
| Find Edges | `ec.stylize.findedges` | 2 | GPU | 32 | Implemented |
| Glow | `ec.stylize.glow` | 14 | GPU | 32 | Implemented |
| Mosaic | `ec.stylize.mosaic` | 3 | GPU | 32 | Implemented |
| Motion Tile | `ec.stylize.motiontile` | 8 | | 32 | Implemented |
| Posterize | `ec.stylize.posterize` | 1 | | 32 | Implemented |
| Roughen Edges | `ec.stylize.roughenedges` | 13 | | 32 | Implemented |
| Scatter | `ec.stylize.scatter` | 3 | | 32 | Implemented |
| Strobe Light | `ec.stylize.strobe` | 7 | | 32 | Implemented |
| Texturize | `ec.stylize.texturize` | 4 | | 32 | Implemented |
| Threshold | `ec.stylize.threshold` | 1 | | 32 | Implemented |
| Motion Tile | `ec.stylize.motiontile` | 8 | GPU | 32 | Implemented |
| Posterize | `ec.stylize.posterize` | 1 | GPU | 32 | Implemented |
| Roughen Edges | `ec.stylize.roughenedges` | 13 | GPU | 32 | Implemented |
| Scatter | `ec.stylize.scatter` | 3 | GPU | 32 | Implemented |
| Strobe Light | `ec.stylize.strobe` | 7 | GPU | 32 | Implemented |
| Texturize | `ec.stylize.texturize` | 4 | GPU | 32 | Implemented |
| Threshold | `ec.stylize.threshold` | 1 | GPU | 32 | Implemented |
## Text
@ -379,9 +379,9 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|---|---|---:|:-:|:-:|---|
| CC Force Motion Blur | `ec.time.ccforcemotionblur` | 4 | | 32 | Implemented |
| CC Wide Time | `ec.time.ccwidetime` | 3 | | 32 | Implemented |
| Echo | `ec.time.echo` | 5 | | 32 | Implemented |
| Echo | `ec.time.echo` | 5 | GPU | 32 | Implemented |
| Pixel Motion Blur | `ec.time.pixelmotionblur` | 4 | | 32 | Implemented |
| Posterize Time | `ec.time.posterizetime` | 1 | | 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 |

View File

@ -10,7 +10,7 @@ How close EffectCraft is to After Effects 2026, feature by feature, and how much
| Unweighted | ≈ 94% (half-credit) / 98% (fractions) |
| P0 / P1 / P2 | ≈ 96% / 92% / 85% (half-credit); 99% / 97% / 96% (fractions) |
| Features done / partial / missing | 80 / 12 / 0 of 92 |
| **Effects** | **298 of 298** After Effects 2026 effects exist; 42 still simplified ([effects.md](effects.md)); 58 run on the GPU |
| **Effects** | **298 of 298** After Effects 2026 effects exist; 42 still simplified ([effects.md](effects.md)); 152 run on the GPU |
| Disabled menu entries left | 0 (the stub list is empty) |
| Remaining work | ≈ 19–22 agent-hours at the pace measured so far (≈ 58 on the conservative audit scale) |
| **Wall-clock estimate** | **≈ 4–5 hours** with five agents in parallel; ≈ 3 hours for 100% of P0 + P1 |
@ -136,7 +136,7 @@ the disk cache in the formats wave; the 3D Camera Tracker in M12.6.)
| Text | ≈ 95% | 0.4 | no extruded strokes, the Variable Font Axes animator changes outlines but not advances (M13.6: variable font axes in the character style — `layer.setText variations`, the Character panel's Variable Font Axes fields — shape with HVAR / gvar advances and draw at that design-space position; OpenType features — stylistic sets, discretionary ligatures, contextual / stylistic alternates, swash, titling, ordinals, fractions, figure styles, true small caps / all small caps and superior / inferior glyphs with faux fallback — per character with the Character panel's OpenType popup and `text.fontFeatures` landed in M13.2; vertical Roman / Tate-Chu-Yoko, forced LTR paragraphs, caret on animated and path text, Variable Font Axes and Lottie style runs landed in M13.5; extruded, bevelled text in M7.6; per-character styles, paragraph settings, on-canvas editing and the `sourceText` style API in M9.9–M9.10) |
| Web | 85% | 1.0 | viewer frames and Roto Brush propagation still on the page's thread; GPU effects in the browser (browser storage, Web Audio, Web Worker renders/analyses, WebGPU viewer and offline install landed in M15.2) |
| 3D | 88% | 5.5 | multi-view layouts, the Extended Viewer for Advanced 3D comps (Classic 3D Extended Viewer landed in M13.5 UI completion); collapsed precomps of another size seen through the parent's camera render (fixed in M13.2); stereo rigs, orbit nulls, lights controlled by the camera, cameras/lights from glTF models, environment backgrounds, Advanced 3D motion blur, blend modes and track mattes landed in M7.7; Classic 3D iris-shaped bokeh with highlights, progressive depth of field on tilted layers and the focus-link commands landed in M13.5; Advanced 3D depth of field with the iris and highlight options, collapsed precomps as real Advanced 3D geometry and extruded text/shape strokes landed in M13.8; Advanced 3D (glTF/OBJ models, primitives, extruded text and shapes, PBR, image-based light, shadow maps, GPU rasteriser) in M7.4–M7.6 |
| Effects | ≈ 80% | 4.0 | GPU versions of the remaining effects (58 run on the GPU since M12.7) and the missing controls listed as partial in [effects.md](effects.md) (every After Effects effect exists since M9.11, M12.5 and M12.6; parameter names, order, twirl-downs, popups, units and defaults were aligned in M9.12) |
| Effects | ≈ 85% | 3.0 | GPU versions of the remaining effects (152 run on the GPU since M13.9, EFF-5: keying, mattes, channel, colour correction, stylize, distort, noise, blurs and time effects joined the 58 of M12.7; Warp, Bezier Warp, Reshape, Smear, Cartoon, CC Page Turn and others still render on the CPU) and the missing controls listed as partial in [effects.md](effects.md) (every After Effects effect exists since M9.11, M12.5 and M12.6; parameter names, order, twirl-downs, popups, units and defaults were aligned in M9.12) |
| Interface | 74% | 5.0 | more Learn tutorials and pixel-level fidelity of dialogs (the Home ▸ Learn tab with interactive tutorials and a UI fidelity pass landed in M13.5 UI completion; Timeline outline and Project panel columns scroll horizontally, the Layer Style dialog, ROI resize handles, Pan Behind snapping and 3D Reference Axes landed in M13.5; native macOS menu bar, Timeline columns/search/reveal-add, Home screen with recent projects and all AE workspaces landed; viewer rulers/snapping/channels/snapshots landed in M0.13) |
| Project | ≈ 68% | 5.5 | auto-save, folder moves, OCIO displays beyond the built-in tone map (Color Engine with OCIO/ACES working spaces, HDR compand/tone mapping, Rec. 2100 PQ/HLG output, Feet + Frames, display colour management, Simulate Output and the locked viewer landed in M7.7; proxies and Interpret Footage fields / pixel aspect / alpha guess landed: PRJ-8, PRJ-3) |
| Masks & roto | 74% | 5.0 | Roto Brush's learned (3.0) segmentation model (variable-width mask feather points with the Mask Feather tool landed in M13.5; mask tracking and Mask Interpolation landed in M6.6; Roto Brush & Refine Edge with graph-cut segmentation, flow propagation, edge matting, decontamination and Freeze in M6.7) |
@ -247,7 +247,7 @@ Browser in the web app.
2. Effect Controls widgets: angle dial, point crosshair, eyedropper, curves and levels editors.
3. ~~Viewer basics: snapping, rulers, channel view, snapshots, exposure, a drawable region of interest.~~ (M0.13)
4. ~~A GPU (wgpu) compositor, then GPU effects~~ (M12.2: 2D compositing and 16 GPU effects; M12.7:
Classic 3D runs, adjustment layers, 58 GPU effects and GPU particles).
Classic 3D runs, adjustment layers, 58 GPU effects and GPU particles; M13.9: 152 GPU effects).
5. ~~Pen tool for shape paths, shape vertex editing, free transform~~ (M6.5); the Layer viewer.
6. ~~Motion-path handles in the viewer; graph editor transform box and snapping.~~ (M5.8)
7. Point tracking and stabilization (in progress).