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 <noreply@anthropic.com>
This commit is contained in:
vangel76andClaude Opus 5.5 committed 2026-11-05 17:39:00 +01:00
1 parent b9182972b2
commit af7044d7db
1 file changed
+23 -13
+23 -13
View File
@@ -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];