Files
effectcraft/crates/render/src/tests_mask_blur.rs
T
bflatasticandClaude Opus 5.5 995d99f938 M4.11: one motion blur gate for layers, masks, effects and Advanced 3D
`Renderer::mb_on` (render options × the comp's switch × the layer's switch as the precomp
layers above limit it) now decides everywhere:
- Mask motion blur checked only the comp's switch and the layer's own switch, so Render
  Settings ▸ Motion Blur ▸ Off for All Layers and a precomp layer with motion blur off still
  blurred masks; it now takes the gate (`masks::MaskBlur`) and the comp's Samples Per Frame
  (four in Draft) instead of a fixed eight.
- Effects that read the shutter (`EffectEnv::shutter`) get it only when the layer is motion
  blurred in this render.
- Advanced 3D layers follow the precomp layers' Motion Blur switches too.
- The layer cache key of a layer with masks or effects includes whether it is blurred and the
  comp's shutter and samples, so a buffer blurred for the viewer is never reused by a render
  with motion blur off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-05 01:55:34 +07:00

53 lines
2.6 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]
}
#[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");
}