mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-15 19:24:45 +00:00
Blend goes up to 10000 (it keeps its minimum of 1, below After Effects' 5) and Jitter up to 500 %, After Effects' recorded valid ranges in the same units. Defaults and sliders are unchanged. The CPU and GPU renderers capped Jitter's dither at 100 %, so higher values would be stored but render as 100. Both now take it from one function, four_color_jitter, which keeps the dither linear up to 500 %. Blend 10000 is a finite softening (Blend squared) at the same cost. Jitter was written as "percent" in keyframe text because its range was 0..100; a UNITS row keeps that unit.
436 lines
18 KiB
Rust
436 lines
18 KiB
Rust
//! 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_color::BlendMode;
|
||
use effectcraft_effects::{EffectCtx, GrainLook};
|
||
|
||
use crate::context::{Enc, Params};
|
||
use crate::effects::{GBuf, gaussian_blur};
|
||
|
||
/// Compute entry points in `fx_generate.wgsl`.
|
||
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] = &[
|
||
"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> {
|
||
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.scratch(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).
|
||
pub(crate) 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, mut 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);
|
||
}
|
||
if !ctx.adjustment {
|
||
b.pad(e, effectcraft_effects::radial_fast_pad(b.to_px(ctx.params.v2("center")), b.img.width, b.img.height, s))?;
|
||
}
|
||
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 = effectcraft_effects::camera_lens_radius(ctx, 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.scratch(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(), crate::context::KernelImages { src: &src, aux: None, out: &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.scratch(w, h);
|
||
e.pixels("gen_lens_blur", &p, &src, pre.as_ref(), &out, Some(&buf));
|
||
b.img = out;
|
||
Some(b)
|
||
}
|
||
|
||
// ---------------------------------------------------------------- transitions
|
||
|
||
/// Linear Wipe (`effects::misc::linear_wipe`): the hard matte at `lo + completion·(hi − lo)`
|
||
/// blurred by Feather with the layer's edges repeated, or the legacy linear ramp.
|
||
fn linear_wipe(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
||
let done = ctx.params.f("completion") / 100.0;
|
||
let (w, h) = (b.img.width as f64, b.img.height as f64);
|
||
let ([dx, dy], lo, hi) = effectcraft_effects::linear_wipe_axis(ctx.params.f("angle"), w, h);
|
||
// Per column ((x + 0.5)·dx − edge), per row ((y + 0.5)·dy), as (hi, lo) pairs.
|
||
let table = |edge: f64| {
|
||
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));
|
||
}
|
||
data
|
||
};
|
||
let params = |width: f64, mode: u32| {
|
||
let mut p = Params::default();
|
||
p.u[0][0] = 2 * b.img.width;
|
||
p.u[0][1] = mode;
|
||
p.f[0][0] = width as f32;
|
||
p
|
||
};
|
||
if ctx.params.b("legacyFeather") {
|
||
let feather = (ctx.params.f("feather") * b.scale).max(0.001);
|
||
let data = table(lo - feather + (hi - lo + 2.0 * feather) * done);
|
||
return run(e, "gen_linear_wipe", ¶ms(feather, 0), b, None, Some(&data));
|
||
}
|
||
if done.is_nan() || done <= 0.0 {
|
||
return Some(b);
|
||
}
|
||
// 100 % clears the layer whatever the angle (the CPU clears before it reads it).
|
||
if done >= 1.0 {
|
||
b.img = e.image(b.img.width, b.img.height);
|
||
return Some(b);
|
||
}
|
||
let edge = lo + (hi - lo) * done;
|
||
if !(edge.is_finite() && dx.is_finite() && dy.is_finite()) {
|
||
return Some(b);
|
||
}
|
||
let data = table(edge);
|
||
let sigma = effectcraft_effects::linear_wipe_sigma(ctx.params.f("feather"), b.scale, w, h);
|
||
if sigma <= 1e-3 {
|
||
return run(e, "gen_linear_wipe", ¶ms(1.0, 0), b, None, Some(&data));
|
||
}
|
||
let buf = e.data(&data);
|
||
let matte = e.scratch(b.img.width, b.img.height);
|
||
e.pixels("gen_linear_wipe", ¶ms(1.0, 1), &b.img, None, &matte, Some(&buf));
|
||
let blurred = gaussian_blur(e, &matte, sigma, sigma, true);
|
||
run(e, "gen_linear_wipe", ¶ms(1.0, 2), b, Some(&blurred), Some(&data))
|
||
}
|
||
|
||
fn venetian(e: &mut Enc, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||
let done = (ctx.params.f("completion") / 100.0).clamp(0.0, 1.0);
|
||
if done <= 0.0 {
|
||
return Some(b);
|
||
}
|
||
let ang = ctx.params.f("direction").to_radians();
|
||
let width = (ctx.params.f("width") * b.scale).max(1.0);
|
||
let sigma = ctx.params.f("feather").max(0.0) * b.scale / 2.0;
|
||
// The CPU's even veil and term count (effects::venetian_cover).
|
||
if sigma > 8.0 * width {
|
||
return None;
|
||
}
|
||
let (dx, dy) = (ang.cos(), ang.sin());
|
||
let (ox, oy) = b.to_px(effectcraft_effects::venetian_origin(ctx.layer_size));
|
||
let mut data = Vec::with_capacity((4 * b.img.width + 2 * b.img.height) as usize);
|
||
for x in 0..b.img.width {
|
||
let cx = ((x as f64 + 0.5 - ox) * dx).rem_euclid(width);
|
||
data.extend(hl(cx));
|
||
data.extend(hl(cx - width));
|
||
}
|
||
for y in 0..b.img.height {
|
||
data.extend(hl(((y as f64 + 0.5 - oy) * dy).rem_euclid(width)));
|
||
}
|
||
let mut p = Params::default();
|
||
p.u[0][0] = 4 * b.img.width;
|
||
p.u[0][1] = if sigma < 1e-3 { 0 } else { ((4.0 * sigma / width).ceil() as u32 + 1).clamp(1, 40) };
|
||
p.f[0] = [width as f32, (done * width) as f32, sigma as f32, 0.0];
|
||
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).clamp(0.0, 1.0);
|
||
if done <= 0.0 {
|
||
return Some(b);
|
||
}
|
||
let start = ctx.params.f("startAngle");
|
||
let c = b.to_px(ctx.params.v2("center"));
|
||
let (w0, span) = effectcraft_effects::radial_wipe_wedge(ctx.params.e("wipe"), done);
|
||
let sigma = effectcraft_effects::radial_wipe_sigma(ctx.params.f("feather")) * b.scale;
|
||
let ray0 = start + w0;
|
||
let (s0, c0) = ray0.to_radians().sin_cos();
|
||
let (s1, c1) = (ray0 + span).to_radians().sin_cos();
|
||
let mut p = Params::default();
|
||
p.u[0] = [(done >= 1.0) as u32, (sigma < 1e-3) as u32, 0, 0];
|
||
p.f[0] = [c.0 as f32, c.1 as f32, sigma.max(1e-3) as f32, ray0.rem_euclid(360.0) as f32];
|
||
p.f[1] = [s0 as f32, c0 as f32, s1 as f32, c1 as f32];
|
||
p.f[2][0] = span as f32;
|
||
run(e, "gen_radial_wipe", &p, b, None, None)
|
||
}
|
||
|
||
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.scratch(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 k = effectcraft_effects::four_color_softening(ctx.params.f("blend"), b.scale);
|
||
let jitter = effectcraft_effects::four_color_jitter(ctx.params.f("jitter"));
|
||
p.u[0] = [gen_mode(ctx.params.e("blendingMode")), ctx.seed, 0, 0];
|
||
p.f[8] = [k 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)
|
||
}
|