Skip to main content

ffcine_render/
bake.rs

1//! The LUT baker: a camera LUT, a grade LUT and the look ([`LookParams`]) evaluated over a lattice
2//! into one 3D LUT, with no ffmpeg in the loop. The result goes on the source as one
3//! `pl_custom_lut` (or out as a `.cube` for other tools), in the order the GPU pass runs them:
4//! camera LUT, grade LUT, look.
5//!
6//! LUTs are sampled trilinearly, as libplacebo samples them, so a baked LUT matches the unbaked
7//! chain up to the lattice spacing.
8
9use ffcine_core::lut::Lut;
10
11use crate::shaders::LookParams;
12
13/// The default lattice: 65^3, the prototype's bake size (and DJI's and Canon's LUTs').
14pub const DEFAULT_SIZE: usize = 65;
15
16/// What to bake, applied in this order.
17#[derive(Debug, Clone, Default)]
18pub struct Bake<'a> {
19    /// Log -> Rec.709 camera LUT ([`ffcine_core::camera`]).
20    pub camera: Option<&'a Lut>,
21    /// A creative or show LUT, on the Rec.709 picture.
22    pub lut: Option<&'a Lut>,
23    /// The post-LUT look.
24    pub look: LookParams,
25}
26
27impl Bake<'_> {
28    /// One value through the chain.
29    pub fn eval(&self, rgb: [f32; 3]) -> [f32; 3] {
30        let mut v = rgb;
31        for l in [self.camera, self.lut].into_iter().flatten() {
32            v = l.sample(v);
33        }
34        if self.look.is_identity() {
35            v
36        } else {
37            self.look.apply(v)
38        }
39    }
40
41    /// The chain as a `size`^3 LUT on 0..1 input.
42    pub fn bake(&self, size: usize) -> Lut {
43        let mut lut = Lut::identity(size.max(2));
44        for e in &mut lut.data {
45            *e = self.eval(*e);
46        }
47        let parts: Vec<&str> = [self.camera, self.lut]
48            .into_iter()
49            .flatten()
50            .map(|l| l.title.as_deref().unwrap_or("LUT"))
51            .chain((!self.look.is_identity()).then_some("look"))
52            .collect();
53        lut.title = Some(format!("ffcine bake: {}", parts.join(" + ")));
54        lut
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61
62    fn mono(f: impl Fn(f32) -> f32) -> Lut {
63        let mut l = Lut::identity(17);
64        for e in &mut l.data {
65            *e = e.map(&f);
66        }
67        l
68    }
69
70    #[test]
71    fn identity_bakes_to_identity() {
72        let b = Bake::default().bake(9);
73        assert_eq!(b.data, Lut::identity(9).data);
74    }
75
76    #[test]
77    fn chain_order_and_accuracy() {
78        let cam = mono(|x| x * x);
79        let grade = mono(|x| 0.5 + 0.5 * x);
80        let look = LookParams {
81            saturation: 0.8,
82            contrast: 0.1,
83            ..LookParams::default()
84        };
85        let b = Bake {
86            camera: Some(&cam),
87            lut: Some(&grade),
88            look,
89        };
90        let baked = b.bake(DEFAULT_SIZE);
91        // The baked LUT against the chain, off the lattice.
92        for v in [[0.1, 0.4, 0.9], [0.33, 0.33, 0.7], [0.95, 0.05, 0.5]] {
93            let want = b.eval(v);
94            let got = baked.sample(v);
95            for c in 0..3 {
96                assert!(
97                    (want[c] - got[c]).abs() < 0.01,
98                    "{v:?}: {want:?} vs {got:?}"
99                );
100            }
101        }
102        // Camera before grade: grade(cam(0.5)) = 0.625, not cam(grade(0.5)) = 0.5625.
103        let plain = Bake {
104            camera: Some(&cam),
105            lut: Some(&grade),
106            look: LookParams::default(),
107        };
108        assert!((plain.eval([0.5; 3])[0] - 0.625).abs() < 0.01);
109        assert!(baked.title.unwrap().ends_with("+ look"));
110    }
111}