From 2bef8aa6696da6a48a812b96e99789eb72af1951 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Thu, 29 Oct 2026 06:46:51 -0700 Subject: [PATCH] Cineon: the Cineon Converter keeps its Gamma scale cineon_log_to_lin takes the exponent scale from its caller. Interpret Footage > Cineon Settings passes 1 (Gamma 1 gives linear light) and the Cineon Converter passes 1.7, as before, so the converter's output and its default Gamma 1.7 are unchanged. The converter's Linear to Log, Log to Log reference and GPU shader return to their earlier code. A test checks the converter at its default Gamma and at 2.4, for all three conversion types, against the earlier formula. --- crates/effects/src/utility.rs | 74 +++++++++++++++++++++++---- crates/gpu/src/fx_pixel2.rs | 2 +- crates/gpu/src/shaders/fx_pixel2.wgsl | 6 +-- crates/project/src/footage.rs | 16 +++--- crates/project/src/lib.rs | 2 +- 5 files changed, 76 insertions(+), 24 deletions(-) diff --git a/crates/effects/src/utility.rs b/crates/effects/src/utility.rs index 1f2ddeb7b..e41b42e99 100644 --- a/crates/effects/src/utility.rs +++ b/crates/effects/src/utility.rs @@ -100,11 +100,11 @@ impl Cineon { fn knee(&self) -> f32 { 1.0 - (self.rolloff / 100.0).clamp(0.0, 0.99) } - /// Normalized code value (code / 1023) → linear, raised to `1 / gamma` + /// Normalized code value (code / 1023) → linear, raised to `1.7 / gamma` /// ([`effectcraft_project::cineon_log_to_lin`], which Interpret Footage ▸ Cineon Settings - /// also uses). + /// also uses with `1 / gamma`). pub fn log_to_lin(&self, v: f32) -> f32 { - let lin = effectcraft_project::cineon_log_to_lin(v, self.black, self.white, self.gamma, self.rolloff); + let lin = effectcraft_project::cineon_log_to_lin(v, self.black, self.white, 1.7, self.gamma, self.rolloff); self.ib + (self.iw - self.ib) * lin } /// Linear → normalised code value. @@ -117,7 +117,7 @@ impl Cineon { let w = 1.0 - k; lin = k - w * (1.0 - (lin - k) / w).max(1e-6).ln(); } - lin = lin.max(0.0).powf(self.gamma.max(effectcraft_project::CINEON_MIN_GAMMA)); + lin = lin.max(0.0).powf(self.gamma.max(0.01) / 1.7); let code = self.white + (lin * (1.0 - off) + off).max(1e-9).log10() * 0.6 / 0.002; code / 1023.0 } @@ -132,7 +132,7 @@ fn cineon(ctx: &EffectCtx, mut b: Buf) -> Buf { gamma: ctx.params.f("gamma") as f32, rolloff: ctx.params.f("highlightRolloff") as f32, }; - let std = Cineon { black: 95.0, white: 685.0, ib: 0.0, iw: 1.0, gamma: 1.0, rolloff: c.rolloff }; + let std = Cineon { black: 95.0, white: 685.0, ib: 0.0, iw: 1.0, gamma: 1.7, rolloff: c.rolloff }; let kind = ctx.params.e("conversionType"); b.img.map_straight(|v| { v.map(|x| match kind { @@ -490,18 +490,19 @@ mod tests { } /// Interpret Footage ▸ Cineon Settings and the Cineon Converter's Log to Linear give the same - /// values for the same points, Gamma and Highlight Rolloff: at the default Gamma (1.7) and at - /// Gamma 1, which gives linear light (code 400 is 0.10251). + /// values for the same points and Highlight Rolloff when the converter's Gamma is 1.7 times + /// Cineon Settings' Gamma: the converter's default 1.7 and Cineon Settings' Gamma 1 both give + /// linear light (code 400 is 0.10251). #[test] fn cineon_converter_matches_interpret_footage() { let codes = [0.0f32, 95.0, 250.0, 400.0, 560.0, 685.0, 800.0, 1023.0]; - for (gamma, rolloff) in [(1.7, 20.0), (1.0, 0.0), (2.4, 50.0)] { + for (gamma, rolloff) in [(1.0, 20.0), (1.0, 0.0), (1.7, 50.0), (2.4, 50.0)] { let mut img = Image::new(codes.len() as u32, 1); for (x, code) in codes.iter().enumerate() { let v = code / 1023.0; img.set(x as u32, 0, [v, v, v, 1.0]); } - let vals = [("conversionType", Value::Enum(1)), ("gamma", num(gamma)), ("highlightRolloff", num(rolloff))]; + let vals = [("conversionType", Value::Enum(1)), ("gamma", num(gamma * 1.7)), ("highlightRolloff", num(rolloff))]; let out = run("ec.utility.cineon", &vals, img); let settings = effectcraft_project::CineonSettings { log_conversion: true, gamma: gamma as f32, rolloff: rolloff as f32, ..Default::default() }; for (x, code) in codes.iter().enumerate() { @@ -514,6 +515,61 @@ mod tests { assert!((linear.convert(400.0 / 1023.0) - 0.10251).abs() < 1e-4); } + /// The Cineon Converter gives the same values as before Cineon Settings shared its + /// log-to-linear function, at the default Gamma (1.7) and at Gamma 2.4, for all three + /// conversion types. + #[test] + fn cineon_converter_output_is_unchanged() { + fn log_to_lin(v: f32, gamma: f32, rolloff: f32, black: f32, white: f32) -> f32 { + let off = 10f32.powf((black - white) * 0.002 / 0.6); + let mut lin = (10f32.powf((v * 1023.0 - white) * 0.002 / 0.6) - off) / (1.0 - off).max(1e-6); + lin = lin.max(0.0).powf(1.7 / gamma.max(0.01)); + let k = 1.0 - (rolloff / 100.0).clamp(0.0, 0.99); + if k < 1.0 && lin > k { + let w = 1.0 - k; + lin = k + w * (1.0 - (-(lin - k) / w).exp()); + } + lin + } + fn lin_to_log(v: f32, gamma: f32, rolloff: f32, black: f32, white: f32) -> f32 { + let off = 10f32.powf((black - white) * 0.002 / 0.6); + let mut lin = v; + let k = 1.0 - (rolloff / 100.0).clamp(0.0, 0.99); + if k < 1.0 && lin > k { + let w = 1.0 - k; + lin = k - w * (1.0 - (lin - k) / w).max(1e-6).ln(); + } + lin = lin.max(0.0).powf(gamma.max(0.01) / 1.7); + (white + (lin * (1.0 - off) + off).max(1e-9).log10() * 0.6 / 0.002) / 1023.0 + } + let codes = [0.0f32, 95.0, 250.0, 400.0, 560.0, 685.0, 800.0, 1023.0]; + for gamma in [None, Some(2.4)] { + for kind in 0..3 { + let mut img = Image::new(codes.len() as u32, 1); + for (x, code) in codes.iter().enumerate() { + let v = code / 1023.0; + img.set(x as u32, 0, [v, v, v, 1.0]); + } + let mut vals = vec![("conversionType", Value::Enum(kind))]; + if let Some(g) = gamma { + vals.push(("gamma", num(g))); + } + let out = run("ec.utility.cineon", &vals, img); + let g = gamma.unwrap_or(1.7) as f32; + for (x, code) in codes.iter().enumerate() { + let v = code / 1023.0; + let want = match kind { + 0 => lin_to_log(v, g, 20.0, 95.0, 685.0), + 1 => log_to_lin(v, g, 20.0, 95.0, 685.0), + _ => lin_to_log(log_to_lin(v, g, 20.0, 95.0, 685.0), 1.7, 20.0, 95.0, 685.0), + }; + let got = out.img.data[x][0]; + assert!((want - got).abs() < 1e-6, "gamma {g}, type {kind}, code {code}: {want} vs {got}"); + } + } + } + } + #[test] fn hdr_compression_behaviour() { let mut img = Image::new(2, 1); diff --git a/crates/gpu/src/fx_pixel2.rs b/crates/gpu/src/fx_pixel2.rs index 6e3bc4ab0..acba936d0 100644 --- a/crates/gpu/src/fx_pixel2.rs +++ b/crates/gpu/src/fx_pixel2.rs @@ -228,7 +228,7 @@ fn point(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option { p.u[0] = [0, pr.e("conversionType"), 0, 0]; p.f[0] = [offset(black, white), knee, white, pr.f("gamma") as f32]; p.f[1] = [pr.f("internalBlackPoint") as f32, pr.f("internalWhitePoint") as f32, 0.0, 0.0]; - p.f[2] = [offset(95.0, 685.0), knee, 685.0, 1.0]; + p.f[2] = [offset(95.0, 685.0), knee, 685.0, 1.7]; } "ec.utility.hdrcompander" => { p.u[0] = [1, (pr.e("mode") == 1) as u32, 0, 0]; diff --git a/crates/gpu/src/shaders/fx_pixel2.wgsl b/crates/gpu/src/shaders/fx_pixel2.wgsl index 2b4dc0577..a63a91fed 100644 --- a/crates/gpu/src/shaders/fx_pixel2.wgsl +++ b/crates/gpu/src/shaders/fx_pixel2.wgsl @@ -257,13 +257,11 @@ fn fp2_vector(@builtin(global_invocation_id) gid: vec3) { // u[0].x = effect: 0 Cineon Converter, 1 HDR Compander, 2 HDR Highlight Compression, // 3 CC Overbrights, 4 Color Link (alpha), 5 Color Link (blend), 6 CC Composite. -const FP2_CINEON_MIN_GAMMA: f32 = 0.01 / 1.7; - // utility::Cineon::log_to_lin; c = (offset, knee, white, gamma), r = (internal black, white). fn fp2_log_to_lin(v: f32, c: vec4, r: vec2) -> f32 { let code = v * 1023.0; var lin = (pow(10.0, (code - c.z) * 0.002 / 0.6) - c.x) / max(1.0 - c.x, 1e-6); - lin = powz(max(lin, 0.0), 1.0 / max(c.w, FP2_CINEON_MIN_GAMMA)); + lin = powz(max(lin, 0.0), 1.7 / max(c.w, 0.01)); let k = c.y; if (k < 1.0 && lin > k) { let w = 1.0 - k; @@ -281,7 +279,7 @@ fn fp2_lin_to_log(v: f32, c: vec4, r: vec2) -> f32 { let w = 1.0 - k; lin = k - w * log(max(1.0 - (lin - k) / w, 1e-6)); } - lin = powz(max(lin, 0.0), max(c.w, FP2_CINEON_MIN_GAMMA)); + lin = powz(max(lin, 0.0), max(c.w, 0.01) / 1.7); let code = c.z + log(max(lin * (1.0 - c.x) + c.x, 1e-9)) * 0.4342944819032518 * 0.6 / 0.002; return code / 1023.0; } diff --git a/crates/project/src/footage.rs b/crates/project/src/footage.rs index e26d77417..ae5f113d5 100644 --- a/crates/project/src/footage.rs +++ b/crates/project/src/footage.rs @@ -161,27 +161,25 @@ impl CineonSettings { /// A code value (0–1 of the 10-bit range) converted from log to the Current Gamma and the /// converted black and white levels (0–1; above 1 for over-range highlights). pub fn convert(&self, v: f32) -> f32 { - let lin = cineon_log_to_lin(v, self.black, self.white, self.gamma, self.rolloff); + let lin = cineon_log_to_lin(v, self.black, self.white, 1.0, self.gamma, self.rolloff); let out = (self.converted_black + (self.converted_white - self.converted_black) * lin) / Self::MAX; if out.is_finite() { out } else { 0.0 } } } -/// The smallest Gamma [`cineon_log_to_lin`] and the Cineon Converter's Linear to Log use. -pub const CINEON_MIN_GAMMA: f32 = 0.01 / 1.7; - /// Cineon log to linear, shared by Interpret Footage ▸ Cineon Settings and the Cineon Converter /// effect. `v` is a code value as 0–1 of the 10-bit range; `black` and `white` are the 10 Bit /// Black and White Points. The result is 0 at the black point and 1 at the white point, raised to -/// `1 / gamma` (Gamma 1 gives linear light), with values above `1 - rolloff / 100` ramped off -/// toward 1 (Highlight Rolloff 0–100; 0 keeps over-range values). A Gamma below -/// [`CINEON_MIN_GAMMA`] is raised to it. -pub fn cineon_log_to_lin(v: f32, black: f32, white: f32, gamma: f32, rolloff: f32) -> f32 { +/// `scale / gamma`, with values above `1 - rolloff / 100` ramped off toward 1 (Highlight Rolloff +/// 0–100; 0 keeps over-range values). A Gamma below 0.01 is raised to 0.01. Cineon Settings passes +/// a `scale` of 1 (Gamma 1 gives linear light) and the Cineon Converter passes 1.7 (its default +/// Gamma 1.7 gives linear light). +pub fn cineon_log_to_lin(v: f32, black: f32, white: f32, scale: f32, gamma: f32, rolloff: f32) -> f32 { // Kodak printing density: 0.002 density per code value, negative gamma 0.6. let exposure = |code: f32| 10f32.powf((code - white) * 0.002 / 0.6); let offset = exposure(black); let mut lin = ((exposure(v * CineonSettings::MAX) - offset) / (1.0 - offset).max(1e-6)).max(0.0); - lin = lin.powf(1.0 / gamma.max(CINEON_MIN_GAMMA)); + lin = lin.powf(scale / gamma.max(0.01)); let knee = 1.0 - (rolloff / 100.0).clamp(0.0, 0.99); if knee < 1.0 && lin > knee { let w = 1.0 - knee; diff --git a/crates/project/src/lib.rs b/crates/project/src/lib.rs index 52f55fcac..37584eec4 100644 --- a/crates/project/src/lib.rs +++ b/crates/project/src/lib.rs @@ -36,7 +36,7 @@ mod layer; mod settings; pub use comp::{Comp, Guide, Renderer, Resolution}; -pub use footage::{AlphaMode, CINEON_MIN_GAMMA, CineonSettings, CineonUnits, FieldOrder, Footage, FootageKind, Proxy, Solid, SourceLayer, cineon_log_to_lin}; +pub use footage::{AlphaMode, CineonSettings, CineonUnits, FieldOrder, Footage, FootageKind, Proxy, Solid, SourceLayer, cineon_log_to_lin}; pub use item::{Item, ItemId, ItemKind, LayerId}; pub use layer::{AutoOrient, FrameBlend, Layer, LayerSource, LightKind, MatteKind, PrimitiveKind, Quality, Sampling, Switches, TrackMatte}; pub use settings::{