diff --git a/crates/gpu/src/adv3d.rs b/crates/gpu/src/adv3d.rs index 677db3e5..4a7a6723 100644 --- a/crates/gpu/src/adv3d.rs +++ b/crates/gpu/src/adv3d.rs @@ -358,7 +358,7 @@ pub(crate) struct Post { dummy_out: wgpu::TextureView, } -const KERNELS: &[&str] = &["resolve", "radius_of", "boost", "prefix_rows", "gather", "finish", "wire"]; +const KERNELS: &[&str] = &["resolve", "radius_of", "boost", "prefix_rows", "gather", "finish", "wire", "occlude"]; impl Post { fn new(device: &wgpu::Device) -> Post { @@ -544,7 +544,7 @@ fn read_buffer(e: &mut Enc, buf: &wgpu::Buffer, size: u64) -> Option> { /// A resolved run on the GPU: premultiplied linear (or working-space) colour and camera depth /// (`BIG` where nothing was drawn), one entry per output pixel. -struct Resolved { +pub(crate) struct Resolved { acc: wgpu::Buffer, depth: wgpu::Buffer, width: u32, @@ -557,7 +557,7 @@ const BIG: f32 = 3.0e38; /// Rasterise every motion-blur sub-sample of a prepared run, resolve and average them, then /// apply the depth of field (`render_prepared`'s steps). `None` = can't run here (sizes, /// limits, or a depth of field without readback); `Some(None)` = nothing drawn. -fn resolve_run(e: &mut Enc, prep: &Prepared) -> Option> { +pub(crate) fn resolve_run(e: &mut Enc, prep: &Prepared) -> Option> { let (w, h) = prep.out; let g = e.g; if !g.fits(w, h) || (prep.dof.is_some() && !g.can_readback()) { @@ -661,7 +661,7 @@ fn depth_of_field(e: &mut Enc, acc: &wgpu::Buffer, depth: &wgpu::Buffer, (w, h): } /// The resolved run as an image in the canvas's encoding, composited over `canvas` when given. -fn finish(e: &mut Enc, r: &Resolved, encode: bool, canvas: Option<&GpuImage>) -> GpuImage { +pub(crate) fn finish(e: &mut Enc, r: &Resolved, encode: bool, canvas: Option<&GpuImage>) -> GpuImage { let out = e.image(r.width, r.height); let ov = out.texture.create_view(&Default::default()); let cv = canvas.map(|c| c.texture.create_view(&Default::default())); @@ -704,6 +704,38 @@ pub(crate) fn draw_run(e: &mut Enc, prep: &Prepared, canvas: &GpuImage) -> Optio } } +/// A layer rendered on its own (`iso`), finished, and hidden where the run's main scene is +/// nearer (`adv::draw_run`'s 2D compositing path). +pub(crate) fn occluded(e: &mut Enc, iso: &Resolved, main: &Resolved, encode: bool) -> GpuImage { + let img = finish(e, iso, encode, None); + let (w, h) = (iso.width, iso.height); + let out = e.image(w, h); + let (iv, ov) = (img.texture.create_view(&Default::default()), out.texture.create_view(&Default::default())); + let b = + Binds { color: Some(&iv), bufs: [None, Some(&iso.depth), Some(&main.acc), None, Some(&main.depth), None, None], out: Some(&ov), ..Default::default() }; + dispatch(e, "occlude", [w, h, 0, 0], [0.0; 4], b, pixel_groups(w, h)); + out +} + +/// Kernels of `sky.wgsl` (the shared compute module). +pub(crate) const SKY_KERNELS: &[&str] = &["adv_sky"]; + +/// An Environment Light Background layer drawn over `canvas` (`three_d::compose::draw_sky`). +pub(crate) fn sky(e: &mut Enc, sky: &effectcraft_render::three_d::SkyDraw, canvas: &GpuImage) -> Option { + let img = e.g.upload_buf(&sky.buf)?; + let f3 = |v: [f64; 3], w: f64| [v[0] as f32, v[1] as f32, v[2] as f32, w as f32]; + let mut p = crate::context::Params::default(); + p.f[0] = f3(sky.fwd, sky.zoom); + p.f[1] = f3(sky.right, sky.ortho as u32 as f64); + p.f[2] = [sky.down[0] as f32, sky.down[1] as f32, sky.down[2] as f32, sky.rotation]; + let s = sky.scale; + p.f[3] = [(sky.roi.0 / s - sky.view.0 / 2.0) as f32, (sky.roi.1 / s - sky.view.1 / 2.0) as f32, (1.0 / s) as f32, sky.opacity]; + p.f[4] = [img.width as f32, img.height as f32, 0.0, 0.0]; + let out = e.scratch(canvas.width, canvas.height); + e.pixels("adv_sky", &p, canvas, Some(&img), &out, None); + Some(out) +} + /// Wireframe-quality layer outlines: `canvas` with `pixels` set to opaque white /// ([`effectcraft_render::Renderer::wireframe_pixels`]). pub(crate) fn wireframe(e: &mut Enc, canvas: &GpuImage, pixels: &[(u32, u32)]) -> GpuImage { diff --git a/crates/gpu/src/context.rs b/crates/gpu/src/context.rs index a8d2e372..afea936c 100644 --- a/crates/gpu/src/context.rs +++ b/crates/gpu/src/context.rs @@ -318,6 +318,7 @@ impl GpuContext { include_str!("shaders/fx_transition.wgsl"), include_str!("shaders/fx_text.wgsl"), include_str!("shaders/fx_time.wgsl"), + include_str!("shaders/sky.wgsl"), ] .concat(); let module = @@ -381,6 +382,7 @@ impl GpuContext { .chain(crate::fx_transition::KERNELS) .chain(crate::fx_text::KERNELS) .chain(crate::fx_time::KERNELS) + .chain(crate::adv3d::SKY_KERNELS) .map(|e| (e, &layout)) .chain(EXT_ENTRIES.iter().map(|e| (e, &layout_ext))) .map(|(e, layout)| { diff --git a/crates/gpu/src/lib.rs b/crates/gpu/src/lib.rs index 3c75ba88..5abf9d82 100644 --- a/crates/gpu/src/lib.rs +++ b/crates/gpu/src/lib.rs @@ -13,15 +13,18 @@ //! Advanced 3D comps rasterise on a render pipeline (`advanced3d.wgsl`: depth buffer, PBR, //! image-based light, shadow maps) and finish in compute kernels (`adv3d.wgsl`: supersampling //! resolve, motion-blur sub-samples, iris depth of field, encoding, compositing), see -//! [`Accelerator::render_3d`] and `Renderer::prepare_adv_run`. Advanced 3D layers with blend -//! modes, track mattes or Preserve Transparency still composite on the CPU (their scenes -//! render here through [`Accelerator::render_3d`]). +//! [`Accelerator::render_3d`] and `Renderer::prepare_adv_run`. Runs with layers on the 2D +//! compositing path (blend modes, track mattes, Preserve Transparency: +//! `Renderer::split_adv_run`) render each such layer alone, hide it behind the nearer main +//! scene and composite it through the 2D path here too; environment backgrounds (Classic and +//! Advanced 3D) draw their sky in a kernel (`Renderer::sky_draw`). //! //! GPU effects ([`effectcraft_effects::GPU_EFFECTS`]) run as compute kernels with the CPU //! effect's exact steps (padding, box-blur radii, parameter conversions); chains of them are //! uploaded and read back once. Each family lives in its own module with its own WGSL file -//! (`fx_color`, `fx_distort`, `fx_extra`, `fx_generate`, `fx_key`, `fx_noise`, `fx_stylize`, -//! `fx_tone`, `fx_warp`); settings a kernel cannot match fall back to the CPU +//! (`fx_color`, `fx_distort`, `fx_extra`, `fx_generate`, `fx_key`, `fx_light`, `fx_noise`, +//! `fx_stylize`, `fx_text`, `fx_time`, `fx_tone`, `fx_transition`, `fx_warp`); settings a +//! kernel cannot match fall back to the CPU //! (`catalog::gpu_supported`, or `None` from the family's `apply`). //! //! Working textures come from a pool (`context::Pool`): a frame allocates several full-frame diff --git a/crates/gpu/src/shaders/adv3d.wgsl b/crates/gpu/src/shaders/adv3d.wgsl index 35e5a1bc..1a2d3c81 100644 --- a/crates/gpu/src/shaders/adv3d.wgsl +++ b/crates/gpu/src/shaders/adv3d.wgsl @@ -255,3 +255,24 @@ fn wire(@builtin(global_invocation_id) id: vec3) { let p = table[id.x]; textureStore(out, vec2(i32(p.x), i32(p.y)), vec4(1.0)); } + +// A layer rendered on its own (color_in, finished) hidden where the run's main scene is nearer +// (`adv::draw_run`): depth = its depth, src = the main scene's colour, radius = the main +// scene's depth. Pixels it didn't draw (BIG) are never hidden, as ∞ compares on the CPU. +// P.u0 = (width, height) +@compute @workgroup_size(16, 16) +fn occlude(@builtin(global_invocation_id) id: vec3) { + let w = P.u0.x; + if (id.x >= w || id.y >= P.u0.y) { + return; + } + let i = id.y * w + id.x; + let q = vec2(i32(id.x), i32(id.y)); + var p = textureLoad(color_in, q, 0); + let z = depth[i]; + let mz = radius[i]; + if (z < BIG * 0.5 && mz < z - 0.01 * max(abs(z), 1.0)) { + p *= 1.0 - clamp(src[i].w, 0.0, 1.0); + } + textureStore(out, q, p); +} diff --git a/crates/gpu/src/shaders/sky.wgsl b/crates/gpu/src/shaders/sky.wgsl new file mode 100644 index 00000000..4ee6b460 --- /dev/null +++ b/crates/gpu/src/shaders/sky.wgsl @@ -0,0 +1,32 @@ +// Environment Light Background layers (`three_d::compose::draw_sky`): the layer's +// equirectangular image looked up by each pixel's view ray through the camera, composited over +// the canvas. src = the canvas, aux = the layer's buffer. f[0] = (forward, zoom); f[1] = +// (right, orthographic); f[2] = (down, environment rotation); f[3] = (x offset, y offset, +// 1 / scale, opacity) with the offsets ROI / scale − view size / 2; f[4] = (image width, height). + +@compute @workgroup_size(16, 16) +fn adv_sky(@builtin(global_invocation_id) gid: vec3) { + let p = vec2(gid.xy); + let dims = out_dims(); + if (p.x >= dims.x || p.y >= dims.y) { + return; + } + let fwd = P.f[0].xyz; + var dir = fwd; + if (P.f[1].w == 0.0) { + let cx = (f32(p.x) + 0.5) * P.f[3].z + P.f[3].x; + let cy = (f32(p.y) + 0.5) * P.f[3].z + P.f[3].y; + dir = normalize(fwd * P.f[0].w + P.f[1].xyz * cx + P.f[2].xyz * cy); + } + // shade::equirect_uv. + let pi = 3.141592653589793; + var u = 0.5 + (atan2(dir.x, dir.z) + P.f[2].w) / (2.0 * pi); + let v = acos(clamp(-dir.y, -1.0, 1.0)) / pi; + u = u - floor(u); + let iw = P.f[4].x; + let ih = P.f[4].y; + let s = sample_bilinear(aux, clamp(u * iw, 0.5, iw - 0.5), clamp(v * ih, 0.5, ih - 0.5)); + let op = P.f[3].w; + let d = textureLoad(src, p, 0); + textureStore(out, p, s * op + d * (1.0 - s.w * op)); +} diff --git a/crates/gpu/src/tests_adv3d.rs b/crates/gpu/src/tests_adv3d.rs index 596c41a4..736931af 100644 --- a/crates/gpu/src/tests_adv3d.rs +++ b/crates/gpu/src/tests_adv3d.rs @@ -356,7 +356,7 @@ fn gpu_compositor_draws_advanced_3d_runs_on_the_device() { let (share, max) = off_share(&cpu.data, &gimg.data); eprintln!("walk: {:.3} % beyond 1/255 (max {max:.4})", share * 100.0); assert!(share <= 0.01, "{:.3} % of pixels differ", share * 100.0); - // A blend mode in the run takes the CPU's 2D path (its scene still renders on the GPU). + // A blend mode in the run takes the 2D compositing path (`SplitRun`, also on the GPU). p.comp_mut(cid).unwrap().layers.iter_mut().find(|l| l.switches.three_d && !l.is_light() && l.source.is_av()).unwrap().blend_mode = effectcraft_color::BlendMode::Screen; let (ctx, run) = run_of(&p, cid); @@ -440,3 +440,90 @@ fn gpu_environment_light_on_flat_extrusions_has_no_nan() { assert_eq!(nan, 0, "non-finite pixels"); compare(&s, &g); } + +/// Advanced 3D runs whose layers take the 2D compositing path (blend modes, a 2D and a 3D track +/// matte, Preserve Transparency) and environment backgrounds composite on the GPU: against the +/// CPU compositor on the same (GPU) rasters they agree to float rounding; against the CPU +/// rasteriser within the raster tolerance. +#[test] +fn gpu_compositor_draws_split_advanced_3d_runs_and_skies() { + let Some(g) = gpu() else { return }; + for case in 0..10 { + let (mut p, cid) = scene_project(); + if case >= 4 { + p.settings.bit_depth = effectcraft_project::BitDepth::Bpc32; + } + let classic = case >= 8; + let case = if classic { 3 } else { case % 4 }; + if classic { + // Classic 3D: the sky on the GPU too, then the run's planes. + p.comp_mut(cid).unwrap().renderer = R3::Classic3D; + } + let bg = p.add_item("B", Default::default(), None, ItemKind::Solid(Solid { color: [0.1, 0.1, 0.15], width: 240, height: 160, pixel_aspect: 1.0 })); + let comp = p.comp(cid).unwrap().clone(); + let l = build::layer(&mut p, &comp, "B", LayerSource::Solid { item: bg }, (240, 160), None); + p.comp_mut(cid).unwrap().layers.push(l); + // A varying environment image (so the sky's lookup shows; a CPU effect, so the frame's + // only readback is its own). + let mut next = p.next_id; + let spec = effectcraft_effects::find("ec.generate.lensflare").unwrap(); + let ramp = effectcraft_effects::instantiate(spec, &mut build::Ids(&mut next), spec.name, [32.0, 16.0]); + p.next_id = next; + if case == 3 { + // A 2D track matte layer above the run. + let sid = p.add_item("M", Default::default(), None, ItemKind::Solid(Solid { color: [1.0, 1.0, 1.0], width: 120, height: 90, pixel_aspect: 1.0 })); + add(&mut p, cid, LayerSource::Solid { item: sid }, |m| { + m.switches.video = false; + set(m, "transform/rotation", Value::Scalar(20.0)); + }); + } + let c = p.comp_mut(cid).unwrap(); + let env = c.layers.iter_mut().find(|l| l.environment).unwrap(); + env.props.sub_mut("effects").unwrap().children.push(ramp.into()); + let ids: Vec<_> = c.layers.iter().filter(|l| l.switches.three_d && !l.is_light() && !l.environment && l.source.is_av()).map(|l| l.id).collect(); + let top = c.layers[0].id; + fn by_id(c: &mut Comp, id: effectcraft_project::LayerId) -> &mut effectcraft_project::Layer { + c.layers.iter_mut().find(|l| l.id == id).unwrap() + } + match case { + 0 => by_id(c, ids[1]).blend_mode = effectcraft_color::BlendMode::Screen, + 1 => { + // The card is a 3D track matte (luma) for the sphere; the torus multiplies. + let card = ids[0]; + by_id(c, ids[1]).blend_mode = effectcraft_color::BlendMode::Multiply; + by_id(c, ids[2]).track_matte = Some(effectcraft_project::TrackMatte { layer: card, kind: effectcraft_project::MatteKind::Luma }); + by_id(c, ids[0]).switches.video = false; + } + 2 => { + // Preserve Transparency and an Overlay layer. + by_id(c, ids[0]).preserve_transparency = true; + by_id(c, ids[1]).blend_mode = effectcraft_color::BlendMode::Overlay; + } + _ => { + // An environment background behind the run, and the 2D track matte. + c.layers.iter_mut().find(|l| l.environment).unwrap().environment_background = true; + by_id(c, ids[0]).track_matte = Some(effectcraft_project::TrackMatte { layer: top, kind: effectcraft_project::MatteKind::Alpha }); + } + } + let (ctx, run) = run_of(&p, cid); + let r0 = Renderer::new(&p, &NoFootage, RenderOpts::default()); + if case < 3 { + assert!(r0.split_adv_run(&ctx, &run, (240, 160)).is_some(), "case {case}: split"); + } + let cpu = r0.comp_frame(cid, Tick::ZERO); + let mut r = Renderer::new(&p, &NoFootage, RenderOpts { backend: effectcraft_render::Backend::Gpu, ..Default::default() }); + r.accel = Some(&g); + // The CPU compositor on the GPU's rasters. + let hybrid = r.comp_frame_cpu(cid, Tick::ZERO); + let before = g.context().transfer_stats().readbacks; + let gimg = g.render(&r, cid, Tick::ZERO).expect("gpu walk"); + // On the device end to end: the frame's own readback only (no CPU fallback). + assert_eq!(g.context().transfer_stats().readbacks - before, 1, "case {case}: readbacks"); + let (share, max) = off_share(&hybrid.data, &gimg.data); + eprintln!("split case {case}: gpu walk vs cpu compositing {:.4} % beyond 1/255 (max {max:.4})", share * 100.0); + assert!(share <= 0.001, "case {case}: {:.3} % of pixels differ (max {max})", share * 100.0); + let (share, max) = off_share(&cpu.data, &gimg.data); + eprintln!("split case {case}: gpu vs cpu {:.3} % beyond 1/255 (max {max:.4})", share * 100.0); + assert!(share <= 0.015, "case {case}: {:.3} % of pixels differ from the CPU", share * 100.0); + } +} diff --git a/crates/gpu/src/walk.rs b/crates/gpu/src/walk.rs index a8075e10..b5bd166c 100644 --- a/crates/gpu/src/walk.rs +++ b/crates/gpu/src/walk.rs @@ -1,9 +1,9 @@ //! The per-frame compositing walk on the GPU: the same steps as `Renderer::draw_comp` (layer //! order, 3D runs, collapsed precomps, track mattes, Preserve Transparency, layer styles, motion //! blur, bit-depth quantisation, colour conversions), with layer pixels taken from the CPU -//! renderer and its layer cache. Classic 3D runs and adjustment layers run on the GPU; steps -//! without a GPU implementation (Advanced 3D runs, wireframes, anything a kernel declines) read -//! the canvas back, run on the CPU and upload the result. +//! renderer and its layer cache. Classic and Advanced 3D runs (environment skies included) and +//! adjustment layers run on the GPU; steps without a GPU implementation (anything a kernel +//! declines) read the canvas back, run on the CPU and upload the result. use std::sync::Arc; @@ -99,6 +99,17 @@ fn draw_comp<'a>(e: &mut Enc, r: &Renderer<'a>, ctx: &EvalCtx<'a>, canvas: &mut /// layers splitting the run as on the CPU; Advanced 3D, wireframes and runs the kernel cannot /// hold draw on the CPU. fn draw_3d<'a>(e: &mut Enc, r: &Renderer<'a>, ctx: &EvalCtx<'a>, run: &[&'a Layer], canvas: &mut GpuImage) -> Option<()> { + // Environment Light Background layers: the sky behind the rest of the run (drawn first, as + // `three_d::compose::draw_run` does). + if run.iter().any(|l| l.environment_background) { + for l in run.iter().filter(|l| l.environment_background) { + if let Some(sky) = r.sky_draw(ctx, l) { + *canvas = crate::adv3d::sky(e, &sky, canvas)?; + } + } + let rest: Vec<&'a Layer> = run.iter().copied().filter(|l| !l.environment_background).collect(); + return draw_3d(e, r, ctx, &rest, canvas); + } if let Some(k) = run.iter().position(|l| l.switches.adjustment) { draw_3d(e, r, ctx, &run[..k], canvas)?; draw_layer(e, r, ctx, run[k], canvas)?; @@ -115,6 +126,15 @@ fn draw_3d<'a>(e: &mut Enc, r: &Renderer<'a>, ctx: &EvalCtx<'a>, run: &[&'a Laye } return Some(()); } + // Advanced 3D with layers on the 2D compositing path (blend modes, track mattes, Preserve + // Transparency). + if let Some(split) = r.split_adv_run(ctx, run, (canvas.width, canvas.height)) { + match draw_split(e, r, ctx, &split, canvas) { + Some(img) => *canvas = img, + None => on_cpu(e, canvas, |img| r.draw_3d_run(ctx, run, img))?, + } + return Some(()); + } match r.prepare_3d_run(ctx, run, (canvas.width, canvas.height)).and_then(|prep| crate::classic3d::draw_run(e, &prep, canvas)) { Some(img) => *canvas = img, None => on_cpu(e, canvas, |img| r.draw_3d_run(ctx, run, img))?, @@ -122,6 +142,44 @@ fn draw_3d<'a>(e: &mut Enc, r: &Renderer<'a>, ctx: &EvalCtx<'a>, run: &[&'a Laye Some(()) } +/// `three_d::adv::draw_run`'s split path on the GPU canvas: the plain layers as one scene over +/// the canvas, then each special layer (far to near) rendered alone, hidden where the main +/// scene is nearer, matted (a 3D matte through the camera, else the 2D placement), Preserve +/// Transparency and blended. `None` = render the run on the CPU. +fn draw_split(e: &mut Enc, r: &Renderer, ctx: &EvalCtx, split: &effectcraft_render::three_d::adv::SplitRun, canvas: &GpuImage) -> Option { + let (w, h) = (canvas.width, canvas.height); + let main_prep = split.main(); + if main_prep.out != (w, h) { + return None; + } + let encode = !main_prep.linear; + let main = crate::adv3d::resolve_run(e, &main_prep)?; + let mut cv = match &main { + Some(m) => crate::adv3d::finish(e, m, encode, Some(canvas)), + None => canvas.clone(), + }; + for i in 0..split.len() { + let Some(iso) = crate::adv3d::resolve_run(e, &split.special(i))? else { continue }; + let iso = match &main { + Some(m) => crate::adv3d::occluded(e, &iso, m, encode), + None => crate::adv3d::finish(e, &iso, encode, None), + }; + let matte3d = split.with_matte3d(i, |p| match p { + None => Some(e.zeros(w, h)), + Some(p) => Some(match crate::adv3d::resolve_run(e, p)? { + Some(m) => crate::adv3d::finish(e, &m, encode, None), + None => e.zeros(w, h), + }), + }); + let matte3d = match matte3d { + Some(m) => Some(m?), + None => None, + }; + composite_iso_with(e, r, ctx, split.layer(i), iso, &mut cv, 1.0, matte3d, None)?; + } + Some(cv) +} + fn draw_collapsed<'a>(e: &mut Enc, r: &Renderer<'a>, ctx: &EvalCtx<'a>, layer: &Layer, item: ItemId, canvas: &mut GpuImage) -> Option<()> { let opacity = r.layer_opacity(ctx, layer); if opacity <= 0.0 { @@ -225,21 +283,44 @@ fn composite_layer(e: &mut Enc, r: &Renderer, ctx: &EvalCtx, layer: &Layer, buf: } /// Track matte / Preserve Transparency on an isolated layer render, then blend. -fn composite_iso(e: &mut Enc, r: &Renderer, ctx: &EvalCtx, layer: &Layer, mut iso: GpuImage, canvas: &mut GpuImage, opacity: f32) -> Option<()> { +fn composite_iso(e: &mut Enc, r: &Renderer, ctx: &EvalCtx, layer: &Layer, iso: GpuImage, canvas: &mut GpuImage, opacity: f32) -> Option<()> { + composite_iso_with(e, r, ctx, layer, iso, canvas, opacity, None, r.pipe().levels) +} + +/// `Renderer::composite_iso_with`: the matte from `matte_img` when given (a 3D matte drawn +/// through the camera), else the matte layer placed in 2D; the blend quantised to `levels`. +#[allow(clippy::too_many_arguments)] +fn composite_iso_with( + e: &mut Enc, + r: &Renderer, + ctx: &EvalCtx, + layer: &Layer, + mut iso: GpuImage, + canvas: &mut GpuImage, + opacity: f32, + matte_img: Option, + levels: Option, +) -> Option<()> { if let Some((m, kind)) = r.track_matte(ctx, layer) { - let mut mimg = e.zeros(canvas.width, canvas.height); - if m.is_active_at(ctx.time) - && let Some(mb) = r.layer_buf(ctx, m) - { - let mo = ctx.opacity(m) as f32; - mimg = place(e, r, ctx, m, &mb, &mimg, BlendMode::Normal, mo, None)?; - } + let mimg = match matte_img.filter(|i| (i.width, i.height) == (canvas.width, canvas.height)) { + Some(i) => i, + None => { + let mut mimg = e.zeros(canvas.width, canvas.height); + if m.is_active_at(ctx.time) + && let Some(mb) = r.layer_buf(ctx, m) + { + let mo = ctx.opacity(m) as f32; + mimg = place(e, r, ctx, m, &mb, &mimg, BlendMode::Normal, mo, None)?; + } + mimg + } + }; iso = ops::matte(e, &iso, &mimg, kind); } if layer.preserve_transparency { iso = ops::preserve(e, &iso, canvas); } - *canvas = ops::blend_full(e, canvas, &iso, layer.blend_mode, opacity, layer.id.0 as u32, r.pipe().levels); + *canvas = ops::blend_full(e, canvas, &iso, layer.blend_mode, opacity, layer.id.0 as u32, levels); Some(()) } diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index 390c68c6..613a5186 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -1606,6 +1606,25 @@ impl<'a> Renderer<'a> { three_d::adv::prepare_run(self, ctx, run, out) } + /// A run of consecutive 3D layers of an Advanced 3D comp whose layers need the 2D + /// compositing path (blend mode, track matte, Preserve Transparency), split for an + /// accelerator's compositor ([`three_d::adv::SplitRun`]). `None` = [`Self::prepare_adv_run`] + /// covers it, or it isn't an Advanced 3D run. + pub fn split_adv_run<'p>(&'p self, ctx: &'p EvalCtx<'a>, run: &'p [&'p Layer], out: (u32, u32)) -> Option> { + three_d::adv::split_run(self, ctx, run, out) + } + + /// Whether the comp's 3D layers draw with the Advanced 3D renderer. + pub fn is_adv3d(&self, ctx: &EvalCtx) -> bool { + three_d::adv::active(self, ctx) + } + + /// An Environment Light Background layer ready to draw on an accelerator + /// ([`three_d::SkyDraw`]; the CPU draws it in `draw_3d_run`). `None` = nothing to draw. + pub fn sky_draw(&self, ctx: &EvalCtx, layer: &Layer) -> Option { + three_d::compose::sky_draw(self, ctx, layer) + } + /// The pixels a Wireframe-quality layer sets (white, opaque) on a `size` canvas: its /// bounds' outline, one pixel wide. Empty when the layer has no bounds. pub fn wireframe_pixels(&self, ctx: &EvalCtx, layer: &Layer, size: (u32, u32)) -> Vec<(u32, u32)> { diff --git a/crates/render/src/three_d/adv/mod.rs b/crates/render/src/three_d/adv/mod.rs index 0fae1be7..a5685c3a 100644 --- a/crates/render/src/three_d/adv/mod.rs +++ b/crates/render/src/three_d/adv/mod.rs @@ -7,7 +7,9 @@ //! [`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. +//! 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; @@ -217,7 +219,7 @@ pub(crate) fn render_layers(r: &Renderer, ctx: &EvalCtx, layers: &[&Layer], out: /// 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` +/// 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)) { @@ -226,6 +228,102 @@ pub(crate) fn prepare_run<'p, 'a>(r: &'p Renderer<'a>, ctx: &'p EvalCtx<'a>, run 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 @@ -252,16 +350,7 @@ pub(crate) fn draw_run(r: &Renderer, ctx: &EvalCtx, run: &[&Layer], canvas: &mut if special.is_empty() { return; } - 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)); - for (_, l) in order { + 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))| { @@ -274,17 +363,8 @@ pub(crate) fn draw_run(r: &Renderer, ctx: &EvalCtx, run: &[&Layer], canvas: &mut }); } // A 3D matte layer is drawn through the camera too. - let matte3d = l.track_matte.and_then(|tm| ctx.comp.layer(tm.layer).filter(|m| m.id != l.id && m.is_3d())).map(|m| { - let mut solo = m.clone(); - solo.track_matte = None; - solo.blend_mode = effectcraft_color::BlendMode::Normal; - solo.preserve_transparency = false; - solo.switches.video = true; - if m.is_active_at(ctx.time) { - 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) - } + 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); } diff --git a/crates/render/src/three_d/compose.rs b/crates/render/src/three_d/compose.rs index 5ef271be..0d70b32f 100644 --- a/crates/render/src/three_d/compose.rs +++ b/crates/render/src/three_d/compose.rs @@ -971,30 +971,71 @@ pub(crate) fn environment_rotation(ctx: &EvalCtx) -> f32 { .to_radians() as f32 } -/// Draw an Environment Light Background layer: its (equirectangular) image looked up by each -/// pixel's view direction through the camera, as an infinitely distant backdrop. -pub(crate) fn draw_sky(r: &Renderer, ctx: &EvalCtx, layer: &Layer, canvas: &mut Image) { - let Some(buf) = r.blend_layer_buf(ctx, layer) else { return }; - let img = &buf.img; - if img.width == 0 || img.height == 0 { - return; +/// An Environment Light Background layer ready to draw (see [`draw_sky`]): the layer's +/// buffer in the blending space, its opacity and the camera's view rays. Also handed to an +/// accelerator's compositor (`Renderer::sky_draw`). +#[derive(Clone)] +pub struct SkyDraw { + pub buf: Arc, + pub opacity: f32, + /// Camera basis: forward, right, down. + pub fwd: [f64; 3], + pub right: [f64; 3], + pub down: [f64; 3], + pub zoom: f64, + pub ortho: bool, + /// View size (comp pixels), region-of-interest offset (output pixels), output scale. + pub view: (f64, f64), + pub roi: (f64, f64), + pub scale: f64, + /// The Environment light's rotation (radians). + pub rotation: f32, +} + +pub(crate) fn sky_draw(r: &Renderer, ctx: &EvalCtx, layer: &Layer) -> Option { + let buf = r.blend_layer_buf(ctx, layer)?; + if buf.img.width == 0 || buf.img.height == 0 { + return None; } let op = (ctx.opacity(layer) as f32 * r.opacity_mul()).clamp(0.0, 1.0); if op <= 0.0 { - return; + return None; } let cam = camera_for(r, ctx); - let (fwd, right, down) = (cam.forward(), cam.right(), cam.down()); - let s = r.opts.scale.max(1e-9); - let (cw, ch) = view_size(r, ctx); - let (ox, oy) = r.roi_offset().map_or((0.0, 0.0), |(x, y, _, _)| (x, y)); - let rot = environment_rotation(ctx); + let v3 = |v: Vec3| [v.x, v.y, v.z]; + Some(SkyDraw { + buf, + opacity: op, + fwd: v3(cam.forward()), + right: v3(cam.right()), + down: v3(cam.down()), + zoom: cam.zoom, + ortho: cam.ortho, + view: view_size(r, ctx), + roi: r.roi_offset().map_or((0.0, 0.0), |(x, y, _, _)| (x, y)), + scale: r.opts.scale.max(1e-9), + rotation: environment_rotation(ctx), + }) +} + +/// Draw an Environment Light Background layer: its (equirectangular) image looked up by each +/// pixel's view direction through the camera, as an infinitely distant backdrop. +pub(crate) fn draw_sky(r: &Renderer, ctx: &EvalCtx, layer: &Layer, canvas: &mut Image) { + let Some(sky) = sky_draw(r, ctx, layer) else { return }; + let img = &sky.buf.img; + let op = sky.opacity; + let v3 = |v: [f64; 3]| vec3(v[0], v[1], v[2]); + let (fwd, right, down) = (v3(sky.fwd), v3(sky.right), v3(sky.down)); + let s = sky.scale; + let (cw, ch) = sky.view; + let (ox, oy) = sky.roi; + let rot = sky.rotation; let (iw, ih) = (img.width as f64, img.height as f64); canvas.rows_mut().for_each(|(y, row)| { let cy = (y as f64 + 0.5 + oy) / s; for (x, d) in row.iter_mut().enumerate() { let cx = (x as f64 + 0.5 + ox) / s; - let dir = if cam.ortho { fwd } else { (fwd * cam.zoom + right * (cx - cw / 2.0) + down * (cy - ch / 2.0)).normalize() }; + let dir = if sky.ortho { fwd } else { (fwd * sky.zoom + right * (cx - cw / 2.0) + down * (cy - ch / 2.0)).normalize() }; let (u, v) = super::adv::shade::equirect_uv([dir.x as f32, dir.y as f32, dir.z as f32], rot); // Wrap horizontally across the seam. let sx = (u as f64 * iw).clamp(0.5, iw - 0.5); diff --git a/crates/render/src/three_d/mod.rs b/crates/render/src/three_d/mod.rs index 80d5dd19..bccfc3e0 100644 --- a/crates/render/src/three_d/mod.rs +++ b/crates/render/src/three_d/mod.rs @@ -17,7 +17,7 @@ mod tests; mod tests_orient; pub use camera::{CameraState, Dof, Rig, View3D, ViewCam, Views3D, active_camera, default_camera, default_view_cam}; -pub use compose::{Geo as PlaneGeo, Plane3d, PlaneDof, Run3d}; +pub use compose::{Geo as PlaneGeo, Plane3d, PlaneDof, Run3d, SkyDraw}; pub use light::{LightState, Material, lights_at}; use effectcraft_geom::Mat3;