Files
effectcraft/crates/render/src/layers.rs
T

667 lines
33 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.
use super::frames::adaptive_samples;
use super::opts::{LayerTiming, Through};
use super::{
Arc, BlendMode, Buf, Comp, EvalCtx, Image, IndexedParallelIterator, IntoParallelRefIterator, IntoParallelRefMutIterator, Layer, LayerSource, Mat3,
MatteKind, ParallelIterator, Quality, Region, Renderer, Sampling, Tick, WarpOpts, cache, color, composite_warp_changed, masks, styles, vec2,
};
/// Blend policy for placing a layer, including optional output-depth quantization.
struct PlacementBlend {
mode: BlendMode,
opacity: f32,
quantize: Option<f32>,
}
/// A motion-blur shutter: the comp's, or for a collapsed precomp's layers the outermost
/// containing comp's (they are drawn into its frames).
#[derive(Clone, Copy)]
struct Shutter {
/// Shutter angle and phase as fractions of a frame.
angle: f64,
phase: f64,
/// Frame duration (seconds).
frame: f64,
/// Samples Per Frame and Adaptive Sample Limit.
samples: u32,
limit: u32,
}
impl Shutter {
/// Seconds from the frame time to sub-sample `i` of `n`, spread over the open shutter.
fn offset(&self, i: usize, n: usize) -> f64 {
let f = if n > 1 { i as f64 / (n - 1) as f64 } else { 0.0 };
(self.phase + self.angle * f) * self.frame
}
}
/// `files` (a content's) with the files of the buffers made of it that the layer cache kept:
/// `files` itself when they are all its own.
fn with_kept_files(files: Arc<crate::reads::ReadFiles>, kept: &[Arc<crate::reads::ReadFiles>]) -> Arc<crate::reads::ReadFiles> {
if kept.iter().all(|f| Arc::ptr_eq(f, &files)) {
return files;
}
let mut all = (*files).clone();
kept.iter().for_each(|f| all.extend(f));
Arc::new(all)
}
impl<'a> Renderer<'a> {
/// The layer with its styles: exterior passes and body (cached per pass).
pub(crate) fn styled(&self, ctx: &EvalCtx, layer: &Layer, mut timing: Option<&mut LayerTiming>) -> Option<StyledLayer> {
let (content, ckey, files) = self.content_buf_timed(ctx, layer, timing.as_deref_mut())?;
if !styles::active(ctx, layer) {
return Some(StyledLayer { passes: vec![], body: content.clone(), content, key: ckey, ckey, plain: true, files });
}
let modes = styles::pass_modes(ctx, layer);
let key = ckey.map(|k| cache::styles_key(ctx, layer, k));
// (a cached body or pass may have been made of other files than the content: another
// worker's, or one from disk; the styled layer lists the files of every buffer it holds)
if let Some((body, body_files)) = self.cached(key) {
let mut kept_files = vec![body_files];
let passes: Option<Vec<_>> = modes
.iter()
.enumerate()
.map(|(i, m)| {
self.cached(key.map(|k| cache::derive(k, i as u64 + 1))).map(|(b, f)| {
kept_files.push(f);
(b, *m)
})
})
.collect();
if let Some(passes) = passes {
let files = with_kept_files(files, &kept_files);
return Some(StyledLayer { passes, body, content, key, ckey, plain: false, files });
}
}
let t0 = web_time::Instant::now();
let size = ctx.source_size(layer);
let layer_size = if size.0 == 0 { [ctx.comp.width as f64, ctx.comp.height as f64] } else { [size.0 as f64, size.1 as f64] };
let st = styles::render(ctx, layer, &content, layer_size);
if let Some(t) = timing {
t.cached = false;
t.effects.push(("layerStyles".into(), t0.elapsed().as_secs_f64() * 1e3));
}
// The buffers the cache keeps (another worker's, if it kept them first), made of the
// content's files; the styled layer then lists the files of every buffer it holds.
let mut kept_files = vec![];
let (body, f) = self.keep(key, Arc::new(st.body), &files);
kept_files.push(f);
let passes: Vec<_> = st
.passes
.into_iter()
.enumerate()
.map(|(i, (b, m))| {
let (b, f) = self.keep(key.map(|k| cache::derive(k, i as u64 + 1)), Arc::new(b), &files);
kept_files.push(f);
(b, m)
})
.collect();
let files = with_kept_files(files, &kept_files);
Some(StyledLayer { passes, body, content, key, ckey, plain: false, files })
}
/// The styled layer's buffers in the blending space.
pub(crate) fn styled_to_blend(&self, st: StyledLayer) -> StyledLayer {
if self.pipe.to_blend().is_none() {
return st;
}
let body = self.to_blend(st.body, st.key, &st.files);
let content = if st.plain { body.clone() } else { self.to_blend(st.content, st.ckey, &st.files) };
let passes =
st.passes.into_iter().enumerate().map(|(i, (b, m))| (self.to_blend(b, st.key.map(|k| cache::derive(k, i as u64 + 1)), &st.files), m)).collect();
StyledLayer { passes, body, content, ..st }
}
pub(crate) fn sampling(&self, layer: &Layer) -> effectcraft_raster::Sampling {
if self.quality(layer) == Quality::Draft {
// Draft quality: no interpolation (nearest neighbour), as After Effects' Draft.
effectcraft_raster::Sampling::Nearest
} else if self.opts.draft {
effectcraft_raster::Sampling::Bilinear
} else if layer.switches.sampling == Sampling::Bicubic {
effectcraft_raster::Sampling::Bicubic
} else {
effectcraft_raster::Sampling::Bilinear
}
}
/// The region of interest of a top-level 2D frame: (x, y) offset of the output in output
/// pixels and its size. `None` renders the whole comp (no ROI, or a nested comp).
pub(crate) fn roi_offset(&self) -> Option<(f64, f64, u32, u32)> {
let r = self.opts.roi.filter(|_| self.top)?;
let s = self.opts.scale;
if r[2] <= 0.0 || r[3] <= 0.0 {
return None;
}
Some(((r[0] * s).round(), (r[1] * s).round(), ((r[2] * s).round() as u32).max(1), ((r[3] * s).round() as u32).max(1)))
}
/// The region of interest's offset in output pixels for 3D projection (0 without one).
pub(crate) fn out_offset(&self) -> (f64, f64) {
self.roi_offset().map_or((0.0, 0.0), |(x, y, _, _)| (x, y))
}
/// Scaled comp pixel → output pixel: the region of interest's offset.
fn out_matrix(&self) -> Mat3 {
match self.roi_offset() {
Some((x, y, _, _)) => Mat3::translate(vec2(-x, -y)),
None => Mat3::IDENTITY,
}
}
/// Buffer pixel → output pixel matrix for the layer at the context time.
fn buf_matrix(&self, ctx: &EvalCtx, layer: &Layer, buf: &Buf) -> Mat3 {
let (l2c, _) = ctx.layer_to_comp(layer);
self.place_matrix(self.outer.map_or(l2c, |o| o * l2c), buf)
}
/// Buffer pixel → output pixel matrix for a layer → (top-level) comp matrix.
fn place_matrix(&self, l2c: Mat3, buf: &Buf) -> Mat3 {
let s = self.opts.scale;
self.out_matrix()
* Mat3::scale(vec2(s, s))
* l2c
* Mat3::scale(vec2(1.0 / buf.scale, 1.0 / buf.scale))
* Mat3::translate(vec2(-buf.offset[0], -buf.offset[1]))
}
/// The collapsed precomp layers this comp is drawn through, outermost first.
fn links(&self) -> impl Iterator<Item = &Through<'a>> {
self.through.iter().map_while(Option::as_ref)
}
/// The shutter of a layer's motion blur: its comp's, or for a collapsed precomp's layers the
/// outermost containing comp's.
fn shutter(&self, ctx: &EvalCtx) -> Shutter {
let c: &Comp = match self.links().next() {
Some(l) => l.comp,
None => ctx.comp,
};
Shutter {
angle: c.shutter_angle / 360.0,
phase: c.shutter_phase / 360.0,
frame: c.frame_duration().seconds(),
samples: c.motion_blur_samples,
limit: c.motion_blur_adaptive_limit,
}
}
/// Whether a layer moves within the shutter: its own motion blur, or that of a collapsed
/// precomp layer it is drawn through.
fn blurred(&self, ctx: &EvalCtx, layer: &Layer) -> bool {
self.mb_on(ctx, layer) || (!self.through_lost && self.links().any(|l| l.blur))
}
/// The collapsed precomps' matrix (nested comp → the frame's comp) and the nested comp's time
/// `dt` seconds into the outermost comp's shutter: each precomp layer moves if it is motion
/// blurred, and times map down through each one (stretch, time remapping).
fn outer_at(&self, ctx: &EvalCtx, dt: f64) -> (Option<Mat3>, Tick) {
let shift = Tick::from_seconds_f64(dt);
let mut outer: Option<Mat3> = None;
let mut t: Option<Tick> = None;
for l in self.links() {
let at = EvalCtx { comp_id: l.comp_id, comp: l.comp, time: t.unwrap_or(l.time + shift), ..*ctx };
let (m, _) = if l.blur { at.layer_to_comp(l.layer) } else { EvalCtx { time: l.time, ..at }.layer_to_comp(l.layer) };
outer = Some(outer.map_or(m, |o| o * m));
t = Some(at.nested_time(l.layer));
}
(outer, t.unwrap_or(ctx.time + shift))
}
/// [`Self::buf_matrix`] for the motion-blur sub-sample `dt` seconds into the shutter: the
/// layer moves if it is motion blurred, the collapsed precomps above it if theirs are.
fn buf_matrix_at(&self, ctx: &EvalCtx, layer: &Layer, buf: &Buf, dt: f64) -> Mat3 {
if self.through_lost || self.links().next().is_none() {
return self.buf_matrix(&ctx.at(ctx.time + Tick::from_seconds_f64(dt)), layer, buf);
}
let (outer, t) = self.outer_at(ctx, dt);
let lctx = if self.mb_on(ctx, layer) { ctx.at(t) } else { *ctx };
let (l2c, _) = lctx.layer_to_comp(layer);
self.place_matrix(outer.map_or(l2c, |o| o * l2c), buf)
}
/// Shape and text content that animates within the shutter (paths, Trim Paths, text
/// animators…) of a motion-blurred layer: the times its source is drawn at and averaged,
/// Samples Per Frame of them, as After Effects does for shape layers. `None` when the content
/// holds still (compared through its cache key, which leaves the transform out).
pub(crate) fn content_times(&self, ctx: &EvalCtx, layer: &Layer) -> Option<Vec<Tick>> {
if !matches!(layer.source, LayerSource::Shape | LayerSource::Text) || !self.mb_on(ctx, layer) {
return None;
}
let sh = self.shutter(ctx);
if sh.angle <= 0.0 {
return None;
}
let n = if self.opts.draft { 4 } else { (sh.samples as usize).clamp(2, 64) };
let times: Vec<Tick> = (0..n).map(|i| self.outer_at(ctx, sh.offset(i, n)).1).collect();
let key = |t: Tick| cache::layer_key(&ctx.at(t), layer, 1.0, false, false);
let k0 = key(*times.first()?)?;
let moves = [times.get(n / 2), times.last()].into_iter().flatten().any(|t| key(*t) != Some(k0));
moves.then_some(times)
}
/// The layer's source drawn at each of `times` and averaged (premultiplied) on the union of
/// their bounds. Content that also changes its raster scale within the shutter, or would need
/// an outsized buffer, is drawn once.
pub(crate) fn blurred_source(&self, ctx: &EvalCtx, layer: &Layer, times: &[Tick]) -> Option<Buf> {
let bufs: Vec<Buf> = times.iter().filter_map(|t| self.source(&ctx.at(*t), layer)).collect();
let scale = bufs.first()?.scale;
// Pixel = layer point × scale + offset: with the largest offset every buffer lands at
// x + (off - its offset) ≥ 0.
let off = [0, 1].map(|i| bufs.iter().map(|b| b.offset[i]).fold(f64::MIN, f64::max));
let w = bufs.iter().map(|b| off[0] - b.offset[0] + b.img.width as f64).fold(0.0, f64::max).ceil();
let h = bufs.iter().map(|b| off[1] - b.offset[1] + b.img.height as f64).fold(0.0, f64::max).ceil();
if bufs.iter().any(|b| (b.scale - scale).abs() > 1e-9) || !(w * h).is_finite() || w * h > (1u64 << 26) as f64 {
return self.source(ctx, layer);
}
let mut acc = Image::new(w as u32, h as u32);
let k = 1.0 / bufs.len() as f32;
for b in &bufs {
let m = Mat3::translate(vec2(off[0] - b.offset[0], off[1] - b.offset[1]));
effectcraft_raster::accumulate_warp(&mut acc, &b.img, &m, effectcraft_raster::Sampling::Bilinear, k);
}
Some(Buf { img: acc, offset: off, scale })
}
/// A processed-layer cache key that also covers content motion blur (the averaged source
/// depends on the content at every sub-sample).
pub(crate) fn content_key(&self, ctx: &EvalCtx, layer: &Layer, key: Option<u64>) -> Option<u64> {
let k = self.mask_gate_key(ctx, layer, key)?;
// A layer that reads other layers through its effects' layer parameters gets them drawn
// by this renderer: the switches it passes down (Switches Affect Nested Comps, the
// precomp layers above, motion blur on) change those pixels, not just the layers' own.
let k = if cache::reads_layers(ctx, layer) { cache::derive(k, self.nested_look()) } else { k };
let Some(times) = self.content_times(ctx, layer) else { return Some(k) };
times.iter().try_fold(cache::derive(k, 0x6d62_6c72), |d, t| Some(cache::derive(d, cache::layer_key(&ctx.at(*t), layer, 1.0, false, false)?)))
}
/// Whether a layer is motion blurred: the render options, the comp's switch and the layer's
/// 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)
}
/// What a layer's mask motion blur may do in this render (see [`masks::MaskBlur`]).
pub(crate) 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 },
}
}
/// A cache key of a masked layer's buffer, told apart when mask motion blur can't run at all
/// in this render ([`Self::mask_blur`]'s gate: the render options, the precomp layers above).
/// The key has the layer's own motion blur state, but a mask set to On blurs without it.
pub(crate) fn mask_gate_key(&self, ctx: &EvalCtx, layer: &Layer, key: Option<u64>) -> Option<u64> {
// Queue jobs share this cache but may override Proxy Use differently. The item's
// proxy switch alone does not describe the source a renderer actually reads.
let k = key?;
let k = match self.opts.proxy {
effectcraft_project::render_queue::ProxyUse::CurrentSettings => k,
effectcraft_project::render_queue::ProxyUse::UseAll => cache::derive(k, 0x7072_6f78_7901),
effectcraft_project::render_queue::ProxyUse::UseCompOnly => cache::derive(k, 0x7072_6f78_7902),
effectcraft_project::render_queue::ProxyUse::UseNone => cache::derive(k, 0x7072_6f78_7903),
};
let masked = layer.masks().is_some_and(|m| !m.children.is_empty());
Some(if masked && !self.mask_blur(ctx, layer).allowed { cache::derive(k, 0x6d62_6f66) } else { k })
}
/// 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,
/// at least Samples Per Frame and at most the comp's Adaptive Sample Limit; a layer that
/// does not move within the shutter is drawn once.
fn mb_samples(&self, ctx: &EvalCtx, layer: &Layer, buf: &Buf) -> usize {
if !self.blurred(ctx, layer) {
return 1;
}
let travel = self.shutter_travel(ctx, layer, buf);
if travel < 1e-3 {
return 1;
}
if self.opts.draft {
return 4;
}
let sh = self.shutter(ctx);
adaptive_samples(travel, sh.samples, sh.limit)
}
/// Largest distance (output pixels) a corner of the layer buffer moves during the shutter,
/// measured along the path at a few points.
fn shutter_travel(&self, ctx: &EvalCtx, layer: &Layer, buf: &Buf) -> f64 {
let sh = self.shutter(ctx);
let (w, h) = (buf.img.width as f64, buf.img.height as f64);
let corners = [(0.0, 0.0), (w, 0.0), (0.0, h), (w, h), (w * 0.5, h * 0.5)];
const PROBES: usize = 8;
let mut prev: Option<Vec<effectcraft_geom::Vec2>> = None;
let mut travel = vec![0.0f64; corners.len()];
for i in 0..PROBES {
let m = self.buf_matrix_at(ctx, layer, buf, sh.offset(i, PROBES));
let pts: Vec<_> = corners.iter().map(|&(x, y)| m.apply(vec2(x, y))).collect();
if let Some(p) = &prev {
for (k, (a, b)) in p.iter().zip(&pts).enumerate() {
travel[k] += (b.x - a.x).hypot(b.y - a.y);
}
}
prev = Some(pts);
}
travel.into_iter().fold(0.0, f64::max)
}
/// Canvas pixels that compositing `buf` for `layer` can change: the transformed buffer's
/// bounds plus the resampling footprint. Stencil/silhouette modes, motion blur and
/// perspective fall back to the whole canvas.
///
/// A quantised buffer placed pixel-exactly (integer translation, Normal, 100%) over a blank
/// canvas leaves exactly its own (quantised) pixels: nothing to re-quantise.
fn footprint(&self, ctx: &EvalCtx, layer: &Layer, buf: &Buf, opacity: f32, blank: bool) -> Region {
if layer.blend_mode.is_stencil() || self.mb_samples(ctx, layer, buf) > 1 {
return Region::Full;
}
let m = self.buf_matrix(ctx, layer, buf);
if !m.is_affine() {
return Region::Full;
}
let matte = layer.track_matte.is_some_and(|tm| tm.layer != layer.id && ctx.comp.layer(tm.layer).is_some());
let k = &m.0;
let integral = |v: f64| v == v.round();
if blank
&& layer.blend_mode == BlendMode::Normal
&& opacity == 1.0
&& !matte
&& !layer.preserve_transparency
&& self.pipe.to_blend().is_none()
&& k[0][0] == 1.0
&& k[1][1] == 1.0
&& k[0][1] == 0.0
&& k[1][0] == 0.0
&& integral(k[0][2])
&& integral(k[1][2])
{
return Region::Empty;
}
let (w, h) = (buf.img.width as f64, buf.img.height as f64);
let c = [(0.0, 0.0), (w, 0.0), (0.0, h), (w, h)].map(|(x, y)| m.apply(vec2(x, y)));
let (x0, x1) = (c.iter().map(|p| p.x).fold(f64::INFINITY, f64::min), c.iter().map(|p| p.x).fold(f64::NEG_INFINITY, f64::max));
let (y0, y1) = (c.iter().map(|p| p.y).fold(f64::INFINITY, f64::min), c.iter().map(|p| p.y).fold(f64::NEG_INFINITY, f64::max));
color::PxRect::covering(x0, y0, x1, y1, 3.0).map_or(Region::Full, Region::Rect)
}
/// How a processed layer buffer lands in the output: one buffer → output matrix, or one per
/// motion-blur sub-sample (averaged), with the layer's sampling filter and dissolve seed.
pub fn placement(&self, ctx: &EvalCtx, layer: &Layer, buf: &Buf) -> Placement {
let samples = self.mb_samples(ctx, layer, buf);
let (sampling, seed) = (self.sampling(layer), layer.id.0 as u32);
if samples <= 1 {
return Placement { matrices: vec![self.buf_matrix(ctx, layer, buf)], sampling, seed };
}
let sh = self.shutter(ctx);
let matrices = (0..samples).map(|i| self.buf_matrix_at(ctx, layer, buf, sh.offset(i, samples))).collect();
Placement { matrices, sampling, seed }
}
/// Draw a processed layer into `target` (transform, motion blur) with `mode`/`opacity`.
pub(crate) fn place(&self, ctx: &EvalCtx, layer: &Layer, buf: &Buf, target: &mut Image, mode: BlendMode, opacity: f32) {
self.place_changed(ctx, layer, buf, target, PlacementBlend { mode, opacity, quantize: None });
}
/// [`Self::place`], returning the target pixels it changed when the warp tracked them (then
/// also quantised to `quantize` levels; see [`composite_warp_changed`]).
fn place_changed(&self, ctx: &EvalCtx, layer: &Layer, buf: &Buf, target: &mut Image, blend: PlacementBlend) -> Option<[u32; 4]> {
let PlacementBlend { mode, opacity, quantize } = blend;
let pl = self.placement(ctx, layer, buf);
let opts = WarpOpts { sampling: pl.sampling, opacity, mode, seed: pl.seed, clip: None };
if let [m] = pl.matrices.as_slice() {
return composite_warp_changed(target, &buf.img, m, &opts, quantize);
}
// Sub-samples sum straight into one buffer (each warp is row-parallel).
let mut acc = Image::new(target.width, target.height);
let k = 1.0 / pl.matrices.len() as f32;
for m in &pl.matrices {
effectcraft_raster::accumulate_warp(&mut acc, &buf.img, m, opts.sampling, k);
}
target.blend_from(&acc, mode, opacity, pl.seed);
None
}
/// Draw a 2D layer into `canvas`; returns the region it may have changed.
pub(crate) fn draw_layer(&self, ctx: &EvalCtx, layer: &Layer, canvas: &mut Image, blank: bool) -> Region {
let opacity = ctx.opacity(layer) as f32 * self.opacity_mul;
if self.quality(layer) == Quality::Wireframe && !layer.switches.adjustment {
self.draw_wireframe(ctx, layer, canvas);
return Region::Full;
}
if opacity <= 0.0 && !layer.switches.adjustment {
return Region::Empty;
}
if layer.switches.adjustment {
self.draw_adjustment(ctx, layer, canvas, opacity);
return Region::Full;
}
let mut timing = self.profile.map(|_| LayerTiming { depth: self.depth, layer: layer.name.clone(), ..Default::default() });
let t0 = web_time::Instant::now();
let Some(st) = self.styled(ctx, layer, timing.as_mut()) else { return Region::Empty };
let st = self.styled_to_blend(st);
let t1 = web_time::Instant::now();
let changed = if st.plain {
// Pixels the warp tracks are quantised as it blends them (nothing left to do); the
// footprint is still needed when it doesn't, or when no pixel needs quantising.
let footprint = self.footprint(ctx, layer, &st.body, opacity, blank);
let quantize = if footprint == Region::Empty { None } else { self.pipe.levels };
match self.composite_layer(ctx, layer, &st.body, canvas, opacity, quantize) {
Some(_) if quantize.is_some() => Region::Empty,
drawn => narrow(footprint, drawn),
}
} else {
self.composite_styled(ctx, layer, &st, canvas, opacity);
Region::Full
};
if let (Some(prof), Some(mut timing)) = (self.profile, timing) {
timing.process_ms = (t1 - t0).as_secs_f64() * 1e3;
timing.composite_ms = t1.elapsed().as_secs_f64() * 1e3;
if let Ok(mut v) = prof.lock() {
v.push(timing);
}
}
changed
}
/// Composite a styled layer: exterior passes with their own modes, then the body with the
/// layer's mode; the layer's opacity fades the whole stack. Knockout clears what is below the
/// layer's content; switched-off R/G/B channels keep the values below.
fn composite_styled(&self, ctx: &EvalCtx, layer: &Layer, st: &StyledLayer, canvas: &mut Image, opacity: f32) {
let bl = styles::blending(ctx, layer);
let matte = layer.track_matte.is_some_and(|tm| tm.layer != layer.id && ctx.comp.layer(tm.layer).is_some());
if matte || layer.preserve_transparency {
let mut iso = Image::new(canvas.width, canvas.height);
for (b, m) in &st.passes {
self.place(ctx, layer, b, &mut iso, *m, 1.0);
}
self.place(ctx, layer, &st.body, &mut iso, BlendMode::Normal, 1.0);
self.composite_iso(ctx, layer, iso, canvas, opacity);
return;
}
let mut tmp = canvas.clone();
if bl.knockout > 0 {
let mut k = Image::new(canvas.width, canvas.height);
self.place(ctx, layer, &st.content, &mut k, BlendMode::Normal, 1.0);
tmp.data.par_iter_mut().zip(k.data.par_iter()).for_each(|(p, q)| {
let keep = 1.0 - q[3].clamp(0.0, 1.0);
for c in p.iter_mut() {
*c *= keep;
}
});
}
for (b, m) in &st.passes {
self.place(ctx, layer, b, &mut tmp, *m, 1.0);
}
self.place(ctx, layer, &st.body, &mut tmp, layer.blend_mode, 1.0);
let ch = bl.channels;
canvas.data.par_iter_mut().zip(tmp.data.par_iter()).for_each(|(c, t)| {
for i in 0..4 {
let v = if i < 3 && !ch[i] { c[i] } else { t[i] };
c[i] += (v - c[i]) * opacity;
}
});
}
/// Composite a plain layer; returns the canvas pixels it changed when they are known (then
/// also quantised to `quantize` levels).
fn composite_layer(&self, ctx: &EvalCtx, layer: &Layer, buf: &Buf, canvas: &mut Image, opacity: f32, quantize: Option<f32>) -> Option<[u32; 4]> {
let matte = layer.track_matte.is_some_and(|tm| tm.layer != layer.id && ctx.comp.layer(tm.layer).is_some());
if !matte && !layer.preserve_transparency {
return self.place_changed(ctx, layer, buf, canvas, PlacementBlend { mode: layer.blend_mode, opacity, quantize });
}
// Render in isolation, then matte / preserve transparency, then blend.
let mut iso = Image::new(canvas.width, canvas.height);
self.place(ctx, layer, buf, &mut iso, BlendMode::Normal, 1.0);
self.composite_iso(ctx, layer, iso, canvas, opacity);
None
}
/// Apply the track matte / preserve transparency to an isolated layer render, then blend.
pub(crate) fn composite_iso(&self, ctx: &EvalCtx, layer: &Layer, iso: Image, canvas: &mut Image, opacity: f32) {
self.composite_iso_with(ctx, layer, iso, canvas, opacity, None);
}
/// [`Self::composite_iso`] with the track matte's pixels already drawn (`matte`, e.g. a 3D
/// matte seen through the camera); otherwise the matte layer is placed in 2D.
pub(crate) fn composite_iso_with(&self, ctx: &EvalCtx, layer: &Layer, mut iso: Image, canvas: &mut Image, opacity: f32, matte_img: Option<Image>) {
self.apply_track_matte(ctx, layer, &mut iso, matte_img);
if layer.preserve_transparency {
iso.data.par_iter_mut().zip(canvas.data.par_iter()).for_each(|(p, q)| {
for c in p.iter_mut() {
*c *= q[3];
}
});
}
canvas.blend_from(&iso, layer.blend_mode, opacity, layer.id.0 as u32);
}
/// Multiply `img` (comp-sized) by the layer's track matte, if it has one; `matte_img` as in
/// [`Self::composite_iso_with`].
fn apply_track_matte(&self, ctx: &EvalCtx, layer: &Layer, img: &mut Image, matte_img: Option<Image>) {
let matte = layer.track_matte.and_then(|tm| ctx.comp.layer(tm.layer).filter(|m| m.id != layer.id).map(|m| (m, tm.kind)));
if let Some((m, kind)) = matte {
let mimg = match matte_img {
Some(i) if i.width == img.width && i.height == img.height => i,
_ => {
let mut mimg = Image::new(img.width, img.height);
if m.is_active_at(ctx.time)
&& let Some(mb) = self.layer_buf(ctx, m)
{
let mo = ctx.opacity(m) as f32;
self.place(ctx, m, &mb, &mut mimg, BlendMode::Normal, mo);
}
mimg
}
};
img.data.par_iter_mut().zip(mimg.data.par_iter()).for_each(|(p, q)| {
let k = match kind {
MatteKind::Alpha => q[3],
MatteKind::AlphaInverted => 1.0 - q[3],
MatteKind::Luma => effectcraft_color::luminance(q[0], q[1], q[2]),
MatteKind::LumaInverted => 1.0 - effectcraft_color::luminance(q[0], q[1], q[2]),
}
.clamp(0.0, 1.0);
for c in p.iter_mut() {
*c *= k;
}
});
}
}
/// Adjustment layer: effects applied to the comp below, limited to the layer's footprint
/// and its track matte, and blended back in the layer's blend mode.
fn draw_adjustment(&self, ctx: &EvalCtx, layer: &Layer, canvas: &mut Image, opacity: f32) {
let Some(mut foot) = self.source(ctx, layer) else { return };
let _ = masks::apply(ctx, layer, &mut foot, self.mask_blur(ctx, layer));
let mut matte = Image::new(canvas.width, canvas.height);
self.place(ctx, layer, &foot, &mut matte, BlendMode::Normal, 1.0);
self.apply_track_matte(ctx, layer, &mut matte, None);
let o = self.roi_offset().map(|(x, y, _, _)| [-x, -y]).unwrap_or([0.0; 2]);
let mut below = Buf { img: canvas.clone(), offset: o, scale: self.opts.scale };
// Effects run in the working space, not the linear blending space.
if let Some(c) = self.pipe.from_blend() {
color::convert(&mut below.img, &c);
}
let mut adjusted = self.apply_effects(ctx, layer, below, true);
// An effect that changed the frame (Motion Tile's Output size): back onto the canvas.
if let Some((dx, dy)) =
effectcraft_effects::adjustment_crop(adjusted.offset, o, (adjusted.img.width, adjusted.img.height), (canvas.width, canvas.height))
{
adjusted.img = adjusted.img.crop(dx, dy, canvas.width, canvas.height);
}
if let Some(c) = self.pipe.to_blend() {
color::convert(&mut adjusted.img, &c);
}
if adjusted.img.width != canvas.width || adjusted.img.height != canvas.height {
return;
}
if layer.blend_mode != BlendMode::Normal {
// The adjusted comp is a layer in that mode over the comp below, as in After Effects:
// an adjustment layer with no effects in Screen screens the comp with itself (#2618).
let mut iso = adjusted.img;
iso.data.par_iter_mut().zip(matte.data.par_iter()).for_each(|(p, m)| {
for c in p.iter_mut() {
*c *= m[3];
}
});
canvas.blend_from(&iso, layer.blend_mode, opacity, layer.id.0 as u32);
return;
}
canvas.data.par_iter_mut().zip(adjusted.img.data.par_iter()).zip(matte.data.par_iter()).for_each(|((c, a), m)| {
let k = m[3] * opacity;
for i in 0..4 {
c[i] += (a[i] - c[i]) * k;
}
});
}
}
/// A layer ready to composite (see [`Renderer::styled_layer`]).
pub struct StyledLayer {
/// Exterior style passes (Drop Shadow, Outer Glow) with their blend modes.
pub passes: Vec<(Arc<Buf>, BlendMode)>,
pub body: Arc<Buf>,
/// Pre-style pixels (knockout shape).
pub content: Arc<Buf>,
/// Styled cache key (the content key when plain).
pub(crate) key: Option<u64>,
/// Content cache key.
ckey: Option<u64>,
/// No layer styles: composite `body` the usual way.
pub plain: bool,
/// The source files the layer's buffers were made of (see [`crate::reads`]).
pub(crate) files: Arc<crate::reads::ReadFiles>,
}
/// Where a layer buffer lands (see [`Renderer::placement`]).
#[derive(Clone, Debug)]
pub struct Placement {
/// Buffer pixel → output pixel; several = motion-blur sub-samples (equal weights).
pub matrices: Vec<Mat3>,
pub sampling: effectcraft_raster::Sampling,
/// Dissolve noise seed.
pub seed: u32,
}
/// The part of `footprint` a warp actually changed (`drawn`, see [`composite_warp_changed`]):
/// pixels it left alone are still quantised.
fn narrow(footprint: Region, drawn: Option<[u32; 4]>) -> Region {
let Some([x0, y0, x1, y1]) = drawn else { return footprint };
if x0 >= x1 || y0 >= y1 {
return Region::Empty;
}
let r = color::PxRect { x0, y0, x1, y1 };
match footprint {
Region::Empty => Region::Empty,
Region::Full => Region::Rect(r),
Region::Rect(f) => {
let q = color::PxRect { x0: r.x0.max(f.x0), y0: r.y0.max(f.y0), x1: r.x1.min(f.x1), y1: r.y1.min(f.y1) };
if q.x0 >= q.x1 || q.y0 >= q.y1 { Region::Empty } else { Region::Rect(q) }
}
}
}