mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-27 11:37:02 -06:00
Squashed from #211 without its before/after PNG (we don't commit binary images). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
594 lines
24 KiB
Rust
594 lines
24 KiB
Rust
//! 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 up to radius [`MEDIAN_BISECT_R`], above that with the
|
|
//! CPU's sliding 512-bin histogram per channel (one invocation per row segment). 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_huang",
|
|
"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) taken by bisection over the window; larger radii
|
|
/// slide a histogram along each row (`fxn_median_huang`), whose cost grows with the radius,
|
|
/// not its square.
|
|
pub(crate) const MEDIAN_BISECT_R: usize = 4;
|
|
|
|
/// 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.scratch(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`).
|
|
pub(crate) 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);
|
|
// Preserve the CPU's rounded level / 511 values before the strict Dust & Scratches
|
|
// threshold. GPU division (even residual-corrected) may differ by one ulp.
|
|
let levels = e.data(&(0..512).map(|level| level as f32 / 511.0).collect::<Vec<_>>());
|
|
if r <= MEDIAN_BISECT_R {
|
|
return run(e, "fxn_median", &with_u0([r as u32, 0, 0, 0]), &q, None, Some(&levels));
|
|
}
|
|
// Segments long enough that filling the first window (2r + 1)² costs about as much as
|
|
// sliding along the rest.
|
|
let seg = (4 * r as u32).clamp(32, 512).min(q.width);
|
|
let out = e.scratch(q.width, q.height);
|
|
e.dispatch("fxn_median_huang", &with_u0([r as u32, seg, 0, 0]), &q, None, &out, Some(&levels), (q.height.div_ceil(64), q.width.div_ceil(seg)));
|
|
out
|
|
}
|
|
|
|
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);
|
|
}
|
|
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`).
|
|
pub(crate) 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.scratch(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.scratch(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.scratch(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, ×) 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),
|
|
}
|
|
}
|