diff --git a/crates/render/src/accel.rs b/crates/render/src/accel.rs index b26175117..026fe6da1 100644 --- a/crates/render/src/accel.rs +++ b/crates/render/src/accel.rs @@ -2,7 +2,9 @@ use super::frames::content_bounds; use super::fx_host::MAX_FX_DEPTH; use super::layers::StyledLayer; use super::opts::LayerTiming; -use super::*; +use super::{ + Arc, AutoPick, Buf, EffectCtx, EvalCtx, Image, ItemId, ItemKind, Layer, LayerSource, Mat3, MatteKind, Renderer, Tick, cache, color, masks, three_d, vec2, +}; /// Which compositor renders a frame (Project Settings ▸ Video Rendering and Effects). #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] @@ -283,7 +285,7 @@ impl<'a> Renderer<'a> { /// layers). pub fn run_effects_on(&self, ctx: &EvalCtx, layer: &Layer, adjustment: bool, target: &mut dyn FxTarget) { // `target` is used as it is: the comp below for adjustment layers (the callers). - self.run_effect_stack(ctx, layer, adjustment, usize::MAX, None, [0.0; 2], target); + self.run_effect_stack(ctx, layer, crate::pipeline::EffectStack { adjustment, limit: usize::MAX, origin: [0.0; 2] }, None, target); } /// [`Renderer::run_effects_on`] for a (non-adjustment) layer's own pixels: `target` holds @@ -291,7 +293,7 @@ impl<'a> Renderer<'a> { /// there. Returns the effect-space origin in layer coordinates (rebase by its negation). pub fn run_layer_effects_on(&self, ctx: &EvalCtx, layer: &Layer, target: &mut dyn FxTarget) -> [f64; 2] { let origin = ctx.effect_bounds(layer).1; - self.run_effect_stack(ctx, layer, false, usize::MAX, None, origin, target); + self.run_effect_stack(ctx, layer, crate::pipeline::EffectStack { adjustment: false, limit: usize::MAX, origin }, None, target); origin } diff --git a/crates/render/src/audio.rs b/crates/render/src/audio.rs index 68be63fe4..66b54b6d0 100644 --- a/crates/render/src/audio.rs +++ b/crates/render/src/audio.rs @@ -25,7 +25,7 @@ const MAX_DEPTH: usize = 16; /// interleaved (L R L R …). pub fn mix_comp(project: &Project, footage: &dyn FootageSource, expr: Option<&dyn ExprHost>, comp: ItemId, start: Tick, frames: usize, rate: u32) -> Vec { let mut out = vec![0.0f32; frames * 2]; - mix_into(project, footage, expr, comp, start, rate, &mut out, 1.0, 1.0, 0); + mix_into(&AudioSources { project, footage, expr, rate }, comp, start, &mut out, 0); out } @@ -151,7 +151,7 @@ impl MixEnv<'_> { return out; } let mut sub = vec![0.0f32; src_n * 2]; - mix_into(self.project, self.footage, self.expr, *item, lt, rate, &mut sub, 1.0, 1.0, self.depth + 1); + mix_into(&AudioSources { project: self.project, footage: self.footage, expr: self.expr, rate }, *item, lt, &mut sub, self.depth + 1); sub } _ => return out, @@ -206,19 +206,16 @@ impl MixEnv<'_> { } } -#[allow(clippy::too_many_arguments)] -fn mix_into( - project: &Project, - footage: &dyn FootageSource, - expr: Option<&dyn ExprHost>, - comp_id: ItemId, - start: Tick, +/// Shared inputs for composition audio mixing, including nested compositions. +struct AudioSources<'a> { + project: &'a Project, + footage: &'a dyn FootageSource, + expr: Option<&'a dyn ExprHost>, rate: u32, - out: &mut [f32], - gl: f32, - gr: f32, - depth: usize, -) { +} + +fn mix_into(sources: &AudioSources<'_>, comp_id: ItemId, start: Tick, out: &mut [f32], depth: usize) { + let AudioSources { project, footage, expr, rate } = *sources; let Some(comp) = project.comp(comp_id) else { return }; if depth >= MAX_DEPTH || rate == 0 { return; @@ -239,7 +236,6 @@ fn mix_into( } let t_a = sample_tick(s0 + a as i64, rate); let (ll, lr) = levels(&env, l, t_a); - let (ll, lr) = (ll * gl, lr * gr); if ll == 0.0 && lr == 0.0 { continue; } diff --git a/crates/render/src/cache.rs b/crates/render/src/cache.rs index 89e9fd1c9..21f745c0e 100644 --- a/crates/render/src/cache.rs +++ b/crates/render/src/cache.rs @@ -580,7 +580,6 @@ fn key_with(ctx: &EvalCtx, layer: &Layer, scale: f64, draft: bool, blur: bool, f /// `blur`: the layer is motion blurred in this render (`Renderer::mb_on`). `effects`: key only /// the first that many effects (a layer's input, see [`input_key`]). -#[allow(clippy::too_many_arguments)] fn key_any(ctx: &EvalCtx, layer: &Layer, scale: f64, draft: bool, blur: bool, footage: bool, effects: Option) -> Option { let mut h = KeyHasher(0xcbf2_9ce4_8422_2325); ctx.comp.frame_rate.as_f64().to_bits().hash(&mut h); diff --git a/crates/render/src/footage.rs b/crates/render/src/footage.rs index e58ef2a57..04dd706db 100644 --- a/crates/render/src/footage.rs +++ b/crates/render/src/footage.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{Arc, AtomicBool, Footage, Image, ItemId, Ordering, Tick}; /// How a [`FootageSource`] hands over float footage (OpenEXR, float TIFF), whose values are /// linear light: encoded with a curve, as the compositor takes footage, or as stored. Other diff --git a/crates/render/src/frames.rs b/crates/render/src/frames.rs index 19ea785ec..7dd0fc0de 100644 --- a/crates/render/src/frames.rs +++ b/crates/render/src/frames.rs @@ -1,6 +1,6 @@ use super::footage::NoFootage; use super::opts::RenderOpts; -use super::*; +use super::{EvalCtx, Footage, FrameBlend, FrameRate, Image, ItemId, Layer, LayerSource, Project, Renderer, TICKS_PER_SECOND, Tick, shapes, source_size, text}; /// The layer's frame blending mode, if the comp's Enable Frame Blending switch is on. /// One field of an interlaced frame as a full frame: the field's lines (`parity` 0 = even / diff --git a/crates/render/src/fx_host.rs b/crates/render/src/fx_host.rs index 6275af5f0..8366cf7c0 100644 --- a/crates/render/src/fx_host.rs +++ b/crates/render/src/fx_host.rs @@ -1,4 +1,4 @@ -use super::*; +use super::{Arc, Buf, EffectHost, EvalCtx, ItemKind, Layer, LayerPixels, LayerSource, Params, Renderer, Tick, three_d}; /// Layer parameters and audio for effects (see [`effectcraft_effects::EffectHost`]). What it /// hands out is in effect space (see [`effect_bounds`]): the other layer's for its pixels, the diff --git a/crates/render/src/layers.rs b/crates/render/src/layers.rs index c24c37ff8..cac2add43 100644 --- a/crates/render/src/layers.rs +++ b/crates/render/src/layers.rs @@ -1,6 +1,16 @@ use super::frames::adaptive_samples; use super::opts::{LayerTiming, Through}; -use super::*; +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, source_size, styles, vec2, +}; + +/// Blend policy for placing a layer, including optional output-depth quantization. +struct PlacementBlend { + mode: BlendMode, + opacity: f32, + quantize: Option, +} /// 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). @@ -342,22 +352,13 @@ impl<'a> Renderer<'a> { /// 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, mode, opacity, None); + 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`]). - #[allow(clippy::too_many_arguments)] - fn place_changed( - &self, - ctx: &EvalCtx, - layer: &Layer, - buf: &Buf, - target: &mut Image, - mode: BlendMode, - opacity: f32, - quantize: Option, - ) -> Option<[u32; 4]> { + 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() { @@ -459,7 +460,7 @@ impl<'a> Renderer<'a> { fn composite_layer(&self, ctx: &EvalCtx, layer: &Layer, buf: &Buf, canvas: &mut Image, opacity: f32, quantize: Option) -> 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, layer.blend_mode, opacity, quantize); + 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); diff --git a/crates/render/src/opts.rs b/crates/render/src/opts.rs index 9d0b2a39d..c6175bf39 100644 --- a/crates/render/src/opts.rs +++ b/crates/render/src/opts.rs @@ -1,5 +1,5 @@ use super::accel::Backend; -use super::*; +use super::{Comp, EvalCtx, ItemId, Layer, Mat4, Tick, three_d}; #[derive(Clone, Copy, Debug, serde::Serialize, serde::Deserialize)] #[serde(default, rename_all = "camelCase")] diff --git a/crates/render/src/pipeline.rs b/crates/render/src/pipeline.rs index 92b5b7f5c..bc1879369 100644 --- a/crates/render/src/pipeline.rs +++ b/crates/render/src/pipeline.rs @@ -3,7 +3,18 @@ use super::footage::{FootageSource, is_vector_footage}; use super::frames::{blend_frames, content_bounds, field_frame, frame_blend_mode, frame_position, frame_size, interpret_pixels, roi_inside}; use super::fx_host::{FxHost, essential_overrides}; use super::opts::{Collapse3d, LayerTiming, MAX_THROUGH, RenderOpts, Through}; -use super::*; +use super::{ + Arc, AutoKey, AutoPick, BlendMode, Buf, Comp, EffectCtx, EffectEnv, EvalCtx, Footage, FootageKind, FrameBlend, FrameRate, GroupKind, Image, ItemId, + ItemKind, Layer, LayerCache, LayerSource, Params, Project, Quality, Region, Renderer, Tick, cache, color, masks, shapes, styles, text, three_d, vec2, +}; + +/// Coordinate space and stack range shared by CPU and accelerated effect evaluation. +#[derive(Clone, Copy)] +pub(crate) struct EffectStack { + pub adjustment: bool, + pub limit: usize, + pub origin: [f64; 2], +} impl<'a> Renderer<'a> { pub fn new(project: &'a Project, footage: &'a dyn FootageSource, opts: RenderOpts) -> Renderer<'a> { @@ -423,26 +434,16 @@ impl<'a> Renderer<'a> { // The comp below an adjustment layer is not in its layer space: it stays as it is. let origin = if adjustment { [0.0; 2] } else { ctx.effect_bounds(layer).1 }; buf.rebase(origin); - let mut out = self.effect_space_stack(ctx, layer, buf, adjustment, limit, timing, origin); + let mut out = self.effect_space_stack(ctx, layer, buf, EffectStack { adjustment, limit, origin }, timing); out.rebase([-origin[0], -origin[1]]); out } /// [`Renderer::apply_effects_timed`] on a buffer already in effect space (measured from /// `origin`, in layer coordinates); the result stays in effect space. - #[allow(clippy::too_many_arguments)] - fn effect_space_stack( - &self, - ctx: &EvalCtx, - layer: &Layer, - buf: Buf, - adjustment: bool, - limit: usize, - timing: Option<&mut Vec<(String, f64)>>, - origin: [f64; 2], - ) -> Buf { + fn effect_space_stack(&self, ctx: &EvalCtx, layer: &Layer, buf: Buf, stack: EffectStack, timing: Option<&mut Vec<(String, f64)>>) -> Buf { let mut t = CpuFx { accel: self.active_accel(), levels: self.pipe.levels, buf: Some(buf) }; - self.run_effect_stack(ctx, layer, adjustment, limit, timing, origin, &mut t); + self.run_effect_stack(ctx, layer, stack, timing, &mut t); t.buf.unwrap_or_else(|| Buf { img: Image::new(1, 1), offset: [0.0; 2], scale: self.opts.scale }) } @@ -450,17 +451,15 @@ impl<'a> Renderer<'a> { /// in layer coordinates): runs of GPU-capable effects go to [`FxTarget::gpu`] (when an /// accelerator is active and supports them), the rest to [`FxTarget::cpu`] one by one /// (quantised to the bit depth after each). - #[allow(clippy::too_many_arguments)] pub(crate) fn run_effect_stack( &self, ctx: &EvalCtx, layer: &Layer, - adjustment: bool, - limit: usize, + stack: EffectStack, mut timing: Option<&mut Vec<(String, f64)>>, - origin: [f64; 2], target: &mut dyn FxTarget, ) { + let EffectStack { adjustment, limit, origin } = stack; if !layer.switches.effects { return; } @@ -873,7 +872,7 @@ impl<'a> Renderer<'a> { let origin = ctx.effect_bounds(layer).1; buf.rebase(origin); if effects > 0 { - buf = self.effect_space_stack(ctx, layer, buf, false, effects, None, origin); + buf = self.effect_space_stack(ctx, layer, buf, EffectStack { adjustment: false, limit: effects, origin }, None); } let buf = Arc::new(buf); // The buffer the cache keeps (another worker's, if it kept one first). diff --git a/crates/render/src/shapes.rs b/crates/render/src/shapes.rs index f8de5a290..a4e4077f8 100644 --- a/crates/render/src/shapes.rs +++ b/crates/render/src/shapes.rs @@ -211,16 +211,16 @@ fn collect(ctx: &EvalCtx, layer: &Layer, contents: &PropGroup, arena: &mut Vec { let star = ctx.e(layer, g, "type") == 0; - let p = effectcraft_path::polystar( + let p = effectcraft_path::polystar(effectcraft_path::Polystar { star, - ctx.f(layer, g, "points", 5.0), - ctx.v2(layer, g, "position", [0.0; 2]), - ctx.f(layer, g, "rotation", 0.0), - ctx.f(layer, g, "innerRadius", 50.0), - ctx.f(layer, g, "outerRadius", 100.0), - ctx.f(layer, g, "innerRoundness", 0.0), - ctx.f(layer, g, "outerRoundness", 0.0), - ); + points: ctx.f(layer, g, "points", 5.0), + pos: ctx.v2(layer, g, "position", [0.0; 2]), + rotation: ctx.f(layer, g, "rotation", 0.0), + inner: ctx.f(layer, g, "innerRadius", 50.0), + outer: ctx.f(layer, g, "outerRadius", 100.0), + inner_round: ctx.f(layer, g, "innerRoundness", 0.0), + outer_round: ctx.f(layer, g, "outerRoundness", 0.0), + }); push_shape(ctx, layer, g, p, arena, &mut live); } "path" => { diff --git a/crates/render/src/text.rs b/crates/render/src/text.rs index 496bc3a60..e1954b597 100644 --- a/crates/render/src/text.rs +++ b/crates/render/src/text.rs @@ -287,11 +287,13 @@ pub fn char_transforms(ctx: &EvalCtx, layer: &Layer, text: &PropGroup, lay: &Tex let pos = get("position").map(|v| c3(v, 0.0)); let anchor = get("anchor").map(|v| c3(v, 0.0)); let scale = get("scale").map(|v| c3(v, 100.0)); - let rot = get1("rotation"); - let rx = get1("rotationX"); - let ry = get1("rotationY"); - let skew = get1("skew"); - let skew_axis = get1("skewAxis"); + let angles = CharacterAngles { + rotation: get1("rotation"), + rotation_x: get1("rotationX"), + rotation_y: get1("rotationY"), + skew: get1("skew"), + skew_axis: get1("skewAxis"), + }; let op = get1("opacity"); let fill = props.get("fillColor").map(|p| ctx.value(layer, p).as_color()); let stroke = props.get("strokeColor").map(|p| ctx.value(layer, p).as_color()); @@ -338,21 +340,7 @@ pub fn char_transforms(ctx: &EvalCtx, layer: &Layer, text: &PropGroup, lay: &Tex c.scale[d] *= 1.0 + (s[d] / 100.0 - 1.0) * k3[d]; } } - if let Some(r) = rot { - c.rotation += r * k; - } - if let Some(r) = rx { - c.rotation_x += r * k; - } - if let Some(r) = ry { - c.rotation_y += r * k; - } - if let Some(s) = skew { - c.skew += s * k; - } - if let Some(a) = skew_axis { - c.skew_axis = a; - } + angles.apply(c, k); if let Some(o) = op { c.opacity *= 1.0 + (o / 100.0 - 1.0) * k; } @@ -381,17 +369,7 @@ pub fn char_transforms(ctx: &EvalCtx, layer: &Layer, text: &PropGroup, lay: &Tex if let Some(w) = sw { c.stroke_width += w * k; } - if let Some(t) = tracking { - let t = t * k; - match tracking_type { - 1 => c.track_before += t, - 2 => c.track_after += t, - _ => { - c.track_before += t * 0.5; - c.track_after += t * 0.5; - } - } - } + apply_tracking(c, tracking, tracking_type, k); if let Some(a) = line_anchor && k.abs() >= c.line_anchor_k { @@ -432,6 +410,50 @@ pub fn char_transforms(ctx: &EvalCtx, layer: &Layer, text: &PropGroup, lay: &Tex out } +/// Optional animator orientation properties; an omitted property leaves the glyph unchanged. +struct CharacterAngles { + rotation: Option, + rotation_x: Option, + rotation_y: Option, + skew: Option, + skew_axis: Option, +} + +impl CharacterAngles { + fn apply(&self, c: &mut CharXf, k: f64) { + if let Some(r) = self.rotation { + c.rotation += r * k; + } + if let Some(r) = self.rotation_x { + c.rotation_x += r * k; + } + if let Some(r) = self.rotation_y { + c.rotation_y += r * k; + } + if let Some(s) = self.skew { + c.skew += s * k; + } + if let Some(a) = self.skew_axis { + c.skew_axis = a; + } + } +} + +/// Distribute selected tracking before and/or after a glyph according to Tracking Type. +fn apply_tracking(c: &mut CharXf, tracking: Option, tracking_type: u32, k: f64) { + if let Some(t) = tracking { + let t = t * k; + match tracking_type { + 1 => c.track_before += t, + 2 => c.track_after += t, + _ => { + c.track_before += t * 0.5; + c.track_after += t * 0.5; + } + } + } +} + /// Character Offset / Character Value substitution (None = unchanged). pub fn substitute(ch: char, x: &CharXf) -> Option { if ch.is_whitespace() { diff --git a/crates/render/src/three_d/adv/shade.rs b/crates/render/src/three_d/adv/shade.rs index 9acc852fe..0ebd0b1ac 100644 --- a/crates/render/src/three_d/adv/shade.rs +++ b/crates/render/src/three_d/adv/shade.rs @@ -82,9 +82,20 @@ pub fn f_schlick(f0: [f32; 3], v_h: f32) -> [f32; 3] { [f0[0] + (1.0 - f0[0]) * k, f0[1] + (1.0 - f0[1]) * k, f0[2] + (1.0 - f0[2]) * k] } +/// Surface parameters used by the direct-light BRDF after texture sampling. +#[derive(Clone, Copy)] +pub struct BrdfSurface { + pub base: [f32; 3], + pub metallic: f32, + pub roughness: f32, + pub specular: f32, + pub diffuse: f32, +} + /// Cook–Torrance BRDF × N·L × π for one light: (diffuse colour factor, specular colour). /// Returned as the reflected fraction of the light's irradiance. -pub fn brdf(base: [f32; 3], metallic: f32, rough: f32, spec_k: f32, diff_k: f32, n: [f32; 3], v: [f32; 3], l: [f32; 3]) -> [f32; 3] { +pub fn brdf(surface: BrdfSurface, n: [f32; 3], v: [f32; 3], l: [f32; 3]) -> [f32; 3] { + let BrdfSurface { base, metallic, roughness: rough, specular: spec_k, diffuse: diff_k } = surface; let n_l = dot(n, l); if n_l <= 0.0 { return [0.0; 3]; @@ -484,7 +495,7 @@ pub fn shade(s: &Scene, f: &Frag) -> Option<[f32; 4]> { continue; } let sh = if m.receives_shadows { light_shadow(s, l, f.pos, ng, n_l) } else { 1.0 }; - let b = brdf(rgb, metallic, rough, m.specular_k, m.diffuse_k, n, v, to_l); + let b = brdf(BrdfSurface { base: rgb, metallic, roughness: rough, specular: m.specular_k, diffuse: m.diffuse_k }, n, v, to_l); for c in 0..3 { out[c] += b[c] * l.color[c] * k * sh; } diff --git a/crates/render/src/three_d/adv/tests.rs b/crates/render/src/three_d/adv/tests.rs index 3f5ee4813..f9a0f2066 100644 --- a/crates/render/src/three_d/adv/tests.rs +++ b/crates/render/src/three_d/adv/tests.rs @@ -16,7 +16,7 @@ fn brdf_matches_cook_torrance_formula() { let v = shade::norm([0.3, 0.1, 1.0]); let l = shade::norm([-0.4, 0.2, 1.0]); let (base, metal, rough) = ([0.8, 0.4, 0.2], 0.3f32, 0.45f32); - let got = shade::brdf(base, metal, rough, 1.0, 1.0, n, v, l); + let got = shade::brdf(shade::BrdfSurface { base, metallic: metal, roughness: rough, specular: 1.0, diffuse: 1.0 }, n, v, l); let h = shade::norm([v[0] + l[0], v[1] + l[1], v[2] + l[2]]); let (nv, nl, nh, vh) = (shade::dot(n, v), shade::dot(n, l), shade::dot(n, h), shade::dot(v, h)); let a2 = (rough * rough).powi(2); @@ -30,9 +30,9 @@ fn brdf_matches_cook_torrance_formula() { assert!((got[c] - expect).abs() < 1e-5, "channel {c}: {} vs {expect}", got[c]); } // No light from behind the surface. - assert_eq!(shade::brdf(base, metal, rough, 1.0, 1.0, n, v, [0.0, 0.0, -1.0]), [0.0; 3]); + assert_eq!(shade::brdf(shade::BrdfSurface { base, metallic: metal, roughness: rough, specular: 1.0, diffuse: 1.0 }, n, v, [0.0, 0.0, -1.0]), [0.0; 3]); // A white rough dielectric lit head-on reflects about its base colour (π folded in). - let w = shade::brdf([1.0; 3], 0.0, 1.0, 1.0, 1.0, n, n, n); + let w = shade::brdf(shade::BrdfSurface { base: [1.0; 3], metallic: 0.0, roughness: 1.0, specular: 1.0, diffuse: 1.0 }, n, n, n); assert!(w[0] > 0.95 && w[0] < 1.05, "{w:?}"); } diff --git a/crates/render/src/three_d/compose.rs b/crates/render/src/three_d/compose.rs index 929b48d45..8e932819a 100644 --- a/crates/render/src/three_d/compose.rs +++ b/crates/render/src/three_d/compose.rs @@ -197,7 +197,7 @@ fn mb_samples(r: &Renderer, ctx: &EvalCtx, layer: &Layer) -> usize { /// Prepare a layer (buffer, geometry, material). `None` when it shows nothing. pub(crate) fn prepare<'a>(r: &Renderer, ctx: &EvalCtx<'a>, layer: &'a Layer, order: usize, out: (u32, u32), in_run: bool) -> Option> { let buf = r.blend_layer_buf(ctx, layer)?; - prepare_with(r, ctx, layer, order, out, in_run, buf, Mat4::IDENTITY) + prepare_with(r, ctx, layer, order, out, in_run, PlaneInput { buf, local: Mat4::IDENTITY }) } /// Prepare a layer as planes: one per character for per-character 3D text, else one. @@ -205,24 +205,21 @@ pub(crate) fn prepare_all<'a>(r: &Renderer, ctx: &EvalCtx<'a>, layer: &'a Layer, if crate::text::per_char_3d(ctx, layer) { return crate::text::per_char_planes(ctx, layer, r.opts.scale) .into_iter() - .filter_map(|(buf, m)| prepare_with(r, ctx, layer, order, out, in_run, r.to_blend(Arc::new(buf), None), m)) + .filter_map(|(buf, m)| prepare_with(r, ctx, layer, order, out, in_run, PlaneInput { buf: r.to_blend(Arc::new(buf), None), local: m })) .collect(); } prepare(r, ctx, layer, order, out, in_run).into_iter().collect() } -/// Prepare a plane showing `buf`, whose space maps into the layer through `local`. -#[allow(clippy::too_many_arguments)] -fn prepare_with<'a>( - r: &Renderer, - ctx: &EvalCtx<'a>, - layer: &'a Layer, - order: usize, - out: (u32, u32), - in_run: bool, - mut buf: Arc, +/// A processed plane and its mapping into the layer's coordinate system. +struct PlaneInput { + buf: Arc, local: Mat4, -) -> Option> { +} + +/// Prepare the plane's geometry, motion samples and material. +fn prepare_with<'a>(r: &Renderer, ctx: &EvalCtx<'a>, layer: &'a Layer, order: usize, out: (u32, u32), in_run: bool, plane: PlaneInput) -> Option> { + let PlaneInput { mut buf, local } = plane; if buf.img.is_empty() { return None; }