From 94c8bde240ab0a4da34310fd486292f6b2ba025c Mon Sep 17 00:00:00 2001 From: bflatastic Date: Fri, 30 Oct 2026 11:43:19 +0700 Subject: [PATCH] Adjustment layers blend in their blend mode (#2618) An adjustment layer replaced the comp below with its effects' result whatever its blend mode. As in After Effects, a mode other than Normal now blends the adjusted comp onto the comp below in that mode, inside the footprint, track matte and opacity (an adjustment layer with no effects in Screen screens the comp with itself). CPU and GPU paths. Co-Authored-By: Claude Opus 5.5 --- crates/gpu/src/tests.rs | 6 ++++++ crates/gpu/src/walk.rs | 12 +++++++++--- crates/render/src/layers.rs | 14 +++++++++++++- crates/render/src/tests.rs | 26 ++++++++++++++++++++++++++ 4 files changed, 54 insertions(+), 4 deletions(-) diff --git a/crates/gpu/src/tests.rs b/crates/gpu/src/tests.rs index c57c1ef84..44e8922a6 100644 --- a/crates/gpu/src/tests.rs +++ b/crates/gpu/src/tests.rs @@ -510,6 +510,12 @@ fn layer_styles_collapsed_precomps_and_cpu_fallbacks() { adj.switches.adjustment = true; s.effect(&mut adj, "ec.channel.invert", &[]); s.push(adj); + // An adjustment layer in another blend mode blends the adjusted comp in that mode (#2618). + let mut adj = s.solid([1.0, 1.0, 1.0], 70, 40); + adj.switches.adjustment = true; + adj.blend_mode = BlendMode::Screen; + set(&mut adj, "transform/opacity", Value::Scalar(60.0)); + s.push(adj); check(&format!("collapse + adjustment {depth:?}"), compare_at(&s, opts(), Tick::ZERO), 0.0); } } diff --git a/crates/gpu/src/walk.rs b/crates/gpu/src/walk.rs index d4d7a62a1..fdcb58dd4 100644 --- a/crates/gpu/src/walk.rs +++ b/crates/gpu/src/walk.rs @@ -352,8 +352,8 @@ fn draw_precomp(e: &mut Enc, r: &Renderer, ctx: &EvalCtx, layer: &Layer, canvas: /// Adjustment layer (`Renderer::draw_adjustment`): the effect stack runs on the comp below, /// resident on the GPU (non-GPU effects read back, run on the CPU and upload), and the result -/// replaces the canvas inside the layer's footprint × track matte × opacity. `None` = draw it -/// on the CPU. +/// replaces the canvas inside the layer's footprint × track matte × opacity, or blends onto it in +/// the layer's blend mode when that isn't Normal. `None` = draw it on the CPU. fn draw_adjustment(e: &mut Enc, r: &Renderer, ctx: &EvalCtx, layer: &Layer, canvas: &mut GpuImage) -> Option<()> { let opacity = r.layer_opacity(ctx, layer); let Some(foot) = r.adjustment_footprint(ctx, layer) else { return Some(()) }; @@ -379,7 +379,13 @@ fn draw_adjustment(e: &mut Enc, r: &Renderer, ctx: &EvalCtx, layer: &Layer, canv if let Some(c) = pipe.to_blend() { adjusted = ops::convert(e, &adjusted, &c); } - *canvas = ops::adjust_mix(e, canvas, &adjusted, &matte, opacity); + *canvas = if layer.blend_mode == BlendMode::Normal { + ops::adjust_mix(e, canvas, &adjusted, &matte, opacity) + } else { + // The adjusted comp is a layer in that mode over the comp below (#2618). + let iso = ops::matte(e, &adjusted, &matte, effectcraft_project::MatteKind::Alpha); + ops::blend_full(e, canvas, &iso, layer.blend_mode, opacity, layer.id.0 as u32, None) + }; Some(()) } diff --git a/crates/render/src/layers.rs b/crates/render/src/layers.rs index ce0a2dcde..d105c6f13 100644 --- a/crates/render/src/layers.rs +++ b/crates/render/src/layers.rs @@ -522,7 +522,7 @@ impl<'a> Renderer<'a> { } /// Adjustment layer: effects applied to the comp below, limited to the layer's footprint - /// and its track matte. + /// and its track matte, and blended back in the layer's blend mode. fn draw_adjustment(&self, ctx: &EvalCtx, layer: &Layer, canvas: &mut Image, opacity: f32) { let Some(mut foot) = self.source(ctx, layer) else { return }; let _ = masks::apply(ctx, layer, &mut foot, self.mask_blur(ctx, layer)); @@ -548,6 +548,18 @@ impl<'a> Renderer<'a> { if adjusted.img.width != canvas.width || adjusted.img.height != canvas.height { return; } + if layer.blend_mode != BlendMode::Normal { + // The adjusted comp is a layer in that mode over the comp below, as in After Effects: + // an adjustment layer with no effects in Screen screens the comp with itself (#2618). + let mut iso = adjusted.img; + iso.data.par_iter_mut().zip(matte.data.par_iter()).for_each(|(p, m)| { + for c in p.iter_mut() { + *c *= m[3]; + } + }); + canvas.blend_from(&iso, layer.blend_mode, opacity, layer.id.0 as u32); + return; + } canvas.data.par_iter_mut().zip(adjusted.img.data.par_iter()).zip(matte.data.par_iter()).for_each(|((c, a), m)| { let k = m[3] * opacity; for i in 0..4 { diff --git a/crates/render/src/tests.rs b/crates/render/src/tests.rs index 488f38ec7..3d2134e89 100644 --- a/crates/render/src/tests.rs +++ b/crates/render/src/tests.rs @@ -206,6 +206,32 @@ fn track_matte_limits_an_adjustment_layer() { } } +/// An adjustment layer's blend mode blends the adjusted comp onto the comp below, inside its +/// footprint: with no effects, Screen screens a 0.25 grey with itself, Multiply darkens it, +/// Normal leaves it, and Opacity mixes toward the result (#2618). +#[test] +fn adjustment_layer_blends_in_its_mode() { + for (mode, opacity, want) in + [(BlendMode::Normal, 100.0, 0.25), (BlendMode::Screen, 100.0, 0.4375), (BlendMode::Multiply, 100.0, 0.0625), (BlendMode::Screen, 50.0, 0.34375)] + { + let (mut p, cid, comp) = setup(); + let mut adj = solid(&mut p, &comp, [1.0, 1.0, 1.0], 100, 100); + adj.props.prop_mut("transform/position").unwrap().value = Value::Vec3([50.0, 50.0, 0.0]); + adj.props.prop_mut("transform/opacity").unwrap().value = Value::Scalar(opacity); + adj.switches.adjustment = true; + adj.blend_mode = mode; + let bg = solid(&mut p, &comp, [0.25, 0.25, 0.25], 200, 100); + let c = p.comp_mut(cid).unwrap(); + c.layers.push(adj); + c.layers.push(bg); + let img = render_frame(&p, cid, Tick::ZERO, 1.0); + let (inside, outside) = (img.get(50, 50), img.get(150, 50)); + assert!((inside[0] - want).abs() < 1e-4, "{mode:?} {opacity}: {inside:?}"); + assert!((outside[0] - 0.25).abs() < 1e-4, "{mode:?} {opacity}: outside the footprint {outside:?}"); + assert_eq!(inside[3], 1.0, "{mode:?} keeps the comp opaque"); + } +} + #[test] fn shape_layer_draws_fill_and_stroke() { let (mut p, cid, comp) = setup();