M13.22: GPU compositing of Advanced 3D runs with blend modes, track mattes (2D and 3D), Preserve Transparency, and environment backgrounds (Classic and Advanced 3D skies)

render: three_d::adv::SplitRun (Renderer::split_adv_run) hands draw_run's split path to an
accelerator as data (main scene, special layers far to near, 3D mattes drawn solo);
Renderer::sky_draw / three_d::SkyDraw for Environment Light Background layers. gpu: the walk
draws skies (adv_sky kernel), resolves each special layer, hides it behind the nearer main
scene (occlude kernel) and composites it through the 2D path without readbacks.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas 2026-10-03 18:26:06 -04:00
parent a909de43b7
commit 0c4e533460
11 changed files with 458 additions and 60 deletions

View File

@ -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<Vec<u8>> {
/// 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<Option<Resolved>> {
pub(crate) fn resolve_run(e: &mut Enc, prep: &Prepared) -> Option<Option<Resolved>> {
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<GpuImage> {
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 {

View File

@ -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)| {

View File

@ -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

View File

@ -255,3 +255,24 @@ fn wire(@builtin(global_invocation_id) id: vec3<u32>) {
let p = table[id.x];
textureStore(out, vec2<i32>(i32(p.x), i32(p.y)), vec4<f32>(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<u32>) {
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>(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);
}

View File

@ -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<u32>) {
let p = vec2<i32>(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));
}

View File

@ -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);
}
}

View File

@ -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<GpuImage> {
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<GpuImage>,
levels: Option<f32>,
) -> 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(())
}

View File

@ -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::SplitRun<'p, 'a>> {
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::SkyDraw> {
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)> {

View File

@ -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<Prepared<'p, 'a>> {
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<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
@ -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);
}

View File

@ -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<Buf>,
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<SkyDraw> {
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);

View File

@ -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;