mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 04:57:54 -06:00
Glow: per-channel bright pass with After Effects' gain, glows above the threshold as bright as After Effects
With Glow Colors = Original Colors, After Effects thresholds each channel of the straight colour separately and the glow is 8 x (value - threshold + 0.061) per channel (times Intensity), nothing below: fitted to AE 2026 renders of grey 0.65 / 0.8 / 0.9 / 1, orange (1, 0.5, 0) and (0.2, 0.6, 1) solids at Threshold 40 / 60 / 80 % and Radius 10 / 30 (36 channel values, exact to the 8-bit capture; an orange solid glows red only). EffectCraft scaled the colour by a luminance threshold (k = (L - thr) / (1 - thr)), about 3.7 x too dim on white. Cases 806 / 810 / 811 (32 bpc): outside the layer now within 3/255 of AE (806 white at Intensity 1: 119 = AE, was 32); grid 0 points off for 806-810. The glow's alpha (the blurred layer alpha) and the Arbitrary Map / A & B colours and alpha-based glows are unchanged. Stacked on fix-glow-halo (same lines). Not changed: Composite Original's semantics are swapped against AE's names (EffectCraft's "On Top" adds the glow over the original, which AE calls "Behind"; AE's "On Top" keeps the original over the glow), so imported AE Glows at the default Behind show no glow inside the layer (811 interior 204 vs AE 255). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
f4fbdc6c25
commit
a8d4514a1b
@@ -127,6 +127,20 @@ pub fn glow_sigma(radius: f64) -> f64 {
|
||||
(radius * 0.384).max(0.5)
|
||||
}
|
||||
|
||||
/// Glow's bright pass for the original colours, per channel of the straight colour `v`:
|
||||
/// `8 × (v − threshold + 0.0611)`, nothing below. Fitted to After Effects 2026 renders of grey
|
||||
/// 0.65 / 0.8 / 0.9 / 1, orange and light-blue solids at Threshold 40 / 60 / 80 % and Radius 10
|
||||
/// / 30 (36 channel values, exact to the 8-bit capture): the glow is linear in each channel,
|
||||
/// its gain is the same at every threshold and radius, and channels below the threshold add
|
||||
/// nothing (an orange solid glows red only).
|
||||
pub fn glow_bright_channel(v: f32, thr: f32) -> f32 {
|
||||
(GLOW_GAIN * (v - thr + GLOW_KNEE)).max(0.0)
|
||||
}
|
||||
|
||||
/// [`glow_bright_channel`]'s gain and threshold offset.
|
||||
pub const GLOW_GAIN: f32 = 8.0;
|
||||
pub const GLOW_KNEE: f32 = 0.0611;
|
||||
|
||||
/// Glow Operation options (blend modes the glow is applied with).
|
||||
pub const GLOW_OPERATIONS: [&str; 17] = [
|
||||
"Normal",
|
||||
@@ -201,8 +215,12 @@ fn glow(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let t = glow_ab_t(l, looping, loops, phase, mid);
|
||||
let c = [ca[0] + (cb[0] - ca[0]) * t, ca[1] + (cb[1] - ca[1]) * t, ca[2] + (cb[2] - ca[2]) * t];
|
||||
*px = [c[0] * k * a, c[1] * k * a, c[2] * k * a, ga];
|
||||
} else {
|
||||
} else if alpha_based {
|
||||
*px = [px[0] * k, px[1] * k, px[2] * k, ga];
|
||||
} else {
|
||||
// Original colours: each channel of the straight colour against the threshold.
|
||||
let c = [px[0] / a, px[1] / a, px[2] / a].map(|v| glow_bright_channel(v, thr) * a);
|
||||
*px = [c[0], c[1], c[2], ga];
|
||||
}
|
||||
});
|
||||
let s = glow_sigma(radius);
|
||||
@@ -792,6 +810,38 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
/// After Effects 2026, Glow at Threshold 60 %, Radius 10, Intensity 0.25 on 200 × 200
|
||||
/// solids over red: the glow 1.5 px outside the left edge, premultiplied (the green / blue
|
||||
/// channels of the render, where the red background adds nothing), and its alpha 0.353.
|
||||
#[test]
|
||||
fn glow_brightness_matches_after_effects() {
|
||||
// (solid colour, channel, AE glow value 0..255)
|
||||
let cases = [
|
||||
([0.65, 0.65, 0.65], 1, 20.0),
|
||||
([0.8, 0.8, 0.8], 1, 47.0),
|
||||
([0.9, 0.9, 0.9], 1, 65.0),
|
||||
([1.0, 1.0, 1.0], 1, 83.0),
|
||||
([1.0, 0.5, 0.0], 1, 0.0),
|
||||
([0.2, 0.6, 1.0], 1, 11.0),
|
||||
([0.2, 0.6, 1.0], 2, 83.0),
|
||||
];
|
||||
for (c, ch, ae) in cases {
|
||||
let img = effectcraft_raster::Image::filled(200, 200, [c[0], c[1], c[2], 1.0]);
|
||||
let set = [("threshold", num(60.0)), ("radius", num(10.0)), ("intensity", num(0.25))];
|
||||
let o = run_fx("ec.stylize.glow", &set, img, 0.0, EffectEnv::default());
|
||||
let (bx, by) = o.to_px([-1.5, 100.0]);
|
||||
let p = o.img.get(bx.floor() as i64, by.floor() as i64);
|
||||
assert!((p[ch] * 255.0 - ae).abs() <= 2.5, "{c:?} channel {ch}: {} vs AE {ae}", p[ch] * 255.0);
|
||||
assert!((p[3] - 0.353).abs() < 3.0 / 255.0, "{c:?}: alpha {}", p[3]);
|
||||
}
|
||||
// Intensity scales the colour linearly: white at Intensity 1 is 3.7 × its alpha.
|
||||
let white = effectcraft_raster::Image::filled(200, 200, [1.0, 1.0, 1.0, 1.0]);
|
||||
let o = run_fx("ec.stylize.glow", &[("threshold", num(60.0)), ("radius", num(10.0))], white, 0.0, EffectEnv::default());
|
||||
let (bx, by) = o.to_px([-1.5, 100.0]);
|
||||
let p = o.img.get(bx.floor() as i64, by.floor() as i64);
|
||||
assert!((p[1] / p[3] - 3.69).abs() < 0.05, "{p:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn glow_operation_and_arbitrary_map() {
|
||||
// Pixels addressed in layer space (Glow pads its buffer).
|
||||
|
||||
@@ -391,8 +391,12 @@ fn glow_bright(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
let cb = P.f[2].xyz;
|
||||
let c = ca + (cb - ca) * t;
|
||||
textureStore(out, p, vec4<f32>(c * k * a, ga));
|
||||
} else {
|
||||
} else if (P.u[0].y != 0u) {
|
||||
textureStore(out, p, vec4<f32>(px.xyz * k, ga));
|
||||
} else {
|
||||
// Original colours, per channel (effects::misc::glow_bright_channel).
|
||||
let c = max(8.0 * (px.xyz / a - vec3<f32>(thr - 0.0611)), vec3<f32>(0.0));
|
||||
textureStore(out, p, vec4<f32>(c * a, ga));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@ corpus compares renders, each effect is assumed to differ somewhere, and users k
|
||||
| Paint | 1 | 0 | Tablet pressure not read ([hardware-parity.md](hardware-parity.md)) |
|
||||
| Perspective | 10 | 9 | Chromatic Aberrations pan/tilt crash (#580) |
|
||||
| Simulation | 18 | 18 | Particle Playground lacks Particle Exploder, text particles, ephemeral mapper; CC Particle World camera fixed (#397) |
|
||||
| Stylize | 25 | 25 | Glow's halo (coverage of a dim layer's edge) and blur width match After Effects 2026 renders; the brightness of glows above the threshold is not matched yet; Motion Tile output size (#369) |
|
||||
| Stylize | 25 | 25 | Glow's halo (coverage of a dim layer's edge) and blur width match After Effects 2026 renders; glows above the threshold match too (each channel 8 × (value − threshold + 0.061), measured at three thresholds and two radii); Composite Original's labels are swapped against After Effects (EffectCraft's On Top adds the glow over the original, which After Effects calls Behind); Motion Tile output size (#369) |
|
||||
| Text | 2 | 2 | — |
|
||||
| Time | 8 | 8 | #535 (Pixel Motion Blur, Vector Detail) is closed |
|
||||
| Transition | 17 | 17 | — |
|
||||
@@ -64,6 +64,7 @@ the GPU is performance, not parity.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 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-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 |
|
||||
|
||||
Reference in New Issue
Block a user