Merge M13.9 gpu noise, blurs and time (fx_noise)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Brandon ThomasandClaude Opus 5.5 committed 2026-10-03 08:21:23 -04:00
commit 6cbb6bfb93
10 files changed
+1778 -82

No files matched your search

+1
View File
@@ -87,6 +87,7 @@ 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"]),
];
/// Whether the GPU kernel of effect `id` covers the instance's current settings (see
+21
View File
@@ -86,6 +86,10 @@ 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};
/// Effect categories in Effects & Presets order.
pub const CATEGORIES: &[&str] = &[
@@ -855,6 +859,23 @@ pub const GPU_EFFECTS: &[&str] = &[
// effectcraft-gpu fx_stylize (stylize, distort)
//
// 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",
//
];
+23 -14
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);
+83
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 {
+6
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 }
}
+1
View File
@@ -120,6 +120,7 @@ fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
"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),
+570 -8
View File
@@ -1,19 +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_effects::EffectCtx;
use effectcraft_color::BlendMode;
use effectcraft_effects::{Buf, EffectCtx};
use effectcraft_keyframe::Value;
use crate::context::Enc;
use crate::effects::GBuf;
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] = &[];
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.
pub(crate) const IDS: &[&str] = &[];
/// 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> {
let _ = (e, id, ctx, b);
None
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),
}
}
+822
View File
@@ -1,3 +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);
}
+191
View File
@@ -1,2 +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);
}
}
}
+60 -60
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**: 118 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 |
| 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
@@ -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 |
| 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
@@ -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 |