Skip to main content

ffcine_render/
blend.rs

1//! Blend modes for [`crate::Layer`]s (`ffcine.comp.blend/1` `mode`, Resolve's composite modes):
2//! the W3C compositing formulas (Photoshop's where W3C has none), as a CPU reference and as the
3//! GLSL the backend's composite pass runs. Both work on encoded RGB in 0..1 (the output's
4//! transfer), the backdrop opaque: `out = (1 − a) · b + a · B(b, s)` with `a` the layer's alpha
5//! times its opacity.
6
7use std::fmt;
8use std::str::FromStr;
9
10use crate::Error;
11
12macro_rules! modes {
13    ($($(#[$m:meta])* $v:ident = $s:literal),+ $(,)?) => {
14        /// How a layer combines with the picture under it.
15        #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
16        pub enum BlendMode {
17            #[default]
18            $($(#[$m])* $v),+
19        }
20
21        impl BlendMode {
22            /// Every mode, in shader index order.
23            pub const ALL: &[Self] = &[$(Self::$v),+];
24
25            /// The `ffcine.comp.blend/1` name.
26            pub fn as_str(self) -> &'static str {
27                match self { $(Self::$v => $s),+ }
28            }
29        }
30
31        impl FromStr for BlendMode {
32            type Err = Error;
33            fn from_str(s: &str) -> Result<Self, Error> {
34                match s {
35                    $($s => Ok(Self::$v),)+
36                    _ => Err(Error::Invalid { what: "blend mode", value: s.to_owned() }),
37                }
38            }
39        }
40    };
41}
42
43modes! {
44    Normal = "normal",
45    Add = "add",
46    Subtract = "subtract",
47    Difference = "difference",
48    Multiply = "multiply",
49    Screen = "screen",
50    Overlay = "overlay",
51    HardLight = "hardlight",
52    SoftLight = "softlight",
53    Darken = "darken",
54    Lighten = "lighten",
55    ColorDodge = "color_dodge",
56    ColorBurn = "color_burn",
57    Exclusion = "exclusion",
58    Hue = "hue",
59    Saturation = "saturation",
60    Color = "color",
61    Luminosity = "luminosity",
62    Divide = "divide",
63    /// Same as `add` (Resolve lists both).
64    LinearDodge = "linear_dodge",
65    LinearBurn = "linear_burn",
66    LinearLight = "linear_light",
67    VividLight = "vivid_light",
68    PinLight = "pin_light",
69    HardMix = "hard_mix",
70    LighterColor = "lighter_color",
71    DarkerColor = "darker_color",
72}
73
74impl fmt::Display for BlendMode {
75    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76        f.write_str(self.as_str())
77    }
78}
79
80impl BlendMode {
81    /// The `mode` uniform of [`glsl`].
82    pub fn index(self) -> i32 {
83        Self::ALL
84            .iter()
85            .position(|m| *m == self)
86            .and_then(|i| i32::try_from(i).ok())
87            .unwrap_or(0)
88    }
89
90    /// `B(b, s)`: backdrop `b`, layer `s`, both clamped to 0..1.
91    pub fn apply(self, b: [f32; 3], s: [f32; 3]) -> [f32; 3] {
92        let (b, s) = (b.map(|x| x.clamp(0.0, 1.0)), s.map(|x| x.clamp(0.0, 1.0)));
93        let sep = |f: fn(f32, f32) -> f32| [f(b[0], s[0]), f(b[1], s[1]), f(b[2], s[2])];
94        match self {
95            Self::Normal => s,
96            Self::Add | Self::LinearDodge => sep(|b, s| (b + s).min(1.0)),
97            Self::Subtract => sep(|b, s| (b - s).max(0.0)),
98            Self::Difference => sep(|b, s| (b - s).abs()),
99            Self::Multiply => sep(|b, s| b * s),
100            Self::Screen => sep(screen),
101            Self::Overlay => sep(|b, s| hard_light(s, b)),
102            Self::HardLight => sep(hard_light),
103            Self::SoftLight => sep(soft_light),
104            Self::Darken => sep(f32::min),
105            Self::Lighten => sep(f32::max),
106            Self::ColorDodge => sep(dodge),
107            Self::ColorBurn => sep(burn),
108            Self::Exclusion => sep(|b, s| b + s - 2.0 * b * s),
109            Self::Hue => set_lum(set_sat(s, sat(b)), lum(b)),
110            Self::Saturation => set_lum(set_sat(b, sat(s)), lum(b)),
111            Self::Color => set_lum(s, lum(b)),
112            Self::Luminosity => set_lum(b, lum(s)),
113            Self::Divide => sep(|b, s| {
114                if s <= 0.0 {
115                    if b > 0.0 { 1.0 } else { 0.0 }
116                } else {
117                    (b / s).min(1.0)
118                }
119            }),
120            Self::LinearBurn => sep(|b, s| (b + s - 1.0).max(0.0)),
121            Self::LinearLight => sep(|b, s| (b + 2.0 * s - 1.0).clamp(0.0, 1.0)),
122            Self::VividLight => sep(|b, s| {
123                if s <= 0.5 {
124                    burn(b, 2.0 * s)
125                } else {
126                    dodge(b, 2.0 * s - 1.0)
127                }
128            }),
129            Self::PinLight => sep(|b, s| {
130                if s <= 0.5 {
131                    b.min(2.0 * s)
132                } else {
133                    b.max(2.0 * s - 1.0)
134                }
135            }),
136            Self::HardMix => sep(|b, s| if b + s >= 1.0 { 1.0 } else { 0.0 }),
137            Self::LighterColor => {
138                if lum(s) > lum(b) {
139                    s
140                } else {
141                    b
142                }
143            }
144            Self::DarkerColor => {
145                if lum(s) < lum(b) {
146                    s
147                } else {
148                    b
149                }
150            }
151        }
152    }
153
154    /// The composite of layer `s` with alpha `a` (times opacity) over opaque backdrop `b`.
155    pub fn composite(self, b: [f32; 3], s: [f32; 3], a: f32) -> [f32; 3] {
156        let a = a.clamp(0.0, 1.0);
157        let m = self.apply(b, s);
158        [0, 1, 2].map(|i| b[i] + a * (m[i] - b[i]))
159    }
160}
161
162fn screen(b: f32, s: f32) -> f32 {
163    b + s - b * s
164}
165
166fn hard_light(b: f32, s: f32) -> f32 {
167    if s <= 0.5 {
168        b * 2.0 * s
169    } else {
170        screen(b, 2.0 * s - 1.0)
171    }
172}
173
174fn soft_light(b: f32, s: f32) -> f32 {
175    if s <= 0.5 {
176        b - (1.0 - 2.0 * s) * b * (1.0 - b)
177    } else {
178        let d = if b <= 0.25 {
179            ((16.0 * b - 12.0) * b + 4.0) * b
180        } else {
181            b.sqrt()
182        };
183        b + (2.0 * s - 1.0) * (d - b)
184    }
185}
186
187fn dodge(b: f32, s: f32) -> f32 {
188    if b <= 0.0 {
189        0.0
190    } else if s >= 1.0 {
191        1.0
192    } else {
193        (b / (1.0 - s)).min(1.0)
194    }
195}
196
197fn burn(b: f32, s: f32) -> f32 {
198    if b >= 1.0 {
199        1.0
200    } else if s <= 0.0 {
201        0.0
202    } else {
203        1.0 - ((1.0 - b) / s).min(1.0)
204    }
205}
206
207fn lum(c: [f32; 3]) -> f32 {
208    0.3 * c[0] + 0.59 * c[1] + 0.11 * c[2]
209}
210
211fn clip_color(c: [f32; 3]) -> [f32; 3] {
212    let l = lum(c);
213    let n = c[0].min(c[1]).min(c[2]);
214    let x = c[0].max(c[1]).max(c[2]);
215    let mut c = c;
216    if n < 0.0 {
217        c = c.map(|v| l + (v - l) * l / (l - n));
218    }
219    if x > 1.0 {
220        c = c.map(|v| l + (v - l) * (1.0 - l) / (x - l));
221    }
222    c
223}
224
225fn set_lum(c: [f32; 3], l: f32) -> [f32; 3] {
226    let d = l - lum(c);
227    clip_color(c.map(|v| v + d))
228}
229
230fn sat(c: [f32; 3]) -> f32 {
231    c[0].max(c[1]).max(c[2]) - c[0].min(c[1]).min(c[2])
232}
233
234fn set_sat(c: [f32; 3], s: f32) -> [f32; 3] {
235    let n = c[0].min(c[1]).min(c[2]);
236    let x = c[0].max(c[1]).max(c[2]);
237    if x > n {
238        c.map(|v| (v - n) * s / (x - n))
239    } else {
240        [0.0; 3]
241    }
242}
243
244/// GLSL for the composite pass: `vec3 blend_mode(int mode, vec3 b, vec3 s)`, the same formulas
245/// as [`BlendMode::apply`], `mode` as [`BlendMode::index`].
246pub fn glsl() -> &'static str {
247    r"
248float bl_screen(float b, float s) { return b + s - b * s; }
249float bl_hard(float b, float s) { return s <= 0.5 ? b * 2.0 * s : bl_screen(b, 2.0 * s - 1.0); }
250float bl_soft(float b, float s) {
251    if (s <= 0.5)
252        return b - (1.0 - 2.0 * s) * b * (1.0 - b);
253    float d = b <= 0.25 ? ((16.0 * b - 12.0) * b + 4.0) * b : sqrt(b);
254    return b + (2.0 * s - 1.0) * (d - b);
255}
256float bl_dodge(float b, float s) {
257    return b <= 0.0 ? 0.0 : (s >= 1.0 ? 1.0 : min(b / (1.0 - s), 1.0));
258}
259float bl_burn(float b, float s) {
260    return b >= 1.0 ? 1.0 : (s <= 0.0 ? 0.0 : 1.0 - min((1.0 - b) / s, 1.0));
261}
262float bl_divide(float b, float s) {
263    return s <= 0.0 ? (b > 0.0 ? 1.0 : 0.0) : min(b / s, 1.0);
264}
265float bl_vivid(float b, float s) { return s <= 0.5 ? bl_burn(b, 2.0 * s) : bl_dodge(b, 2.0 * s - 1.0); }
266float bl_pin(float b, float s) { return s <= 0.5 ? min(b, 2.0 * s) : max(b, 2.0 * s - 1.0); }
267float bl_lum(vec3 c) { return dot(c, vec3(0.3, 0.59, 0.11)); }
268vec3 bl_clip(vec3 c) {
269    float l = bl_lum(c);
270    float n = min(c.r, min(c.g, c.b));
271    float x = max(c.r, max(c.g, c.b));
272    if (n < 0.0)
273        c = l + (c - l) * l / (l - n);
274    if (x > 1.0)
275        c = l + (c - l) * (1.0 - l) / (x - l);
276    return c;
277}
278vec3 bl_set_lum(vec3 c, float l) { return bl_clip(c + (l - bl_lum(c))); }
279float bl_sat(vec3 c) { return max(c.r, max(c.g, c.b)) - min(c.r, min(c.g, c.b)); }
280vec3 bl_set_sat(vec3 c, float s) {
281    float n = min(c.r, min(c.g, c.b));
282    float x = max(c.r, max(c.g, c.b));
283    return x > n ? (c - n) * s / (x - n) : vec3(0.0);
284}
285#define BL_SEP(f) vec3(f(b.r, s.r), f(b.g, s.g), f(b.b, s.b))
286vec3 blend_mode(int mode, vec3 b, vec3 s) {
287    b = clamp(b, 0.0, 1.0);
288    s = clamp(s, 0.0, 1.0);
289    switch (mode) {
290    case 1: case 19: return min(b + s, 1.0);
291    case 2: return max(b - s, 0.0);
292    case 3: return abs(b - s);
293    case 4: return b * s;
294    case 5: return b + s - b * s;
295    case 6: return vec3(bl_hard(s.r, b.r), bl_hard(s.g, b.g), bl_hard(s.b, b.b));
296    case 7: return BL_SEP(bl_hard);
297    case 8: return BL_SEP(bl_soft);
298    case 9: return min(b, s);
299    case 10: return max(b, s);
300    case 11: return BL_SEP(bl_dodge);
301    case 12: return BL_SEP(bl_burn);
302    case 13: return b + s - 2.0 * b * s;
303    case 14: return bl_set_lum(bl_set_sat(s, bl_sat(b)), bl_lum(b));
304    case 15: return bl_set_lum(bl_set_sat(b, bl_sat(s)), bl_lum(b));
305    case 16: return bl_set_lum(s, bl_lum(b));
306    case 17: return bl_set_lum(b, bl_lum(s));
307    case 18: return BL_SEP(bl_divide);
308    case 20: return max(b + s - 1.0, 0.0);
309    case 21: return clamp(b + 2.0 * s - 1.0, 0.0, 1.0);
310    case 22: return BL_SEP(bl_vivid);
311    case 23: return BL_SEP(bl_pin);
312    case 24: return step(1.0, b + s);
313    case 25: return bl_lum(s) > bl_lum(b) ? s : b;
314    case 26: return bl_lum(s) < bl_lum(b) ? s : b;
315    default: return s;
316    }
317}
318"
319}
320
321#[cfg(test)]
322mod tests {
323    use super::*;
324
325    fn near(a: [f32; 3], b: [f32; 3]) -> bool {
326        a.iter().zip(&b).all(|(x, y)| (x - y).abs() < 1e-5)
327    }
328
329    #[test]
330    fn names_round_trip_and_index_matches_glsl() {
331        assert_eq!(BlendMode::ALL.len(), 27);
332        for (i, m) in BlendMode::ALL.iter().enumerate() {
333            assert_eq!(m.as_str().parse::<BlendMode>().unwrap(), *m);
334            assert_eq!(m.index(), i32::try_from(i).unwrap());
335            if !matches!(
336                m,
337                BlendMode::Normal | BlendMode::Add | BlendMode::LinearDodge
338            ) {
339                assert!(glsl().contains(&format!("case {i}:")), "{m}");
340            }
341        }
342        assert!(glsl().contains("case 1: case 19:"));
343        assert!("bogus".parse::<BlendMode>().is_err());
344        assert_eq!(BlendMode::default(), BlendMode::Normal);
345    }
346
347    #[test]
348    fn separable_modes() {
349        let (b, s) = ([0.2, 0.5, 0.8], [0.6, 0.5, 0.1]);
350        let t = |m: BlendMode| m.apply(b, s);
351        assert!(near(t(BlendMode::Normal), s));
352        assert!(near(t(BlendMode::Add), [0.8, 1.0, 0.9]));
353        assert!(near(t(BlendMode::LinearDodge), t(BlendMode::Add)));
354        assert!(near(t(BlendMode::Subtract), [0.0, 0.0, 0.7]));
355        assert!(near(t(BlendMode::Difference), [0.4, 0.0, 0.7]));
356        assert!(near(t(BlendMode::Multiply), [0.12, 0.25, 0.08]));
357        assert!(near(t(BlendMode::Screen), [0.68, 0.75, 0.82]));
358        assert!(near(t(BlendMode::Darken), [0.2, 0.5, 0.1]));
359        assert!(near(t(BlendMode::Lighten), [0.6, 0.5, 0.8]));
360        assert!(near(t(BlendMode::Exclusion), [0.56, 0.5, 0.74]));
361        assert!(near(t(BlendMode::LinearBurn), [0.0, 0.0, 0.0]));
362        assert!(near(t(BlendMode::LinearLight), [0.4, 0.5, 0.0]));
363        // overlay(b, s) = hardlight(s, b): b 0.2 -> 2·0.2·0.6, b 0.8 -> screen(0.1, 0.6).
364        assert!(near(t(BlendMode::Overlay), [0.24, 0.5, 0.64]));
365        assert!(near(t(BlendMode::HardLight), [0.36, 0.5, 0.16]));
366        assert!(near(t(BlendMode::ColorDodge), [0.5, 1.0, 0.888_889]));
367        assert!(near(t(BlendMode::ColorBurn), [0.0, 0.0, 0.0]));
368        assert!(near(t(BlendMode::Divide), [1.0 / 3.0, 1.0, 1.0]));
369        assert!(near(t(BlendMode::PinLight), [0.2, 0.5, 0.2]));
370        assert!(near(t(BlendMode::HardMix), [0.0, 1.0, 0.0]));
371        // Soft light: s 0.5 leaves b; s 1 over b 0.81 is sqrt(b).
372        assert!(near(
373            BlendMode::SoftLight.apply([0.3; 3], [0.5; 3]),
374            [0.3; 3]
375        ));
376        assert!(near(
377            BlendMode::SoftLight.apply([0.81; 3], [1.0; 3]),
378            [0.9; 3]
379        ));
380        // Vivid light: burn below a half, dodge above.
381        assert!(near(
382            BlendMode::VividLight.apply([0.6; 3], [0.25; 3]),
383            [0.2; 3]
384        ));
385        assert!(near(
386            BlendMode::VividLight.apply([0.3; 3], [0.75; 3]),
387            [0.6; 3]
388        ));
389    }
390
391    #[test]
392    #[allow(clippy::many_single_char_names)]
393    fn non_separable_modes() {
394        let (b, s) = ([0.2, 0.4, 0.6], [0.9, 0.1, 0.1]);
395        // Luminosity keeps the backdrop's hue and takes the layer's luma, and colour the reverse.
396        let l = BlendMode::Luminosity.apply(b, s);
397        assert!((lum(l) - lum(s)).abs() < 1e-5);
398        let c = BlendMode::Color.apply(b, s);
399        assert!((lum(c) - lum(b)).abs() < 1e-5);
400        assert!(c[0] > c[1] && (c[1] - c[2]).abs() < 1e-6);
401        // Saturation of a grey layer leaves grey at the backdrop's luma.
402        let g = BlendMode::Saturation.apply(b, [0.5; 3]);
403        assert!(near(g, [lum(b); 3]));
404        // Hue takes the layer's hue at the backdrop's saturation and luma.
405        let h = BlendMode::Hue.apply(b, s);
406        assert!((lum(h) - lum(b)).abs() < 1e-5);
407        assert!((sat(h) - sat(b)).abs() < 1e-5);
408        assert!(h[0] > h[1]);
409        assert!(near(BlendMode::LighterColor.apply(b, s), b));
410        assert!(near(BlendMode::DarkerColor.apply(b, s), s));
411    }
412
413    #[test]
414    fn composite_mixes_by_alpha() {
415        let b = [0.2, 0.4, 0.6];
416        let s = [1.0, 0.0, 0.5];
417        assert!(near(BlendMode::Multiply.composite(b, s, 0.0), b));
418        assert!(near(
419            BlendMode::Multiply.composite(b, s, 1.0),
420            [0.2, 0.0, 0.3]
421        ));
422        assert!(near(
423            BlendMode::Multiply.composite(b, s, 0.5),
424            [0.2, 0.2, 0.45]
425        ));
426    }
427}