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 <noreply@anthropic.com>
This commit is contained in:
vangel76
2026-10-19 16:23:42 +02:00
co-authored by Claude Opus 5.5
parent a8d4514a1b
commit ae7081077f
4 changed files with 36 additions and 3 deletions
+27
View File
@@ -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).
+4 -1
View File
@@ -67,7 +67,10 @@ fn gen_radial_blur(@builtin(global_invocation_id) gid: vec3<u32>) {
let cs = data[2u * i + 1u];
s = vec2<f32>(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>(f32(dims.x), f32(dims.y)) - vec2<f32>(0.5);
let sc = clamp(s, vec2<f32>(0.5), max(hi, vec2<f32>(0.5)));
acc += sample_bilinear(src, sc.x, sc.y);
}
textureStore(out, p, acc / f32(n));
}
+3 -1
View File
@@ -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];
}
+2 -1
View File
@@ -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 |