mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-31 21:48:20 -06:00
Merge branch 'fix/f4': community issue fixes
This commit is contained in:
@@ -14,7 +14,8 @@
|
||||
//! * **Blend Colors Using 1.0 Gamma**: only blending is linear. Each layer's finished pixels
|
||||
//! are linearised just before they are transformed and blended, the comp accumulates in linear
|
||||
//! and is encoded back to the working space when the comp is done (so precomps hand encoded
|
||||
//! pixels to their parent). Without a working space the sRGB curve is used.
|
||||
//! pixels to their parent). Without a working space the Working Gamma's curve is used (sRGB
|
||||
//! at 2.2, a pure 2.4 power curve at 2.4); so is it for Interpret As Linear Light footage.
|
||||
|
||||
use effectcraft_color::{ColorSpace, Conversion};
|
||||
use effectcraft_project::{Footage, HdrMode, ProjectSettings};
|
||||
@@ -36,6 +37,8 @@ pub struct Pipe {
|
||||
pub out: ColorSpace,
|
||||
/// HDR handling for standard-dynamic-range output.
|
||||
pub hdr: HdrMode,
|
||||
/// Unmanaged with Working Gamma 2.4: the pure 2.4 power curve instead of sRGB's.
|
||||
pub gamma24: bool,
|
||||
}
|
||||
|
||||
impl Pipe {
|
||||
@@ -49,12 +52,20 @@ impl Pipe {
|
||||
linear_blend: s.blend_linear && !linear,
|
||||
out: s.output_space.unwrap_or(ColorSpace::Srgb),
|
||||
hdr: s.hdr,
|
||||
gamma24: s.working_space.is_none() && s.working_gamma_24(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The space whose curve encodes working-space pixels (sRGB when unmanaged).
|
||||
/// The space whose curve encodes working-space pixels (unmanaged: the Working Gamma's, sRGB
|
||||
/// at 2.2 and Rec. 709's pure 2.4 power curve at 2.4).
|
||||
fn curve(&self) -> ColorSpace {
|
||||
self.space.unwrap_or(ColorSpace::Srgb)
|
||||
self.space.unwrap_or(if self.gamma24 { ColorSpace::Rec709 } else { ColorSpace::Srgb })
|
||||
}
|
||||
|
||||
/// The space Interpret As Linear Light encodes footage `f` in: its colour profile, else sRGB
|
||||
/// (managed; the working space converts from there) or the Working Gamma's curve (unmanaged).
|
||||
pub fn linear_footage_space(&self, f: &Footage) -> ColorSpace {
|
||||
f.color_profile.unwrap_or(if self.space.is_some() { ColorSpace::Srgb } else { self.curve() })
|
||||
}
|
||||
|
||||
/// Footage `f` from its colour profile (`None` = sRGB) → working space; nothing for
|
||||
@@ -149,6 +160,7 @@ impl Pipe {
|
||||
k = k.wrapping_mul(31).wrapping_add(self.space.map_or(7, |s| s as u64 + 11));
|
||||
k = k.wrapping_mul(31).wrapping_add(self.linear as u64 * 2 + self.linear_blend as u64);
|
||||
k = k.wrapping_mul(31).wrapping_add(self.out as u64 * 4 + self.hdr as u64);
|
||||
k = k.wrapping_mul(31).wrapping_add(self.gamma24 as u64);
|
||||
k
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1200,12 +1200,12 @@ impl<'a> Renderer<'a> {
|
||||
let dominant_upper = f.fields == effectcraft_project::FieldOrder::UpperFirst;
|
||||
// Upper field = even lines (0, 2, …).
|
||||
let parity = if (i % 2 == 0) == dominant_upper { 0 } else { 1 };
|
||||
return Some(Arc::new(interpret_pixels(&field_frame(&img, parity), f)));
|
||||
return Some(Arc::new(interpret_pixels(&field_frame(&img, parity), f, self.pipe.linear_footage_space(f))));
|
||||
}
|
||||
// Interpretation is per pixel but not linear: it runs on the frame before resampling.
|
||||
let size = size.filter(|_| !(f.invert_alpha || f.linear_light_applies()));
|
||||
let img = self.footage_frame_raw(ctx, layer, item, f, t, size)?;
|
||||
if f.invert_alpha || f.linear_light_applies() { Some(Arc::new(interpret_pixels(&img, f))) } else { Some(img) }
|
||||
if f.invert_alpha || f.linear_light_applies() { Some(Arc::new(interpret_pixels(&img, f, self.pipe.linear_footage_space(f)))) } else { Some(img) }
|
||||
}
|
||||
|
||||
fn footage_frame_raw(&self, ctx: &EvalCtx, layer: &Layer, item: ItemId, f: &Footage, t: Tick, size: Option<(u32, u32)>) -> Option<Arc<Image>> {
|
||||
@@ -2074,13 +2074,12 @@ pub fn field_frame(img: &Image, parity: usize) -> Image {
|
||||
}
|
||||
|
||||
/// Interpret Footage ▸ Invert Alpha and Interpret As Linear Light on decoded (premultiplied)
|
||||
/// pixels.
|
||||
pub fn interpret_pixels(img: &Image, f: &Footage) -> Image {
|
||||
/// pixels; linear light is encoded in `space` ([`color::Pipe::linear_footage_space`]).
|
||||
pub fn interpret_pixels(img: &Image, f: &Footage, space: effectcraft_color::ColorSpace) -> Image {
|
||||
let linear = f.linear_light_applies();
|
||||
if !f.invert_alpha && !linear {
|
||||
return img.clone();
|
||||
}
|
||||
let space = f.color_profile.unwrap_or(effectcraft_color::ColorSpace::Srgb);
|
||||
let mut out = img.clone();
|
||||
for p in out.data.iter_mut() {
|
||||
let a = p[3];
|
||||
|
||||
@@ -254,3 +254,43 @@ fn layer_cache_is_keyed_by_colour_settings() {
|
||||
assert!((frame(&p) - a).abs() < 1e-9);
|
||||
assert_eq!(cache.stats().hits, hits + 1);
|
||||
}
|
||||
|
||||
/// #495: with no working space, Interpret As Linear Light footage is encoded with the project's
|
||||
/// Working Gamma: the sRGB curve at 2.2, a pure 2.4 power curve at 2.4 (as After Effects does);
|
||||
/// Blend Colors Using 1.0 Gamma linearizes with the same curve. A working space ignores it.
|
||||
#[test]
|
||||
fn working_gamma_24_encodes_linear_footage_with_a_power_curve() {
|
||||
let render = |gamma: f64, ws: Option<ColorSpace>| {
|
||||
let (mut p, cid, comp) = setup(BitDepth::Bpc32);
|
||||
p.settings.working_gamma = gamma;
|
||||
p.settings.working_space = ws;
|
||||
let fid = p.add_item("F", Label::Aqua, None, ItemKind::Footage(Footage { linear_light: true, ..footage(None) }));
|
||||
let l = build::layer(&mut p, &comp, "F", LayerSource::Footage { item: fid }, (40, 20), None);
|
||||
p.comp_mut(cid).unwrap().layers.push(l);
|
||||
let src = Flat([0.051, 0.18, 0.5]);
|
||||
Renderer::new(&p, &src, RenderOpts::default()).comp_frame(cid, Tick::ZERO).get(5, 5)
|
||||
};
|
||||
let near = |o: [f32; 4], want: [f32; 3]| (0..3).all(|i| (o[i] - want[i]).abs() < 2e-3);
|
||||
let o = render(2.4, None);
|
||||
assert!(near(o, [0.290, 0.490, 0.749]), "2.4: {o:?}");
|
||||
let o = render(2.2, None);
|
||||
assert!(near(o, [0.250, 0.461, 0.735]), "2.2: {o:?}");
|
||||
// A working space ignores Working Gamma.
|
||||
assert_eq!(render(2.4, Some(ColorSpace::Srgb)), render(2.2, Some(ColorSpace::Srgb)));
|
||||
// Blending with 1.0 gamma: 50% white over black is 0.5 linear, encoded with the curve.
|
||||
let blend = |gamma: f64| {
|
||||
let (mut p, cid, _) = setup(BitDepth::Bpc32);
|
||||
p.settings.working_gamma = gamma;
|
||||
p.settings.blend_linear = true;
|
||||
add_solid(&mut p, cid, [0.0, 0.0, 0.0], BlendMode::Normal, 100.0);
|
||||
add_solid(&mut p, cid, [1.0, 1.0, 1.0], BlendMode::Normal, 50.0);
|
||||
px(&p, cid)[0]
|
||||
};
|
||||
assert!((blend(2.4) - 0.5f32.powf(1.0 / 2.4)).abs() < 2e-3, "{}", blend(2.4));
|
||||
assert!((blend(2.2) - 0.7354).abs() < 2e-3, "{}", blend(2.2));
|
||||
assert_ne!(crate::color::Pipe::of(&Project::default().settings).key(), {
|
||||
let mut s = Project::default().settings;
|
||||
s.working_gamma = 2.4;
|
||||
crate::color::Pipe::of(&s).key()
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user