mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
M13.9: gpu noise, blurs and time (median, dust & scratches, remove grain, turbulent / fractal noise modes, noise alpha / HLS, smart / bilateral / channel / compound blur, sharpen, unsharp mask, minimax, echo, posterize time)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
47f61debcb
commit
4afe56cbca
10 files changed
+1736
-40
No files matched your search
@@ -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
|
||||
|
||||
@@ -85,6 +85,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] = &[
|
||||
@@ -811,6 +815,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
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -117,6 +117,7 @@ fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
"ec.perspective.dropshadow" => drop_shadow(e, ctx, b),
|
||||
"ec.distort.transform" => transform(e, ctx, b),
|
||||
"ec.color.curves" => curves(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
@@ -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, ×) 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),
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+18
-18
@@ -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**: 76 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,22 +46,22 @@ 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
|
||||
|
||||
@@ -74,7 +74,7 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
||||
| Channel Combiner | `ec.channel.combiner` | 6 | | 32 | Implemented |
|
||||
| Compound Arithmetic | `ec.channel.compoundarithmetic` | 6 | | 32 | Implemented |
|
||||
| Invert | `ec.channel.invert` | 2 | GPU | 32 | Implemented |
|
||||
| Minimax | `ec.channel.minimax` | 5 | | 32 | Implemented |
|
||||
| Minimax | `ec.channel.minimax` | 5 | GPU | 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 |
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
Reference in new issue
Block a user