Merge branch 'worktree-agent-aba044eaf08eded94'

# Conflicts:
#	crates/render/src/three_d/compose.rs
This commit is contained in:
Brandon Thomas
2026-10-03 03:33:36 -04:00
58 changed files with 4325 additions and 291 deletions
+55 -4
View File
@@ -53,6 +53,9 @@ pub trait FootageSource: Send + Sync {
/// Set the decoded-frame cache budget in bytes (Settings ▸ Memory & CPU); sources without
/// a cache ignore it.
fn set_cache_budget(&self, _bytes: usize) {}
/// Settings ▸ Disk ▸ Conformed Audio Folder: where decoded audio is kept between reads
/// (`None` = off); sources that don't decode audio ignore it.
fn set_conform_folder(&self, _folder: Option<std::path::PathBuf>) {}
/// The decoded-frame cache budget in bytes, if the source has a cache.
fn cache_budget(&self) -> Option<usize> {
None
@@ -303,6 +306,19 @@ pub struct RenderOpts {
pub roi: Option<[f64; 4]>,
/// Which proxies stand in for footage and compositions (Render Settings ▸ Proxy Use).
pub proxy: effectcraft_project::render_queue::ProxyUse,
/// Switches Affect Nested Comps (Settings ▸ General): a precomp layer's Quality and Motion
/// Blur switches also limit the layers of the nested comp (Draft/Wireframe or motion blur off
/// propagate down; they never raise a nested layer's own setting).
pub nested_switches: bool,
/// Realtime Shadows in Draft (Settings ▸ 3D): draft renders (`draft`) still cast shadows.
pub draft_shadows: bool,
}
impl RenderOpts {
/// Whether 3D layers cast shadows in this render.
pub fn shadows(&self) -> bool {
!self.draft || self.draft_shadows
}
}
impl Default for RenderOpts {
@@ -315,6 +331,8 @@ impl Default for RenderOpts {
view: None,
roi: None,
backend: Backend::Cpu,
nested_switches: true,
draft_shadows: true,
proxy: effectcraft_project::render_queue::ProxyUse::CurrentSettings,
}
}
@@ -342,6 +360,9 @@ pub struct Renderer<'a> {
opacity_mul: f32,
/// Collapsed precomp: nested 3D layers join the parent's 3D space.
pub(crate) collapse3d: Option<Collapse3d<'a>>,
/// Switches inherited from the precomp layers this comp is rendered through (see
/// [`RenderOpts::nested_switches`]): the worst quality and whether motion blur is allowed.
pub(crate) inherited: Option<(Quality, bool)>,
}
/// How a collapsed precomp's 3D layers are placed in the parent (see `Renderer::collapse_into`).
@@ -385,9 +406,39 @@ impl<'a> Renderer<'a> {
outer: None,
opacity_mul: 1.0,
collapse3d: None,
inherited: None,
}
}
/// A layer's Quality switch, limited by the precomp layers above it when
/// [`RenderOpts::nested_switches`] is on.
pub fn quality(&self, layer: &Layer) -> Quality {
let rank = |q: Quality| match q {
Quality::Best => 0,
Quality::Draft => 1,
Quality::Wireframe => 2,
};
match self.inherited {
Some((q, _)) if rank(q) > rank(layer.switches.quality) => q,
_ => layer.switches.quality,
}
}
/// A layer's Motion Blur switch, limited by the precomp layers above it.
pub(crate) fn layer_motion_blur(&self, layer: &Layer) -> bool {
layer.switches.motion_blur && self.inherited.is_none_or(|(_, mb)| mb)
}
/// The renderer for the nested comp of precomp layer `layer`: its switches pass down when
/// [`RenderOpts::nested_switches`] is on.
fn nested_through(&self, layer: &Layer) -> Renderer<'a> {
let mut sub = self.nested();
if self.opts.nested_switches {
sub.inherited = Some((self.quality(layer), self.layer_motion_blur(layer)));
}
sub
}
/// A renderer for nested work (precomps, layers read by effects): one level deeper.
fn nested(&self) -> Renderer<'a> {
Renderer { depth: self.depth + 1, outer: None, opacity_mul: 1.0, collapse3d: None, ..*self }
@@ -769,7 +820,7 @@ impl<'a> Renderer<'a> {
// values.
let ov = essential_overrides(ctx, layer);
let tmp = if ov.is_empty() { None } else { effectcraft_project::essential::with_overrides(self.project, *item, &ov) };
let base = self.nested();
let base = self.nested_through(layer);
let sub = match &tmp {
Some(p) => Renderer { project: p, ..base },
None => base,
@@ -1058,7 +1109,7 @@ impl<'a> Renderer<'a> {
}
fn sampling(&self, layer: &Layer) -> effectcraft_raster::Sampling {
if layer.switches.quality == Quality::Draft {
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 {
@@ -1103,7 +1154,7 @@ impl<'a> Renderer<'a> {
/// Whether a layer is motion blurred.
fn mb_on(&self, ctx: &EvalCtx, layer: &Layer) -> bool {
self.opts.motion_blur && ctx.comp.enable_motion_blur && layer.switches.motion_blur
self.opts.motion_blur && ctx.comp.enable_motion_blur && self.layer_motion_blur(layer)
}
/// Motion-blur sub-samples for a layer buffer (1 = no motion blur). As in After Effects, a 2D
@@ -1227,7 +1278,7 @@ impl<'a> Renderer<'a> {
/// Draw a 2D layer into `canvas`; returns the region it may have changed.
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 layer.switches.quality == Quality::Wireframe && !layer.switches.adjustment {
if self.quality(layer) == Quality::Wireframe && !layer.switches.adjustment {
self.draw_wireframe(ctx, layer, canvas);
return Region::Full;
}
+8 -1
View File
@@ -855,7 +855,14 @@ pub(crate) fn build_at(r: &Renderer, ctx: &EvalCtx, sub: Option<&EvalCtx>, run:
{
add_environment(&mut b, w, h, d, k, rot);
}
super::raster::shadow_maps(&mut b.s, r.opts.draft);
if r.opts.shadows() {
super::raster::shadow_maps(&mut b.s, r.opts.draft);
} else {
// Settings ▸ 3D ▸ Realtime Shadows in Draft off: draft renders skip shadows.
for l in &mut b.s.lights {
l.shadow = -1;
}
}
}
// Camera matrices.
let s = r.opts.scale;
+6 -6
View File
@@ -163,7 +163,7 @@ pub(crate) fn camera_for(r: &Renderer, ctx: &EvalCtx) -> CameraState {
/// Number of motion-blur sub-samples for a layer.
fn mb_samples(r: &Renderer, ctx: &EvalCtx, layer: &Layer) -> usize {
if r.opts.motion_blur && ctx.comp.enable_motion_blur && layer.switches.motion_blur {
if r.opts.motion_blur && ctx.comp.enable_motion_blur && r.layer_motion_blur(layer) {
if r.opts.draft { 4 } else { ctx.comp.motion_blur_samples.clamp(2, 64) as usize }
} else {
1
@@ -366,9 +366,9 @@ pub(crate) fn draw_run(r: &Renderer, ctx: &EvalCtx, run: &[&Layer], canvas: &mut
}
let out = (canvas.width, canvas.height);
let g = gather(r, ctx, run, out);
let casters: Vec<&Item> = g.items.iter().chain(g.extra.iter()).filter(|i| i.mat.casts_shadows != 0 && !i.geos.is_empty()).collect();
let casters: Vec<&Item> = g.items.iter().chain(g.extra.iter()).filter(|i| r.opts.shadows() && i.mat.casts_shadows != 0 && !i.geos.is_empty()).collect();
composite(canvas, &g.items, &g.lights, &casters, run[0].id.0 as u32);
for l in run.iter().filter(|l| l.switches.quality == effectcraft_project::Quality::Wireframe) {
for l in run.iter().filter(|l| r.quality(l) == effectcraft_project::Quality::Wireframe) {
r.draw_layer(ctx, l, canvas, false);
}
}
@@ -387,7 +387,7 @@ fn gather<'a>(r: &Renderer<'a>, ctx: &EvalCtx<'a>, run: &[&'a Layer], out: (u32,
let mut items: Vec<Item> = run
.par_iter()
.enumerate()
.filter(|(_, l)| ctx.opacity(l) > 0.0 && l.switches.quality != effectcraft_project::Quality::Wireframe)
.filter(|(_, l)| ctx.opacity(l) > 0.0 && r.quality(l) != effectcraft_project::Quality::Wireframe)
.flat_map_iter(|(i, l)| {
// Stack order with room for a collapsed precomp's layers in between.
let i = i * 1024;
@@ -682,7 +682,7 @@ pub struct Plane3d {
/// Prepare a run of 3D layers for an accelerator. `None` when the run must be drawn on the CPU
/// (adjustment, wireframe or environment background layers, Advanced 3D).
pub(crate) fn gpu_run(r: &Renderer, ctx: &EvalCtx, run: &[&Layer], out: (u32, u32)) -> Option<Run3d> {
if run.is_empty() || run.iter().any(|l| l.switches.adjustment || l.environment_background || l.switches.quality == effectcraft_project::Quality::Wireframe)
if run.is_empty() || run.iter().any(|l| l.switches.adjustment || l.environment_background || r.quality(l) == effectcraft_project::Quality::Wireframe)
{
return None;
}
@@ -711,7 +711,7 @@ pub(crate) fn gpu_run(r: &Renderer, ctx: &EvalCtx, run: &[&Layer], out: (u32, u3
order: it.order,
})
.collect();
let casters = planes.iter().enumerate().filter(|(_, p)| p.mat.casts_shadows != 0 && !p.geos.is_empty()).map(|(i, _)| i).collect();
let casters = planes.iter().enumerate().filter(|(_, p)| r.opts.shadows() && p.mat.casts_shadows != 0 && !p.geos.is_empty()).map(|(i, _)| i).collect();
Some(Run3d { planes, casters, lights: g.lights, seed: run[0].id.0 as u32 })
}