From 601a15bc1f01a8405d11969daca496b27d701f4e Mon Sep 17 00:00:00 2001 From: Minher0 Date: Sun, 25 Oct 2026 13:21:57 +0000 Subject: [PATCH] render: a 3D layer read outside its in/out range is transparent solo_3d passed the layer to the 3D pipelines whatever the comp time, so an instance (CC Composite referencing a model layer) rendered before the source layer's in-point or after its out-point. Like any other layer reference, there is nothing to read outside the range: return the transparent canvas. --- crates/engine/src/tests_instance3d.rs | 22 ++++++++++++++++++++++ crates/render/src/lib.rs | 7 ++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/crates/engine/src/tests_instance3d.rs b/crates/engine/src/tests_instance3d.rs index 46b8ec434..c8c91ccf7 100644 --- a/crates/engine/src/tests_instance3d.rs +++ b/crates/engine/src/tests_instance3d.rs @@ -155,3 +155,25 @@ fn instance3d_needs_a_3d_layer() { s.execute("layer.setSwitch", json!({"layers": [text], "switch": "threeD", "value": true})).unwrap(); assert!(s.execute_checked("layer.create", json!({"op": "instance3d", "layers": [text]})).is_err()); } + +#[test] +fn instance3d_shows_nothing_outside_the_source_layer_range() { + let mut s = session(); + let model = triangle_model(&mut s); + // The model's range ends at 1 s; the comp runs to 2 s. + let comp = cid(&s); + s.edit("trim", None, |p, _| { + p.comp_mut(comp).unwrap().layer_mut(LayerId(model)).unwrap().out_point = Tick::from_seconds_f64(1.0); + Ok(()) + }) + .unwrap(); + s.execute("layer.select", json!({"layers": [model]})).unwrap(); + let r = s.execute("layer.create", json!({"op": "instance3d"})).unwrap(); + let _copy = r["layers"][0].as_u64().unwrap(); + // Inside the model's range the copy shows it, transparent around it. + let inside = s.render(comp, Tick::from_seconds_f64(0.5), RenderOpts::default()); + assert!(px(&inside, 320, 180)[3] > 0.9, "the copy shows the model: {:?}", px(&inside, 320, 180)); + // After its out point the source has nothing to read: the copy is transparent. + let outside = s.render(comp, Tick::from_seconds_f64(1.5), RenderOpts::default()); + assert!(outside.data.iter().all(|p| p[3] < 1e-3), "transparent after the source's out point: {:?}", px(&outside, 320, 180)); +} diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index 88d6e8cfe..73e59f252 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -1510,12 +1510,17 @@ impl<'a> Renderer<'a> { let s = self.opts.scale; let w = ((ctx.comp.width as f64 * s).ceil() as u32).max(1); let h = ((ctx.comp.height as f64 * s).ceil() as u32).max(1); + let mut canvas = Image::new(w, h); + // Outside the layer's in/out points there is nothing to read: the copy is transparent + // there, like any other layer reference (the 3D pipelines don't gate on the range). + if !layer.is_active_at(ctx.time) { + return Buf { img: canvas, offset: [0.0; 2], scale: s }; + } let mut solo = layer.clone(); solo.track_matte = None; solo.blend_mode = effectcraft_color::BlendMode::Normal; solo.preserve_transparency = false; solo.switches.video = true; - let mut canvas = Image::new(w, h); three_d::compose::draw_run(self, ctx, &[&solo], &mut canvas); Buf { img: canvas, offset: [0.0; 2], scale: s } }