mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 21:16:52 -06:00
Cineon: Interpret Footage and the Cineon Converter share one log-to-linear function
effectcraft_project::cineon_log_to_lin does the log-to-linear conversion for Interpret Footage > Cineon Settings and the Cineon Converter's Log to Linear. Both raise the result to 1 / Gamma, so Gamma 1 gives linear light and the same Gamma gives the same values in both. The converter's Linear to Log, its Log to Log reference and the GPU shader use the same Gamma scale, with a smallest Gamma of 0.01 / 1.7 (CINEON_MIN_GAMMA). A test runs the converter at Gamma 1.7 (the default), 1 and 2.4 and checks it against Cineon Settings at each code value.
This commit is contained in:
@@ -100,17 +100,11 @@ impl Cineon {
|
||||
fn knee(&self) -> f32 {
|
||||
1.0 - (self.rolloff / 100.0).clamp(0.0, 0.99)
|
||||
}
|
||||
/// Normalised code value (code / 1023) → linear.
|
||||
/// Normalized code value (code / 1023) → linear, raised to `1 / gamma`
|
||||
/// ([`effectcraft_project::cineon_log_to_lin`], which Interpret Footage ▸ Cineon Settings
|
||||
/// also uses).
|
||||
pub fn log_to_lin(&self, v: f32) -> f32 {
|
||||
let off = self.offset();
|
||||
let code = v * 1023.0;
|
||||
let mut lin = (10f32.powf((code - self.white) * 0.002 / 0.6) - off) / (1.0 - off).max(1e-6);
|
||||
lin = lin.max(0.0).powf(1.7 / self.gamma.max(0.01));
|
||||
let k = self.knee();
|
||||
if k < 1.0 && lin > k {
|
||||
let w = 1.0 - k;
|
||||
lin = k + w * (1.0 - (-(lin - k) / w).exp());
|
||||
}
|
||||
let lin = effectcraft_project::cineon_log_to_lin(v, self.black, self.white, self.gamma, self.rolloff);
|
||||
self.ib + (self.iw - self.ib) * lin
|
||||
}
|
||||
/// Linear → normalised code value.
|
||||
@@ -123,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(0.01) / 1.7);
|
||||
lin = lin.max(0.0).powf(self.gamma.max(effectcraft_project::CINEON_MIN_GAMMA));
|
||||
let code = self.white + (lin * (1.0 - off) + off).max(1e-9).log10() * 0.6 / 0.002;
|
||||
code / 1023.0
|
||||
}
|
||||
@@ -138,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.7, rolloff: c.rolloff };
|
||||
let std = Cineon { black: 95.0, white: 685.0, ib: 0.0, iw: 1.0, gamma: 1.0, rolloff: c.rolloff };
|
||||
let kind = ctx.params.e("conversionType");
|
||||
b.img.map_straight(|v| {
|
||||
v.map(|x| match kind {
|
||||
@@ -495,6 +489,31 @@ mod tests {
|
||||
assert!(c.log_to_lin(95.0 / 1023.0).abs() < 1e-4);
|
||||
}
|
||||
|
||||
/// 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).
|
||||
#[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)] {
|
||||
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 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() {
|
||||
let footage = settings.convert(code / 1023.0);
|
||||
let effect = out.img.data[x][0];
|
||||
assert!((footage - effect).abs() < 1e-6, "gamma {gamma}, rolloff {rolloff}, code {code}: {footage} vs {effect}");
|
||||
}
|
||||
}
|
||||
let linear = effectcraft_project::CineonSettings { gamma: 1.0, rolloff: 0.0, ..Default::default() };
|
||||
assert!((linear.convert(400.0 / 1023.0) - 0.10251).abs() < 1e-4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hdr_compression_behaviour() {
|
||||
let mut img = Image::new(2, 1);
|
||||
|
||||
@@ -228,7 +228,7 @@ fn point(e: &mut Enc, id: &str, ctx: &EffectCtx, b: GBuf) -> Option<GBuf> {
|
||||
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.7];
|
||||
p.f[2] = [offset(95.0, 685.0), knee, 685.0, 1.0];
|
||||
}
|
||||
"ec.utility.hdrcompander" => {
|
||||
p.u[0] = [1, (pr.e("mode") == 1) as u32, 0, 0];
|
||||
|
||||
@@ -257,11 +257,13 @@ fn fp2_vector(@builtin(global_invocation_id) gid: vec3<u32>) {
|
||||
// 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<f32>, r: vec2<f32>) -> 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.7 / max(c.w, 0.01));
|
||||
lin = powz(max(lin, 0.0), 1.0 / max(c.w, FP2_CINEON_MIN_GAMMA));
|
||||
let k = c.y;
|
||||
if (k < 1.0 && lin > k) {
|
||||
let w = 1.0 - k;
|
||||
@@ -279,7 +281,7 @@ fn fp2_lin_to_log(v: f32, c: vec4<f32>, r: vec2<f32>) -> 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, 0.01) / 1.7);
|
||||
lin = powz(max(lin, 0.0), max(c.w, FP2_CINEON_MIN_GAMMA));
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -161,21 +161,35 @@ 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 {
|
||||
// Kodak printing density: 0.002 density per code value, negative gamma 0.6.
|
||||
let exposure = |code: f32| 10f32.powf((code - self.white) * 0.002 / 0.6);
|
||||
let offset = exposure(self.black);
|
||||
let mut lin = ((exposure(v * Self::MAX) - offset) / (1.0 - offset).max(1e-6)).max(0.0);
|
||||
lin = lin.powf(1.0 / self.gamma.max(0.01));
|
||||
let knee = 1.0 - (self.rolloff / 100.0).clamp(0.0, 0.99);
|
||||
if knee < 1.0 && lin > knee {
|
||||
let w = 1.0 - knee;
|
||||
lin = knee + w * (1.0 - (-(lin - knee) / w).exp());
|
||||
}
|
||||
let lin = cineon_log_to_lin(v, self.black, self.white, 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 {
|
||||
// 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));
|
||||
let knee = 1.0 - (rolloff / 100.0).clamp(0.0, 0.99);
|
||||
if knee < 1.0 && lin > knee {
|
||||
let w = 1.0 - knee;
|
||||
lin = knee + w * (1.0 - (-(lin - knee) / w).exp());
|
||||
}
|
||||
lin
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Footage {
|
||||
pub path: String,
|
||||
|
||||
@@ -36,7 +36,7 @@ mod layer;
|
||||
mod settings;
|
||||
|
||||
pub use comp::{Comp, Guide, Renderer, Resolution};
|
||||
pub use footage::{AlphaMode, CineonSettings, CineonUnits, FieldOrder, Footage, FootageKind, Proxy, Solid, SourceLayer};
|
||||
pub use footage::{AlphaMode, CINEON_MIN_GAMMA, 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::{
|
||||
|
||||
Reference in New Issue
Block a user