From af7044d7db041fefeb913fc989776def6174ddb0 Mon Sep 17 00:00:00 2001 From: vangel76 Date: Thu, 5 Nov 2026 17:39:00 +0100 Subject: [PATCH] HDR Highlight Compression: check the curve at Amount 25, 50 and 75 (#3123) New After Effects 2026 renders of the same EXR at Amount 25, 50 and 75 (our own 1110-extra sample, 16-bit PNGs) confirm the transfer curve that was fitted at 84, 90 and 100. The worst channel error over all pixels is 4.0, 5.5 and 4.3 of 65535, against 997, 2415 and 3867 for the old soft knee. The code is unchanged. The measured-reference test now covers all six Amounts. Co-Authored-By: Claude Opus 5.5 --- crates/effects/src/utility.rs | 36 ++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/crates/effects/src/utility.rs b/crates/effects/src/utility.rs index 2448733b0..65de35cf6 100644 --- a/crates/effects/src/utility.rs +++ b/crates/effects/src/utility.rs @@ -68,7 +68,7 @@ fn hdr_spow(v: f32, e: f32) -> f32 { /// One straight-colour pixel through HDR Highlight Compression at blend `t` (Amount / 100, > 0). /// /// Measured behaviour (16-bit renders of a 0–4 HDR ramp and coloured patches at Amount 100, -/// 90 and 84): every channel is divided by `1 + Y` (`Y` = 0.30 R + 0.59 G + 0.11 B, so a grey +/// 90, 84, 75, 50 and 25): every channel is divided by `1 + Y` (`Y` = 0.30 R + 0.59 G + 0.11 B, so a grey /// value `x` lands on `x / (1 + x)`), clipped at 1, then mixed with the input as `x^0.4` values. fn hdr_compress_pixel(c: [f32; 3], t: f32) -> [f32; 3] { let y = (HDR_LUMA[0] * c[0] + HDR_LUMA[1] * c[1] + HDR_LUMA[2] * c[2]).max(0.0); @@ -600,26 +600,36 @@ mod tests { } /// Our own measurements of After Effects 2026 (32 bpc, no working space; 16-bit PNG renders - /// decoded back to linear): input pixel, then the output at Amount 100, 90 and 84. + /// decoded back to linear): input pixel, then the output at Amount 100, 90, 84, 75, 50 and 25. #[test] fn hdr_compression_matches_measured_reference() { + const AMOUNTS: [f64; 6] = [100.0, 90.0, 84.0, 75.0, 50.0, 25.0]; #[rustfmt::skip] - let table: [([f32; 3], [[f32; 3]; 3]); 9] = [ - ([0.18005371; 3], [[0.1524; 3], [0.1550; 3], [0.1566; 3]]), - ([0.49365234; 3], [[0.3302; 3], [0.3450; 3], [0.3540; 3]]), - ([1.0058594; 3], [[0.5014; 3], [0.5427; 3], [0.5683; 3]]), - ([2.0058594; 3], [[0.6673; 3], [0.7637; 3], [0.8256; 3]]), - ([2.9941406; 3], [[0.7496; 3], [0.8962; 3], [0.9922; 3]]), - ([3.9941406; 3], [[0.7998; 3], [0.9929; 3], [1.1213; 3]]), - ([0.7998047, 0.099975586, 0.099975586], [[0.6101, 0.0762, 0.0762], [0.6279, 0.0784, 0.0784], [0.6386, 0.0798, 0.0798]]), - ([0.099975586, 0.7001953, 0.19995117], [[0.0681, 0.4776, 0.1363], [0.0711, 0.4976, 0.1420], [0.0727, 0.5097, 0.1456]]), - ([0.30004883, 0.5, 2.5], [[0.1807, 0.3012, 0.9998], [0.1911, 0.3184, 1.1147], [0.1972, 0.3288, 1.1860]]), + let table: [([f32; 3], [[f32; 3]; 6]); 9] = [ + ([0.18005371; 3], [[0.1524; 3], [0.1550; 3], [0.1566; 3], [0.1590; 3], [0.1660; 3], [0.1729; 3]]), + ([0.49365234; 3], [[0.3302; 3], [0.3450; 3], [0.3540; 3], [0.3674; 3], [0.4073; 3], [0.4492; 3]]), + ([1.0058594; 3], [[0.5014; 3], [0.5427; 3], [0.5683; 3], [0.6085; 3], [0.7280; 3], [0.8602; 3]]), + ([2.0058594; 3], [[0.6673; 3], [0.7637; 3], [0.8256; 3], [0.9232; 3], [1.2292; 3], [1.5892; 3]]), + ([2.9941406; 3], [[0.7496; 3], [0.8962; 3], [0.9922; 3], [1.1465; 3], [1.6475; 3], [2.2604; 3]]), + ([3.9941406; 3], [[0.7998; 3], [0.9929; 3], [1.1213; 3], [1.3303; 3], [2.0312; 3], [2.9141; 3]]), + ([0.7998047, 0.099975586, 0.099975586], [ + [0.6101, 0.0762, 0.0762], [0.6279, 0.0784, 0.0784], [0.6386, 0.0798, 0.0798], + [0.6547, 0.0817, 0.0817], [0.7012, 0.0877, 0.0877], [0.7496, 0.0937, 0.0937], + ]), + ([0.099975586, 0.7001953, 0.19995117], [ + [0.0681, 0.4776, 0.1363], [0.0711, 0.4976, 0.1420], [0.0727, 0.5097, 0.1456], + [0.0754, 0.5287, 0.1508], [0.0832, 0.5829, 0.1662], [0.0912, 0.6397, 0.1825], + ]), + ([0.30004883, 0.5, 2.5], [ + [0.1807, 0.3012, 0.9998], [0.1911, 0.3184, 1.1147], [0.1972, 0.3288, 1.1860], + [0.2071, 0.3453, 1.2995], [0.2359, 0.3931, 1.6484], [0.2669, 0.4444, 2.0482], + ]), ]; let mut img = Image::new(table.len() as u32, 1); for (x, (c, _)) in table.iter().enumerate() { img.set(x as u32, 0, [c[0], c[1], c[2], 1.0]); } - for (i, amount) in [100.0, 90.0, 84.0].into_iter().enumerate() { + for (i, amount) in AMOUNTS.into_iter().enumerate() { let out = run("ec.utility.hdrcompression", &[("amount", num(amount))], img.clone()); for (x, (input, want)) in table.iter().enumerate() { let got = out.img.data[x];