mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-24 10:09:32 -06:00
Tritone: Rec. 601 luminance, as After Effects
After Effects' Tritone weighs the working-space values with Rec. 601 (0.299, 0.587, 0.114), as its Tint does; EffectCraft used Rec. 709. Case 718 (red solid, Highlights [1,1,0.8], Midtones [0.6,0.2,0.2], Shadows [0,0,0.3], Blend 25): AE (132, 23, 46), EffectCraft was (113, 16, 49), now (132, 23, 46). On a grey ramp (case 815) the shadows / midtones / highlights mapping already matched and still does within 1/255. CPU and GPU (fx_color op 4). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
fb16d5573e
commit
960f41d50b
@@ -14,10 +14,13 @@ fn mix(a: [f32; 3], b: [f32; 3], t: f32) -> [f32; 3] {
|
||||
[a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t, a[2] + (b[2] - a[2]) * t]
|
||||
}
|
||||
|
||||
/// Tint's luminance: Rec. 601 weights on the working-space values, as After Effects 2026
|
||||
/// renders it (sRGB working space, not linearised: red 76/255, green 150/255, blue 29/255).
|
||||
/// The GPU kernel (`pointwise` op 1) mirrors it.
|
||||
fn tint_luma(c: [f32; 3]) -> f32 {
|
||||
/// Rec. 601 luminance on the working-space values, as After Effects 2026 renders Tint, Tritone
|
||||
/// and the other Rec. 601 effects (sRGB working space, not linearised: red 76/255, green
|
||||
/// 150/255, blue 29/255). Tint and Tritone use it (Tritone, case 718: a red solid with
|
||||
/// Blend With Original 25 gives (132, 23, 46), from luminance 0.299; Rec. 709's 0.213 gave
|
||||
/// (113, 16, 49); on a grey ramp both weightings agree, case 815). The GPU kernels
|
||||
/// (`pointwise` op 1, `fx_color` op 4) mirror it.
|
||||
fn luma_601(c: [f32; 3]) -> f32 {
|
||||
0.299 * c[0] + 0.587 * c[1] + 0.114 * c[2]
|
||||
}
|
||||
|
||||
@@ -26,7 +29,7 @@ fn tint(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let white = ctx.params.color("white");
|
||||
let amt = ctx.params.f("amount") as f32 / 100.0;
|
||||
b.img.map_straight(|c| {
|
||||
let l = tint_luma(c).clamp(0.0, 1.0);
|
||||
let l = luma_601(c).clamp(0.0, 1.0);
|
||||
let t = [black[0] + (white[0] - black[0]) * l, black[1] + (white[1] - black[1]) * l, black[2] + (white[2] - black[2]) * l];
|
||||
mix(c, t, amt)
|
||||
});
|
||||
@@ -39,7 +42,7 @@ fn tritone(ctx: &EffectCtx, mut b: Buf) -> Buf {
|
||||
let sh = ctx.params.color("shadows");
|
||||
let blend = 1.0 - ctx.params.f("blend") as f32 / 100.0;
|
||||
b.img.map_straight(|c| {
|
||||
let l = luminance(c[0], c[1], c[2]).clamp(0.0, 1.0);
|
||||
let l = luma_601(c).clamp(0.0, 1.0);
|
||||
let t = if l < 0.5 {
|
||||
mix([sh[0], sh[1], sh[2]], [mid[0], mid[1], mid[2]], l * 2.0)
|
||||
} else {
|
||||
@@ -1248,6 +1251,29 @@ mod tests {
|
||||
/// #1463: Black & White's sliders set the gray of each pure hue and blend between
|
||||
/// neighbouring hues; neutral pixels keep their value, overbright ones too, and an overbright
|
||||
/// colour stays finite (it used to divide by zero).
|
||||
/// After Effects 2026: Tritone Highlights [1, 1, 0.8], Midtones [0.6, 0.2, 0.2], Shadows
|
||||
/// [0, 0, 0.3]. On a red solid with Blend With Original 25 (case 718) AE gives (132, 23, 46);
|
||||
/// on a grey ramp (case 815) levels 0, 63.9, 127.6, 191.4 give (0, 0, 77), (77, 26, 64),
|
||||
/// (153, 51, 51), (204, 153, 128).
|
||||
#[test]
|
||||
fn tritone_matches_after_effects() {
|
||||
let colours = [
|
||||
("highlights", Value::Color([1.0, 1.0, 0.8, 1.0])),
|
||||
("midtones", Value::Color([0.6, 0.2, 0.2, 1.0])),
|
||||
("shadows", Value::Color([0.0, 0.0, 0.3, 1.0])),
|
||||
];
|
||||
let near = |o: [f32; 4], ae: [f32; 3]| (0..3).all(|i| (o[i] * 255.0 - ae[i]).abs() <= 1.0);
|
||||
let mut red = colours.to_vec();
|
||||
red.push(("blend", num(25.0)));
|
||||
let o = px("ec.color.tritone", &red, [1.0, 0.0, 0.0, 1.0]);
|
||||
assert!(near(o, [132.0, 23.0, 46.0]), "{:?}", o.map(|v| v * 255.0));
|
||||
for (level, ae) in [(0.1f32, [0.0, 0.0, 77.0]), (63.9, [77.0, 26.0, 64.0]), (127.6, [153.0, 51.0, 51.0]), (191.4, [204.0, 153.0, 128.0])] {
|
||||
let v = level / 255.0;
|
||||
let o = px("ec.color.tritone", &colours, [v, v, v, 1.0]);
|
||||
assert!(near(o, ae), "level {level}: {:?} vs {ae:?}", o.map(|v| v * 255.0));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn black_white_sliders_set_the_gray_of_each_hue() {
|
||||
let bw = |vals: &[(&str, Value)], c: [f32; 3]| px("ec.color.blackwhite", vals, [c[0], c[1], c[2], 1.0]);
|
||||
|
||||
@@ -167,7 +167,8 @@ fn fxc_point(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
case 4u: {
|
||||
if (a > 0.0) {
|
||||
let c = px.xyz / a;
|
||||
let l = clamp(luminance(c), 0.0, 1.0);
|
||||
// Rec. 601 weights, as effects::color_fx::tritone.
|
||||
let l = clamp(dot(c, vec3<f32>(0.299, 0.587, 0.114)), 0.0, 1.0);
|
||||
var t: vec3<f32>;
|
||||
if (l < 0.5) {
|
||||
t = fxc_mix(P.f[2].xyz, P.f[1].xyz, l * 2.0);
|
||||
|
||||
@@ -605,7 +605,7 @@ fn pointwise(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
case 1u: {
|
||||
if (a > 0.0) {
|
||||
let c = straight(px);
|
||||
// Rec. 601 weights, as After Effects (effects crate tint_luma).
|
||||
// Rec. 601 weights, as After Effects (effects crate luma_601).
|
||||
let l = clamp(0.299 * c.x + 0.587 * c.y + 0.114 * c.z, 0.0, 1.0);
|
||||
let blk = P.f[0].xyz;
|
||||
let wht = P.f[1].xyz;
|
||||
|
||||
@@ -852,7 +852,7 @@ fn effects_whose_kernels_failed_render_on_the_cpu() {
|
||||
}
|
||||
|
||||
/// The GPU Tint gives After Effects 2026's greys for full-frame solids at its defaults (sRGB
|
||||
/// working space, not linearised): Rec. 601 weights, as the CPU (`color_fx::tint_luma`).
|
||||
/// working space, not linearised): Rec. 601 weights, as the CPU (`color_fx::luma_601`).
|
||||
#[test]
|
||||
fn gpu_tint_defaults_match_after_effects_greys() {
|
||||
let Some(g) = gpu() else { return };
|
||||
|
||||
@@ -30,7 +30,7 @@ corpus compares renders, each effect is assumed to differ somewhere, and users k
|
||||
| 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)) |
|
||||
| Channel | 13 | 13 | — |
|
||||
| Color Correction | 40 | 40 | Tint has Swap Colors (#963) and matches After Effects 2026 renders of primary and grey solids (Rec. 601 luminance); ACES 2.0 / OCIO 2.5 (26.5); Apply Color LUT (#514) and 32-bpc blending (#494) are closed |
|
||||
| Color Correction | 40 | 40 | Tint and Tritone match After Effects 2026 renders (Rec. 601 luminance); 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 |
|
||||
| Expression Controls | 9 | 9 | — |
|
||||
| Generate | 26 | 25 | Fractal on the CPU (needs f64); #526 (Reset) and #536 (Write-on marks) are closed |
|
||||
@@ -64,6 +64,7 @@ the GPU is performance, not parity.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-19 | minor | Tritone uses Rec. 601 luminance, measured against After Effects 2026 renders of a red solid and a grey ramp (CPU and GPU) |
|
||||
| 2026-10-18 | minor | Tint uses Rec. 601 luminance weights, measured against After Effects 2026 renders of red, green, blue and grey solids (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