mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-01 05:58:19 -06:00
Review: mask motion blur draws an animated mask at the shutter's other times, but the layer key hashed its value at this time only. Two frames with the same mask value (a hold before a move) shared a cached layer, with the wrong blur. This applied to solids, text and shapes already, and to stills since this PR. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
78 lines
3.9 KiB
Rust
78 lines
3.9 KiB
Rust
//! 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");
|
||
}
|