228 lines
11 KiB
Rust
228 lines
11 KiB
Rust
//! GPU noise, blur and time family effects vs the CPU effects (the oracle): direct on a buffer at full and half
|
|
//! resolution, as adjustment, and composited at 8 and 32 bpc.
|
|
|
|
use effectcraft_color::Label;
|
|
use effectcraft_keyframe::{Keyframe, Value};
|
|
use effectcraft_project::build;
|
|
use effectcraft_project::{BitDepth, Comp, ItemKind, LayerSource};
|
|
use effectcraft_render::RenderOpts;
|
|
use effectcraft_time::{FrameRate, Tick};
|
|
|
|
use crate::tests::{Scene, c, check, compare_at, effect_case, n, opts, set};
|
|
|
|
fn e(v: u32) -> Value {
|
|
Value::Enum(v)
|
|
}
|
|
|
|
fn on() -> Value {
|
|
Value::Bool(true)
|
|
}
|
|
|
|
fn pt(x: f64, y: f64) -> Value {
|
|
Value::Vec2([x, y])
|
|
}
|
|
|
|
#[test]
|
|
fn medians() {
|
|
effect_case("ec.noise.median", &[("radius", n(2.0))]);
|
|
effect_case("ec.noise.median", &[("radius", n(5.0)), ("operateOnAlpha", on())]);
|
|
effect_case("ec.noise.medianlegacy", &[("radius", n(3.0))]);
|
|
effect_case("ec.noise.medianlegacy", &[("radius", n(2.0)), ("operateOnAlphaChannel", on())]);
|
|
effect_case("ec.noise.dustscratches", &[("radius", n(3.0)), ("threshold", n(10.0))]);
|
|
effect_case("ec.noise.dustscratches", &[("radius", n(2.0)), ("threshold", n(0.0)), ("operateOnAlpha", on())]);
|
|
}
|
|
|
|
#[test]
|
|
fn large_radius_medians() {
|
|
// Past the bisection radius: the sliding-histogram kernel (windows larger than the layer).
|
|
effect_case("ec.noise.median", &[("radius", n(9.0))]);
|
|
effect_case("ec.noise.median", &[("radius", n(24.0)), ("operateOnAlpha", on())]);
|
|
effect_case("ec.noise.medianlegacy", &[("radius", n(17.0))]);
|
|
effect_case("ec.noise.medianlegacy", &[("radius", n(40.0)), ("operateOnAlphaChannel", on())]);
|
|
effect_case("ec.noise.dustscratches", &[("radius", n(20.0)), ("threshold", n(10.0))]);
|
|
effect_case("ec.noise.dustscratches", &[("radius", n(60.0)), ("threshold", n(0.0)), ("operateOnAlpha", on())]);
|
|
}
|
|
|
|
#[test]
|
|
fn edge_preserving_blurs() {
|
|
effect_case("ec.blur.bilateral", &[]);
|
|
effect_case("ec.blur.bilateral", &[("radius", n(9.0)), ("threshold", n(30.0)), ("colorize", Value::Bool(false))]);
|
|
effect_case("ec.blur.bilateral", &[("radius", n(14.0)), ("threshold", n(5.0))]);
|
|
effect_case("ec.blur.smart", &[]);
|
|
effect_case("ec.blur.smart", &[("radius", n(6.0)), ("threshold", n(15.0)), ("mode", e(1))]);
|
|
effect_case("ec.blur.smart", &[("radius", n(4.0)), ("threshold", n(40.0)), ("mode", e(2))]);
|
|
}
|
|
|
|
#[test]
|
|
fn sharpen_and_unsharp_mask() {
|
|
effect_case("ec.blur.sharpen", &[("amount", n(60.0))]);
|
|
effect_case("ec.blur.sharpen", &[("amount", n(250.0))]);
|
|
effect_case("ec.blur.unsharp", &[("amount", n(120.0)), ("radius", n(4.0))]);
|
|
effect_case("ec.blur.unsharp", &[("amount", n(80.0)), ("radius", n(2.5)), ("threshold", n(12.0))]);
|
|
}
|
|
|
|
#[test]
|
|
fn channel_and_compound_blur() {
|
|
effect_case("ec.blur.channel", &[("redBlurriness", n(8.0)), ("blueBlurriness", n(3.0))]);
|
|
effect_case("ec.blur.channel", &[("greenBlurriness", n(6.0)), ("alphaBlurriness", n(6.0)), ("repeatEdge", on())]);
|
|
effect_case("ec.blur.channel", &[("redBlurriness", n(5.0)), ("alphaBlurriness", n(10.0)), ("dimensions", e(1))]);
|
|
effect_case("ec.blur.compound", &[]);
|
|
effect_case("ec.blur.compound", &[("maximumBlur", n(12.0)), ("invertBlur", on())]);
|
|
}
|
|
|
|
/// Compound Blur reading another layer (stretched and centred), composited.
|
|
#[test]
|
|
fn compound_blur_layer() {
|
|
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
|
|
for stretch in [true, false] {
|
|
let mut s = Scene::new(depth);
|
|
let bg = s.solid([0.15, 0.1, 0.2], 97, 61);
|
|
s.push(bg);
|
|
let mut map = s.footage(50, 36);
|
|
map.switches.video = false;
|
|
let mid = s.push(map);
|
|
let mut l = s.footage(70, 44);
|
|
s.effect(
|
|
&mut l,
|
|
"ec.blur.compound",
|
|
&[("blurLayer", Value::Layer(Some(mid.0))), ("maximumBlur", n(10.0)), ("stretchMapToFit", Value::Bool(stretch))],
|
|
);
|
|
set(&mut l, "transform/rotation", Value::Scalar(8.0));
|
|
s.push(l);
|
|
let label = format!("compound blur layer {stretch} {depth:?}");
|
|
check(&label, compare_at(&s, opts(), Tick::ZERO), 0.0);
|
|
check(&format!("{label} half"), compare_at(&s, RenderOpts { scale: 0.5, ..opts() }, Tick::ZERO), 0.0);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn minimax() {
|
|
effect_case("ec.channel.minimax", &[("radius", n(3.0))]);
|
|
effect_case("ec.channel.minimax", &[("radius", n(2.0)), ("operation", e(0)), ("channel", e(2))]);
|
|
effect_case("ec.channel.minimax", &[("radius", n(4.0)), ("operation", e(2)), ("channel", e(1)), ("direction", e(1))]);
|
|
effect_case("ec.channel.minimax", &[("radius", n(3.0)), ("operation", e(3)), ("channel", e(4)), ("dontShrinkEdges", on()), ("direction", e(2))]);
|
|
}
|
|
|
|
#[test]
|
|
fn turbulent_noise() {
|
|
effect_case("ec.noise.turbulent", &[]);
|
|
effect_case(
|
|
"ec.noise.turbulent",
|
|
&[("fractalType", e(3)), ("noiseType", e(3)), ("evolution", n(120.0)), ("evolutionOptions/turbulenceFactor", n(1.5)), ("blendingMode", e(5))],
|
|
);
|
|
effect_case(
|
|
"ec.noise.turbulent",
|
|
&[("fractalType", e(5)), ("noiseType", e(1)), ("complexity", n(3.5)), ("transform/scale", n(40.0)), ("overflow", e(1)), ("blendingMode", e(0))],
|
|
);
|
|
effect_case("ec.noise.turbulent", &[("fractalType", e(7)), ("noiseType", e(0)), ("transform/perspectiveOffset", on()), ("opacity", n(70.0))]);
|
|
}
|
|
|
|
#[test]
|
|
fn fractal_noise_other_modes() {
|
|
effect_case("ec.noise.fractal", &[("noiseType", e(3)), ("fractalType", e(4)), ("blendingMode", e(3))]);
|
|
effect_case(
|
|
"ec.noise.fractal",
|
|
&[
|
|
("fractalType", e(6)),
|
|
("evolutionOptions/cycleEvolution", on()),
|
|
("evolutionOptions/cycle", n(2.0)),
|
|
("evolution", n(500.0)),
|
|
("subSettings/subRotation", n(25.0)),
|
|
("subSettings/subOffset", pt(4.0, -3.0)),
|
|
],
|
|
);
|
|
effect_case("ec.noise.fractal", &[("fractalType", e(8)), ("noiseType", e(1)), ("blendingMode", e(12)), ("contrast", n(160.0)), ("invert", on())]);
|
|
}
|
|
|
|
#[test]
|
|
fn noise_alpha_and_hls() {
|
|
effect_case("ec.noise.noisealpha", &[("amount", n(40.0))]);
|
|
effect_case("ec.noise.noisealpha", &[("amount", n(70.0)), ("noise", e(3)), ("originalAlpha", e(2)), ("overflow", e(1)), ("noisePhase", n(100.0))]);
|
|
effect_case(
|
|
"ec.noise.noisealpha",
|
|
&[("amount", n(60.0)), ("noise", e(2)), ("originalAlpha", e(3)), ("overflow", e(2)), ("noiseOptions/cycleNoise", on()), ("randomSeed", n(9.0))],
|
|
);
|
|
effect_case("ec.noise.noisehls", &[("hue", n(30.0)), ("lightness", n(20.0)), ("saturation", n(40.0))]);
|
|
effect_case("ec.noise.noisehls", &[("noise", e(1)), ("lightness", n(50.0)), ("noisePhase", n(200.0))]);
|
|
effect_case("ec.noise.noisehls", &[("noise", e(2)), ("hue", n(60.0)), ("saturation", n(25.0)), ("grainSize", n(3.0))]);
|
|
effect_case("ec.noise.noisehlsauto", &[("noise", e(0)), ("hue", n(20.0)), ("lightness", n(30.0)), ("noiseAnimationSpeed", n(7.0))]);
|
|
}
|
|
|
|
#[test]
|
|
fn remove_grain() {
|
|
effect_case("ec.noise.removegrain", &[]);
|
|
effect_case(
|
|
"ec.noise.removegrain",
|
|
&[
|
|
("noiseReductionSettings/noiseReduction", n(2.5)),
|
|
("noiseReductionSettings/passes", n(2.0)),
|
|
("noiseReductionSettings/mode", e(1)),
|
|
("fineTuning/texture", n(0.3)),
|
|
],
|
|
);
|
|
effect_case("ec.noise.removegrain", &[("unsharpMask/amount", n(80.0)), ("unsharpMask/radius", n(2.0)), ("unsharpMask/threshold", n(4.0))]);
|
|
effect_case("ec.noise.removegrain", &[("viewingMode", e(0)), ("previewRegion/showBox", on()), ("previewRegion/boxColor", c(1.0, 0.2, 0.1))]);
|
|
}
|
|
|
|
/// Echo and Posterize Time fetch other frames through the effect host: a layer whose rotation
|
|
/// is keyframed, at a nonzero time (the direct cases have no host and pass the layer through).
|
|
#[test]
|
|
fn echo_and_posterize_time() {
|
|
effect_case("ec.time.echo", &[]);
|
|
effect_case("ec.time.posterizetime", &[]);
|
|
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
|
|
for (id, vals) in [
|
|
("ec.time.echo", vec![("numberOfEchoes", n(3.0)), ("decay", n(0.7)), ("echoTime", n(-0.1))]),
|
|
("ec.time.echo", vec![("numberOfEchoes", n(4.0)), ("echoOperator", e(2)), ("echoTime", n(0.05))]),
|
|
("ec.time.echo", vec![("numberOfEchoes", n(2.0)), ("echoOperator", e(4)), ("startingIntensity", n(0.8))]),
|
|
("ec.time.echo", vec![("numberOfEchoes", n(5.0)), ("echoOperator", e(6))]),
|
|
("ec.time.posterizetime", vec![("frameRate", n(4.0))]),
|
|
] {
|
|
let mut s = Scene::new(depth);
|
|
let bg = s.solid([0.15, 0.1, 0.2], 97, 61);
|
|
s.push(bg);
|
|
// A precomp whose content moves, so the frames differ.
|
|
let inner = Comp::new(60, 40, FrameRate::FPS_30, Tick::from_seconds_f64(2.0));
|
|
let inner_id = s.p.add_item("Inner", Label::Sandstone, None, ItemKind::Comp(inner.into()));
|
|
let mut a = s.footage(30, 24);
|
|
let pos = a.props.prop_mut("transform/position").unwrap();
|
|
pos.keys = vec![Keyframe::new(Tick::ZERO, crate::tests::v3(10.0, 12.0)), Keyframe::new(Tick::from_seconds_f64(1.0), crate::tests::v3(50.0, 30.0))];
|
|
let rot = a.props.prop_mut("transform/rotation").unwrap();
|
|
rot.keys = vec![Keyframe::new(Tick::ZERO, Value::Scalar(0.0)), Keyframe::new(Tick::from_seconds_f64(1.0), Value::Scalar(90.0))];
|
|
s.p.comp_mut(inner_id).unwrap().layers = vec![a];
|
|
let mut l = build::layer(&mut s.p, &s.comp, "Inner", LayerSource::Comp { item: inner_id }, (60, 40), None);
|
|
s.effect(&mut l, id, &vals);
|
|
set(&mut l, "transform/rotation", Value::Scalar(8.0));
|
|
s.push(l);
|
|
let t = Tick::from_seconds_f64(0.4);
|
|
let label = format!("{id} {vals:?} {depth:?}");
|
|
check(&label, compare_at(&s, opts(), t), 0.0);
|
|
check(&format!("{label} half"), compare_at(&s, RenderOpts { scale: 0.5, ..opts() }, t), 0.0);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn dust_threshold_on_quantized_input() {
|
|
let Some(g) = crate::tests::gpu() else { return };
|
|
let spec = effectcraft_effects::find("ec.noise.dustscratches").unwrap();
|
|
let size = [70.0, 44.0];
|
|
let mut params =
|
|
effectcraft_effects::Params { values: spec.params.iter().map(|p| (p.id.to_string(), effectcraft_effects::default_value(p, size))).collect() };
|
|
params.values.insert("radius".into(), n(3.0));
|
|
params.values.insert("threshold".into(), n(10.0));
|
|
let ctx = || effectcraft_effects::EffectCtx { params: ¶ms, time: 0.25, layer_size: size, seed: 11, adjustment: false, env: Default::default() };
|
|
for seed in 1..32 {
|
|
let mut img = crate::tests::pattern(seed, 70, 44);
|
|
effectcraft_render::color::quantize(&mut img, 255.0);
|
|
let buf = effectcraft_effects::Buf { img, offset: [0.0; 2], scale: 1.0 };
|
|
let cpu = (spec.render)(&ctx(), buf.clone());
|
|
let gpu = effectcraft_render::Accelerator::effects(g, &[effectcraft_render::FxStep { spec, ctx: ctx() }], &buf, None).unwrap();
|
|
for (i, (a, b)) in cpu.img.data.iter().zip(&gpu.img.data).enumerate() {
|
|
let d = a.iter().zip(b).map(|(x, y)| (x - y).abs()).fold(0.0_f32, f32::max);
|
|
assert!(d < 1e-6, "seed {seed} pixel {i} input {:?} CPU {a:?} GPU {b:?}", buf.img.data[i]);
|
|
}
|
|
}
|
|
}
|