mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-26 11:07:54 -06:00
Layer cache: animated masks with mask motion blur key the layer time
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>
This commit is contained in:
@@ -521,6 +521,27 @@ fn still_footage(ctx: &EvalCtx, item: effectcraft_project::ItemId) -> bool {
|
||||
ctx.project.item(item).is_some_and(|it| it.proxy.is_none() && matches!(&it.kind, ItemKind::Footage(f) if f.kind == FootageKind::Still))
|
||||
}
|
||||
|
||||
/// Does the layer have an animated mask that motion blur may draw at other times (Layer ▸ Mask ▸
|
||||
/// Motion Blur On, or Same As Layer with the layer blurred)?
|
||||
fn mask_motion_blur(ctx: &EvalCtx, layer: &Layer, blur: bool) -> bool {
|
||||
use effectcraft_project::{GroupKind, MaskMotionBlur};
|
||||
fn animated(g: &PropGroup) -> bool {
|
||||
g.children.iter().any(|c| match c {
|
||||
Node::Prop(p) => !p.keys.is_empty() || p.has_expression(),
|
||||
Node::Group(sub) => animated(sub),
|
||||
})
|
||||
}
|
||||
ctx.comp.enable_motion_blur
|
||||
&& layer.masks().is_some_and(|m| {
|
||||
m.groups().any(|g| match g.kind {
|
||||
GroupKind::Mask { motion_blur, .. } => {
|
||||
(motion_blur == MaskMotionBlur::On || (motion_blur == MaskMotionBlur::SameAsLayer && blur)) && animated(g)
|
||||
}
|
||||
_ => false,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn has_expression(g: &PropGroup) -> bool {
|
||||
g.children.iter().any(|c| match c {
|
||||
Node::Prop(p) => p.has_expression(),
|
||||
@@ -635,6 +656,11 @@ fn key_any(ctx: &EvalCtx, layer: &Layer, scale: f64, draft: bool, blur: bool, fo
|
||||
effectcraft_effects::paint::cache_key(g, lt).0.hash(&mut h);
|
||||
}
|
||||
}
|
||||
// Mask motion blur draws animated masks at the shutter's other times too: their values at
|
||||
// this time don't tell two frames apart (a hold before a move).
|
||||
if mask_motion_blur(ctx, layer, blur) {
|
||||
layer.layer_time(ctx.time).0.hash(&mut h);
|
||||
}
|
||||
if time_dependent(layer, effects != Some(0)) {
|
||||
layer.layer_time(ctx.time).0.hash(&mut h);
|
||||
// Effects also see the layer's time mapping.
|
||||
|
||||
@@ -41,6 +41,31 @@ fn alpha_at_25(p: &Project, cid: ItemId, motion_blur: bool) -> f32 {
|
||||
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);
|
||||
|
||||
Reference in New Issue
Block a user