Files
effectcraft/crates/engine/src/tests/motion_blur_depth.rs
T

123 lines
6.0 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Motion blur of collapsed precomps (the precomp layer's own motion; the containing comp's
//! shutter) and of animated shape content, rendered through the commands.
use effectcraft_raster::Image;
use effectcraft_time::Tick;
use serde_json::{Value, json};
use crate::Session;
use crate::render::RenderOpts;
const W: u32 = 160;
const H: u32 = 90;
fn id(v: &Value, k: &str) -> u64 {
v[k].as_u64().unwrap()
}
fn render(s: &Session, comp: u64) -> Image {
s.render(effectcraft_project::ItemId(comp), Tick::from_seconds_f64(0.5), RenderOpts::default())
}
/// Pixels with partial alpha (motion-blur smear).
fn partial(img: &Image) -> usize {
img.data.iter().filter(|p| p[3] > 0.05 && p[3] < 0.95).count()
}
fn max_diff(a: &Image, b: &Image) -> f32 {
a.data.iter().zip(&b.data).flat_map(|(p, q)| (0..4).map(move |c| (p[c] - q[c]).abs())).fold(0.0, f32::max)
}
/// "Main" holding "Pre" (a 20×20 white square in the middle), the precomp layer moving 480 px/s
/// (20 px a frame at 24 fps) with motion blur on. `inner_move`: the square moves inside Pre too
/// (with its own motion blur switch), and Pre's shutter is closed (0°).
fn scene(collapse: bool, inner_move: bool) -> (Session, u64) {
let mut s = Session::default();
let pre = id(&s.execute("comp.new", json!({"name": "Pre", "width": W, "height": H, "frameRate": 24, "duration": 2})).unwrap(), "comp");
let sq = id(&s.execute("layer.newSolid", json!({"name": "Square", "color": "#ffffff", "width": 20, "height": 20})).unwrap(), "layer");
if inner_move {
s.execute("comp.settings", json!({"shutterAngle": 0})).unwrap();
s.execute("comp.setSwitch", json!({"switch": "motionBlur", "value": true})).unwrap();
s.execute("layer.setSwitch", json!({"layers": [sq], "switch": "motionBlur", "value": true})).unwrap();
for (t, x) in [(0.0, -160.0), (1.0, 320.0)] {
s.execute("prop.addKey", json!({"layer": sq, "path": "transform/position", "time": t, "value": [x, 45.0]})).unwrap();
}
}
let main = id(&s.execute("comp.new", json!({"name": "Main", "width": W, "height": H, "frameRate": 24, "duration": 2})).unwrap(), "comp");
s.execute("comp.setSwitch", json!({"switch": "motionBlur", "value": true})).unwrap();
let l = id(&s.execute("layer.addItem", json!({"item": pre})).unwrap(), "layer");
s.execute("layer.setSwitch", json!({"layers": [l], "switch": "motionBlur", "value": true})).unwrap();
s.execute("layer.setSwitch", json!({"layers": [l], "switch": "collapse", "value": collapse})).unwrap();
if !inner_move {
for (t, x) in [(0.0, -160.0), (1.0, 320.0)] {
s.execute("prop.addKey", json!({"layer": l, "path": "transform/position", "time": t, "value": [x, 45.0]})).unwrap();
}
}
(s, main)
}
#[test]
fn a_collapsed_precomp_layer_blurs_with_its_own_motion() {
let (s, main) = scene(true, false);
let collapsed = render(&s, main);
// The square sits at x = 80 at 0.5 s and smears over 10 px (180° of 20 px).
assert!(partial(&collapsed) > 100, "smeared: {}", partial(&collapsed));
// As blurred as the same precomp rendered flat.
let (s, main) = scene(false, false);
let flat = render(&s, main);
assert!(max_diff(&collapsed, &flat) < 0.08, "{}", max_diff(&collapsed, &flat));
// Motion blur off for the render: sharp.
let sharp = s.render(effectcraft_project::ItemId(main), Tick::from_seconds_f64(0.5), RenderOpts { motion_blur: false, ..Default::default() });
assert!(partial(&sharp) < 10, "{}", partial(&sharp));
}
#[test]
fn collapsed_layers_use_the_containing_comps_shutter() {
// Pre's own shutter is closed; drawn into Main (180°) the square's own motion blurs.
let (s, main) = scene(true, true);
assert!(partial(&render(&s, main)) > 100, "{}", partial(&render(&s, main)));
// Rendered flat, Pre uses its own (closed) shutter.
let (s, main) = scene(false, true);
assert!(partial(&render(&s, main)) < 10, "{}", partial(&render(&s, main)));
}
/// A shape layer whose rectangle moves inside its contents (not its transform): motion blur
/// smears it like the same move made with the layer's Position.
#[test]
fn animated_shape_content_is_motion_blurred() {
let shape = |content: bool, blur: bool| {
let mut s = Session::default();
let c = id(&s.execute("comp.new", json!({"name": "S", "width": W, "height": H, "frameRate": 24, "duration": 2})).unwrap(), "comp");
s.execute("comp.setSwitch", json!({"switch": "motionBlur", "value": true})).unwrap();
let l = id(&s.execute("layer.newShape", json!({"kind": "rect", "size": [20, 20], "fill": "#ffffff", "position": [80, 45]})).unwrap(), "layer");
s.execute("layer.setSwitch", json!({"layers": [l], "switch": "motionBlur", "value": blur})).unwrap();
let (target, base) = if content {
let layer = s.active_comp().unwrap().layer(effectcraft_project::LayerId(l)).unwrap().clone();
let mut found = None;
layer.props.group("contents").unwrap().walk("contents", &mut |_, p| {
if found.is_none() && p.match_id == "position" {
found = Some((json!({"prop": p.uid}), p.value.as_vec2()));
}
});
found.unwrap()
} else {
(json!({"path": "transform/position"}), [80.0, 45.0])
};
for (t, dx) in [(0.0, -240.0), (1.0, 240.0)] {
let mut p = target.clone();
p["layer"] = json!(l);
p["time"] = json!(t);
p["value"] = json!([base[0] + dx, base[1]]);
s.execute("prop.addKey", p).unwrap();
}
render(&s, c)
};
let content = shape(true, true);
assert!(partial(&content) > 100, "smeared: {}", partial(&content));
// Like the layer moved by its transform…
let moved = shape(false, true);
assert!(max_diff(&content, &moved) < 0.15, "{}", max_diff(&content, &moved));
// …and sharp with the layer's motion blur switch off.
assert!(partial(&shape(true, false)) < 40, "{}", partial(&shape(true, false)));
}