Files
effectcraft/crates/render/src/tests/mask_blur.rs
T

78 lines
3.9 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.
//! Mask motion blur follows the renderer's motion blur gate: the render options (Render
//! Settings ▸ Motion Blur, previews), the comp's switch and the precomp layers above.
use effectcraft_color::Label;
use effectcraft_keyframe::{Keyframe, ShapePath, Value};
use effectcraft_project::build::{self, Ids};
use effectcraft_project::{Comp, GroupKind, ItemId, ItemKind, LayerSource, MaskMode, MaskMotionBlur, Node, Project, Solid};
use effectcraft_time::{FrameRate, Tick};
use crate::{NoFootage, RenderOpts, Renderer};
/// A white 100×100 solid whose rect mask jumps from x 10 (frame 0) to x 50 (frame 1), Mask
/// Motion Blur On, the layer's own switch off. Rendered half way, unblurred the mask covers x
/// 30..50; blurred over the shutter it reaches down to x ≈ 20.
fn project(comp_blur: bool) -> (Project, ItemId) {
let mut p = Project::default();
let mut comp = Comp::new(100, 100, FrameRate::FPS_30, Tick::from_seconds_f64(1.0));
comp.enable_motion_blur = comp_blur;
let sid = p.add_item("S", Label::Red, None, ItemKind::Solid(Solid { color: [1.0; 3], width: 100, height: 100, pixel_aspect: 1.0 }));
let mut l = build::layer(&mut p, &comp, "S", LayerSource::Solid { item: sid }, (100, 100), None);
let mut next = p.next_id;
let mut m = build::mask(&mut Ids(&mut next), "Mask 1", ShapePath::rect([20.0, 50.0], 20.0, 100.0), MaskMode::Add, [255, 0, 0]);
p.next_id = next;
if let GroupKind::Mask { motion_blur, .. } = &mut m.kind {
*motion_blur = MaskMotionBlur::On;
}
let path = m.get_mut("path").unwrap();
path.keys = vec![
Keyframe::new(Tick::ZERO, Value::Path(ShapePath::rect([20.0, 50.0], 20.0, 100.0))),
Keyframe::new(FrameRate::FPS_30.tick_of(1), Value::Path(ShapePath::rect([60.0, 50.0], 20.0, 100.0))),
];
l.props.sub_mut("masks").unwrap().children.push(Node::Group(m));
l.switches.motion_blur = false;
let cid = p.add_item("C", Label::Sandstone, None, ItemKind::Comp(comp.into()));
p.comp_mut(cid).unwrap().layers.push(l);
(p, cid)
}
fn alpha_at_25(p: &Project, cid: ItemId, motion_blur: bool) -> f32 {
let half = Tick::from_seconds_f64(0.5 / 30.0);
Renderer::new(p, &NoFootage, RenderOpts { motion_blur, ..Default::default() }).comp_frame(cid, half).get(25, 50)[3]
}
/// A mask that holds from frame 0 to 2, then moves: frames 1 and 2 have the same mask value, but at
/// frame 2 the shutter already sees it move. The layer cache must not serve frame 1's layer there.
#[test]
fn mask_motion_blur_keys_the_layer_cache_by_time() {
let (mut p, cid) = project(true);
let rect = |x: f64| Value::Path(ShapePath::rect([x, 50.0], 20.0, 100.0));
let l = &mut p.comp_mut(cid).unwrap().layers[0];
let Some(Node::Group(m)) = l.props.sub_mut("masks").unwrap().children.first_mut() else { panic!("mask") };
m.get_mut("path").unwrap().keys = vec![
Keyframe::new(Tick::ZERO, rect(20.0)),
Keyframe::new(FrameRate::FPS_30.tick_of(2), rect(20.0)),
Keyframe::new(FrameRate::FPS_30.tick_of(3), rect(60.0)),
];
let cache = crate::LayerCache::default();
let render = |t: Tick, cached: bool| {
let mut r = Renderer::new(&p, &NoFootage, RenderOpts { motion_blur: true, ..Default::default() });
r.cache = cached.then_some(&cache);
r.comp_frame(cid, t)
};
let (f1, f2) = (FrameRate::FPS_30.tick_of(1), FrameRate::FPS_30.tick_of(2));
assert_ne!(render(f1, false), render(f2, false), "the shutter at frame 2 sees the move");
render(f1, true);
assert_eq!(render(f2, true), render(f2, false));
}
#[test]
fn mask_motion_blur_follows_the_render_and_the_comp() {
let (p, cid) = project(true);
let a = alpha_at_25(&p, cid, true);
assert!(a > 0.05 && a < 0.95, "blurred: {a}");
assert!(alpha_at_25(&p, cid, false) < 0.01, "Render Settings ▸ Motion Blur off");
let (p, cid) = project(false);
assert!(alpha_at_25(&p, cid, true) < 0.01, "the comp's switch off");
}