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>
This commit is contained in:
parent
0b5cff1743
commit
995d99f938
|
|
@ -478,21 +478,21 @@ fn hash_values(h: &mut KeyHasher, ctx: &EvalCtx, layer: &Layer, g: &PropGroup) {
|
||||||
/// Cache key for the processed (source → masks → effects) buffer of `layer` at the context
|
/// Cache key for the processed (source → masks → effects) buffer of `layer` at the context
|
||||||
/// time, or `None` when the layer must not be cached (footage, precomps, cameras, adjustment
|
/// time, or `None` when the layer must not be cached (footage, precomps, cameras, adjustment
|
||||||
/// layers…).
|
/// layers…).
|
||||||
pub fn layer_key(ctx: &EvalCtx, layer: &Layer, scale: f64, draft: bool) -> Option<u64> {
|
pub fn layer_key(ctx: &EvalCtx, layer: &Layer, scale: f64, draft: bool, blur: bool) -> Option<u64> {
|
||||||
// Time effects see property values at other times. Keyframes are part of the hashed
|
// Time effects see property values at other times. Keyframes are part of the hashed
|
||||||
// structure (and the layer time is folded in), but expressions may read other layers at
|
// structure (and the layer time is folded in), but expressions may read other layers at
|
||||||
// those times, which the key cannot see: such layers are not cached.
|
// those times, which the key cannot see: such layers are not cached.
|
||||||
if reads_other_times(layer) && layer.props.children.iter().any(|c| matches!(c, Node::Group(g) if g.match_id != "transform" && has_expression(g))) {
|
if reads_other_times(layer) && layer.props.children.iter().any(|c| matches!(c, Node::Group(g) if g.match_id != "transform" && has_expression(g))) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
key_with(ctx, layer, scale, draft, false)
|
key_with(ctx, layer, scale, draft, blur, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cache key for a layer's *input* at the context time: source → masks → its first `effects`
|
/// Cache key for a layer's *input* at the context time: source → masks → its first `effects`
|
||||||
/// effects (see `Renderer::layer_input`). Unlike [`layer_key`], footage layers are cached here
|
/// effects (see `Renderer::layer_input`). Unlike [`layer_key`], footage layers are cached here
|
||||||
/// (keyed by item and source time), since Time effects read many neighbouring frames.
|
/// (keyed by item and source time), since Time effects read many neighbouring frames.
|
||||||
pub fn input_key(ctx: &EvalCtx, layer: &Layer, scale: f64, draft: bool, effects: usize) -> Option<u64> {
|
pub fn input_key(ctx: &EvalCtx, layer: &Layer, scale: f64, draft: bool, blur: bool, effects: usize) -> Option<u64> {
|
||||||
let base = key_with(ctx, layer, scale, draft, true)?;
|
let base = key_with(ctx, layer, scale, draft, blur, true)?;
|
||||||
let mut h = KeyHasher(base ^ 0x5bd1_e995_7a3c_11d3);
|
let mut h = KeyHasher(base ^ 0x5bd1_e995_7a3c_11d3);
|
||||||
effects.hash(&mut h);
|
effects.hash(&mut h);
|
||||||
Some(h.finish())
|
Some(h.finish())
|
||||||
|
|
@ -515,18 +515,19 @@ fn has_expression(g: &PropGroup) -> bool {
|
||||||
|
|
||||||
/// Cache key for an adjustment layer's footprint (its source through its masks, see
|
/// Cache key for an adjustment layer's footprint (its source through its masks, see
|
||||||
/// `Renderer::adjustment_footprint`): the GPU compositor then uploads it once, not every frame.
|
/// `Renderer::adjustment_footprint`): the GPU compositor then uploads it once, not every frame.
|
||||||
pub fn footprint_key(ctx: &EvalCtx, layer: &Layer, scale: f64, draft: bool) -> Option<u64> {
|
pub fn footprint_key(ctx: &EvalCtx, layer: &Layer, scale: f64, draft: bool, blur: bool) -> Option<u64> {
|
||||||
Some(derive(key_any(ctx, layer, scale, draft, false)?, 0xf007_9417))
|
Some(derive(key_any(ctx, layer, scale, draft, blur, false)?, 0xf007_9417))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn key_with(ctx: &EvalCtx, layer: &Layer, scale: f64, draft: bool, footage: bool) -> Option<u64> {
|
fn key_with(ctx: &EvalCtx, layer: &Layer, scale: f64, draft: bool, blur: bool, footage: bool) -> Option<u64> {
|
||||||
if layer.switches.adjustment {
|
if layer.switches.adjustment {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
key_any(ctx, layer, scale, draft, footage)
|
key_any(ctx, layer, scale, draft, blur, footage)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn key_any(ctx: &EvalCtx, layer: &Layer, scale: f64, draft: bool, footage: bool) -> Option<u64> {
|
/// `blur`: the layer is motion blurred in this render (`Renderer::mb_on`).
|
||||||
|
fn key_any(ctx: &EvalCtx, layer: &Layer, scale: f64, draft: bool, blur: bool, footage: bool) -> Option<u64> {
|
||||||
let mut h = KeyHasher(0xcbf2_9ce4_8422_2325);
|
let mut h = KeyHasher(0xcbf2_9ce4_8422_2325);
|
||||||
match &layer.source {
|
match &layer.source {
|
||||||
LayerSource::Solid { item } => {
|
LayerSource::Solid { item } => {
|
||||||
|
|
@ -558,11 +559,14 @@ fn key_any(ctx: &EvalCtx, layer: &Layer, scale: f64, draft: bool, footage: bool)
|
||||||
layer.id.hash(&mut h);
|
layer.id.hash(&mut h);
|
||||||
hash_debug(&mut h, &layer.source);
|
hash_debug(&mut h, &layer.source);
|
||||||
hash_debug(&mut h, &layer.switches);
|
hash_debug(&mut h, &layer.switches);
|
||||||
// Mask motion blur samples the comp's shutter.
|
// Mask motion blur and effects that read the shutter depend on whether the layer is motion
|
||||||
if layer.masks().is_some_and(|m| !m.children.is_empty()) {
|
// blurred in this render and on the comp's shutter.
|
||||||
|
if layer.masks().is_some_and(|m| !m.children.is_empty()) || layer.effects().is_some_and(|fx| !fx.children.is_empty()) {
|
||||||
|
blur.hash(&mut h);
|
||||||
ctx.comp.enable_motion_blur.hash(&mut h);
|
ctx.comp.enable_motion_blur.hash(&mut h);
|
||||||
ctx.comp.shutter_angle.to_bits().hash(&mut h);
|
ctx.comp.shutter_angle.to_bits().hash(&mut h);
|
||||||
ctx.comp.shutter_phase.to_bits().hash(&mut h);
|
ctx.comp.shutter_phase.to_bits().hash(&mut h);
|
||||||
|
ctx.comp.motion_blur_samples.hash(&mut h);
|
||||||
}
|
}
|
||||||
for c in &layer.props.children {
|
for c in &layer.props.children {
|
||||||
// Transform is applied later; Layer Styles are keyed separately (see [`styles_key`]).
|
// Transform is applied later; Layer Styles are keyed separately (see [`styles_key`]).
|
||||||
|
|
|
||||||
|
|
@ -841,11 +841,7 @@ impl<'a> Renderer<'a> {
|
||||||
effect_index: 0,
|
effect_index: 0,
|
||||||
working_space: self.pipe.space,
|
working_space: self.pipe.space,
|
||||||
working_linear: self.pipe.linear,
|
working_linear: self.pipe.linear,
|
||||||
shutter: (ctx.comp.enable_motion_blur && layer.switches.motion_blur).then_some((
|
shutter: self.mb_on(ctx, layer).then_some((ctx.comp.shutter_angle, ctx.comp.shutter_phase, ctx.comp.motion_blur_samples)),
|
||||||
ctx.comp.shutter_angle,
|
|
||||||
ctx.comp.shutter_phase,
|
|
||||||
ctx.comp.motion_blur_samples,
|
|
||||||
)),
|
|
||||||
};
|
};
|
||||||
// Video effects in stack order (index, group, spec); disabled and audio effects skipped.
|
// Video effects in stack order (index, group, spec); disabled and audio effects skipped.
|
||||||
let stack: Vec<(usize, &effectcraft_project::PropGroup, &'static effectcraft_effects::EffectSpec)> = fx
|
let stack: Vec<(usize, &effectcraft_project::PropGroup, &'static effectcraft_effects::EffectSpec)> = fx
|
||||||
|
|
@ -1085,7 +1081,7 @@ impl<'a> Renderer<'a> {
|
||||||
let mut buf = self.source(ctx, layer)?;
|
let mut buf = self.source(ctx, layer)?;
|
||||||
// A solid is filled with a quantised colour: only masks can take it off the grid.
|
// A solid is filled with a quantised colour: only masks can take it off the grid.
|
||||||
let solid = matches!(layer.source, LayerSource::Solid { .. });
|
let solid = matches!(layer.source, LayerSource::Solid { .. });
|
||||||
if masks::apply(ctx, layer, &mut buf) || !solid {
|
if masks::apply(ctx, layer, &mut buf, self.mask_blur(ctx, layer)) || !solid {
|
||||||
self.pipe.quantize(&mut buf.img);
|
self.pipe.quantize(&mut buf.img);
|
||||||
}
|
}
|
||||||
Some(buf)
|
Some(buf)
|
||||||
|
|
@ -1152,7 +1148,7 @@ impl<'a> Renderer<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn content_buf_timed(&self, ctx: &EvalCtx, layer: &Layer, mut timing: Option<&mut LayerTiming>) -> Option<(Arc<Buf>, Option<u64>)> {
|
fn content_buf_timed(&self, ctx: &EvalCtx, layer: &Layer, mut timing: Option<&mut LayerTiming>) -> Option<(Arc<Buf>, Option<u64>)> {
|
||||||
let key = self.cache.and_then(|_| cache::layer_key(ctx, layer, self.raster_scale(ctx, layer), self.opts.draft));
|
let key = self.cache.and_then(|_| cache::layer_key(ctx, layer, self.raster_scale(ctx, layer), self.opts.draft, self.mb_on(ctx, layer)));
|
||||||
if let (Some(c), Some(k)) = (self.cache, key)
|
if let (Some(c), Some(k)) = (self.cache, key)
|
||||||
&& let Some(b) = c.get(k)
|
&& let Some(b) = c.get(k)
|
||||||
{
|
{
|
||||||
|
|
@ -1173,7 +1169,7 @@ impl<'a> Renderer<'a> {
|
||||||
/// (what Time effects read at neighbouring times). Served from / stored in the layer cache
|
/// (what Time effects read at neighbouring times). Served from / stored in the layer cache
|
||||||
/// under its own key, so scrubbing reuses frames rendered for earlier output frames.
|
/// under its own key, so scrubbing reuses frames rendered for earlier output frames.
|
||||||
pub fn layer_input(&self, ctx: &EvalCtx, layer: &Layer, effects: usize) -> Option<Arc<Buf>> {
|
pub fn layer_input(&self, ctx: &EvalCtx, layer: &Layer, effects: usize) -> Option<Arc<Buf>> {
|
||||||
let key = self.cache.and_then(|_| cache::input_key(ctx, layer, self.raster_scale(ctx, layer), self.opts.draft, effects));
|
let key = self.cache.and_then(|_| cache::input_key(ctx, layer, self.raster_scale(ctx, layer), self.opts.draft, self.mb_on(ctx, layer), effects));
|
||||||
if let (Some(c), Some(k)) = (self.cache, key)
|
if let (Some(c), Some(k)) = (self.cache, key)
|
||||||
&& let Some(b) = c.get(k)
|
&& let Some(b) = c.get(k)
|
||||||
{
|
{
|
||||||
|
|
@ -1285,11 +1281,22 @@ impl<'a> Renderer<'a> {
|
||||||
* Mat3::translate(vec2(-buf.offset[0], -buf.offset[1]))
|
* Mat3::translate(vec2(-buf.offset[0], -buf.offset[1]))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether a layer is motion blurred.
|
/// Whether a layer is motion blurred: the render options, the comp's switch and the layer's
|
||||||
fn mb_on(&self, ctx: &EvalCtx, layer: &Layer) -> bool {
|
/// switch (limited by the precomp layers above when switches affect nested comps). Layer
|
||||||
|
/// motion, mask motion blur, effects that read the shutter and Advanced 3D all ask this.
|
||||||
|
pub(crate) fn mb_on(&self, ctx: &EvalCtx, layer: &Layer) -> bool {
|
||||||
self.opts.motion_blur && ctx.comp.enable_motion_blur && self.layer_motion_blur(layer)
|
self.opts.motion_blur && ctx.comp.enable_motion_blur && self.layer_motion_blur(layer)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// What a layer's mask motion blur may do in this render (see [`masks::MaskBlur`]).
|
||||||
|
fn mask_blur(&self, ctx: &EvalCtx, layer: &Layer) -> masks::MaskBlur {
|
||||||
|
masks::MaskBlur {
|
||||||
|
allowed: self.opts.motion_blur && ctx.comp.enable_motion_blur && self.inherited.is_none_or(|(_, mb)| mb),
|
||||||
|
layer: self.layer_motion_blur(layer),
|
||||||
|
samples: if self.opts.draft { 4 } else { ctx.comp.motion_blur_samples as usize },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Motion-blur sub-samples for a layer buffer (1 = no motion blur). As in After Effects, a 2D
|
/// Motion-blur sub-samples for a layer buffer (1 = no motion blur). As in After Effects, a 2D
|
||||||
/// layer gets about one sample per output pixel its corners travel during the shutter,
|
/// layer gets about one sample per output pixel its corners travel during the shutter,
|
||||||
/// at least Samples Per Frame and at most the comp's Adaptive Sample Limit; a layer that
|
/// at least Samples Per Frame and at most the comp's Adaptive Sample Limit; a layer that
|
||||||
|
|
@ -1545,7 +1552,7 @@ impl<'a> Renderer<'a> {
|
||||||
/// Adjustment layer: effects applied to the comp below, limited to the layer's footprint.
|
/// Adjustment layer: effects applied to the comp below, limited to the layer's footprint.
|
||||||
fn draw_adjustment(&self, ctx: &EvalCtx, layer: &Layer, canvas: &mut Image, opacity: f32) {
|
fn draw_adjustment(&self, ctx: &EvalCtx, layer: &Layer, canvas: &mut Image, opacity: f32) {
|
||||||
let Some(mut foot) = self.source(ctx, layer) else { return };
|
let Some(mut foot) = self.source(ctx, layer) else { return };
|
||||||
let _ = masks::apply(ctx, layer, &mut foot);
|
let _ = masks::apply(ctx, layer, &mut foot, self.mask_blur(ctx, layer));
|
||||||
let mut matte = Image::new(canvas.width, canvas.height);
|
let mut matte = Image::new(canvas.width, canvas.height);
|
||||||
self.place(ctx, layer, &foot, &mut matte, BlendMode::Normal, 1.0);
|
self.place(ctx, layer, &foot, &mut matte, BlendMode::Normal, 1.0);
|
||||||
let o = self.roi_offset().map(|(x, y, _, _)| [-x, -y]).unwrap_or([0.0; 2]);
|
let o = self.roi_offset().map(|(x, y, _, _)| [-x, -y]).unwrap_or([0.0; 2]);
|
||||||
|
|
@ -1708,14 +1715,14 @@ impl<'a> Renderer<'a> {
|
||||||
/// its effects show. `None` = the layer changes nothing.
|
/// its effects show. `None` = the layer changes nothing.
|
||||||
pub fn adjustment_footprint(&self, ctx: &EvalCtx, layer: &Layer) -> Option<Arc<Buf>> {
|
pub fn adjustment_footprint(&self, ctx: &EvalCtx, layer: &Layer) -> Option<Arc<Buf>> {
|
||||||
// Cached, so an accelerator sees the same buffer (and uploads it once) every frame.
|
// Cached, so an accelerator sees the same buffer (and uploads it once) every frame.
|
||||||
let key = self.cache.and_then(|_| cache::footprint_key(ctx, layer, self.raster_scale(ctx, layer), self.opts.draft));
|
let key = self.cache.and_then(|_| cache::footprint_key(ctx, layer, self.raster_scale(ctx, layer), self.opts.draft, self.mb_on(ctx, layer)));
|
||||||
if let (Some(c), Some(k)) = (self.cache, key)
|
if let (Some(c), Some(k)) = (self.cache, key)
|
||||||
&& let Some(b) = c.get(k)
|
&& let Some(b) = c.get(k)
|
||||||
{
|
{
|
||||||
return Some(b);
|
return Some(b);
|
||||||
}
|
}
|
||||||
let mut foot = self.source(ctx, layer)?;
|
let mut foot = self.source(ctx, layer)?;
|
||||||
let _ = masks::apply(ctx, layer, &mut foot);
|
let _ = masks::apply(ctx, layer, &mut foot, self.mask_blur(ctx, layer));
|
||||||
let foot = Arc::new(foot);
|
let foot = Arc::new(foot);
|
||||||
if let (Some(c), Some(k)) = (self.cache, key) {
|
if let (Some(c), Some(k)) = (self.cache, key) {
|
||||||
c.insert(k, foot.clone());
|
c.insert(k, foot.clone());
|
||||||
|
|
@ -1914,6 +1921,8 @@ mod tests_color;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests_frame_blend;
|
mod tests_frame_blend;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
|
mod tests_mask_blur;
|
||||||
|
#[cfg(test)]
|
||||||
mod tests_nested;
|
mod tests_nested;
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests_paint;
|
mod tests_paint;
|
||||||
|
|
|
||||||
|
|
@ -10,29 +10,40 @@ use rayon::prelude::*;
|
||||||
|
|
||||||
use crate::eval::EvalCtx;
|
use crate::eval::EvalCtx;
|
||||||
|
|
||||||
/// Sub-frame samples for mask motion blur (animated mask paths only).
|
/// What mask motion blur may do in a render (from the renderer's motion blur gate).
|
||||||
const MASK_BLUR_SAMPLES: usize = 8;
|
#[derive(Clone, Copy, Debug, Default)]
|
||||||
|
pub struct MaskBlur {
|
||||||
|
/// Motion blur is on in this render: the render options, the comp's switch and the Motion
|
||||||
|
/// Blur switches of the precomp layers above (when switches affect nested comps).
|
||||||
|
pub allowed: bool,
|
||||||
|
/// The layer's own Motion Blur switch, as those precomp layers limit it (Same As Layer).
|
||||||
|
pub layer: bool,
|
||||||
|
/// Sub-frame samples (the comp's Samples Per Frame; fewer in Draft).
|
||||||
|
pub samples: usize,
|
||||||
|
}
|
||||||
|
|
||||||
/// Coverage of one mask in buffer pixels (before mode/opacity/invert). With mask motion blur
|
/// Coverage of one mask in buffer pixels (before mode/opacity/invert). With mask motion blur
|
||||||
/// (Layer ▸ Mask ▸ Motion Blur: On, or Same As Layer with the layer's switch on) an animated
|
/// (Layer ▸ Mask ▸ Motion Blur: On, or Same As Layer with the layer's switch on) an animated
|
||||||
/// path is averaged over the comp's shutter.
|
/// path is averaged over the comp's shutter.
|
||||||
fn coverage(ctx: &EvalCtx, layer: &Layer, g: &effectcraft_project::PropGroup, buf: &Buf) -> Option<Mask> {
|
fn coverage(ctx: &EvalCtx, layer: &Layer, g: &effectcraft_project::PropGroup, buf: &Buf, blur: MaskBlur) -> Option<Mask> {
|
||||||
let mb = match g.kind {
|
let mb = blur.allowed
|
||||||
GroupKind::Mask { motion_blur, .. } => match motion_blur {
|
&& match g.kind {
|
||||||
MaskMotionBlur::On => true,
|
GroupKind::Mask { motion_blur, .. } => match motion_blur {
|
||||||
MaskMotionBlur::Off => false,
|
MaskMotionBlur::On => true,
|
||||||
MaskMotionBlur::SameAsLayer => layer.switches.motion_blur,
|
MaskMotionBlur::Off => false,
|
||||||
},
|
MaskMotionBlur::SameAsLayer => blur.layer,
|
||||||
_ => false,
|
},
|
||||||
};
|
_ => false,
|
||||||
|
};
|
||||||
let animated = g.get("path").is_some_and(|p| p.keys.len() > 1 || p.has_expression());
|
let animated = g.get("path").is_some_and(|p| p.keys.len() > 1 || p.has_expression());
|
||||||
if mb && animated && ctx.comp.enable_motion_blur {
|
if mb && animated {
|
||||||
let fd = ctx.comp.frame_duration().seconds();
|
let fd = ctx.comp.frame_duration().seconds();
|
||||||
let (angle, phase) = (ctx.comp.shutter_angle / 360.0, ctx.comp.shutter_phase / 360.0);
|
let (angle, phase) = (ctx.comp.shutter_angle / 360.0, ctx.comp.shutter_phase / 360.0);
|
||||||
let mut acc: Option<Mask> = None;
|
let mut acc: Option<Mask> = None;
|
||||||
let k = 1.0 / MASK_BLUR_SAMPLES as f32;
|
let n = blur.samples.max(2);
|
||||||
for i in 0..MASK_BLUR_SAMPLES {
|
let k = 1.0 / n as f32;
|
||||||
let f = phase + angle * i as f64 / (MASK_BLUR_SAMPLES - 1) as f64;
|
for i in 0..n {
|
||||||
|
let f = phase + angle * i as f64 / (n - 1) as f64;
|
||||||
let sub = ctx.at(ctx.time + effectcraft_time::Tick::from_seconds_f64(f * fd));
|
let sub = ctx.at(ctx.time + effectcraft_time::Tick::from_seconds_f64(f * fd));
|
||||||
let Some(c) = coverage_at(&sub, layer, g, buf) else { continue };
|
let Some(c) = coverage_at(&sub, layer, g, buf) else { continue };
|
||||||
match &mut acc {
|
match &mut acc {
|
||||||
|
|
@ -121,7 +132,7 @@ pub fn shapes(ctx: &EvalCtx, layer: &Layer) -> Vec<effectcraft_effects::MaskShap
|
||||||
|
|
||||||
/// Apply the layer's masks to its buffer.
|
/// Apply the layer's masks to its buffer.
|
||||||
/// Returns whether any mask was applied.
|
/// Returns whether any mask was applied.
|
||||||
pub fn apply(ctx: &EvalCtx, layer: &Layer, buf: &mut Buf) -> bool {
|
pub fn apply(ctx: &EvalCtx, layer: &Layer, buf: &mut Buf, blur: MaskBlur) -> bool {
|
||||||
let Some(masks) = layer.masks() else { return false };
|
let Some(masks) = layer.masks() else { return false };
|
||||||
let list: Vec<_> = masks.groups().filter(|g| g.enabled && matches!(g.kind, GroupKind::Mask { mode, .. } if mode != MaskMode::None)).collect();
|
let list: Vec<_> = masks.groups().filter(|g| g.enabled && matches!(g.kind, GroupKind::Mask { mode, .. } if mode != MaskMode::None)).collect();
|
||||||
if list.is_empty() {
|
if list.is_empty() {
|
||||||
|
|
@ -133,7 +144,7 @@ pub fn apply(ctx: &EvalCtx, layer: &Layer, buf: &mut Buf) -> bool {
|
||||||
let mut acc = vec![if first_sub { 1.0f32 } else { 0.0 }; n];
|
let mut acc = vec![if first_sub { 1.0f32 } else { 0.0 }; n];
|
||||||
for g in list {
|
for g in list {
|
||||||
let GroupKind::Mask { mode, inverted, .. } = g.kind else { continue };
|
let GroupKind::Mask { mode, inverted, .. } = g.kind else { continue };
|
||||||
let Some(mut cov) = coverage(ctx, layer, g, buf) else { continue };
|
let Some(mut cov) = coverage(ctx, layer, g, buf, blur) else { continue };
|
||||||
let op = (ctx.f(layer, g, "opacity", 100.0) / 100.0) as f32;
|
let op = (ctx.f(layer, g, "opacity", 100.0) / 100.0) as f32;
|
||||||
cov.data.par_iter_mut().for_each(|c| {
|
cov.data.par_iter_mut().for_each(|c| {
|
||||||
if inverted {
|
if inverted {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,52 @@
|
||||||
|
//! 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");
|
||||||
|
}
|
||||||
|
|
@ -157,8 +157,8 @@ fn wiggly_selector_is_deterministic_and_time_dependent() {
|
||||||
assert!(ys(&a).iter().all(|y| y.abs() <= 50.0 + 1e-9));
|
assert!(ys(&a).iter().all(|y| y.abs() <= 50.0 + 1e-9));
|
||||||
assert!(ys(&a).windows(2).any(|w| (w[0] - w[1]).abs() > 1e-6));
|
assert!(ys(&a).windows(2).any(|w| (w[0] - w[1]).abs() > 1e-6));
|
||||||
// The layer cache keys a wiggly text layer by time.
|
// The layer cache keys a wiggly text layer by time.
|
||||||
let k1 = crate::cache::layer_key(&ctx(&p, cid, 0.5), l, 1.0, false);
|
let k1 = crate::cache::layer_key(&ctx(&p, cid, 0.5), l, 1.0, false, false);
|
||||||
let k2 = crate::cache::layer_key(&ctx(&p, cid, 1.3), l, 1.0, false);
|
let k2 = crate::cache::layer_key(&ctx(&p, cid, 1.3), l, 1.0, false, false);
|
||||||
assert_ne!(k1, k2);
|
assert_ne!(k1, k2);
|
||||||
// Renders identically twice.
|
// Renders identically twice.
|
||||||
let r1 = crate::render_frame(&p, cid, Tick::from_seconds_f64(0.5), 0.5);
|
let r1 = crate::render_frame(&p, cid, Tick::from_seconds_f64(0.5), 0.5);
|
||||||
|
|
|
||||||
|
|
@ -882,9 +882,10 @@ pub(crate) fn build(r: &Renderer, ctx: &EvalCtx, run: &[&Layer], out: (u32, u32)
|
||||||
build_at(r, ctx, None, run, out)
|
build_at(r, ctx, None, run, out)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Whether a layer's motion is blurred (Motion Blur switch, comp setting and render option).
|
/// Whether a layer's motion is blurred (the renderer's gate: Motion Blur switch, comp setting,
|
||||||
|
/// render option and the precomp layers above).
|
||||||
pub(crate) fn motion_blurred(r: &Renderer, ctx: &EvalCtx, layer: &Layer) -> bool {
|
pub(crate) fn motion_blurred(r: &Renderer, ctx: &EvalCtx, layer: &Layer) -> bool {
|
||||||
r.opts.motion_blur && ctx.comp.enable_motion_blur && layer.switches.motion_blur
|
r.mb_on(ctx, layer)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// [`build`] at a motion-blur sub-sample: layers with Motion Blur on, the camera and the lights
|
/// [`build`] at a motion-blur sub-sample: layers with Motion Blur on, the camera and the lights
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue