diff --git a/crates/render/src/tests_collapse.rs b/crates/render/src/tests_collapse.rs index d3cf954c..0da0288f 100644 --- a/crates/render/src/tests_collapse.rs +++ b/crates/render/src/tests_collapse.rs @@ -200,3 +200,37 @@ fn wireframe_quality_draws_the_layer_bounds() { let img = render_frame(&p, cid, Tick::ZERO, 1.0); assert!(img.data.iter().all(|q| q[3] == 0.0 || q[3] == 1.0)); } + +#[test] +fn collapsed_precomp_of_another_size_renders_through_the_parent_camera() { + // A 1000×600 scene comp with a blue 3D solid at its centre, collapsed into a 300×200 comp + // whose camera looks at the precomp layer (at the parent's centre). The nested layers must + // be projected with the parent comp's view (its centre and size), not the scene comp's. + let run = |three_d_precomp: bool, camera: bool| { + let mut p = project(); + let outer = Comp::new(300, 200, FrameRate::FPS_30, Tick::from_seconds_f64(2.0)); + let inner = Comp::new(1000, 600, FrameRate::FPS_30, Tick::from_seconds_f64(2.0)); + let mut blue = solid(&mut p, &inner, [0.0, 0.0, 1.0], (40, 40), [500.0, 300.0, 0.0]); + blue.switches.three_d = true; + let mut inner = inner; + inner.layers = vec![blue]; + let iid = p.add_item("Scene", Label::Sandstone, None, ItemKind::Comp(inner.into())); + let mut pre = build::layer(&mut p, &outer, "Scene", LayerSource::Comp { item: iid }, (1000, 600), None); + pre.switches.three_d = three_d_precomp; + pre.switches.collapse = true; + let mut c = outer.clone(); + c.layers = vec![pre]; + if camera { + let cam = build::layer(&mut p, &outer, "Camera", LayerSource::Camera, (300, 200), None); + c.layers.insert(0, cam); + } + let cid = p.add_item("Face", Label::Sandstone, None, ItemKind::Comp(c.into())); + render_frame(&p, cid, Tick::ZERO, 1.0) + }; + for (three_d, camera) in [(true, true), (true, false), (false, true)] { + let img = run(three_d, camera); + let c = img.get(150, 100); + assert!(c[2] > 0.99 && c[3] > 0.99, "3D precomp {three_d}, camera {camera}: centre {c:?}"); + assert!(img.get(100, 100)[3] < 0.01, "only the 40 px solid is drawn"); + } +} diff --git a/crates/render/src/three_d/compose.rs b/crates/render/src/three_d/compose.rs index b134c88f..c97b78bb 100644 --- a/crates/render/src/three_d/compose.rs +++ b/crates/render/src/three_d/compose.rs @@ -161,6 +161,13 @@ pub(crate) fn camera_for(r: &Renderer, ctx: &EvalCtx) -> CameraState { } } +/// The size of the comp the camera looks through: a collapsed precomp's layers are projected +/// into the outermost comp they collapse into (its centre and size), not their own comp's. +pub(crate) fn view_size(r: &Renderer, ctx: &EvalCtx) -> (f64, f64) { + let c = r.collapse3d.map_or(ctx.comp, |c| c.parent.comp); + (c.width as f64, c.height as f64) +} + /// Number of motion-blur sub-samples for a layer. fn mb_samples(r: &Renderer, ctx: &EvalCtx, layer: &Layer) -> usize { if r.opts.motion_blur && ctx.comp.enable_motion_blur && layer.switches.motion_blur { @@ -202,7 +209,7 @@ fn prepare_with<'a>( if buf.img.is_empty() { return None; } - let comp = (ctx.comp.width as f64, ctx.comp.height as f64); + let comp = view_size(r, ctx); let s = r.opts.scale; let cam = camera_for(r, ctx); let outer = r.collapse3d.map_or(Mat4::IDENTITY, |c| c.world); @@ -920,7 +927,7 @@ pub(crate) fn draw_sky(r: &Renderer, ctx: &EvalCtx, layer: &Layer, canvas: &mut 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) = (ctx.comp.width as f64, ctx.comp.height as f64); + 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 (iw, ih) = (img.width as f64, img.height as f64);