mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
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:
1 parent
9a6389db86
commit
c8623b1864
11 files changed
+1275
-32
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -149,3 +149,5 @@ mod tests_3d;
|
||||
mod tests_adjust;
|
||||
#[cfg(test)]
|
||||
mod tests_adv3d;
|
||||
#[cfg(test)]
|
||||
mod tests_fx_generate;
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
Reference in new issue
Block a user