effectcraft/crates/gpu/src/tests_fx_key.rs

252 lines
13 KiB
Rust

//! GPU keying, matte and channel 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_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 on() -> Value {
Value::Bool(true)
}
fn off() -> Value {
Value::Bool(false)
}
/// [`effect_case`] for hard thresholds (8-bit quantisation, bitwise operators): the composited
/// renders may flip a few pixels sitting on the threshold after the layer transform's float
/// rounding, so up to 0.1 % of them may differ by any amount.
fn discontinuous_case(id: &str, vals: &[(&str, Value)]) {
crate::tests::effect_direct(id, vals, false);
crate::tests::effect_direct(id, vals, true);
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
let mut s = Scene::new(depth);
let bg = s.solid([0.15, 0.1, 0.2], 97, 61);
s.push(bg);
let mut l = s.footage(70, 44);
s.effect(&mut l, id, vals);
set(&mut l, "transform/rotation", Value::Scalar(8.0));
s.push(l);
check(&format!("{id} {vals:?} {depth:?}"), compare_at(&s, opts(), Tick::ZERO), 0.001);
check(&format!("{id} {vals:?} {depth:?} half"), compare_at(&s, RenderOpts { scale: 0.5, ..opts() }, Tick::ZERO), 0.001);
}
}
#[test]
fn colour_luma_range_extract_keys() {
effect_case("ec.key.colorkey", &[("keyColor", c(0.7, 0.6, 0.5)), ("colorTolerance", n(60.0))]);
effect_case("ec.key.colorkey", &[("keyColor", c(0.5, 0.6, 0.5)), ("colorTolerance", n(70.0)), ("edgeThin", n(1.5)), ("edgeFeather", n(4.0))]);
effect_case("ec.key.colorkey", &[("keyColor", c(0.5, 0.6, 0.5)), ("colorTolerance", n(70.0)), ("edgeThin", n(-2.0))]);
effect_case("ec.key.luma", &[("threshold", n(120.0)), ("tolerance", n(40.0))]);
effect_case("ec.key.luma", &[("keyType", e(0)), ("threshold", n(140.0)), ("tolerance", n(30.0)), ("edgeFeather", n(3.0))]);
effect_case("ec.key.luma", &[("keyType", e(2)), ("threshold", n(128.0)), ("tolerance", n(25.0)), ("edgeThin", n(1.0))]);
effect_case("ec.key.luma", &[("keyType", e(3)), ("threshold", n(100.0)), ("tolerance", n(50.0))]);
effect_case("ec.key.colorrange", &[("fuzziness", n(20.0))]);
effect_case("ec.key.colorrange", &[("colorSpace", e(1)), ("minL", n(60.0)), ("maxA", n(150.0)), ("minB", n(90.0)), ("fuzziness", n(15.0))]);
effect_case("ec.key.colorrange", &[("colorSpace", e(2)), ("minL", n(80.0)), ("maxA", n(180.0)), ("minB", n(40.0)), ("fuzziness", n(30.0))]);
effect_case("ec.key.extract", &[("blackPoint", n(60.0)), ("whitePoint", n(200.0)), ("blackSoftness", n(30.0)), ("whiteSoftness", n(20.0))]);
discontinuous_case("ec.key.extract", &[("channel", e(1)), ("blackPoint", n(90.0)), ("invert", on())]);
effect_case("ec.key.extract", &[("channel", e(4)), ("whitePoint", n(200.0)), ("whiteSoftness", n(40.0))]);
}
#[test]
fn difference_colour_difference_and_screen_keys() {
effect_case("ec.key.differencematte", &[]);
effect_case("ec.key.differencematte", &[("view", e(2)), ("matchingTolerance", n(0.0)), ("blurBeforeDifference", n(3.0))]);
effect_case("ec.key.colordifference", &[("keyColor", c(0.2, 0.3, 0.8))]);
effect_case(
"ec.key.colordifference",
&[
("keyColor", c(0.3, 0.8, 0.2)),
("colorMatchingAccuracy", e(1)),
("partialAInBlack", n(20.0)),
("partialAGamma", n(1.4)),
("partialBOutWhite", n(200.0)),
("blackLevel", n(10.0)),
("gamma", n(0.8)),
],
);
for view in [1, 2, 3, 4, 5, 6, 8] {
effect_case("ec.key.colordifference", &[("keyColor", c(0.2, 0.3, 0.8)), ("view", e(view)), ("partialBInWhite", n(180.0))]);
}
effect_case("ec.key.screen", &[("screenColor", c(0.4, 0.75, 0.3))]);
effect_case(
"ec.key.screen",
&[
("screenColor", c(0.3, 0.4, 0.8)),
("screenGain", n(130.0)),
("screenBalance", n(30.0)),
("despill", n(60.0)),
("clipBlack", n(10.0)),
("clipWhite", n(85.0)),
("screenPreblur", n(2.0)),
("screenShrinkGrow", n(-1.5)),
("screenSoftness", n(3.0)),
],
);
effect_case("ec.key.screen", &[("screenColor", c(0.8, 0.4, 0.3)), ("screenShrinkGrow", n(2.0)), ("edgeColor", e(1))]);
for view in [1, 2] {
effect_case("ec.key.screen", &[("screenColor", c(0.4, 0.75, 0.3)), ("view", e(view))]);
}
}
#[test]
fn spill_unmult_and_key_cleaner() {
effect_case("ec.key.spill", &[("colorToSuppress", c(0.2, 0.9, 0.1))]);
effect_case("ec.key.spill", &[("colorToSuppress", c(0.9, 0.8, 0.1)), ("colorAccuracy", e(1)), ("suppression", n(150.0))]);
effect_case("ec.key.advancedspill", &[]);
effect_case("ec.key.advancedspill", &[("suppression", n(80.0)), ("ultraSettings/spillRange", n(20.0)), ("ultraSettings/lumaCorrection", n(50.0))]);
effect_case(
"ec.key.advancedspill",
&[
("method", e(1)),
("ultraSettings/keyColor", c(0.9, 0.2, 0.3)),
("ultraSettings/tolerance", n(40.0)),
("ultraSettings/desaturate", n(50.0)),
("ultraSettings/spillColorCorrection", n(30.0)),
],
);
effect_case("ec.key.unmult", &[]);
effect_case("ec.key.unmult", &[("backgroundColor", e(1)), ("whiteLevel", n(90.0)), ("softness", n(40.0)), ("clipHdrResults", off())]);
effect_case("ec.key.unmult", &[("blackLevel", n(30.0)), ("softness", n(0.0)), ("removeColorMatting", off())]);
effect_case("ec.key.keycleaner", &[]);
effect_case("ec.key.keycleaner", &[("additionalEdgeRadius", n(6.0)), ("reduceChatter", on()), ("alphaContrast", n(250.0)), ("strength", n(60.0))]);
}
#[test]
fn chokers() {
effect_case("ec.matte.simplechoker", &[("chokeMatte", n(1.5))]);
effect_case("ec.matte.simplechoker", &[("chokeMatte", n(-2.5))]);
effect_case("ec.matte.simplechoker", &[("chokeMatte", n(-1.0)), ("view", e(1))]);
effect_case("ec.matte.mattechoker", &[]);
effect_case(
"ec.matte.mattechoker",
&[
("geometricSoftness1", n(2.0)),
("choke1", n(-40.0)),
("geometricSoftness2", n(3.0)),
("choke2", n(30.0)),
("grayLevelSoftness2", n(40.0)),
("iterations", n(2.0)),
],
);
}
#[test]
fn refine_mattes() {
effect_case("ec.matte.refinesoft", &[]);
effect_case("ec.matte.refinesoft", &[("radius", n(4.0)), ("smooth", n(40.0)), ("feather", n(30.0)), ("contrast", n(20.0)), ("shiftEdge", n(-25.0))]);
effect_case("ec.matte.refinesoft", &[("calculateEdgeDetails", off()), ("smooth", n(50.0)), ("shiftEdge", n(30.0))]);
effect_case("ec.matte.refinesoft", &[("radius", n(3.0)), ("viewEdgeRegion", on())]);
effect_case("ec.matte.refinesoft", &[("decontamination/viewDecontaminationMap", on()), ("decontamination/extendWhereSmoothed", off())]);
effect_case("ec.matte.refinesoft", &[("decontamination/increaseDecontaminationRadius", n(4.0)), ("decontamination/decontaminationAmount", n(60.0))]);
effect_case("ec.matte.refinehard", &[]);
effect_case("ec.matte.refinehard", &[("choke", n(20.0)), ("feather", n(20.0)), ("decontaminate", off())]);
}
#[test]
fn channel_effects() {
effect_case("ec.channel.setchannels", &[("setRedTo", e(2)), ("setGreenTo", e(4)), ("setBlueTo", e(5)), ("setAlphaTo", e(6))]);
effect_case("ec.channel.shiftchannels", &[("takeRedFrom", e(3)), ("takeGreenFrom", e(7)), ("takeAlphaFrom", e(4))]);
effect_case("ec.channel.removecolormatting", &[("backgroundColor", c(0.3, 0.5, 0.7))]);
effect_case("ec.channel.removecolormatting", &[("backgroundColor", c(0.8, 0.2, 0.1)), ("clipHdr", off())]);
for op in [0, 2, 3, 4, 8, 10, 12] {
discontinuous_case("ec.channel.arithmetic", &[("operator", e(op)), ("redValue", n(120.0)), ("greenValue", n(60.0)), ("blueValue", n(200.0))]);
}
effect_case("ec.channel.arithmetic", &[("operator", e(3)), ("redValue", n(200.0)), ("clip", off())]);
for mode in [0, 2, 4, 9, 11] {
effect_case("ec.channel.solidcomposite", &[("blendingMode", e(mode)), ("color", c(0.8, 0.3, 0.2)), ("opacity", n(70.0)), ("sourceOpacity", n(80.0))]);
}
effect_case("ec.channel.setmatte", &[("takeMatteFrom", e(4))]);
effect_case("ec.channel.setmatte", &[("takeMatteFrom", e(3)), ("invertMatte", on())]);
effect_case("ec.channel.setmatte", &[("takeMatteFrom", e(1)), ("compositeMatteWithOriginal", off()), ("premultiplyMatteLayer", off())]);
for (from, to) in [(0, 0), (1, 0), (2, 0), (3, 0), (8, 4), (9, 5), (10, 6), (11, 3), (12, 8), (13, 7), (7, 1)] {
effect_case("ec.channel.combiner", &[("from", e(from)), ("to", e(to))]);
}
effect_case("ec.channel.combiner", &[("from", e(4)), ("to", e(2)), ("invert", on()), ("solidAlpha", on())]);
effect_case("ec.channel.blend", &[("mode", e(1)), ("blendWithOriginal", n(30.0))]);
effect_case("ec.channel.calculations", &[("inputChannel", e(1))]);
effect_case("ec.channel.calculations", &[("inputChannel", e(0)), ("invertInput", on())]);
effect_case("ec.channel.compoundarithmetic", &[("operator", e(3)), ("operateOnChannels", e(1))]);
}
/// The channel and keying effects that read another layer, composited.
#[test]
fn layer_inputs() {
let cases: Vec<(&str, Vec<(&str, Value)>)> = vec![
("ec.key.differencematte", vec![("matchingTolerance", n(10.0)), ("matchingSoftness", n(20.0))]),
("ec.key.differencematte", vec![("ifLayerSizesDiffer", e(1)), ("blurBeforeDifference", n(2.0)), ("view", e(2))]),
("ec.channel.setmatte", vec![("takeMatteFrom", e(4)), ("stretchMatteToFit", off())]),
("ec.channel.setmatte", vec![("takeMatteFrom", e(3))]),
("ec.channel.setchannels", vec![("setGreenTo", e(0)), ("setAlphaTo", e(4))]),
("ec.channel.combiner", vec![("useSecondLayer", on()), ("from", e(11)), ("to", e(7))]),
("ec.channel.blend", vec![("mode", e(0)), ("blendWithOriginal", n(40.0))]),
("ec.channel.blend", vec![("mode", e(2))]),
("ec.channel.blend", vec![("mode", e(3)), ("ifLayerSizesDiffer", e(1))]),
("ec.channel.blend", vec![("mode", e(4))]),
("ec.channel.calculations", vec![("secondLayerChannel", e(2)), ("blendingMode", e(2)), ("secondLayerOpacity", n(70.0))]),
("ec.channel.calculations", vec![("inputChannel", e(4)), ("blendingMode", e(10)), ("preserveTransparency", on()), ("invertSecondLayer", on())]),
("ec.channel.compoundarithmetic", vec![("operator", e(1)), ("overflowBehavior", e(2))]),
("ec.channel.compoundarithmetic", vec![("operator", e(4)), ("operateOnChannels", e(1)), ("blendWithOriginal", n(25.0))]),
("ec.channel.compoundarithmetic", vec![("operator", e(13)), ("operateOnChannels", e(2))]),
];
let layer_param = |id: &str| match id {
"ec.key.differencematte" => vec!["differenceLayer"],
"ec.channel.setmatte" => vec!["takeMatteFromLayer"],
"ec.channel.setchannels" => vec!["sourceLayer2", "sourceLayer4"],
"ec.channel.combiner" => vec!["sourceLayer"],
"ec.channel.blend" => vec!["blendWithLayer"],
"ec.channel.calculations" => vec!["secondLayer"],
_ => vec!["secondSource"],
};
for depth in [BitDepth::Bpc8, BitDepth::Bpc32] {
for (id, vals) in &cases {
let mut s = Scene::new(depth);
let bg = s.solid([0.15, 0.1, 0.2], 97, 61);
s.push(bg);
let mut other = s.footage(50, 36);
other.switches.video = false;
let oid = s.push(other);
let mut l = s.footage(70, 44);
let mut vals = vals.clone();
for p in layer_param(id) {
vals.push((p, Value::Layer(Some(oid.0))));
}
s.effect(&mut l, id, &vals);
set(&mut l, "transform/rotation", Value::Scalar(8.0));
s.push(l);
let label = format!("{id} {vals:?} {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 arithmetic_preserves_positive_subepsilon_alpha() {
let Some(g) = crate::tests::gpu() else { return };
let spec = effectcraft_effects::find("ec.channel.arithmetic").unwrap();
let size = [1.0, 1.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("operator".into(), Value::Enum(3));
for key in ["redValue", "greenValue", "blueValue"] {
params.values.insert(key.into(), n(0.0));
}
params.values.insert("clip".into(), Value::Bool(false));
let ctx = || effectcraft_effects::EffectCtx { params: &params, time: 0.0, layer_size: size, seed: 0, adjustment: false, env: Default::default() };
let img = effectcraft_raster::Image::filled(1, 1, [1.0, 0.0, 0.0, 5e-7]);
let buf = effectcraft_effects::Buf { img, offset: [0.0; 2], scale: 1.0 };
let cpu = (spec.render)(&ctx(), buf.clone());
let out = effectcraft_render::Accelerator::effects(g, &[effectcraft_render::FxStep { spec, ctx: ctx() }], &buf, None).unwrap();
assert_eq!(cpu.img.get(0, 0), [1.0, 0.0, 0.0, 5e-7]);
assert_eq!(out.img.get(0, 0), cpu.img.get(0, 0));
}