M9.13: Transform shutter-angle motion blur, Glow Operation / Arbitrary Map, Cartoon Edge Enhancement
- Transform: Use Composition's Shutter Angle / Shutter Angle, sampling the transform across the shutter via EffectHost::params_at (EffectEnv::shutter carries the comp shutter when motion blur is on); the GPU path defers to the CPU when blurring. - Glow: Glow Operation (blend modes) and Glow Colors > Arbitrary Map. - Cartoon: Advanced > Edge Enhancement (shock-filter sharpening / softening). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
69202c7b3e
commit
3a7c9c5f61
|
|
@ -11,7 +11,6 @@ pub const PARTIAL: &[(&str, &str)] = &[
|
|||
("ec.blur.cameralens", "no Diffraction Fringe or Blur Map layer"),
|
||||
("ec.color.colorama", "no Add Phase, editable Output Cycle, Modify, Pixel Selection or Masking"),
|
||||
("ec.color.selectivecolor", "simplified Colors / Details layout"),
|
||||
("ec.distort.transform", "no shutter-angle motion blur"),
|
||||
("ec.distort.reshape", "no correspondence points"),
|
||||
("ec.distort.rollingshutterrepair", "Pixel Motion Detail has no effect"),
|
||||
("ec.generate.advancedlightning", "no Alpha Obstacle; most Expert Settings missing"),
|
||||
|
|
@ -28,8 +27,6 @@ pub const PARTIAL: &[(&str, &str)] = &[
|
|||
("ec.vr.converter", "common layouts only"),
|
||||
("ec.sim.caustics", "no Sky group or light type"),
|
||||
("ec.sim.waveworld", "no wireframe controls, dry-area rendering or ground group"),
|
||||
("ec.stylize.glow", "no Glow Operation or arbitrary colour map"),
|
||||
("ec.stylize.cartoon", "no Edge Enhancement"),
|
||||
];
|
||||
|
||||
/// Whether Effect Controls shows parameter (or twirl-down group) `param` (spec id path,
|
||||
|
|
|
|||
|
|
@ -257,6 +257,32 @@ fn polar(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
b
|
||||
}
|
||||
|
||||
/// Transform's shutter: (angle, phase) in degrees and the number of samples, or None when it
|
||||
/// renders without motion blur. Use Composition's Shutter Angle takes the comp's shutter
|
||||
/// (only when the comp and layer motion-blur switches are on); otherwise Shutter Angle, centred
|
||||
/// on the frame.
|
||||
pub fn transform_shutter(ctx: &EffectCtx) -> Option<(f64, f64, u32)> {
|
||||
let (angle, phase, n) = if ctx.params.b("useCompositionShutterAngle") {
|
||||
ctx.env.shutter?
|
||||
} else {
|
||||
let a = ctx.params.f("shutterAngle").clamp(0.0, 720.0);
|
||||
(a, -a * 0.5, 16)
|
||||
};
|
||||
(angle > 0.0).then_some((angle, phase, n.clamp(2, 64)))
|
||||
}
|
||||
|
||||
fn transform_matrix(pr: &crate::Params, b: &Buf) -> Mat3 {
|
||||
let anchor = b.to_px(pr.v2("anchor"));
|
||||
let pos = b.to_px(pr.v2("position"));
|
||||
let sh = pr.f("scaleHeight");
|
||||
let sw = if pr.b("uniform") { sh } else { pr.f("scaleWidth") };
|
||||
Mat3::translate(vec2(pos.0, pos.1))
|
||||
* Mat3::rotate_deg(pr.f("rotation"))
|
||||
* Mat3::skew_deg(pr.f("skew"), pr.f("skewAxis"))
|
||||
* Mat3::scale(vec2(sw / 100.0, sh / 100.0))
|
||||
* Mat3::translate(vec2(-anchor.0, -anchor.1))
|
||||
}
|
||||
|
||||
fn transform(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let anchor = b.to_px(ctx.params.v2("anchor"));
|
||||
let pos = b.to_px(ctx.params.v2("position"));
|
||||
|
|
@ -268,6 +294,34 @@ fn transform(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
let rot = ctx.params.f("rotation");
|
||||
let opacity = ctx.params.f("opacity") as f32 / 100.0;
|
||||
let sampling = if ctx.params.e("sampling") == 1 { effectcraft_raster::Sampling::Bicubic } else { effectcraft_raster::Sampling::Bilinear };
|
||||
// Motion blur: the transform at Samples instants across the shutter, averaged (the
|
||||
// transform at other times comes from the host; without one the frame renders sharp).
|
||||
if let Some((angle, phase, n)) = transform_shutter(ctx)
|
||||
&& let Some(host) = ctx.env.host
|
||||
{
|
||||
let fd = 1.0 / ctx.fps();
|
||||
let mut acc = Image::new(b.img.width, b.img.height);
|
||||
let mut count = 0.0f32;
|
||||
for k in 0..n {
|
||||
let t = ctx.time + (phase + angle * k as f64 / (n - 1) as f64) / 360.0 * fd;
|
||||
let Some(pr) = host.params_at(t) else { continue };
|
||||
let mut one = Image::new(b.img.width, b.img.height);
|
||||
let op = pr.f("opacity") as f32 / 100.0;
|
||||
effectcraft_raster::composite_warp(
|
||||
&mut one,
|
||||
&b.img,
|
||||
&transform_matrix(&pr, &b),
|
||||
&effectcraft_raster::WarpOpts { opacity: op, sampling, ..Default::default() },
|
||||
);
|
||||
acc.data.iter_mut().zip(&one.data).for_each(|(a, o)| (0..4).for_each(|c| a[c] += o[c]));
|
||||
count += 1.0;
|
||||
}
|
||||
if count > 0.0 {
|
||||
acc.data.iter_mut().for_each(|p| p.iter_mut().for_each(|v| *v /= count));
|
||||
b.img = acc;
|
||||
return b;
|
||||
}
|
||||
}
|
||||
let m = Mat3::translate(vec2(pos.0, pos.1))
|
||||
* Mat3::rotate_deg(rot)
|
||||
* Mat3::skew_deg(skew, skew_axis)
|
||||
|
|
@ -397,6 +451,8 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
p("skewAxis", "Skew Axis", num(0.0), ParamUi::Angle),
|
||||
p("rotation", "Rotation", num(0.0), ParamUi::Angle),
|
||||
p("opacity", "Opacity", num(100.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
p("useCompositionShutterAngle", "Use Composition's Shutter Angle", Value::Bool(true), ParamUi::Checkbox),
|
||||
p("shutterAngle", "Shutter Angle", num(0.0), slider(0.0, 720.0, 0.0, 360.0, 1)),
|
||||
p("sampling", "Sampling", Value::Enum(0), popup(&["Bilinear", "Bicubic"])),
|
||||
],
|
||||
transform,
|
||||
|
|
@ -510,4 +566,44 @@ mod tests {
|
|||
let b = run("ec.distort.transform", &[("rotation", num(17.0)), ("sampling", Value::Enum(1))], im);
|
||||
assert!(a.img.data.iter().zip(&b.img.data).any(|(p, q)| differs(*p, *q)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transform_shutter_angle_motion_blur() {
|
||||
/// Position moves right 200 px per second.
|
||||
struct Moving;
|
||||
impl crate::EffectHost for Moving {
|
||||
fn layer(&self, _: u64, _: bool) -> Option<crate::LayerPixels> {
|
||||
None
|
||||
}
|
||||
fn audio(&self, _: u64, _: f64, _: usize, _: u32) -> Option<Vec<f32>> {
|
||||
None
|
||||
}
|
||||
fn params_at(&self, t: f64) -> Option<Params> {
|
||||
let s = crate::find("ec.distort.transform").unwrap();
|
||||
let mut p = Params { values: s.params.iter().map(|p| (p.id.to_string(), crate::default_value(p, [40.0, 20.0]))).collect() };
|
||||
p.values.insert("position".into(), Value::Vec2([20.0 + 200.0 * t, 10.0]));
|
||||
Some(p)
|
||||
}
|
||||
}
|
||||
let mut sq = Image::new(40, 20);
|
||||
for y in 5..15 {
|
||||
for x in 15..25 {
|
||||
sq.set(x, y, [1.0, 1.0, 1.0, 1.0]);
|
||||
}
|
||||
}
|
||||
let render = |vals: &[(&str, Value)], shutter: Option<(f64, f64, u32)>| {
|
||||
let env = crate::EffectEnv { host: Some(&Moving), frame_rate: 10.0, shutter, ..Default::default() };
|
||||
crate::run_fx("ec.distort.transform", vals, sq.clone(), 0.0, env).img
|
||||
};
|
||||
let partial = |i: &Image| i.data.iter().filter(|p| p[3] > 0.02 && p[3] < 0.98).count();
|
||||
// Use Composition Shutter Angle with the comp / layer motion blur off: sharp.
|
||||
assert_eq!(partial(&render(&[], None)), 0);
|
||||
// With the comp shutter (180°): smeared along x over 10 px.
|
||||
let comp = render(&[], Some((180.0, -90.0, 16)));
|
||||
assert!(partial(&comp) > 50, "{}", partial(&comp));
|
||||
// Own Shutter Angle.
|
||||
let own = render(&[("useCompositionShutterAngle", Value::Bool(false)), ("shutterAngle", num(360.0))], None);
|
||||
assert!(partial(&own) > partial(&comp));
|
||||
assert_eq!(partial(&render(&[("useCompositionShutterAngle", Value::Bool(false))], None)), 0, "0 deg: sharp");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,11 +66,12 @@ pub use color_fx::{
|
|||
HUESAT_CHANNELS, LEVELS_CHANNELS, exposure_settings, fill_uses_masks, huesat_ranges_identity, levels_channel_ids, levels_channels_identity, levels_clip,
|
||||
};
|
||||
pub use color2::Curve;
|
||||
pub use distort::transform_shutter;
|
||||
use effectcraft_keyframe::Value;
|
||||
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};
|
||||
pub use misc::{INVERT_ALPHA, INVERT_CHANNELS, glow_operation};
|
||||
|
||||
/// Effect categories in Effects & Presets order.
|
||||
pub const CATEGORIES: &[&str] = &[
|
||||
|
|
@ -292,6 +293,10 @@ pub struct EffectEnv<'a> {
|
|||
pub working_space: Option<effectcraft_color::ColorSpace>,
|
||||
/// Working-space pixels are linear light (Linearize Working Space).
|
||||
pub working_linear: bool,
|
||||
/// The composition's shutter (angle and phase in degrees, samples per frame) when both the
|
||||
/// comp's and the layer's motion blur switches are on (Transform's Use Composition's
|
||||
/// Shutter Angle).
|
||||
pub shutter: Option<(f64, f64, u32)>,
|
||||
}
|
||||
|
||||
/// What an effect gets to render with.
|
||||
|
|
@ -708,6 +713,9 @@ pub const TIME_DEPENDENT: &[&str] = &[
|
|||
"ec.matte.refinehard",
|
||||
// Card Wipe's position / rotation jitter moves with time.
|
||||
"ec.transition.cardwipe",
|
||||
// Transform's motion blur and Radio Waves' birth parameters read the parameters at
|
||||
// other times.
|
||||
"ec.distort.transform",
|
||||
// Temporal Smoothing reads neighbouring frames.
|
||||
"ec.color.autolevels",
|
||||
"ec.color.autocontrast",
|
||||
|
|
|
|||
|
|
@ -120,12 +120,48 @@ pub(crate) fn glow_ab_t(l: f32, looping: u32, loops: f32, phase: f32, mid: f32)
|
|||
t.max(0.0).powf(0.5f32.ln() / m.ln())
|
||||
}
|
||||
|
||||
/// Glow Operation options (blend modes the glow is applied with).
|
||||
pub const GLOW_OPERATIONS: [&str; 17] = [
|
||||
"Normal",
|
||||
"Add",
|
||||
"Multiply",
|
||||
"Screen",
|
||||
"Overlay",
|
||||
"Soft Light",
|
||||
"Hard Light",
|
||||
"Color Dodge",
|
||||
"Color Burn",
|
||||
"Darken",
|
||||
"Lighten",
|
||||
"Difference",
|
||||
"Exclusion",
|
||||
"Hue",
|
||||
"Saturation",
|
||||
"Color",
|
||||
"Luminosity",
|
||||
];
|
||||
|
||||
/// Glow's Glow Operation as a blend mode (Add for projects saved before it existed).
|
||||
pub fn glow_operation(ctx: &EffectCtx) -> effectcraft_color::BlendMode {
|
||||
let name = GLOW_OPERATIONS.get(ctx.params.get("glowOperation").map(|v| v.as_enum()).unwrap_or(1) as usize).copied().unwrap_or("Add");
|
||||
effectcraft_color::BlendMode::ALL.iter().copied().find(|m| m.label() == name).unwrap_or(effectcraft_color::BlendMode::Add)
|
||||
}
|
||||
|
||||
/// Glow's Arbitrary Map: red, green and blue curves of the glow brightness, as Curves point
|
||||
/// lists `x,y …` separated by `|` (an empty or missing curve is the identity).
|
||||
fn glow_map(s: &str) -> Vec<Option<crate::Curve>> {
|
||||
let mut parts = s.split('|');
|
||||
(0..3).map(|_| parts.next().and_then(crate::Curve::parse)).collect()
|
||||
}
|
||||
|
||||
fn glow(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let thr = ctx.params.f("threshold") as f32 / 100.0;
|
||||
let radius = ctx.params.f("radius") * b.scale;
|
||||
let intensity = ctx.params.f("intensity") as f32;
|
||||
let alpha_based = ctx.params.e("based") == 0;
|
||||
let use_colors = ctx.params.e("colors") == 1;
|
||||
let arbitrary = (ctx.params.e("colors") == 2).then(|| glow_map(ctx.params.s("arbitraryMap")));
|
||||
let glow_op = glow_operation(ctx);
|
||||
let looping = ctx.params.e("colorLooping");
|
||||
let loops = ctx.params.f("colorLoops") as f32;
|
||||
let phase = (ctx.params.f("colorPhase") / 360.0) as f32;
|
||||
|
|
@ -147,7 +183,11 @@ fn glow(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
// Glow Based On: brightness from the colours, or from the alpha channel.
|
||||
let l = if alpha_based { a } else { luminance(px[0] / a, px[1] / a, px[2] / a) };
|
||||
let k = ((l - thr) / (1.0 - thr).max(1e-3)).clamp(0.0, 1.0);
|
||||
if use_colors {
|
||||
if let Some(map) = &arbitrary {
|
||||
let t = glow_ab_t(l, looping, loops, phase, mid);
|
||||
let c = map.iter().map(|cv| cv.as_ref().map_or(t, |cv| cv.eval(t))).collect::<Vec<f32>>();
|
||||
*px = [c[0] * k * a, c[1] * k * a, c[2] * k * a, k * a];
|
||||
} else if use_colors {
|
||||
let t = glow_ab_t(l, looping, loops, phase, mid);
|
||||
let c = [ca[0] + (cb[0] - ca[0]) * t, ca[1] + (cb[1] - ca[1]) * t, ca[2] + (cb[2] - ca[2]) * t];
|
||||
*px = [c[0] * k * a, c[1] * k * a, c[2] * k * a, k * a];
|
||||
|
|
@ -170,6 +210,8 @@ fn glow(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
}
|
||||
// None: the glow alone.
|
||||
2 => *o = [g[0], g[1], g[2], g[3]],
|
||||
// On Top with another Glow Operation: that blend mode.
|
||||
_ if glow_op != effectcraft_color::BlendMode::Add => *o = effectcraft_color::blend_pixel(glow_op, *o, g, 0.0),
|
||||
// On Top: add (screen-like add on premultiplied colour; alpha grows by the glow's alpha).
|
||||
_ => {
|
||||
for c in 0..3 {
|
||||
|
|
@ -571,7 +613,10 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
p("radius", "Glow Radius", num(10.0), slider(0.0, 1000.0, 0.0, 100.0, 1)),
|
||||
p("intensity", "Glow Intensity", num(1.0), slider(0.0, 255.0, 0.0, 4.0, 1)),
|
||||
p("operation", "Composite Original", Value::Enum(0), popup(&["On Top", "Behind", "None"])),
|
||||
p("colors", "Glow Colors", Value::Enum(0), popup(&["Original Colors", "A & B Colors"])),
|
||||
p("glowOperation", "Glow Operation", Value::Enum(1), popup(&GLOW_OPERATIONS)),
|
||||
p("colors", "Glow Colors", Value::Enum(0), popup(&["Original Colors", "A & B Colors", "Arbitrary Map"])),
|
||||
// Arbitrary Map: "red points | green points | blue points" (Curves format).
|
||||
p("arbitraryMap", "Arbitrary Map", Value::Str(String::new()), ParamUi::Hidden),
|
||||
p("colorLooping", "Color Looping", Value::Enum(2), popup(&["Sawtooth A>B", "Sawtooth B>A", "Triangle A>B>A", "Triangle B>A>B"])),
|
||||
p("colorLoops", "Color Loops", num(1.0), slider(1.0, 127.0, 1.0, 10.0, 1)),
|
||||
p("colorPhase", "Color Phase", num(0.0), ParamUi::Angle),
|
||||
|
|
@ -687,3 +732,52 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
),
|
||||
]
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{EffectEnv, run_fx};
|
||||
|
||||
/// A bright square on mid grey.
|
||||
fn scene() -> effectcraft_raster::Image {
|
||||
let mut img = effectcraft_raster::Image::filled(24, 24, [0.3, 0.3, 0.3, 1.0]);
|
||||
for y in 8..16 {
|
||||
for x in 8..16 {
|
||||
img.set(x, y, [1.0, 1.0, 1.0, 1.0]);
|
||||
}
|
||||
}
|
||||
img
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn glow_operation_and_arbitrary_map() {
|
||||
// Pixels addressed in layer space (Glow pads its buffer).
|
||||
struct Out(Buf);
|
||||
impl Out {
|
||||
fn get(&self, x: i64, y: i64) -> [f32; 4] {
|
||||
self.0.img.get(x + self.0.offset[0] as i64, y + self.0.offset[1] as i64)
|
||||
}
|
||||
}
|
||||
let run = |vals: &[(&str, Value)]| Out(run_fx("ec.stylize.glow", vals, scene(), 0.0, EffectEnv::default()));
|
||||
let add = run(&[]);
|
||||
assert_eq!(GLOW_OPERATIONS[1], "Add");
|
||||
// Multiply darkens around the square where Add brightens.
|
||||
let mul = run(&[("glowOperation", Value::Enum(2))]);
|
||||
assert!(add.get(6, 12)[0] > 0.31 && mul.get(6, 12)[0] <= 0.3 + 1e-4, "{:?} {:?}", add.get(6, 12), mul.get(6, 12));
|
||||
assert_eq!(
|
||||
glow_operation(&EffectCtx {
|
||||
params: &crate::Params::default(),
|
||||
time: 0.0,
|
||||
layer_size: [1.0; 2],
|
||||
seed: 0,
|
||||
adjustment: false,
|
||||
env: Default::default()
|
||||
}),
|
||||
effectcraft_color::BlendMode::Add
|
||||
);
|
||||
// Arbitrary Map: the glow takes the mapped colour (here pure red for every brightness).
|
||||
let red = run(&[("colors", Value::Enum(2)), ("arbitraryMap", Value::Str("0,1 1,1|0,0 1,0|0,0 1,0".into())), ("operation", Value::Enum(2))]);
|
||||
let p = red.get(6, 12);
|
||||
assert!(p[0] > 0.05 && p[1] < 1e-4 && p[2] < 1e-4, "{p:?}");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,6 +61,41 @@ fn cartoon(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
|||
} else {
|
||||
b.img.clone()
|
||||
};
|
||||
// Advanced ▸ Edge Enhancement: one shock-filter step. Each pixel samples from the side of
|
||||
// the nearest edge it belongs to (along the luma gradient, by the sign of the Laplacian):
|
||||
// positive values sharpen the transitions, negative ones soften them.
|
||||
let enh = ctx.params.f("advanced/edgeEnhancement") / 100.0;
|
||||
let smoothed = if enh < 0.0 {
|
||||
// Softening: blend towards a blur of the picture.
|
||||
let s = 1.5 * b.scale;
|
||||
let blur = effectcraft_raster::gaussian_blur(&smoothed, s, s, true);
|
||||
let k = (-enh) as f32;
|
||||
let mut out = smoothed;
|
||||
out.data.par_iter_mut().zip(blur.data.par_iter()).for_each(|(p, q)| (0..4).for_each(|c| p[c] += (q[c] - p[c]) * k));
|
||||
out
|
||||
} else if enh > 0.0 {
|
||||
let l = Plane::luma(&smoothed);
|
||||
let (w, h) = (l.w as i64, l.h as i64);
|
||||
let s = enh * 2.0 * b.scale;
|
||||
let src = smoothed.clone();
|
||||
let mut out = smoothed;
|
||||
out.data.par_iter_mut().enumerate().for_each(|(i, px)| {
|
||||
let (x, y) = ((i as i64) % w, (i as i64) / w);
|
||||
let g = |dx: i64, dy: i64| l.get_clamped(x + dx, y + dy);
|
||||
let (gx, gy) = ((g(1, 0) - g(-1, 0)) * 0.5, (g(0, 1) - g(0, -1)) * 0.5);
|
||||
let lap = g(1, 0) + g(-1, 0) + g(0, 1) + g(0, -1) - 4.0 * g(0, 0);
|
||||
let m = (gx * gx + gy * gy).sqrt();
|
||||
if m < 1e-4 || lap.abs() < 1e-5 || y >= h {
|
||||
return;
|
||||
}
|
||||
let k = -lap.signum() as f64 * s / m as f64;
|
||||
let (sx, sy) = (x as f64 + 0.5 + gx as f64 * k, y as f64 + 0.5 + gy as f64 * k);
|
||||
*px = src.sample_bilinear_clamped(sx, sy);
|
||||
});
|
||||
out
|
||||
} else {
|
||||
smoothed
|
||||
};
|
||||
let luma = Plane::luma(&smoothed);
|
||||
let edges = if render >= 1 {
|
||||
let mag = sobel(&luma);
|
||||
|
|
@ -543,6 +578,7 @@ pub fn specs() -> Vec<EffectSpec> {
|
|||
p("edge/edgeWidth", "Width", num(1.5), slider(0.0, 10.0, 0.0, 5.0, 2)),
|
||||
p("edge/edgeSoftness", "Softness", num(60.0), pct()),
|
||||
p("edge/edgeOpacity", "Opacity", num(100.0), pct()),
|
||||
p("advanced/edgeEnhancement", "Edge Enhancement", num(0.0), slider(-100.0, 100.0, -100.0, 100.0, 1)),
|
||||
p("advanced/edgeBlackLevel", "Edge Black Level", num(0.0), slider(0.0, 100.0, 0.0, 100.0, 1)),
|
||||
p("advanced/edgeContrast", "Edge Contrast", num(0.5), slider(0.0, 1.0, 0.0, 1.0, 2)),
|
||||
],
|
||||
|
|
@ -993,4 +1029,30 @@ mod tests {
|
|||
assert_eq!(cyc(0.0), cyc(360.0));
|
||||
assert_ne!(cyc(0.0), cyc(180.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cartoon_edge_enhancement_sharpens_or_softens() {
|
||||
// A soft 8-pixel ramp from black to white.
|
||||
let mut img = Image::new(32, 4);
|
||||
for y in 0..4 {
|
||||
for x in 0..32 {
|
||||
let v = 0.5 + 0.5 * ((x as f32 - 15.5) / 4.0).tanh();
|
||||
img.set(x, y, [v, v, v, 1.0]);
|
||||
}
|
||||
}
|
||||
let fill = |e: f64| {
|
||||
let v = [
|
||||
("render", Value::Enum(0)),
|
||||
("detailRadius", num(0.0)),
|
||||
("fill/shadingSteps", num(64.0)),
|
||||
("fill/shadingSmoothness", num(100.0)),
|
||||
("advanced/edgeEnhancement", num(e)),
|
||||
];
|
||||
run_at("ec.stylize.cartoon", &v, img.clone(), 0.0).img
|
||||
};
|
||||
let steep = |o: &Image| (0..31).map(|x| (o.get(x + 1, 2)[0] - o.get(x, 2)[0]).abs()).fold(0.0f32, f32::max);
|
||||
let (base, sharp, soft) = (fill(0.0), fill(100.0), fill(-100.0));
|
||||
assert!(steep(&sharp) > steep(&base) + 0.02, "{} vs {}", steep(&sharp), steep(&base));
|
||||
assert!(steep(&soft) < steep(&base) + 1e-4, "{} vs {}", steep(&soft), steep(&base));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -173,6 +173,10 @@ fn directional(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
|||
}
|
||||
|
||||
fn glow(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
||||
// Other Glow Operations and the Arbitrary Map render on the CPU.
|
||||
if ctx.params.e("colors") == 2 || effectcraft_effects::glow_operation(ctx) != effectcraft_color::BlendMode::Add {
|
||||
return None;
|
||||
}
|
||||
let thr = ctx.params.f("threshold") as f32 / 100.0;
|
||||
let radius = ctx.params.f("radius") * b.scale;
|
||||
let intensity = ctx.params.f("intensity") as f32;
|
||||
|
|
@ -231,6 +235,10 @@ fn drop_shadow(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
|||
}
|
||||
|
||||
fn transform(e: &mut Enc, ctx: &EffectCtx, mut b: GBuf) -> Option<GBuf> {
|
||||
// Motion blur (shutter angle) renders on the CPU.
|
||||
if ctx.env.host.is_some() && effectcraft_effects::transform_shutter(ctx).is_some() {
|
||||
return None;
|
||||
}
|
||||
let anchor = b.to_px(ctx.params.v2("anchor"));
|
||||
let pos = b.to_px(ctx.params.v2("position"));
|
||||
let sh = ctx.params.f("scaleHeight");
|
||||
|
|
|
|||
|
|
@ -653,6 +653,11 @@ impl<'a> Renderer<'a> {
|
|||
effect_index: 0,
|
||||
working_space: self.pipe.space,
|
||||
working_linear: self.pipe.linear,
|
||||
shutter: (ctx.comp.enable_motion_blur && layer.switches.motion_blur).then_some((
|
||||
ctx.comp.shutter_angle,
|
||||
ctx.comp.shutter_phase,
|
||||
ctx.comp.motion_blur_samples,
|
||||
)),
|
||||
};
|
||||
// Video effects in stack order (index, group, spec); disabled and audio effects skipped.
|
||||
let stack: Vec<(usize, &effectcraft_project::PropGroup, &'static effectcraft_effects::EffectSpec)> = fx
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@
|
|||
<!-- Generated from the effect registry (crates/effects/src/catalog.rs); do not edit by hand.
|
||||
Regenerate: UPDATE_DOCS=1 cargo test -p effectcraft-effects --lib effects_doc_is_current -->
|
||||
|
||||
EffectCraft ships 303 effects with 2746 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.
|
||||
EffectCraft ships 303 effects with 2751 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.
|
||||
- **32**: 303 effects process 32-bit float (HDR, overbright) pixels without clamping.
|
||||
- **Status**: 281 implemented in full, 22 partial (what is missing is listed).
|
||||
- **Status**: 284 implemented in full, 19 partial (what is missing is listed).
|
||||
|
||||
Effects are addressed by id (`ec.<category>.<name>`) or display name in commands, scripts and the control channel, and every parameter by its id (`effects/#1/blurriness`).
|
||||
|
||||
|
|
@ -162,7 +162,7 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
|||
| Rolling Shutter Repair | `ec.distort.rollingshutterrepair` | 5 | | 32 | Partial: Pixel Motion Detail has no effect |
|
||||
| Smear | `ec.distort.smear` | 8 | | 32 | Implemented |
|
||||
| Spherize | `ec.distort.spherize` | 2 | | 32 | Implemented |
|
||||
| Transform | `ec.distort.transform` | 10 | GPU | 32 | Partial: no shutter-angle motion blur |
|
||||
| Transform | `ec.distort.transform` | 12 | GPU | 32 | Implemented |
|
||||
| Turbulent Displace | `ec.distort.turbulentdisplace` | 11 | | 32 | Implemented |
|
||||
| Twirl | `ec.distort.twirl` | 3 | | 32 | Implemented |
|
||||
| Twirl (Legacy) | `ec.distort.twirllegacy` | 3 | | 32 | Implemented |
|
||||
|
|
@ -351,11 +351,11 @@ Effects are addressed by id (`ec.<category>.<name>`) or display name in commands
|
|||
| CC Threshold | `ec.stylize.ccthreshold` | 4 | | 32 | Implemented |
|
||||
| CC Threshold RGB | `ec.stylize.ccthresholdrgb` | 7 | | 32 | Implemented |
|
||||
| CC Vignette | `ec.stylize.ccvignette` | 4 | | 32 | Implemented |
|
||||
| Cartoon | `ec.stylize.cartoon` | 11 | | 32 | Partial: no Edge Enhancement |
|
||||
| Cartoon | `ec.stylize.cartoon` | 12 | | 32 | Implemented |
|
||||
| Color Emboss | `ec.stylize.coloremboss` | 4 | | 32 | Implemented |
|
||||
| Emboss | `ec.stylize.emboss` | 4 | | 32 | Implemented |
|
||||
| Find Edges | `ec.stylize.findedges` | 2 | | 32 | Implemented |
|
||||
| Glow | `ec.stylize.glow` | 13 | GPU | 32 | Partial: no Glow Operation or arbitrary colour map |
|
||||
| Glow | `ec.stylize.glow` | 14 | GPU | 32 | Implemented |
|
||||
| Mosaic | `ec.stylize.mosaic` | 3 | | 32 | Implemented |
|
||||
| Motion Tile | `ec.stylize.motiontile` | 8 | | 32 | Implemented |
|
||||
| Posterize | `ec.stylize.posterize` | 1 | | 32 | Implemented |
|
||||
|
|
|
|||
Loading…
Reference in New Issue