189 lines
8.9 KiB
Rust
189 lines
8.9 KiB
Rust
//! GPU warp family effects (Warp, Bezier Warp, CC Bend It, CC Page Turn, Smear, Reshape, Color
|
||
//! Emboss, Cartoon) 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_effects::MaskShape;
|
||
use effectcraft_keyframe::{ShapePath, Value};
|
||
use effectcraft_project::build::{self, Ids};
|
||
use effectcraft_project::{BitDepth, MaskMode};
|
||
use effectcraft_render::RenderOpts;
|
||
use effectcraft_time::Tick;
|
||
|
||
use crate::tests::{Scene, c, check, compare_at, diff, effect_case, gpu, n, opts, pattern};
|
||
|
||
fn e(v: u32) -> Value {
|
||
Value::Enum(v)
|
||
}
|
||
|
||
fn pt(x: f64, y: f64) -> Value {
|
||
Value::Vec2([x, y])
|
||
}
|
||
|
||
#[test]
|
||
fn warp_styles() {
|
||
for style in 0..15 {
|
||
effect_case("ec.distort.warp", &[("warpStyle", e(style)), ("bend", n(45.0))]);
|
||
}
|
||
effect_case("ec.distort.warp", &[("warpStyle", e(8)), ("warpAxis", e(1)), ("bend", n(-60.0)), ("horizontalDistortion", n(20.0))]);
|
||
effect_case("ec.distort.warp", &[("warpStyle", e(4)), ("bend", n(80.0)), ("verticalDistortion", n(-35.0))]);
|
||
effect_case("ec.distort.warp", &[("warpStyle", e(11)), ("bend", n(-50.0))]);
|
||
effect_case("ec.distort.warp", &[("warpStyle", e(14)), ("bend", n(-35.0))]);
|
||
// Strong Fisheye / Twist bends and either with a distortion: the CPU's f64 inverse map.
|
||
for (style, bend) in [(11, 90.0), (11, -100.0), (14, 75.0), (14, -100.0)] {
|
||
effect_case("ec.distort.warp", &[("warpStyle", e(style)), ("bend", n(bend))]);
|
||
}
|
||
effect_case("ec.distort.warp", &[("warpStyle", e(14)), ("verticalDistortion", n(-35.0))]);
|
||
effect_case("ec.distort.warp", &[("warpStyle", e(11)), ("warpAxis", e(1)), ("bend", n(60.0)), ("horizontalDistortion", n(30.0))]);
|
||
effect_case("ec.distort.warp", &[("warpStyle", e(3)), ("bend", n(0.0)), ("horizontalDistortion", n(-40.0)), ("verticalDistortion", n(25.0))]);
|
||
}
|
||
|
||
#[test]
|
||
fn bezier_warp() {
|
||
effect_case("ec.distort.bezierwarp", &[]);
|
||
effect_case(
|
||
"ec.distort.bezierwarp",
|
||
&[("topLeftVertex", pt(8.0, -4.0)), ("topLeftTangent", pt(30.0, 12.0)), ("rightBottomTangent", pt(80.0, 30.0)), ("leftBottomVertex", pt(-6.0, 50.0))],
|
||
);
|
||
effect_case("ec.distort.bezierwarp", &[("topRightTangent", pt(40.0, -20.0)), ("bottomLeftTangent", pt(20.0, 60.0)), ("quality", n(3.0))]);
|
||
}
|
||
|
||
#[test]
|
||
fn bend_it_and_page_turn() {
|
||
effect_case("ec.distort.ccbendit", &[("bend", n(45.0))]);
|
||
effect_case("ec.distort.ccbendit", &[("bend", n(-120.0)), ("start", pt(10.0, 30.0)), ("end", pt(55.0, 8.0)), ("renderPrestart", e(1))]);
|
||
// A slight bend: a large arc radius.
|
||
effect_case("ec.distort.ccbendit", &[("bend", n(0.05)), ("start", pt(5.0, 22.0)), ("end", pt(60.0, 22.0))]);
|
||
effect_case("ec.distort.ccpageturn", &[]);
|
||
effect_case("ec.distort.ccpageturn", &[("foldRadius", n(4.0)), ("foldDirection", n(30.0)), ("foldPosition", pt(40.0, 20.0))]);
|
||
effect_case(
|
||
"ec.distort.ccpageturn",
|
||
&[("render", e(2)), ("backPageOpacity", n(40.0)), ("paperColor", c(0.9, 0.85, 0.7)), ("lightDirection", n(120.0)), ("foldPosition", pt(30.0, 25.0))],
|
||
);
|
||
effect_case("ec.distort.ccpageturn", &[("render", e(1)), ("foldPosition", pt(50.0, 10.0))]);
|
||
}
|
||
|
||
#[test]
|
||
fn color_emboss_and_cartoon() {
|
||
effect_case("ec.stylize.coloremboss", &[]);
|
||
effect_case("ec.stylize.coloremboss", &[("direction", n(30.0)), ("relief", n(3.5)), ("contrast", n(250.0)), ("blend", n(30.0))]);
|
||
effect_case("ec.stylize.cartoon", &[]);
|
||
effect_case("ec.stylize.cartoon", &[("render", e(0)), ("detailRadius", n(4.0)), ("fill/shadingSteps", n(5.0)), ("fill/shadingSmoothness", n(0.0))]);
|
||
effect_case("ec.stylize.cartoon", &[("render", e(1)), ("edge/edgeWidth", n(0.6)), ("advanced/edgeBlackLevel", n(40.0)), ("advanced/edgeContrast", n(0.8))]);
|
||
effect_case("ec.stylize.cartoon", &[("edge/edgeWidth", n(3.3)), ("advanced/edgeEnhancement", n(60.0)), ("edge/edgeSoftness", n(20.0))]);
|
||
effect_case("ec.stylize.cartoon", &[("advanced/edgeEnhancement", n(-50.0)), ("detailRadius", n(0.0))]);
|
||
}
|
||
|
||
/// Mask outlines in layer pixels (70 × 44 layers): a source blob, a destination blob and a
|
||
/// boundary around both.
|
||
fn outlines() -> Vec<MaskShape> {
|
||
let ellipse = |cx: f64, cy: f64, rx: f64, ry: f64| -> Vec<[f64; 2]> {
|
||
(0..40).map(|i| i as f64 / 40.0 * std::f64::consts::TAU).map(|a| [cx + rx * a.cos(), cy + ry * a.sin()]).collect()
|
||
};
|
||
let shape = |name: &str, points| MaskShape { name: name.into(), points, closed: true, inverted: false };
|
||
vec![shape("Mask 1", ellipse(25.0, 20.0, 9.0, 7.0)), shape("Mask 2", ellipse(42.0, 24.0, 12.0, 6.0)), shape("Mask 3", ellipse(34.0, 22.0, 30.0, 19.0))]
|
||
}
|
||
|
||
/// The effect alone on a buffer with masks (`effect_direct` with the layer's masks).
|
||
fn direct_with_masks(id: &str, vals: &[(&str, Value)]) {
|
||
let Some(g) = gpu() else { return };
|
||
let spec = effectcraft_effects::find(id).unwrap();
|
||
let size = [70.0, 44.0];
|
||
let masks = outlines();
|
||
let mut params =
|
||
effectcraft_effects::Params { values: spec.params.iter().map(|ps| (ps.id.to_string(), effectcraft_effects::default_value(ps, size))).collect() };
|
||
for (k, v) in vals {
|
||
params.values.insert(k.to_string(), v.clone());
|
||
}
|
||
for adjustment in [false, true] {
|
||
for scale in [1.0, 0.5] {
|
||
let img = pattern(7, (size[0] * scale) as u32, (size[1] * scale) as u32);
|
||
let buf = effectcraft_effects::Buf { img, offset: [0.0, 0.0], scale };
|
||
let ctx = || effectcraft_effects::EffectCtx {
|
||
params: ¶ms,
|
||
time: 0.25,
|
||
layer_size: size,
|
||
seed: 11,
|
||
adjustment,
|
||
env: effectcraft_effects::EffectEnv { masks: &masks, ..Default::default() },
|
||
};
|
||
let cpu = (spec.render)(&ctx(), buf.clone());
|
||
let out =
|
||
effectcraft_render::Accelerator::effects(g, &[effectcraft_render::FxStep { spec, ctx: ctx() }], &buf, None).expect("the GPU runs the effect");
|
||
assert_eq!((out.offset, out.scale), (cpu.offset, cpu.scale), "{id}: geometry");
|
||
assert!(cpu.img.data != buf.img.data, "{id} {vals:?}: the case changes the picture");
|
||
let d = diff(&cpu.img, &out.img, 1.0 / 255.0);
|
||
// f32 point-in-polygon tests may flip a pixel exactly on an outline.
|
||
assert!(
|
||
d.over * 1000 <= d.total,
|
||
"{id} {vals:?} adj {adjustment} scale {scale}: {} of {} pixels over 1/255 (max {}); worst {:?}",
|
||
d.over,
|
||
d.total,
|
||
d.max,
|
||
d.worst
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Composited at 8 and 32 bpc, with the masks on the layer (mode None: they only feed the effect).
|
||
fn composite_with_masks(id: &str, vals: &[(&str, Value)]) {
|
||
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);
|
||
let mut next = s.p.next_id;
|
||
for m in outlines() {
|
||
let path = ShapePath {
|
||
vertices: m.points.clone(),
|
||
in_tangents: vec![[0.0; 2]; m.points.len()],
|
||
out_tangents: vec![[0.0; 2]; m.points.len()],
|
||
closed: true,
|
||
..Default::default()
|
||
};
|
||
let g = build::mask(&mut Ids(&mut next), &m.name, path, MaskMode::None, [255, 255, 0]);
|
||
l.props.sub_mut("masks").unwrap().children.push(g.into());
|
||
}
|
||
s.p.next_id = next;
|
||
s.effect(&mut l, id, vals);
|
||
s.push(l);
|
||
check(&format!("{id} {vals:?} {depth:?}"), compare_at(&s, opts(), Tick::ZERO), 0.003);
|
||
check(&format!("{id} {vals:?} {depth:?} half"), compare_at(&s, RenderOpts { scale: 0.5, ..opts() }, Tick::ZERO), 0.003);
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn smear_and_reshape() {
|
||
let smear: &[&[(&str, Value)]] = &[
|
||
&[("sourceMask", e(1)), ("boundaryMask", e(3)), ("maskOffset", pt(10.0, 3.0))],
|
||
&[
|
||
("sourceMask", e(1)),
|
||
("boundaryMask", e(3)),
|
||
("maskOffset", pt(-4.0, 6.0)),
|
||
("maskRotation", n(30.0)),
|
||
("maskScale", n(130.0)),
|
||
("elasticity", e(6)),
|
||
("percent", n(70.0)),
|
||
],
|
||
];
|
||
for vals in smear {
|
||
direct_with_masks("ec.distort.smear", vals);
|
||
composite_with_masks("ec.distort.smear", vals);
|
||
}
|
||
let reshape: &[&[(&str, Value)]] = &[
|
||
&[("sourceMask", e(1)), ("destinationMask", e(2))],
|
||
&[("sourceMask", e(1)), ("destinationMask", e(2)), ("boundaryMask", e(3)), ("interpolation", e(2)), ("elasticity", e(0))],
|
||
&[
|
||
("sourceMask", e(1)),
|
||
("destinationMask", e(2)),
|
||
("boundaryMask", e(3)),
|
||
("correspondencePoints", Value::Str("0,0.25 0.5,0.8".into())),
|
||
("percent", n(60.0)),
|
||
],
|
||
];
|
||
for vals in reshape {
|
||
direct_with_masks("ec.distort.reshape", vals);
|
||
composite_with_masks("ec.distort.reshape", vals);
|
||
}
|
||
}
|