From ae7081077fafbe626eac6156222724bc05ec0f29 Mon Sep 17 00:00:00 2001 From: vangel76 Date: Mon, 19 Oct 2026 02:57:20 +0200 Subject: [PATCH] Radial Blur: samples outside the layer repeat its edge, as After Effects After Effects' Radial Blur keeps a layer's edges: Zoom 20 on a flat grey 960x540 solid over red (case 713) stays flat and opaque to its edge, and a zoomed full-frame ramp (case 829) keeps the frame opaque (2/255 at x = 2, 253/255 at x = 1917 on row 540). EffectCraft sampled transparency outside the layer, so its edges faded and the red below showed through (713: 3.9 % of the frame off, max 64/255). Now 713 is identical to AE (max 0/255), CPU and GPU. CC Radial Blur (already matching AE) is unchanged. Co-Authored-By: Claude Opus 5.5 --- crates/effects/src/misc.rs | 27 +++++++++++++++++++++++++ crates/gpu/src/shaders/fx_generate.wgsl | 5 ++++- crates/raster/src/blur.rs | 4 +++- docs/effects-parity.md | 3 ++- 4 files changed, 36 insertions(+), 3 deletions(-) diff --git a/crates/effects/src/misc.rs b/crates/effects/src/misc.rs index 23e9a6844..848a04f2d 100644 --- a/crates/effects/src/misc.rs +++ b/crates/effects/src/misc.rs @@ -842,6 +842,33 @@ mod tests { assert!((p[1] / p[3] - 3.69).abs() < 0.05, "{p:?}"); } + /// After Effects 2026: Radial Blur keeps the layer's edges. Zoom 20 on a flat grey 960 × 540 + /// solid (case 713) leaves it flat and opaque to the edge (EffectCraft faded the edges toward + /// transparency, letting the red layer below show through); on a full-frame horizontal ramp + /// (case 829, centre [960, 540]) row 540 reads 2/255 at x = 2 and 253/255 at x = 1917. + #[test] + fn radial_blur_keeps_the_layer_edges_as_after_effects() { + let zoom = |c: [f64; 2]| [("amount", num(20.0)), ("type", Value::Enum(1)), ("center", Value::Vec2(c))]; + let grey = effectcraft_raster::Image::filled(960, 540, [0.5, 0.5, 0.5, 1.0]); + let o = run_fx("ec.blur.radial", &zoom([600.0, 300.0]), grey, 0.0, EffectEnv::default()); + for (x, y) in [(0, 270), (1, 270), (959, 270), (480, 0), (480, 539), (0, 0), (959, 539)] { + let p = o.img.get(x, y); + assert!((p[3] - 1.0).abs() < 1e-4 && (p[0] - 0.5).abs() < 1e-4, "({x}, {y}): {p:?}"); + } + let mut ramp = effectcraft_raster::Image::new(1920, 1080); + ramp.rows_mut().for_each(|(_, row)| { + for (x, px) in row.iter_mut().enumerate() { + let v = (x as f32 + 0.5) / 1920.0; + *px = [v, v, v, 1.0]; + } + }); + let o = run_fx("ec.blur.radial", &zoom([960.0, 540.0]), ramp, 0.0, EffectEnv::default()); + for (x, ae) in [(2, 2.0), (1917, 253.0)] { + let p = o.img.get(x, 540); + assert!((p[0] * 255.0 - ae).abs() <= 2.0 && (p[3] - 1.0).abs() < 1e-4, "x {x}: {p:?} vs AE {ae}"); + } + } + #[test] fn glow_operation_and_arbitrary_map() { // Pixels addressed in layer space (Glow pads its buffer). diff --git a/crates/gpu/src/shaders/fx_generate.wgsl b/crates/gpu/src/shaders/fx_generate.wgsl index f46b2f0ba..2b571d582 100644 --- a/crates/gpu/src/shaders/fx_generate.wgsl +++ b/crates/gpu/src/shaders/fx_generate.wgsl @@ -67,7 +67,10 @@ fn gen_radial_blur(@builtin(global_invocation_id) gid: vec3) { let cs = data[2u * i + 1u]; s = vec2(c.x + v.x * cs - v.y * sn, c.y + v.x * sn + v.y * cs); } - acc += sample_bilinear(src, s.x, s.y); + // Edge samples repeat (raster::radial_blur). + let hi = vec2(f32(dims.x), f32(dims.y)) - vec2(0.5); + let sc = clamp(s, vec2(0.5), max(hi, vec2(0.5))); + acc += sample_bilinear(src, sc.x, sc.y); } textureStore(out, p, acc / f32(n)); } diff --git a/crates/raster/src/blur.rs b/crates/raster/src/blur.rs index 1c065d77b..df0b84379 100644 --- a/crates/raster/src/blur.rs +++ b/crates/raster/src/blur.rs @@ -203,7 +203,9 @@ pub fn radial_blur(img: &Image, center: (f64, f64), amount: f64, zoom: bool) -> let (s, c) = (t * amount).to_radians().sin_cos(); (center.0 + vx * c - vy * s, center.1 + vx * s + vy * c) }; - let p = img.sample_bilinear(sx, sy); + // Samples outside the image repeat its edge: After Effects' Radial Blur keeps a + // layer's edges (cases 713, 829) instead of fading them toward transparency. + let p = img.sample_bilinear(sx.clamp(0.5, (img.width as f64 - 0.5).max(0.5)), sy.clamp(0.5, (img.height as f64 - 0.5).max(0.5))); for ch in 0..4 { acc[ch] += p[ch]; } diff --git a/docs/effects-parity.md b/docs/effects-parity.md index 67b02d8bd..ffc478182 100644 --- a/docs/effects-parity.md +++ b/docs/effects-parity.md @@ -28,7 +28,7 @@ corpus compares renders, each effect is assumed to differ somewhere, and users k |---|---:|---:|---| | 3D Channel | 8 | 8 | Cryptomatte selection can't be set in the UI and the GPU path is slow (#473; quoted names with commas fixed); multi-layer EXR decode shared (#474 open: f16 storage and cache sizing remain) | | Audio | 13 | 0 | CPU by design (applied in the mixdown) | -| Blur & Sharpen | 16 | 15 | Gaussian Blur interaction and output to be measured first ([fidelity/ae-2026-acceptance.md](fidelity/ae-2026-acceptance.md)) | +| Blur & Sharpen | 16 | 15 | Radial Blur keeps the layer edges as After Effects 2026 (713 now pixel-identical); Gaussian Blur interaction and output to be measured first ([fidelity/ae-2026-acceptance.md](fidelity/ae-2026-acceptance.md)) | | Channel | 13 | 13 | — | | Color Correction | 40 | 40 | Tint has Swap Colors (#963); ACES 2.0 / OCIO 2.5 (26.5); Apply Color LUT (#514) and 32-bpc blending (#494) are closed | | Distort | 39 | 35 | Liquify has no viewer brush; #303 (Mesh Warp drag) and #534 (CC Power Pin Unstretch) are closed | @@ -66,6 +66,7 @@ the GPU is performance, not parity. |---|---|---| | 2026-10-19 | minor | Glow (original colours): bright pass per channel, 8 × (value − threshold + 0.061), measured against After Effects 2026 (CPU and GPU) | | 2026-10-19 | minor | Glow (colour channels): the glow covers the layer's blurred alpha, blur σ = 0.384 × radius, measured against After Effects 2026 renders (CPU and GPU) | +| 2026-10-19 | minor | Radial Blur repeats the edge instead of sampling transparency outside the layer, measured against After Effects 2026 renders (CPU and GPU) | | 2026-10-12 | minor | Parameters 3,014 to 3,017 (effects.md); #303, #514, #526, #533–#536 marked closed instead of pending | | 2026-10-12 | minor | Effect colour labels (26.5) added in Effect Controls | | 2026-10-12 | minor | Tint's Swap Colors button and agent-drivable command (#963); undo/redo, instance targeting, animated layer-time edits and invalid-input tests |