//! The Advanced 3D renderer (Composition Settings ▸ 3D Renderer ▸ Advanced 3D). //! //! A run of 3D layers becomes one [`Scene`] of world-space triangles (models, primitives, //! extruded text and shapes, and textured cards for every other layer), lit physically //! ([`shade`]) by the comp's lights, an Environment light's image and shadow maps. The scene //! is rasterised with a depth buffer on the GPU when an [`crate::Accelerator`] implements //! [`crate::Accelerator::raster_3d`], otherwise by the software rasteriser ([`raster`]), at 2×2 //! supersampling. The resolve (box filter), depth of field (a gather blur by each pixel's //! circle of confusion, shaped by the camera's iris: [`dof`]) and the conversion back to the working encoding run on the CPU for //! both paths, and the result is composited over the layers below the run. An accelerator's //! compositor runs all of it on the device: [`Prepared`] for whole runs, [`SplitRun`] for runs //! with layers on the 2D compositing path (`draw_run`'s steps as data). pub mod dof; pub mod raster; pub mod scene; pub mod shade; use effectcraft_project::Layer; use rayon::prelude::*; pub use dof::depth_of_field; pub use raster::Target; pub use scene::{EnvInfo, Light, Material, Scene, ShadowMap, TexInfo, Vertex}; use super::compose::camera_for; use crate::{EvalCtx, Image, Renderer}; /// Whether a comp draws its 3D layers with the Advanced 3D renderer: its own setting, or for a /// collapsed precomp's layers, the setting of the outermost comp they collapse into. pub(crate) fn active(r: &Renderer, ctx: &EvalCtx) -> bool { r.collapse3d.map_or(ctx.comp, |c| c.parent.comp).renderer == effectcraft_project::Renderer::Advanced3D } /// Box-filter a supersampled target down by `k`: (premultiplied colour, mean depth of the /// covered subsamples, ∞ where none). pub fn resolve(t: &Target, k: u32) -> (Image, Vec) { let (w, h) = (t.width / k, t.height / k); let mut img = Image::new(w, h); let mut depth = vec![f32::INFINITY; (w * h) as usize]; let n = (k * k) as f32; img.data.par_chunks_mut(w as usize).zip(depth.par_chunks_mut(w as usize)).enumerate().for_each(|(y, (row, drow))| { for x in 0..w as usize { let mut acc = [0.0f32; 4]; let (mut dz, mut dn) = (0.0f32, 0.0f32); for j in 0..k as usize { for i in 0..k as usize { let s = (y * k as usize + j) * t.width as usize + x * k as usize + i; let c = t.color[s]; for q in 0..4 { acc[q] += c[q]; } if t.depth[s].is_finite() { dz += t.depth[s]; dn += 1.0; } } } row[x] = acc.map(|v| v / n); if dn > 0.0 { drow[x] = dz / dn; } } }); (img, depth) } /// Linear → sRGB-encoded premultiplied pixels (the working encoding of non-linear projects). pub fn encode(img: &mut Image) { img.data.par_iter_mut().for_each(|p| { let a = p[3]; if a > 1e-6 { for c in 0..3 { p[c] = shade::linear_to_srgb(p[c] / a) * a; } } }); } /// Render a scene on the CPU, resolved: (premultiplied image at output size, depth). pub fn render_cpu(s: &Scene) -> (Image, Vec) { resolve(&raster::render(s), s.ssaa.max(1)) } /// Build the scene of a run (for tests and tools). pub fn scene_of(r: &Renderer, ctx: &EvalCtx, run: &[&Layer], out: (u32, u32)) -> Scene { scene::build(r, ctx, run, out) } /// Rasterise one scene (GPU when available, else CPU) and resolve it. fn raster_resolved(r: &Renderer, s: &Scene) -> (Image, Vec) { let target = r.active_accel().and_then(|a| a.raster_3d(s)).filter(|t| t.width == s.width && t.height == s.height).unwrap_or_else(|| raster::render(s)); resolve(&target, s.ssaa.max(1)) } /// Number of motion-blur sub-samples for a set of layers (1 = none blurred). fn mb_samples(r: &Renderer, ctx: &EvalCtx, layers: &[&Layer]) -> usize { if !layers.iter().any(|l| scene::motion_blurred(r, ctx, l)) { return 1; } if r.opts.draft { 4 } else { ctx.comp.motion_blur_samples.clamp(2, 64) as usize } } /// An Advanced 3D run prepared for an accelerator ([`crate::Accelerator::render_3d`], the GPU /// compositor's walk): the motion-blur sub-sample scenes (built on demand, one at a time), the /// camera's depth of field and the output encoding. The accelerator rasterises every /// sub-sample, resolves the supersampling, averages the sub-samples (nearest depth), applies /// the depth of field and encodes, as [`render_prepared`] does on the CPU. pub struct Prepared<'p, 'a> { r: &'p Renderer<'a>, ctx: &'p EvalCtx<'a>, layers: &'p [&'p Layer], /// Output size (pixels). pub out: (u32, u32), /// Motion-blur sub-samples (1 = none). pub samples: usize, /// Depth of field (the camera at the frame time; `None` = off, Draft 3D or draft renders). pub dof: Option, /// Render scale (circles of confusion are in comp pixels). pub scale: f64, /// The scene is in the working encoding already (linear projects): no sRGB encode. pub linear: bool, } impl<'p, 'a> Prepared<'p, 'a> { pub fn new(r: &'p Renderer<'a>, ctx: &'p EvalCtx<'a>, layers: &'p [&'p Layer], out: (u32, u32)) -> Self { let cam = camera_for(r, ctx); Prepared { r, ctx, layers, out, samples: mb_samples(r, ctx, layers), dof: cam.dof.filter(|_| !ctx.comp.draft_3d && !r.opts.draft), scale: r.opts.scale, linear: r.pipe.linear || r.pipe.linear_blend, } } /// The scene of sub-sample `i` (`0..samples`), spread over the comp's shutter (angle and /// phase). pub fn scene(&self, i: usize) -> Scene { let n = self.samples; if n <= 1 { return scene::build(self.r, self.ctx, self.layers, self.out); } let ctx = self.ctx; let fd = ctx.comp.frame_duration().seconds(); let (angle, phase) = (ctx.comp.shutter_angle / 360.0, ctx.comp.shutter_phase / 360.0); 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)); scene::build_at(self.r, ctx, Some(&sub), self.layers, self.out) } } /// What an accelerator returns for a [`Prepared`] run: `None` when nothing was drawn, else the /// premultiplied image in the canvas's encoding and the camera depth of each pixel (∞ where /// nothing was drawn; the nearest over the motion-blur sub-samples). pub type Rendered = Option<(Image, Vec)>; /// Render a prepared run on the CPU compositor (each scene rasterised by the accelerator's /// [`crate::Accelerator::raster_3d`] when there is one): the reference for /// [`crate::Accelerator::render_3d`]. pub fn render_prepared(p: &Prepared) -> Rendered { let (r, n, out) = (p.r, p.samples, p.out); let (mut img, depth) = if n <= 1 { let s = p.scene(0); if s.indices.is_empty() { return None; } raster_resolved(r, &s) } else { let mut acc = Image::new(out.0, out.1); let mut depth = vec![f32::INFINITY; (out.0 * out.1) as usize]; let mut any = false; let k = 1.0 / n as f32; for i in 0..n { let s = p.scene(i); if s.indices.is_empty() { continue; } any = true; let (img, d) = raster_resolved(r, &s); acc.data.par_iter_mut().zip(img.data.par_iter()).for_each(|(a, p)| { for c in 0..4 { a[c] += p[c] * k; } }); depth.par_iter_mut().zip(d.par_iter()).for_each(|(a, b)| *a = a.min(*b)); } if !any { return None; } (acc, depth) }; if let Some(dof) = &p.dof { img = depth_of_field(&img, &depth, dof, p.scale); } if !p.linear { encode(&mut img); } Some((img, depth)) } /// Render `layers` as one Advanced 3D scene: premultiplied pixels in the canvas's encoding and /// the camera depth of each pixel (∞ where nothing was drawn). With motion blur the scene is /// rendered at sub-samples spread over the comp's shutter (angle and phase) and averaged; the /// depth is the nearest over the samples. The whole run goes to the accelerator when it /// implements [`crate::Accelerator::render_3d`]. pub(crate) fn render_layers(r: &Renderer, ctx: &EvalCtx, layers: &[&Layer], out: (u32, u32)) -> Option<(Image, Vec)> { let p = Prepared::new(r, ctx, layers, out); if let Some(a) = r.active_accel() && let Some(res) = a.render_3d(&p) { return res; } render_prepared(&p) } /// The run as one prepared scene for an accelerator's compositor when it can be composited /// directly over the canvas (Normal-mode layers without track mattes or Preserve Transparency; /// those go through `draw_run`'s 2D path; environment backgrounds draw their sky first). `None` /// when the comp isn't Advanced 3D. pub(crate) fn prepare_run<'p, 'a>(r: &'p Renderer<'a>, ctx: &'p EvalCtx<'a>, run: &'p [&'p Layer], out: (u32, u32)) -> Option> { if !active(r, ctx) || run.iter().any(|l| l.environment_background || needs_2d_composite(ctx, l)) { return None; } Some(Prepared::new(r, ctx, run, out)) } /// A run of an Advanced 3D comp whose layers need the 2D compositing path (blend modes, track /// mattes, Preserve Transparency), prepared for an accelerator's compositor: `draw_run`'s /// steps as data. The plain layers render as one scene ([`SplitRun::main`]) composited over the /// canvas; then each special layer, from the farthest to the nearest, renders on its own /// ([`SplitRun::special`]), is hidden where the main scene is nearer, and goes through the 2D /// path with its track matte (a 3D matte layer drawn through the camera: /// [`SplitRun::with_matte3d`]). pub struct SplitRun<'p, 'a> { r: &'p Renderer<'a>, ctx: &'p EvalCtx<'a>, out: (u32, u32), plain: Vec<&'p Layer>, /// Far to near. special: Vec<&'p Layer>, /// Per special layer: its 3D track matte drawn solo (and whether it is active now). mattes: Vec>, } impl<'p, 'a> SplitRun<'p, 'a> { /// The plain layers as one scene. pub fn main(&self) -> Prepared<'_, 'a> { Prepared::new(self.r, self.ctx, &self.plain, self.out) } /// Number of special layers. pub fn len(&self) -> usize { self.special.len() } pub fn is_empty(&self) -> bool { self.special.is_empty() } /// Special layer `i` (far to near). pub fn layer(&self, i: usize) -> &'p Layer { self.special[i] } /// Special layer `i` rendered on its own. pub fn special(&self, i: usize) -> Prepared<'_, 'a> { Prepared::new(self.r, self.ctx, std::slice::from_ref(&self.special[i]), self.out) } /// `f` with special layer `i`'s 3D track matte as a prepared run (`None` when the matte /// layer isn't active: a transparent matte). `None` when the layer has no 3D matte (its /// matte, if any, is placed in 2D as usual). pub fn with_matte3d(&self, i: usize, f: impl FnOnce(Option<&Prepared<'_, 'a>>) -> R) -> Option { let (solo, active) = self.mattes[i].as_ref()?; if !*active { return Some(f(None)); } let layers = [solo]; let p = Prepared::new(self.r, self.ctx, &layers, self.out); Some(f(Some(&p))) } } /// The run split for an accelerator when some layer needs the 2D compositing path (`None` when /// the comp isn't Advanced 3D, a layer is an environment background, or no layer needs it: /// `prepare_run` then covers the run). pub(crate) fn split_run<'p, 'a>(r: &'p Renderer<'a>, ctx: &'p EvalCtx<'a>, run: &'p [&'p Layer], out: (u32, u32)) -> Option> { if !active(r, ctx) || run.iter().any(|l| l.environment_background) || !run.iter().any(|l| needs_2d_composite(ctx, l)) { return None; } let (special, plain): (Vec<&Layer>, Vec<&Layer>) = run.iter().copied().partition(|l| needs_2d_composite(ctx, l)); let special = far_to_near(r, ctx, special); let mattes = special.iter().map(|l| solo_matte(ctx, l)).collect(); Some(SplitRun { r, ctx, out, plain, special, mattes }) } /// Special layers sorted by their anchor's camera depth, farthest first. fn far_to_near<'p>(r: &Renderer, ctx: &EvalCtx, special: Vec<&'p Layer>) -> Vec<&'p Layer> { let cam = camera_for(r, ctx); let mut order: Vec<(f64, &Layer)> = special .into_iter() .map(|l| { let anchor = l.transform().map_or([0.0; 3], |tr| ctx.v3(l, tr, "anchor", [0.0; 3])); (cam.depth(ctx.world_matrix(l).apply(effectcraft_geom::Vec3::from(anchor))), l) }) .collect(); order.sort_by(|a, b| b.0.total_cmp(&a.0)); order.into_iter().map(|(_, l)| l).collect() } /// A layer's 3D track matte, drawn through the camera on its own (no matte, Normal mode, no /// Preserve Transparency, visible), and whether it is active now. fn solo_matte(ctx: &EvalCtx, l: &Layer) -> Option<(Layer, bool)> { let m = l.track_matte.and_then(|tm| ctx.comp.layer(tm.layer).filter(|m| m.id != l.id && m.is_3d()))?; let mut solo = m.clone(); solo.track_matte = None; solo.blend_mode = effectcraft_color::BlendMode::Normal; solo.preserve_transparency = false; solo.switches.video = true; Some((solo, m.is_active_at(ctx.time))) } /// Layers whose blend mode, track matte or Preserve Transparency needs the 2D compositing path. fn needs_2d_composite(ctx: &EvalCtx, l: &Layer) -> bool { l.blend_mode != effectcraft_color::BlendMode::Normal || l.preserve_transparency || l.track_matte.is_some_and(|tm| tm.layer != l.id && ctx.comp.layer(tm.layer).is_some()) } /// Draw a run of 3D layers with the Advanced 3D renderer into `canvas`. /// /// Layers with a blend mode, a track matte or Preserve Transparency can't blend inside the /// depth-buffered scene: they are rendered on their own (lit by the same lights, through the /// same camera), hidden where the rest of the run is nearer, then composited through the 2D /// path (matte, Preserve Transparency, blend mode) from the farthest to the nearest. pub(crate) fn draw_run(r: &Renderer, ctx: &EvalCtx, run: &[&Layer], canvas: &mut Image) { let out = (canvas.width, canvas.height); let (special, plain): (Vec<&Layer>, Vec<&Layer>) = run.iter().copied().partition(|l| needs_2d_composite(ctx, l)); let main = render_layers(r, ctx, &plain, out); if let Some((img, _)) = &main { canvas.data.par_iter_mut().zip(img.data.par_iter()).for_each(|(d, s)| { let k = 1.0 - s[3]; *d = [s[0] + d[0] * k, s[1] + d[1] * k, s[2] + d[2] * k, s[3] + d[3] * k]; }); } if special.is_empty() { return; } for l in far_to_near(r, ctx, special) { let Some((mut iso, depth)) = render_layers(r, ctx, &[l], out) else { continue }; if let Some((m, md)) = &main { iso.data.par_iter_mut().zip(depth.par_iter()).zip(m.data.par_iter().zip(md.par_iter())).for_each(|((p, z), (q, mz))| { if *mz < *z - 0.01 * z.abs().max(1.0) { let k = 1.0 - q[3].clamp(0.0, 1.0); for c in p.iter_mut() { *c *= k; } } }); } // A 3D matte layer is drawn through the camera too. let matte3d = solo_matte(ctx, l).map(|(solo, active)| { if active { render_layers(r, ctx, &[&solo], out).map(|(i, _)| i).unwrap_or_else(|| Image::new(out.0, out.1)) } else { Image::new(out.0, out.1) } }); r.composite_iso_with(ctx, l, iso, canvas, 1.0, matte3d); } } #[cfg(test)] mod tests;