Merge remote-tracking branch 'origin/main' into fix/environment-light-no-effect-cast-shadows

This commit is contained in:
Z User
2026-10-27 08:54:33 +00:00
488 changed files with 42290 additions and 40792 deletions
-374
View File
@@ -1,374 +0,0 @@
//! 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<f32>) {
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<f32>) {
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<f32>) {
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<super::camera::Dof>,
/// 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<f32>)>;
/// 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<f32>)> {
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<Prepared<'p, 'a>> {
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<Option<(Layer, bool)>>,
}
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<R>(&self, i: usize, f: impl FnOnce(Option<&Prepared<'_, 'a>>) -> R) -> Option<R> {
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<SplitRun<'p, 'a>> {
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;
+1 -1
View File
@@ -230,7 +230,7 @@ const PLANE_SLOPE_MAX: f32 = 64.0;
/// Fraction of light reaching `p` through shadow map `m` (1 = lit). `ng` is the
/// receiver's geometric (not normal-mapped) normal facing the viewer.
///
/// PCF over an N×N tap grid (5 ≤ N ≤ [`PCF_MAX_TAPS`]) spread over a reach derived
/// PCF over an N×N tap grid (5 ≤ N ≤ `PCF_MAX_TAPS`) spread over a reach derived
/// from `radius` (the wanted penumbra in output pixels) divided by the receiver's
/// texel footprint, each tap jittered by an integer hash of the receiver's
/// sub-texel cell and the tap index, and each tap comparing against the four