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:
parent
a909de43b7
commit
0c4e533460
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)| {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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)> {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Reference in New Issue