M12.7: GPU blur/transition/generate effects (radial, camera lens, CC radial fast, wipes, venetian, cell pattern, checkerboard, grid, 4-color, noise, add grain)

Kernels in fx_generate.wgsl mirror the CPU effects; wipe edges use f64
per-column/per-row terms uploaded as (hi, lo) pairs. CPU helpers shared
with the GPU are now public (camera_lens_spans, gen_mode, pattern_kind,
GrainLook, place_layer). GPU tests in tests_fx_generate.rs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Brandon ThomasandClaude Opus 5.5 committed 2026-10-03 02:46:09 -04:00
1 parent 9a6389db86
commit c8623b1864
11 files changed
+1275 -32

No files matched your search

+12 -6
View File
@@ -277,11 +277,9 @@ fn iris_spans(r: f64, sides: u32, rotation_deg: f64, roundness: f64) -> Vec<(i64
spans
}
fn camera_lens_blur(ctx: &EffectCtx, mut b: Buf) -> Buf {
let r = ctx.params.f("blurRadius").max(0.0) * b.scale;
if r < 0.5 {
return b;
}
/// Camera Lens Blur's iris spans at blur radius `r` pixels (shape, rotation, roundness and
/// aspect ratio applied) and the iris' horizontal reach (shared with the GPU kernel).
pub fn camera_lens_spans(ctx: &EffectCtx, r: f64) -> (Vec<(i64, i64, i64)>, f64) {
let shape = ctx.params.e("irisProperties/irisShape");
let sides = if shape >= 8 { 0 } else { shape + 3 };
let mut spans = iris_spans(r, sides, ctx.params.f("irisProperties/irisRotation"), ctx.params.f("irisProperties/irisRoundness").clamp(0.0, 100.0) / 100.0);
@@ -293,7 +291,15 @@ fn camera_lens_blur(ctx: &EffectCtx, mut b: Buf) -> Buf {
s.2 = (s.2 as f64 * aspect).round() as i64;
}
}
let reach = r * aspect.max(1.0);
(spans, r * aspect.max(1.0))
}
fn camera_lens_blur(ctx: &EffectCtx, mut b: Buf) -> Buf {
let r = ctx.params.f("blurRadius").max(0.0) * b.scale;
if r < 0.5 {
return b;
}
let (spans, reach) = camera_lens_spans(ctx, r);
let repeat = ctx.params.b("repeatEdge") || ctx.adjustment;
if !repeat {
b.pad(reach.ceil() as u32 + 1);
+1 -1
View File
@@ -75,7 +75,7 @@ pub(crate) const GEN_BLEND_MODES: [&str; 20] = [
"Silhouette Alpha",
];
fn gen_mode(i: u32) -> Option<BlendMode> {
pub fn gen_mode(i: u32) -> Option<BlendMode> {
Some(match i {
0 => return None,
2 => BlendMode::Add,
+1 -1
View File
@@ -61,7 +61,7 @@ const CELL_PATTERNS: [&str; 12] = [
];
/// Shaping used for Cell Pattern option `i` (see [`cell_value`]).
fn pattern_kind(i: u32) -> u32 {
pub fn pattern_kind(i: u32) -> u32 {
match i {
0..=5 => i,
6 => 1,
+20
View File
@@ -67,6 +67,12 @@ use effectcraft_project::build::Ids;
use effectcraft_project::{GroupKind, ParamUi, PropGroup, Property};
pub use effectcraft_raster::{AuxChannels, Image};
pub use misc::{INVERT_ALPHA, INVERT_CHANNELS};
// CPU helpers the GPU kernels share (effectcraft-gpu).
pub use blur2::camera_lens_spans;
pub use generate::gen_mode;
pub use generate2::pattern_kind as cell_pattern_kind;
pub use noise::GrainLook;
pub use transition::place_layer;
/// Effect categories in Effects & Presets order.
pub const CATEGORIES: &[&str] = &[
@@ -641,6 +647,20 @@ pub const GPU_EFFECTS: &[&str] = &[
"ec.color.exposure",
"ec.channel.invert",
"ec.distort.transform",
// Blur, transition and generate family (effectcraft-gpu `fx_generate`).
"ec.blur.radial",
"ec.blur.cameralens",
"ec.blur.ccradialfast",
"ec.transition.venetian",
"ec.transition.linearwipe",
"ec.transition.radialwipe",
"ec.transition.gradientwipe",
"ec.generate.cellpattern",
"ec.generate.checkerboard",
"ec.generate.grid",
"ec.generate.fourcolor",
"ec.noise.noise",
"ec.noise.addgrain",
];
/// Effects whose output depends on [`EffectCtx::time`] directly (not only through animated
+2 -2
View File
@@ -41,7 +41,7 @@ pub(crate) const GRAIN_BLEND_MODES: [&str; 5] = ["Film", "Multiply", "Add", "Scr
/// The Tweaking / Color / Application / Animation controls Add Grain and Match Grain share
/// (twirl-down groups with the same ids in both effects).
pub(crate) struct GrainLook {
pub struct GrainLook {
pub intensity: f32,
pub channel_intensity: [f32; 3],
pub size: f32,
@@ -62,7 +62,7 @@ pub(crate) struct GrainLook {
}
impl GrainLook {
pub(crate) fn from_params(ctx: &EffectCtx) -> GrainLook {
pub fn from_params(ctx: &EffectCtx) -> GrainLook {
let pr = ctx.params;
let ch = |pre: &str, a: &str, b: &str, c: &str| [a, b, c].map(|k| pr.get(&format!("{pre}{k}")).map(Value::as_f64).unwrap_or(1.0) as f32);
let ft = ctx.time * 24.0 * pr.f("animation/animationSpeed");
+1 -1
View File
@@ -30,7 +30,7 @@ fn scale_px(px: &mut Px, k: f32) {
/// Resample `other` into `b`'s pixel grid with placement `mode` (Gradient Wipe's Gradient
/// Placement, Texturize's Texture Placement): 0 tile from the top-left, 1 centre once,
/// 2 stretch to fit.
pub(crate) fn place_layer(ctx: &EffectCtx, b: &Buf, other: &crate::LayerPixels, mode: u32) -> effectcraft_raster::Image {
pub fn place_layer(ctx: &EffectCtx, b: &Buf, other: &crate::LayerPixels, mode: u32) -> effectcraft_raster::Image {
let (ls, os) = (ctx.layer_size, other.size);
let inv = 1.0 / b.scale.max(1e-9);
crate::util::gen_image(b.img.width, b.img.height, |x, y| {
+393 -7
View File
@@ -1,18 +1,404 @@
//! GPU effects, blur, transition and generate family (kernels in `shaders/fx_generate.wgsl`): the CPU effects' exact
//! steps with the pixel loops as compute kernels.
//!
//! Every effect here covers all of its parameters. Where the CPU works in f64 around a hard
//! edge (the wipes), the per-column and per-row terms are computed in f64 here and uploaded as
//! (hi, lo) f32 pairs so the GPU puts the edge where the CPU does.
use effectcraft_effects::EffectCtx;
use effectcraft_color::BlendMode;
use effectcraft_effects::{EffectCtx, GrainLook};
use crate::context::Enc;
use crate::effects::GBuf;
use crate::context::{Enc, Params};
use crate::effects::{GBuf, gaussian_blur};
/// Compute entry points in `fx_generate.wgsl`.
pub(crate) const KERNELS: &[&str] = &[];
pub(crate) const KERNELS: &[&str] = &[
"gen_radial_blur",
"gen_ccradialfast",
"gen_lens_pre",
"gen_row_prefix",
"gen_lens_blur",
"gen_linear_wipe",
"gen_venetian",
"gen_radial_wipe",
"gen_gradient_wipe",
"gen_cell_pattern",
"gen_checker",
"gen_grid",
"gen_fourcolor",
"gen_noise",
"gen_grain_planes",
"gen_grain_apply",
];
/// Effect ids implemented here.
pub(crate) const IDS: &[&str] = &[];
pub(crate) const IDS: &[&str] = &[
"ec.blur.radial",
"ec.blur.cameralens",
"ec.blur.ccradialfast",
"ec.transition.venetian",
"ec.transition.linearwipe",
"ec.transition.radialwipe",
"ec.transition.gradientwipe",
"ec.generate.cellpattern",
"ec.generate.checkerboard",
"ec.generate.grid",
"ec.generate.fourcolor",
"ec.noise.noise",
"ec.noise.addgrain",
];
/// 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> {
None
pub(crate) fn apply(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
match id {
"ec.blur.radial" => radial_blur(e, ctx, b),
"ec.blur.cameralens" => camera_lens(e, ctx, b),
"ec.blur.ccradialfast" => radial_fast(e, ctx, b),
"ec.transition.venetian" => venetian(e, ctx, b),
"ec.transition.linearwipe" => linear_wipe(e, ctx, b),
"ec.transition.radialwipe" => radial_wipe(e, ctx, b),
"ec.transition.gradientwipe" => gradient_wipe(e, ctx, b),
"ec.generate.cellpattern" => cell_pattern(e, ctx, b),
"ec.generate.checkerboard" => checkerboard(e, ctx, b),
"ec.generate.grid" => grid(e, ctx, b),
"ec.generate.fourcolor" => four_color(e, ctx, b),
"ec.noise.noise" => noise(e, ctx, b),
"ec.noise.addgrain" => add_grain(e, ctx, b),
_ => None,
}
}
/// Run a per-pixel kernel over the buffer.
fn run(e: &mut Enc, entry: &str, p: &Params, b: GBuf, aux: Option<&crate::context::GpuImage>, data: Option<&[f32]>) -> Option<GBuf> {
let buf = data.map(|d| e.data(d));
let out = e.image(b.img.width, b.img.height);
e.pixels(entry, p, &b.img, aux, &out, buf.as_ref());
Some(GBuf { img: out, ..b })
}
/// `v` as an f32 (hi, lo) pair.
fn hl(v: f64) -> [f32; 2] {
let h = v as f32;
[h, (v - h as f64) as f32]
}
/// Generate blending mode index → `BlendMode::ALL` index (255 = None: replace).
fn gen_mode(i: u32) -> u32 {
match effectcraft_effects::gen_mode(i) {
None => 255,
Some(m) => BlendMode::ALL.iter().position(|&x| x == m).unwrap_or(0) as u32,
}
}
// ---------------------------------------------------------------- blurs
fn radial_blur(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let amt = ctx.params.f("amount");
let zoom = ctx.params.e("type") == 1;
let c = b.to_px(ctx.params.v2("center"));
let amount = if zoom { amt / 100.0 } else { amt };
if amount.abs() < 1e-6 {
return Some(b);
}
let n = 32usize;
let mut data = Vec::with_capacity(2 * n);
for i in 0..n {
let t = i as f64 / (n - 1) as f64 - 0.5;
if zoom {
data.push((1.0 + t * amount) as f32);
} else {
let (s, c) = (t * amount).to_radians().sin_cos();
data.extend([s as f32, c as f32]);
}
}
let mut p = Params::default();
p.u[0] = [zoom as u32, n as u32, 0, 0];
p.f[0] = [c.0 as f32, c.1 as f32, 0.0, 0.0];
run(e, "gen_radial_blur", &p, b, None, Some(&data))
}
fn radial_fast(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let s = (ctx.params.f("amount") / 100.0).clamp(0.0, 1.0) * 0.5;
if s <= 0.0 {
return Some(b);
}
let c = b.to_px(ctx.params.v2("center"));
let mut p = Params::default();
p.u[0][0] = ctx.params.e("zoom");
p.f[0] = [c.0 as f32, c.1 as f32, s as f32, 0.0];
run(e, "gen_ccradialfast", &p, b, None, None)
}
fn camera_lens(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
let r = ctx.params.f("blurRadius").max(0.0) * b.scale;
if r < 0.5 {
return Some(b);
}
let (spans, reach) = effectcraft_effects::camera_lens_spans(ctx, r);
let repeat = ctx.params.b("repeatEdge") || ctx.adjustment;
if !repeat {
b.pad(e, reach.ceil() as u32 + 1)?;
}
let linear = ctx.params.b("useLinear");
let thr = ctx.params.f("highlight/specularThreshold") as f32 / 255.0;
let boost = 1.0 + ctx.params.f("highlight/specularBrightness").max(0.0) as f32 / 100.0 * 4.0;
let sat = (ctx.params.f("highlight/highlightSaturation") / 100.0).clamp(0.0, 1.0) as f32;
let (w, h) = (b.img.width, b.img.height);
let mut src = b.img.clone();
if linear || boost > 1.0 {
let mut p = Params::default();
p.u[0] = [linear as u32, (boost > 1.0) as u32, 0, 0];
p.f[0] = [thr, boost, sat, 0.0];
let out = e.image(w, h);
e.pixels("gen_lens_pre", &p, &src, None, &out, None);
src = out;
}
let count: i64 = spans.iter().map(|(_, l, r)| r - l + 1).sum();
let norm = 1.0 / count as f64;
// Large irises sum each span from per-row prefix sums; small ones add the pixels directly.
let prefix = count > 256;
let pre = if prefix {
if !e.g.fits(w + 1, h) {
return None;
}
let pre = e.image(w + 1, h);
e.dispatch("gen_row_prefix", &Params::default(), &src, None, &pre, None, (h.div_ceil(64), 1));
Some(pre)
} else {
None
};
let data: Vec<f32> = spans.iter().flat_map(|&(dy, l, r)| [dy as f32, l as f32, r as f32]).collect();
let mut p = Params::default();
p.u[0] = [spans.len() as u32, repeat as u32, linear as u32, prefix as u32];
p.f[0][0] = norm as f32;
let buf = e.data(&data);
let out = e.image(w, h);
e.pixels("gen_lens_blur", &p, &src, pre.as_ref(), &out, Some(&buf));
b.img = out;
Some(b)
}
// ---------------------------------------------------------------- transitions
fn linear_wipe(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let done = ctx.params.f("completion") / 100.0;
let ang = ctx.params.f("angle").to_radians();
let feather = (ctx.params.f("feather") * b.scale).max(0.001);
let (w, h) = (b.img.width as f64, b.img.height as f64);
let (dx, dy) = (ang.sin(), -ang.cos());
let corners = [(0.0, 0.0), (w, 0.0), (0.0, h), (w, h)];
let proj: Vec<f64> = corners.iter().map(|(x, y)| x * dx + y * dy).collect();
let (lo, hi) = (proj.iter().cloned().fold(f64::INFINITY, f64::min), proj.iter().cloned().fold(f64::NEG_INFINITY, f64::max));
let edge = lo - feather + (hi - lo + 2.0 * feather) * done;
let mut data = Vec::with_capacity(2 * (b.img.width + b.img.height) as usize);
for x in 0..b.img.width {
data.extend(hl((x as f64 + 0.5) * dx - edge));
}
for y in 0..b.img.height {
data.extend(hl((y as f64 + 0.5) * dy));
}
let mut p = Params::default();
p.u[0][0] = 2 * b.img.width;
p.f[0][0] = feather as f32;
run(e, "gen_linear_wipe", &p, b, None, Some(&data))
}
fn venetian(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let done = ctx.params.f("completion") / 100.0;
let ang = ctx.params.f("direction").to_radians();
let width = (ctx.params.f("width") * b.scale).max(1.0);
let feather = (ctx.params.f("feather") * b.scale).max(0.001);
let (dx, dy) = (ang.cos(), ang.sin());
let mut data = Vec::with_capacity((6 * b.img.width + 2 * b.img.height) as usize);
for x in 0..b.img.width {
let cx = ((x as f64 + 0.5) * dx).rem_euclid(width);
data.extend(hl(cx - done * width));
data.extend(hl(cx - width - done * width));
data.extend(hl(cx - width));
}
for y in 0..b.img.height {
data.extend(hl(((y as f64 + 0.5) * dy).rem_euclid(width)));
}
let mut p = Params::default();
p.u[0][0] = 6 * b.img.width;
p.f[0][0] = feather as f32;
run(e, "gen_venetian", &p, b, None, Some(&data))
}
fn radial_wipe(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let done = ctx.params.f("completion") / 100.0;
let start = ctx.params.f("startAngle");
let c = b.to_px(ctx.params.v2("center"));
let dir = ctx.params.e("wipe");
let feather = ctx.params.f("feather").max(0.01);
let edge = if dir == 2 { done * 180.0 } else { done * 360.0 };
// Rotations: the start angle, and the start angle ± the edge (where the wipe's edge lies).
let rots = [start, start + edge, start - edge].map(|r| r.to_radians().sin_cos());
let mut data = Vec::with_capacity(9 * (b.img.width + b.img.height) as usize);
for x in 0..b.img.width {
let ux = x as f64 + 0.5 - c.0;
for (s, co) in rots {
data.extend(hl(ux * co));
data.push((ux * s) as f32);
}
}
for y in 0..b.img.height {
let uy = -(y as f64 + 0.5 - c.1);
for (s, co) in rots {
data.extend(hl(uy * s));
data.push((uy * co) as f32);
}
}
let mut p = Params::default();
p.u[0] = [9 * b.img.width, dir.min(2), 0, 0];
p.f[0] = [edge as f32, feather as f32, 0.0, 0.0];
run(e, "gen_radial_wipe", &p, b, None, Some(&data))
}
fn gradient_wipe(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let done = (ctx.params.f("completion") / 100.0).clamp(0.0, 1.0) as f32;
if done <= 0.0 {
return Some(b);
}
let soft = (ctx.params.f("softness") / 100.0) as f32;
// The gradient layer is fetched and placed on the CPU (as the CPU effect does).
let grad = match ctx.layer_param("gradientLayer", false) {
Some(o) => {
// place_layer reads only the buffer's size, offset and scale.
let img = effectcraft_raster::Image { width: b.img.width, height: b.img.height, data: vec![] };
let cpu = effectcraft_effects::Buf { img, offset: b.offset, scale: b.scale };
let img = effectcraft_effects::place_layer(ctx, &cpu, &o, ctx.params.e("gradientPlacement"));
Some(e.g.upload_image(&img)?)
}
None => None,
};
let mut p = Params::default();
p.u[0][0] = ctx.params.b("invert") as u32;
p.f[0] = [done, soft, 0.0, 0.0];
let out = e.image(b.img.width, b.img.height);
e.pixels("gen_gradient_wipe", &p, &b.img, Some(grad.as_ref().unwrap_or(&b.img)), &out, None);
Some(GBuf { img: out, ..b })
}
// ---------------------------------------------------------------- generate
fn cell_pattern(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let pr = ctx.params;
let size = (pr.f("size") * b.scale).max(1.0);
let off = b.to_px(pr.v2("offset"));
let mut evo = pr.f("evolution") / 360.0;
if pr.b("evolutionOptions/cycleEvolution") {
evo = evo.rem_euclid(pr.f("evolutionOptions/cycle").round().max(1.0));
}
let seed = (pr.f("evolutionOptions/randomSeed") as i64 as u32).wrapping_mul(7919) ^ 0xce11;
let tile = pr.b("tilingOptions/enableTiling");
let nx = pr.f("tilingOptions/cellsHorizontal").round().max(1.0) as i64;
let ny = pr.f("tilingOptions/cellsVertical").round().max(1.0) as i64;
if nx > i32::MAX as i64 || ny > i32::MAX as i64 {
return None;
}
let mut p = Params::default();
p.u[0] = [effectcraft_effects::cell_pattern_kind(pr.e("cellPattern")), pr.e("overflow"), pr.b("invert") as u32, seed];
p.u[1] = [tile as u32, nx as u32, ny as u32, 0];
// cos / sin of 2π·(h + evo) only see evo modulo 1.
p.f[0] = [pr.f("contrast") as f32 / 100.0, pr.f("disperse").clamp(0.0, 1.5) as f32, size as f32, evo.rem_euclid(1.0) as f32];
p.f[1] = [(0.5 - off.0) as f32, (0.5 - off.1) as f32, 0.0, 0.0];
run(e, "gen_cell_pattern", &p, b, None, None)
}
/// generate::cell_size.
fn cell_size(ctx: &EffectCtx, b: &GBuf, anchor: (f64, f64)) -> (f64, f64) {
let (w, h) = match ctx.params.e("sizeFrom") {
0 => {
let c = b.to_px(ctx.params.v2("corner"));
((c.0 - anchor.0).abs(), (c.1 - anchor.1).abs())
}
1 => (ctx.params.f("width") * b.scale, ctx.params.f("width") * b.scale),
_ => (ctx.params.f("width") * b.scale, ctx.params.f("height") * b.scale),
};
(w.max(1.0), h.max(1.0))
}
fn checkerboard(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let anchor = b.to_px(ctx.params.v2("anchor"));
let (w, h) = cell_size(ctx, &b, anchor);
let fw = (ctx.params.f("feather/featherWidth") * b.scale).max(1.0);
let fh = (ctx.params.f("feather/featherHeight") * b.scale).max(1.0);
let mut p = Params::default();
p.u[0][0] = gen_mode(ctx.params.e("blendingMode"));
p.f[0] = [(0.5 - anchor.0) as f32, (0.5 - anchor.1) as f32, w as f32, h as f32];
p.f[1] = [fw as f32, fh as f32, ctx.params.f("opacity") as f32 / 100.0, 0.0];
p.f[2] = ctx.params.color("color");
run(e, "gen_checker", &p, b, None, None)
}
fn grid(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let anchor = b.to_px(ctx.params.v2("anchor"));
let (w, h) = cell_size(ctx, &b, anchor);
let border = (ctx.params.f("border") * b.scale).max(0.0);
let fw = (ctx.params.f("feather/featherWidth") * b.scale).max(1.0);
let fh = (ctx.params.f("feather/featherHeight") * b.scale).max(1.0);
let mut p = Params::default();
p.u[0] = [gen_mode(ctx.params.e("blendingMode")), ctx.params.b("invertGrid") as u32, 0, 0];
p.f[0] = [(0.5 - anchor.0) as f32, (0.5 - anchor.1) as f32, w as f32, h as f32];
p.f[1] = [fw as f32, fh as f32, ctx.params.f("opacity") as f32 / 100.0, border as f32];
p.f[2] = ctx.params.color("color");
run(e, "gen_grid", &p, b, None, None)
}
fn four_color(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let mut p = Params::default();
for i in 0..4 {
let q = b.to_px(ctx.params.v2(&format!("positionsColors/point{}", i + 1)));
p.f[i] = [q.0 as f32, q.1 as f32, 0.0, 0.0];
p.f[4 + i] = ctx.params.color(&format!("positionsColors/color{}", i + 1));
}
let blend = ctx.params.f("blend").max(1.0);
let jitter = (ctx.params.f("jitter") / 100.0).clamp(0.0, 1.0) as f32;
p.u[0] = [gen_mode(ctx.params.e("blendingMode")), ctx.seed, 0, 0];
p.f[8] = [(blend * 100.0) as f32, jitter, ctx.params.f("opacity") as f32 / 100.0, 0.0];
run(e, "gen_fourcolor", &p, b, None, None)
}
// ---------------------------------------------------------------- noise
fn noise(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let amt = ctx.params.f("amount") as f32 / 100.0;
let clip = ctx.params.get("clip").is_none_or(|v| v.as_bool());
let frame_seed = (ctx.time * 1000.0) as u32 ^ ctx.seed;
let mut p = Params::default();
p.u[0] = [frame_seed, ctx.params.b("color") as u32, clip as u32, b.img.width];
p.f[0][0] = amt;
run(e, "gen_noise", &p, b, None, None)
}
fn add_grain(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
let look = GrainLook::from_params(ctx);
if look.intensity <= 0.0 {
return Some(b);
}
let (w, h) = (b.img.width, b.img.height);
// GrainLook::planes.
let size = look.size * b.scale as f32;
let s = |k: usize| (size * if look.mono { 1.0 } else { look.channel_size[k].max(0.05) }).max(0.05);
let mut p = Params::default();
p.u[0] = [look.mono as u32, look.seed, 101, 0];
p.f[0] = [s(0) * look.aspect, s(1) * look.aspect, s(2) * look.aspect, look.frame];
p.f[1] = [s(0), s(1), s(2), 0.0];
let mut planes = e.image(w, h);
e.pixels("gen_grain_planes", &p, &b.img, None, &planes, None);
let soft = look.softness * b.scale;
if soft > 0.05 {
planes = gaussian_blur(e, &planes, soft * look.aspect as f64, soft, true);
}
// GrainLook::color's tint factors.
let l = effectcraft_color::luminance(look.tint[0], look.tint[1], look.tint[2]).max(1e-3);
let tint = [0, 1, 2].map(|k| (1.0 - look.tint_amount) + look.tint_amount * look.tint[k] / l);
let mut p = Params::default();
p.u[0] = [look.mono as u32, look.mode, (look.tint_amount > 0.0) as u32, 0];
p.f[0] = [look.intensity * 0.25, look.saturation, look.midpoint, 0.0];
p.f[1] = [tint[0], tint[1], tint[2], 0.0];
p.f[2] = [look.channel_intensity[0], look.channel_intensity[1], look.channel_intensity[2], 0.0];
p.f[3] = [look.tones[0], look.tones[1], look.tones[2], 0.0];
run(e, "gen_grain_apply", &p, b, Some(&planes), None)
}
+2
View File
@@ -149,3 +149,5 @@ mod tests_3d;
mod tests_adjust;
#[cfg(test)]
mod tests_adv3d;
#[cfg(test)]
mod tests_fx_generate;
+629
View File
@@ -1,2 +1,631 @@
// GPU effects (generate family): see src/fx_generate.rs. Each entry point mirrors the CPU effect in
// effectcraft-effects operation for operation; parameter layouts are documented per entry.
//
// Hard edges (wipes) take per-column and per-row terms computed in f64 on the CPU and uploaded
// as (hi, lo) f32 pairs in `data`: near an edge the hi parts cancel exactly, so the edge sits
// where the CPU's f64 arithmetic puts it.
const GEN_DEG: f32 = 57.29577951308232;
// util::unpremul: (straight colour, alpha).
fn gen_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));
}
// util::smoothstep.
fn gen_smoothstep(e0: f32, e1: f32, x: f32) -> f32 {
if (abs(e1 - e0) < 1e-9) {
return select(1.0, 0.0, x < e0);
}
let t = clamp((x - e0) / (e1 - e0), 0.0, 1.0);
return t * t * (3.0 - 2.0 * t);
}
// generate::gen_blend: mode = index into BlendMode::ALL, 255 = None (replace).
fn gen_blend(o: vec4<f32>, g: vec4<f32>, mode: u32) -> vec4<f32> {
if (mode == 255u) {
return g;
}
return blend_pixel(mode, o, g, 0.5);
}
// A (hi, lo) pair from `data`.
fn gen_hl(i: u32) -> vec2<f32> {
return vec2<f32>(data[i], data[i + 1u]);
}
// (a + b) for (hi, lo) pairs whose hi parts cancel near zero.
fn gen_sum(a: vec2<f32>, b: vec2<f32>) -> f32 {
return (a.x + b.x) + (a.y + b.y);
}
// ---------------------------------------------------------------- Radial Blur (raster::radial_blur)
// u[0] = (zoom, samples); f[0] = (cx, cy); data = per sample (sin, cos) (spin) or k (zoom).
@compute @workgroup_size(16, 16)
fn gen_radial_blur(@builtin(global_invocation_id) gid: vec3<u32>) {
let dims = out_dims();
let p = vec2<i32>(gid.xy);
if (p.x >= dims.x || p.y >= dims.y) {
return;
}
let zoom = P.u[0].x != 0u;
let n = P.u[0].y;
let c = P.f[0].xy;
let v = vec2<f32>(f32(p.x) + 0.5 - c.x, f32(p.y) + 0.5 - c.y);
var acc = vec4<f32>(0.0);
for (var i = 0u; i < n; i++) {
var s: vec2<f32>;
if (zoom) {
let k = data[i];
s = vec2<f32>(c.x + v.x * k, c.y + v.y * k);
} else {
let sn = data[2u * i];
let cs = data[2u * i + 1u];
s = vec2<f32>(c.x + v.x * cs - v.y * sn, c.y + v.x * sn + v.y * cs);
}
acc += sample_bilinear(src, s.x, s.y);
}
textureStore(out, p, acc / f32(n));
}
// ---------------------------------------------------------------- CC Radial Fast Blur
// u[0].x = zoom mode (0 standard, 1 brightest, 2 darkest); f[0] = (cx, cy, s).
@compute @workgroup_size(16, 16)
fn gen_ccradialfast(@builtin(global_invocation_id) gid: vec3<u32>) {
let dims = out_dims();
let p = vec2<i32>(gid.xy);
if (p.x >= dims.x || p.y >= dims.y) {
return;
}
let mode = P.u[0].x;
let c = P.f[0].xy;
let s = P.f[0].z;
let d = vec2<f32>(f32(p.x) + 0.5 - c.x, f32(p.y) + 0.5 - c.y);
let len = sqrt(d.x * d.x + d.y * d.y) * s;
let n = clamp(u32(ceil(len)), 2u, 64u);
var acc = vec4<f32>(0.0);
if (mode == 1u) {
acc = vec4<f32>(-3.0e38);
} else if (mode == 2u) {
acc = vec4<f32>(3.0e38);
}
for (var i = 0u; i < n; i++) {
let k = 1.0 - s * f32(i) / f32(n - 1u);
let q = sample_bilinear(src, c.x + d.x * k, c.y + d.y * k);
if (mode == 1u) {
acc = max(acc, q);
} else if (mode == 2u) {
acc = min(acc, q);
} else {
acc += q / f32(n);
}
}
let a = acc.w;
textureStore(out, p, vec4<f32>(max(min(acc.xyz, vec3<f32>(a)), vec3<f32>(0.0)), clamp(a, 0.0, 1.0)));
}
// ---------------------------------------------------------------- Camera Lens Blur
// Linear working space and highlight boost before the blur.
// u[0] = (linear, boost); f[0] = (threshold, boost, saturation).
@compute @workgroup_size(16, 16)
fn gen_lens_pre(@builtin(global_invocation_id) gid: vec3<u32>) {
let dims = out_dims();
let p = vec2<i32>(gid.xy);
if (p.x >= dims.x || p.y >= dims.y) {
return;
}
var px = textureLoad(src, p, 0);
if (P.u[0].x != 0u) {
let u = gen_unpremul(px);
px = vec4<f32>(srgb_to_linear(u.x) * u.w, srgb_to_linear(u.y) * u.w, srgb_to_linear(u.z) * u.w, u.w);
}
if (P.u[0].y != 0u) {
let u = gen_unpremul(px);
let c = u.xyz;
let a = u.w;
if (a > 0.0 && luminance(c) >= P.f[0].x) {
let boost = P.f[0].y;
let sat = P.f[0].z;
let m = max(max(c.x, c.y), c.z);
let tinted = c + (m - c) * (1.0 - sat);
px = vec4<f32>((c + tinted * (boost - 1.0)) * a, px.w);
}
}
textureStore(out, p, px);
}
// Per-row prefix sums: out(x + 1, y) = sum of src(0..=x, y), out(0, y) = 0. One thread per row.
@compute @workgroup_size(64, 1)
fn gen_row_prefix(@builtin(global_invocation_id) gid: vec3<u32>) {
let dims = vec2<i32>(textureDimensions(src));
let y = i32(gid.x);
if (y >= dims.y) {
return;
}
var acc = vec4<f32>(0.0);
textureStore(out, vec2<i32>(0, y), acc);
for (var x = 0; x < dims.x; x++) {
acc += textureLoad(src, vec2<i32>(x, y), 0);
textureStore(out, vec2<i32>(x + 1, y), acc);
}
}
// The iris sum. data = spans (dy, left, right); aux = row prefix sums (when u[0].w).
// u[0] = (spans, repeat, linear, prefix); f[0].x = 1 / pixel count.
@compute @workgroup_size(16, 16)
fn gen_lens_blur(@builtin(global_invocation_id) gid: vec3<u32>) {
let dims = out_dims();
let p = vec2<i32>(gid.xy);
if (p.x >= dims.x || p.y >= dims.y) {
return;
}
let n = P.u[0].x;
let repeat = P.u[0].y != 0u;
let prefix = P.u[0].w != 0u;
let w = dims.x;
let h = dims.y;
var acc = vec4<f32>(0.0);
for (var s = 0u; s < n; s++) {
var yy = p.y + i32(data[3u * s]);
if (yy < 0 || yy >= h) {
if (!repeat) {
continue;
}
yy = clamp(yy, 0, h - 1);
}
var a = p.x + i32(data[3u * s + 1u]);
var bnd = p.x + i32(data[3u * s + 2u]);
if (!prefix) {
for (var i = a; i <= bnd; i++) {
if (repeat) {
acc += textureLoad(src, vec2<i32>(clamp(i, 0, w - 1), yy), 0);
} else if (i >= 0 && i < w) {
acc += textureLoad(src, vec2<i32>(i, yy), 0);
}
}
continue;
}
if (repeat) {
if (a < 0) {
let k = min(-a, bnd - a + 1);
acc += textureLoad(src, vec2<i32>(0, yy), 0) * f32(k);
a = 0;
}
if (bnd >= w) {
let k = min(bnd - (w - 1), bnd - a + 1);
acc += textureLoad(src, vec2<i32>(w - 1, yy), 0) * f32(k);
bnd = w - 1;
}
} else {
a = max(a, 0);
bnd = min(bnd, w - 1);
}
if (a <= bnd) {
acc += textureLoad(aux, vec2<i32>(bnd + 1, yy), 0) - textureLoad(aux, vec2<i32>(a, yy), 0);
}
}
var px = acc * P.f[0].x;
px.w = clamp(px.w, 0.0, 1.0);
if (P.u[0].z != 0u) {
let u = gen_unpremul(px);
px = vec4<f32>(linear_to_srgb(max(u.x, 0.0)) * u.w, linear_to_srgb(max(u.y, 0.0)) * u.w, linear_to_srgb(max(u.z, 0.0)) * u.w, u.w);
}
textureStore(out, p, px);
}
// ---------------------------------------------------------------- wipes
// Linear Wipe. data = per column ((x + 0.5)·dx − edge) (hi, lo), then per row ((y + 0.5)·dy)
// (hi, lo); u[0].x = row table offset; f[0].x = feather.
@compute @workgroup_size(16, 16)
fn gen_linear_wipe(@builtin(global_invocation_id) gid: vec3<u32>) {
let dims = out_dims();
let p = vec2<i32>(gid.xy);
if (p.x >= dims.x || p.y >= dims.y) {
return;
}
let d = gen_sum(gen_hl(2u * u32(p.x)), gen_hl(P.u[0].x + 2u * u32(p.y)));
let k = clamp(d / P.f[0].x + 0.5, 0.0, 1.0);
textureStore(out, p, textureLoad(src, p, 0) * k);
}
// Venetian Blinds. Per column (6 floats): (cx − done·w), (cx − w − done·w), (cx − w) as
// (hi, lo), with cx = ((x + 0.5)·dx) mod w; per row (2 floats): ((y + 0.5)·dy) mod w.
// u[0].x = row table offset; f[0].x = feather.
@compute @workgroup_size(16, 16)
fn gen_venetian(@builtin(global_invocation_id) gid: vec3<u32>) {
let dims = out_dims();
let p = vec2<i32>(gid.xy);
if (p.x >= dims.x || p.y >= dims.y) {
return;
}
let col = 6u * u32(p.x);
let row = gen_hl(P.u[0].x + 2u * u32(p.y));
// (d mod w) wraps once the column and row terms reach w.
let wrapped = gen_sum(gen_hl(col + 4u), row) >= 0.0;
var t: f32;
if (wrapped) {
t = gen_sum(gen_hl(col + 2u), row);
} else {
t = gen_sum(gen_hl(col), row);
}
let k = clamp(t / P.f[0].x + 0.5, 0.0, 1.0);
textureStore(out, p, textureLoad(src, p, 0) * k);
}
// Angle (degrees, clockwise from up) of the pixel vector rotated by rotation `r` (0 = start,
// 1 = start + edge, 2 = start − edge), from the per-column / per-row tables of Radial Wipe.
fn gen_psi(r: u32, x: u32, y: u32) -> f32 {
let c = 9u * x + 3u * r;
let w = P.u[0].x + 9u * y + 3u * r;
// x' = ux·cos R − uy·sin R (cancels at the edge), y' = uy·cos R + ux·sin R.
let xr = (data[c] - data[w]) + (data[c + 1u] - data[w + 1u]);
let yr = data[w + 2u] + data[c + 2u];
return atan2(xr, yr) * GEN_DEG;
}
// Radial Wipe. data: per column 9 floats (for each rotation: ux·cos R (hi, lo), ux·sin R),
// per row 9 floats (uy·sin R (hi, lo), uy·cos R); u[0] = (row offset, wipe);
// f[0] = (edge, feather).
@compute @workgroup_size(16, 16)
fn gen_radial_wipe(@builtin(global_invocation_id) gid: vec3<u32>) {
let dims = out_dims();
let p = vec2<i32>(gid.xy);
if (p.x >= dims.x || p.y >= dims.y) {
return;
}
let x = u32(p.x);
let y = u32(p.y);
let dir = P.u[0].y;
let edge = P.f[0].x;
var a = gen_psi(0u, x, y);
if (a < 0.0) {
a += 360.0;
}
var base: f32;
if (dir == 0u || (dir == 2u && a <= 180.0)) {
base = a - edge;
if (abs(base) < 90.0) {
base = gen_psi(1u, x, y);
}
} else {
base = (360.0 - a) - edge;
if (abs(base) < 90.0) {
base = -gen_psi(2u, x, y);
}
}
let k = clamp(base / P.f[0].y + 0.5, 0.0, 1.0);
textureStore(out, p, textureLoad(src, p, 0) * k);
}
// Gradient Wipe: aux = the placed gradient layer. u[0].x = invert; f[0] = (done, softness).
@compute @workgroup_size(16, 16)
fn gen_gradient_wipe(@builtin(global_invocation_id) gid: vec3<u32>) {
let dims = out_dims();
let p = vec2<i32>(gid.xy);
if (p.x >= dims.x || p.y >= dims.y) {
return;
}
let done = P.f[0].x;
let soft = P.f[0].y;
let g = gen_unpremul(textureLoad(aux, p, 0));
var t = clamp(luminance(g.xyz), 0.0, 1.0);
if (P.u[0].x != 0u) {
t = 1.0 - t;
}
var k = 0.0;
if (done >= 1.0) {
k = 0.0;
} else if (soft > 0.0) {
k = clamp((t - (done * (1.0 + soft) - soft)) / soft, 0.0, 1.0);
} else if (t >= done) {
k = 1.0;
}
textureStore(out, p, textureLoad(src, p, 0) * k);
}
// ---------------------------------------------------------------- Cell Pattern (generate2)
// Worley distances for (u, v) in cell units: (F1, F2, hash of the nearest cell).
fn gen_worley(u: f32, v: f32, disperse: f32, evo: f32, seed: u32) -> vec3<f32> {
let tile = P.u[1].x != 0u;
let nx = i32(P.u[1].y);
let ny = i32(P.u[1].z);
let cx = i32(floor(u));
let cy = i32(floor(v));
var f1 = 3.0e38;
var f2 = 3.0e38;
var id = 0.0;
for (var j = -1; j <= 1; j++) {
for (var i = -1; i <= 1; i++) {
let gx = cx + i;
let gy = cy + j;
var tx = gx;
var ty = gy;
if (tile) {
tx = ((gx % nx) + nx) % nx;
ty = ((gy % ny) + ny) % ny;
}
let hx = bitcast<u32>(tx);
let hy = bitcast<u32>(ty);
let h1 = hash_noise(hx, hy, seed);
let h2 = hash_noise(hx, hy, seed + 1u);
let h3 = hash_noise(hx, hy, seed + 2u);
let ang = 6.283185307179586 * (h3 + evo);
let px = f32(gx) + 0.5 + (h1 - 0.5) * disperse + 0.15 * disperse * cos(ang);
let py = f32(gy) + 0.5 + (h2 - 0.5) * disperse + 0.15 * disperse * sin(ang);
let d = sqrt((px - u) * (px - u) + (py - v) * (py - v));
if (d < f1) {
f2 = f1;
f1 = d;
id = hash_noise(hx, hy, seed + 3u);
} else if (d < f2) {
f2 = d;
}
}
}
return vec3<f32>(f1, f2, id);
}
fn gen_cell_value(pattern: u32, f1: f32, f2: f32, id: f32) -> f32 {
let e = f2 - f1;
switch pattern {
case 0u: { return clamp(1.0 - f1 * 1.3, 0.0, 1.0); }
case 1u: { return id * 0.8 + 0.2 * clamp(1.0 - f1, 0.0, 1.0); }
case 2u: { return clamp(e * 2.0, 0.0, 1.0); }
case 3u: { return id * gen_smoothstep(0.0, 0.06, e); }
case 4u: { return id; }
case 5u: { return clamp(1.0 - f1 * f1 * 2.0, 0.0, 1.0); }
case 6u: { return id * clamp(e * 3.0, 0.0, 1.0); }
default: { return 1.0 - clamp(abs(e - 0.15) * 5.0, 0.0, 1.0); }
}
}
fn gen_overflow(v: f32, mode: u32) -> f32 {
if (mode == 1u) {
return 0.5 + 0.5 * tanh((v - 0.5) * 2.0);
}
if (mode == 2u) {
let t = v - 2.0 * floor(v * 0.5);
return select(t, 2.0 - t, t > 1.0);
}
return clamp(v, 0.0, 1.0);
}
// u[0] = (pattern kind, overflow, invert, seed); u[1] = (tile, columns, rows);
// f[0] = (contrast, disperse, size, evolution mod 1); f[1] = (0.5 − offset x, 0.5 − offset y).
@compute @workgroup_size(16, 16)
fn gen_cell_pattern(@builtin(global_invocation_id) gid: vec3<u32>) {
let dims = out_dims();
let p = vec2<i32>(gid.xy);
if (p.x >= dims.x || p.y >= dims.y) {
return;
}
let size = P.f[0].z;
let u = (f32(p.x) + P.f[1].x) / size;
let v = (f32(p.y) + P.f[1].y) / size;
let w = gen_worley(u, v, P.f[0].y, P.f[0].w, P.u[0].w);
var val = gen_cell_value(P.u[0].x, w.x, w.y, w.z);
val = gen_overflow((val - 0.5) * P.f[0].x + 0.5, P.u[0].y);
if (P.u[0].z != 0u) {
val = 1.0 - val;
}
textureStore(out, p, vec4<f32>(val, val, val, 1.0));
}
// ---------------------------------------------------------------- Checkerboard / Grid / 4-Color
// Signed distance (in feather units) to the nearest cell edge, positive in even cells.
fn gen_axis(q: f32, size: f32, f: f32) -> f32 {
let u = q / size;
let fl = floor(u);
let fr = u - fl;
let d = min(fr, 1.0 - fr) * size;
let n = i32(fl);
let s = select(-1.0, 1.0, ((n % 2) + 2) % 2 == 0);
return s * min(d / (f * 0.5), 1.0);
}
// u[0].x = mode; f[0] = (0.5 − anchor x, 0.5 − anchor y, cell w, cell h);
// f[1] = (feather w, feather h, opacity); f[2] = colour.
@compute @workgroup_size(16, 16)
fn gen_checker(@builtin(global_invocation_id) gid: vec3<u32>) {
let dims = out_dims();
let p = vec2<i32>(gid.xy);
if (p.x >= dims.x || p.y >= dims.y) {
return;
}
let sx = gen_axis(f32(p.x) + P.f[0].x, P.f[0].z, P.f[1].x);
let sy = gen_axis(f32(p.y) + P.f[0].y, P.f[0].w, P.f[1].y);
let cov = clamp(0.5 + 0.5 * sx * sy, 0.0, 1.0) * P.f[1].z;
let c = P.f[2].xyz;
textureStore(out, p, gen_blend(textureLoad(src, p, 0), vec4<f32>(c * cov, cov), P.u[0].x));
}
// f32::rem_euclid.
fn gen_rem(x: f32, m: f32) -> f32 {
let r = x - m * trunc(x / m);
return select(r, r + m, r < 0.0);
}
// u[0] = (mode, invert); f[0] = (0.5 − anchor x, 0.5 − anchor y, cell w, cell h);
// f[1] = (feather w, feather h, opacity, border); f[2] = colour.
@compute @workgroup_size(16, 16)
fn gen_grid(@builtin(global_invocation_id) gid: vec3<u32>) {
let dims = out_dims();
let p = vec2<i32>(gid.xy);
if (p.x >= dims.x || p.y >= dims.y) {
return;
}
let w = P.f[0].z;
let h = P.f[0].w;
let border = P.f[1].w;
let gx = gen_rem(f32(p.x) + P.f[0].x, w);
let gy = gen_rem(f32(p.y) + P.f[0].y, h);
let dx = min(gx, w - gx);
let dy = min(gy, h - gy);
let cx = clamp((border / 2.0 - dx) / P.f[1].x + 0.5, 0.0, 1.0);
let cy = clamp((border / 2.0 - dy) / P.f[1].y + 0.5, 0.0, 1.0);
var cov = 0.0;
if (border > 0.0) {
cov = max(cx, cy);
}
if (P.u[0].y != 0u) {
cov = 1.0 - cov;
}
let a = cov * P.f[1].z;
textureStore(out, p, gen_blend(textureLoad(src, p, 0), vec4<f32>(P.f[2].xyz * a, a), P.u[0].x));
}
// u[0] = (mode, seed); f[0..4] = points (x, y); f[4..8] = colours;
// f[8] = (blend · 100, jitter, opacity).
@compute @workgroup_size(16, 16)
fn gen_fourcolor(@builtin(global_invocation_id) gid: vec3<u32>) {
let dims = out_dims();
let p = vec2<i32>(gid.xy);
if (p.x >= dims.x || p.y >= dims.y) {
return;
}
let bl = P.f[8].x;
var acc = vec3<f32>(0.0);
var wsum = 0.0;
for (var i = 0; i < 4; i++) {
let q = P.f[i].xy;
let dx = f32(p.x) + 0.5 - q.x;
let dy = f32(p.y) + 0.5 - q.y;
let d2 = dx * dx + dy * dy;
let w = 1.0 / pow(d2 / bl + 1e-6, 1.5);
acc += P.f[4 + i].xyz * w;
wsum += w;
}
var c = acc / wsum;
let jitter = P.f[8].y;
if (jitter > 0.0) {
let n = (hash_noise(u32(p.x), u32(p.y), P.u[0].y) - 0.5) * jitter * (8.0 / 255.0);
c += vec3<f32>(n);
}
let op = P.f[8].z;
textureStore(out, p, gen_blend(textureLoad(src, p, 0), vec4<f32>(c * op, op), P.u[0].x));
}
// ---------------------------------------------------------------- Noise / Add Grain
// Noise (misc::noise). u[0] = (frame seed, colour, clip, width); f[0].x = amount.
@compute @workgroup_size(16, 16)
fn gen_noise(@builtin(global_invocation_id) gid: vec3<u32>) {
let dims = out_dims();
let p = vec2<i32>(gid.xy);
if (p.x >= dims.x || p.y >= dims.y) {
return;
}
var px = textureLoad(src, p, 0);
let a = px.w;
if (a <= 0.0) {
textureStore(out, p, px);
return;
}
let seed = P.u[0].x;
let w = P.u[0].w;
let amt = P.f[0].x;
let x = u32(p.x);
let y = u32(p.y);
var n = vec3<f32>((hash_noise(x, y, seed) - 0.5) * amt);
if (P.u[0].y != 0u) {
n.y = (hash_noise(x + w, y, seed) - 0.5) * amt;
n.z = (hash_noise(x + 2u * w, y, seed) - 0.5) * amt;
}
if (P.u[0].z != 0u) {
px = vec4<f32>(clamp(px.xyz + n * a, vec3<f32>(0.0), vec3<f32>(a)), a);
} else {
// Unclipped: values past black / white wrap around.
px = vec4<f32>(fract_euclid(px.x / a + n.x) * a, fract_euclid(px.y / a + n.y) * a, fract_euclid(px.z / a + n.z) * a, a);
}
textureStore(out, p, px);
}
// Add Grain's noise planes (GrainLook::planes before Softness): rgb = per-channel zero-mean
// value noise. u[0] = (mono, seed, salt); f[0].xyz = x divisors (size · aspect), f[0].w = frame;
// f[1].xyz = y divisors (size).
@compute @workgroup_size(16, 16)
fn gen_grain_planes(@builtin(global_invocation_id) gid: vec3<u32>) {
let dims = out_dims();
let p = vec2<i32>(gid.xy);
if (p.x >= dims.x || p.y >= dims.y) {
return;
}
let x = f32(p.x);
let y = f32(p.y);
let frame = P.f[0].w;
let seed = P.u[0].y;
let salt = P.u[0].z;
let n0 = value_noise(x / P.f[0].x, y / P.f[1].x, frame, seed) - 0.5;
var g = vec3<f32>(n0);
if (P.u[0].x == 0u) {
g.y = value_noise(x / P.f[0].y, y / P.f[1].y, frame, seed + salt) - 0.5;
g.z = value_noise(x / P.f[0].z, y / P.f[1].z, frame, seed + 2u * salt) - 0.5;
}
textureStore(out, p, vec4<f32>(g, 0.0));
}
// GrainLook::blend.
fn gen_grain_blend(mode: u32, c: f32, n: f32) -> f32 {
var v: f32;
switch mode {
case 1u: { v = c * (1.0 + n); }
case 2u: { v = c + n; }
case 3u: { v = 1.0 - (1.0 - c) * (1.0 - abs(n)); }
case 4u: { v = c + n * 4.0 * c * (1.0 - c); }
default: { v = c + n * (1.0 - 0.5 * c); }
}
return max(v, 0.0);
}
// Add Grain applied: aux = noise planes. u[0] = (mono, mode, tint); f[0] = (amp, saturation,
// midpoint); f[1].xyz = tint factors; f[2].xyz = channel intensities; f[3].xyz = tone weights.
@compute @workgroup_size(16, 16)
fn gen_grain_apply(@builtin(global_invocation_id) gid: vec3<u32>) {
let dims = out_dims();
let p = vec2<i32>(gid.xy);
if (p.x >= dims.x || p.y >= dims.y) {
return;
}
let px = textureLoad(src, p, 0);
let u = gen_unpremul(px);
let a = u.w;
if (a <= 0.0) {
textureStore(out, p, px);
return;
}
let c = u.xyz;
var g = textureLoad(aux, p, 0).xyz;
if (P.u[0].x != 0u) {
g = vec3<f32>(g.x);
} else {
let gm = (g.x + g.y + g.z) / 3.0;
g = gm + (g - gm) * P.f[0].y;
}
if (P.u[0].z != 0u) {
g = g * P.f[1].xyz;
}
let mid = P.f[0].z;
let l = clamp(luminance(c), 0.0, 1.0);
let s = 1.0 - gen_smoothstep(0.0, mid, l);
let hi = gen_smoothstep(mid, 1.0, l);
let tones = P.f[3].xyz;
let weight = tones.x * s + tones.y * (1.0 - s - hi) + tones.z * hi;
let amp = P.f[0].x;
let ci = P.f[2].xyz;
let mode = P.u[0].y;
let o = vec3<f32>(
gen_grain_blend(mode, c.x, g.x * amp * ci.x * weight),
gen_grain_blend(mode, c.y, g.y * amp * ci.y * weight),
gen_grain_blend(mode, c.z, g.z * amp * ci.z * weight),
);
textureStore(out, p, vec4<f32>(o * a, a));
}
+200
View File
@@ -0,0 +1,200 @@
//! GPU vs CPU for the blur, transition and generate family (`fx_generate`).
use effectcraft_keyframe::Value;
use effectcraft_project::BitDepth;
use effectcraft_render::RenderOpts;
use effectcraft_time::Tick;
use crate::tests::{Scene, c, check, compare_at, effect_case, n, opts, set};
fn e(v: u32) -> Value {
Value::Enum(v)
}
fn b(v: bool) -> Value {
Value::Bool(v)
}
fn pt(x: f64, y: f64) -> Value {
Value::Vec2([x, y])
}
#[test]
fn radial_blur() {
effect_case("ec.blur.radial", &[("amount", n(12.0))]);
effect_case("ec.blur.radial", &[("amount", n(30.0)), ("type", e(1)), ("center", pt(20.3, 14.7))]);
effect_case("ec.blur.radial", &[("amount", n(-45.0)), ("center", pt(50.0, 30.0))]);
}
#[test]
fn cc_radial_fast_blur() {
effect_case("ec.blur.ccradialfast", &[("amount", n(40.0))]);
effect_case("ec.blur.ccradialfast", &[("amount", n(70.0)), ("zoom", e(1)), ("center", pt(22.4, 16.1))]);
effect_case("ec.blur.ccradialfast", &[("amount", n(25.0)), ("zoom", e(2))]);
}
#[test]
fn camera_lens_blur() {
effect_case("ec.blur.cameralens", &[("blurRadius", n(5.0))]);
effect_case(
"ec.blur.cameralens",
&[
("blurRadius", n(7.5)),
("irisProperties/irisShape", e(2)),
("irisProperties/irisRotation", n(20.0)),
("irisProperties/irisAspectRatio", n(1.6)),
("repeatEdge", b(true)),
],
);
effect_case(
"ec.blur.cameralens",
&[
("blurRadius", n(4.0)),
("irisProperties/irisShape", e(8)),
("highlight/specularBrightness", n(60.0)),
("highlight/specularThreshold", n(150.0)),
("highlight/highlightSaturation", n(40.0)),
("useLinear", b(true)),
],
);
// Large iris: per-row prefix sums.
effect_case("ec.blur.cameralens", &[("blurRadius", n(14.0)), ("irisProperties/irisRoundness", n(60.0))]);
effect_case("ec.blur.cameralens", &[("blurRadius", n(12.0)), ("repeatEdge", b(true)), ("useLinear", b(true))]);
}
#[test]
fn wipes() {
effect_case("ec.transition.linearwipe", &[("completion", n(40.0)), ("angle", n(30.0)), ("feather", n(12.0))]);
effect_case("ec.transition.linearwipe", &[("completion", n(55.0)), ("angle", n(-120.0))]);
effect_case("ec.transition.linearwipe", &[("completion", n(20.0)), ("angle", n(90.0)), ("feather", n(3.0))]);
effect_case("ec.transition.venetian", &[("completion", n(35.0)), ("direction", n(30.0)), ("width", n(11.0)), ("feather", n(2.0))]);
effect_case("ec.transition.venetian", &[("completion", n(60.0)), ("width", n(17.0))]);
effect_case("ec.transition.venetian", &[("completion", n(25.0)), ("direction", n(-70.0)), ("width", n(9.0)), ("feather", n(0.5))]);
effect_case("ec.transition.radialwipe", &[("completion", n(30.0)), ("startAngle", n(25.0)), ("feather", n(10.0))]);
effect_case("ec.transition.radialwipe", &[("completion", n(45.0)), ("wipe", e(1)), ("center", pt(30.2, 19.6))]);
effect_case("ec.transition.radialwipe", &[("completion", n(60.0)), ("wipe", e(2)), ("startAngle", n(-40.0)), ("feather", n(3.0))]);
effect_case("ec.transition.radialwipe", &[("completion", n(0.0)), ("feather", n(20.0))]);
effect_case("ec.transition.gradientwipe", &[("completion", n(40.0))]);
effect_case("ec.transition.gradientwipe", &[("completion", n(55.0)), ("softness", n(30.0)), ("invert", b(true))]);
effect_case("ec.transition.gradientwipe", &[("completion", n(100.0))]);
}
/// Gradient Wipe driven by another layer (fetched through the effect host, placed on the CPU).
#[test]
fn gradient_wipe_with_a_gradient_layer() {
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
for placement in 0..3 {
let mut s = Scene::new(depth);
let bg = s.solid([0.15, 0.1, 0.2], 97, 61);
s.push(bg);
let grad = s.footage(40, 30);
let gid = s.push(grad);
let mut l = s.footage(70, 44);
s.effect(
&mut l,
"ec.transition.gradientwipe",
&[("completion", n(45.0)), ("softness", n(20.0)), ("gradientLayer", Value::Layer(Some(gid.0))), ("gradientPlacement", e(placement))],
);
set(&mut l, "transform/rotation", Value::Scalar(8.0));
s.push(l);
let label = format!("gradient wipe layer {placement} {depth:?}");
check(&label, compare_at(&s, opts(), Tick::ZERO), 0.0);
check(&format!("{label} half"), compare_at(&s, RenderOpts { scale: 0.5, ..opts() }, Tick::ZERO), 0.0);
}
}
}
#[test]
fn generators() {
effect_case("ec.generate.cellpattern", &[("size", n(14.0))]);
effect_case("ec.generate.cellpattern", &[("cellPattern", e(2)), ("size", n(11.0)), ("evolution", n(130.0)), ("overflow", e(1)), ("contrast", n(150.0))]);
effect_case(
"ec.generate.cellpattern",
&[
("cellPattern", e(11)),
("size", n(9.0)),
("tilingOptions/enableTiling", b(true)),
("tilingOptions/cellsHorizontal", n(4.0)),
("tilingOptions/cellsVertical", n(3.0)),
("overflow", e(2)),
("contrast", n(250.0)),
("invert", b(true)),
("evolutionOptions/randomSeed", n(17.0)),
],
);
effect_case("ec.generate.cellpattern", &[("cellPattern", e(5)), ("size", n(16.0)), ("disperse", n(0.6)), ("offset", pt(3.3, -7.1))]);
effect_case("ec.generate.checkerboard", &[("width", n(9.0))]);
effect_case(
"ec.generate.checkerboard",
&[
("sizeFrom", e(2)),
("width", n(13.0)),
("height", n(7.0)),
("feather/featherWidth", n(4.0)),
("anchor", pt(5.3, 2.2)),
("blendingMode", e(5)),
("opacity", n(70.0)),
],
);
effect_case("ec.generate.checkerboard", &[("sizeFrom", e(0)), ("corner", pt(16.0, 11.0)), ("color", c(0.9, 0.3, 0.1)), ("blendingMode", e(1))]);
effect_case("ec.generate.grid", &[("width", n(12.0)), ("border", n(3.0))]);
effect_case(
"ec.generate.grid",
&[
("sizeFrom", e(2)),
("width", n(15.0)),
("height", n(9.0)),
("border", n(2.5)),
("feather/featherWidth", n(3.0)),
("feather/featherHeight", n(2.0)),
("invertGrid", b(true)),
("blendingMode", e(4)),
],
);
effect_case(
"ec.generate.grid",
&[("sizeFrom", e(0)), ("corner", pt(11.0, 8.0)), ("anchor", pt(2.5, 3.5)), ("blendingMode", e(14)), ("color", c(0.2, 0.8, 0.4))],
);
effect_case("ec.generate.fourcolor", &[]);
effect_case("ec.generate.fourcolor", &[("blend", n(40.0)), ("jitter", n(50.0)), ("opacity", n(80.0)), ("blendingMode", e(3))]);
effect_case("ec.generate.fourcolor", &[("blend", n(600.0)), ("blendingMode", e(16)), ("positionsColors/point2", pt(30.0, 20.0))]);
}
#[test]
fn noise_and_grain() {
effect_case("ec.noise.noise", &[("amount", n(30.0))]);
effect_case("ec.noise.noise", &[("amount", n(50.0)), ("color", b(false))]);
effect_case("ec.noise.noise", &[("amount", n(40.0)), ("clip", b(false))]);
effect_case("ec.noise.addgrain", &[("tweaking/intensity", n(2.0))]);
effect_case(
"ec.noise.addgrain",
&[
("tweaking/intensity", n(3.0)),
("tweaking/size", n(2.5)),
("tweaking/softness", n(1.5)),
("tweaking/aspectRatio", n(1.4)),
("tweaking/channelSize/redSize", n(0.6)),
("color/saturation", n(0.5)),
("color/tintAmount", n(0.4)),
("color/tintColor", c(1.0, 0.6, 0.2)),
("application/blendingMode", e(4)),
],
);
effect_case(
"ec.noise.addgrain",
&[
("tweaking/intensity", n(1.5)),
("color/monochromatic", b(true)),
("application/blendingMode", e(3)),
("application/shadows", n(0.3)),
("application/highlights", n(1.8)),
("application/midpoint", n(0.35)),
("animation/randomSeed", n(42.0)),
("animation/animationSpeed", n(2.0)),
],
);
effect_case(
"ec.noise.addgrain",
&[("tweaking/intensity", n(2.0)), ("application/blendingMode", e(1)), ("tweaking/channelIntensities/blueIntensity", n(2.5))],
);
}
+14 -14
View File
@@ -5,7 +5,7 @@
EffectCraft ships 303 effects with 2434 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**: 24 effects also run on the GPU compositor with identical results.
- **GPU**: 37 effects also run on the GPU compositor with identical results.
- **32**: 303 effects process 32-bit float (HDR, overbright) pixels without clamping.
- **Status**: 261 implemented in full, 42 partial (what is missing is listed).
@@ -49,16 +49,16 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
| Bilateral Blur | `ec.blur.bilateral` | 3 | | 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 | | 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` | 10 | | 32 | Partial: no Diffraction Fringe or Blur Map layer |
| Camera Lens Blur | `ec.blur.cameralens` | 10 | GPU | 32 | Partial: no Diffraction Fringe or Blur Map layer |
| 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 |
| 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 | | 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 |
@@ -187,7 +187,7 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
| Effect | Id | Params | GPU | 32 | Status |
|---|---|---:|:-:|:-:|---|
| 4-Color Gradient | `ec.generate.fourcolor` | 12 | | 32 | Implemented |
| 4-Color Gradient | `ec.generate.fourcolor` | 12 | GPU | 32 | Implemented |
| Advanced Lightning | `ec.generate.advancedlightning` | 16 | | 32 | Partial: no Alpha Obstacle; most Expert Settings missing |
| Audio Spectrum | `ec.generate.audiospectrum` | 23 | | 32 | Implemented |
| Audio Waveform | `ec.generate.audiowaveform` | 17 | | 32 | Implemented |
@@ -197,15 +197,15 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
| CC Light Rays | `ec.generate.cclightrays` | 8 | | 32 | Implemented |
| CC Light Sweep | `ec.generate.cclightsweep` | 9 | | 32 | Implemented |
| CC Threads | `ec.generate.ccthreads` | 8 | | 32 | Implemented |
| Cell Pattern | `ec.generate.cellpattern` | 14 | | 32 | Partial: HQ variants render like the standard ones |
| Checkerboard | `ec.generate.checkerboard` | 10 | | 32 | Implemented |
| Cell Pattern | `ec.generate.cellpattern` | 14 | GPU | 32 | Partial: HQ variants render like the standard ones |
| Checkerboard | `ec.generate.checkerboard` | 10 | GPU | 32 | Implemented |
| Circle | `ec.generate.circle` | 11 | | 32 | Implemented |
| Ellipse | `ec.generate.ellipse` | 8 | | 32 | Implemented |
| Eyedropper Fill | `ec.generate.eyedropperfill` | 5 | | 32 | Implemented |
| Fill | `ec.generate.fill` | 3 | GPU | 32 | Partial: no Fill Mask / All Masks / feather |
| Fractal | `ec.generate.fractal` | 21 | | 32 | Implemented |
| Gradient Ramp | `ec.generate.gradientramp` | 7 | GPU | 32 | Implemented |
| Grid | `ec.generate.grid` | 12 | | 32 | Implemented |
| Grid | `ec.generate.grid` | 12 | GPU | 32 | Implemented |
| Lens Flare | `ec.generate.lensflare` | 4 | | 32 | Implemented |
| Paint Bucket | `ec.generate.paintbucket` | 11 | | 32 | Implemented |
| Radio Waves | `ec.generate.radiowaves` | 17 | | 32 | Partial: no Image Contours or Mask wave types, Parameters Are Set At, reflection or stroke profile |
@@ -263,14 +263,14 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
| Effect | Id | Params | GPU | 32 | Status |
|---|---|---:|:-:|:-:|---|
| Add Grain | `ec.noise.addgrain` | 22 | | 32 | Partial: no preview region, presets, channel balance or temporal controls |
| Add Grain | `ec.noise.addgrain` | 22 | GPU | 32 | Partial: no preview region, presets, channel balance or temporal controls |
| Curl Noise | `ec.noise.curlnoise` | 10 | | 32 | Implemented |
| Dust & Scratches | `ec.noise.dustscratches` | 3 | | 32 | Implemented |
| Fractal Noise | `ec.noise.fractal` | 26 | GPU | 32 | Implemented |
| Match Grain | `ec.noise.matchgrain` | 25 | | 32 | Partial: no preview region, presets, sampling or temporal controls |
| Median | `ec.noise.median` | 2 | | 32 | Implemented |
| Median (Legacy) | `ec.noise.medianlegacy` | 2 | | 32 | Implemented |
| Noise | `ec.noise.noise` | 3 | | 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 |
@@ -401,11 +401,11 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
| CC Twister | `ec.transition.cctwister` | 5 | | 32 | Implemented |
| CC WarpoMatic | `ec.transition.ccwarpomatic` | 8 | | 32 | Implemented |
| Card Wipe | `ec.transition.cardwipe` | 12 | | 32 | Partial: 2D flip only; no back layer picker, camera, lighting or jitter |
| Gradient Wipe | `ec.transition.gradientwipe` | 5 | | 32 | Implemented |
| Gradient Wipe | `ec.transition.gradientwipe` | 5 | GPU | 32 | Implemented |
| Iris Wipe | `ec.transition.iriswipe` | 7 | | 32 | Implemented |
| Linear Wipe | `ec.transition.linearwipe` | 3 | | 32 | Implemented |
| Radial Wipe | `ec.transition.radialwipe` | 5 | | 32 | Implemented |
| Venetian Blinds | `ec.transition.venetian` | 4 | | 32 | Implemented |
| Linear Wipe | `ec.transition.linearwipe` | 3 | GPU | 32 | Implemented |
| Radial Wipe | `ec.transition.radialwipe` | 5 | GPU | 32 | Implemented |
| Venetian Blinds | `ec.transition.venetian` | 4 | GPU | 32 | Implemented |
## Utility