Skip to main content

alpha2_render/
masking.rs

1//! Internal effects behind clip shape masks and track mattes.
2//!
3//! The timeline engine inserts these from a clip's `masks` and `matte`. Mask outlines are drawn
4//! as coverage by the vector pass and feathered by the Gaussian blur; these passes combine the
5//! coverages and apply them to the clip.
6
7use std::fmt::Write as _;
8
9use alpha2_gpu::CapabilityTier;
10
11use crate::{EffectDef, EffectKind, InputSpec, ParamSpec, ParamType, ParamUnit, ParamValue};
12
13/// Internal identifier of the track-matte pass.
14pub const TRACK_MATTE_EFFECT_ID: &str = "alpha2.internal.track-matte";
15
16/// Most masks one mask-combine pass takes, each a named input.
17pub const MAX_SHAPE_MASKS: usize = 8;
18
19const _: () = assert!(MAX_SHAPE_MASKS <= crate::MAX_EFFECT_INPUTS);
20
21/// Enum values of a mask's `mode{i}` parameter.
22pub const MASK_MODES: [&str; 3] = ["add", "subtract", "intersect"];
23
24/// Mask-combine pass ids; the pass for `n` masks is `MASK_COMBINE_EFFECT_IDS[n - 1]`.
25pub const MASK_COMBINE_EFFECT_IDS: [&str; MAX_SHAPE_MASKS] = [
26    "alpha2.internal.mask-combine-1",
27    "alpha2.internal.mask-combine-2",
28    "alpha2.internal.mask-combine-3",
29    "alpha2.internal.mask-combine-4",
30    "alpha2.internal.mask-combine-5",
31    "alpha2.internal.mask-combine-6",
32    "alpha2.internal.mask-combine-7",
33    "alpha2.internal.mask-combine-8",
34];
35
36/// Names of one mask's coverage input and parameters in [`mask_combine_definition`].
37#[derive(Copy, Clone, Debug, Eq, PartialEq)]
38pub struct MaskCombineNames {
39    /// Named input with the mask's coverage in alpha: `m{i}`.
40    pub input: &'static str,
41    /// `invert{i}`.
42    pub invert: &'static str,
43    /// `mode{i}`.
44    pub mode: &'static str,
45}
46
47macro_rules! mask_combine_names {
48    ($($index:literal),*) => {
49        [$(MaskCombineNames {
50            input: concat!("m", $index),
51            invert: concat!("invert", $index),
52            mode: concat!("mode", $index),
53        }),*]
54    };
55}
56
57/// Each mask's names by index, static so callers need not format them.
58pub const MASK_COMBINE_NAMES: [MaskCombineNames; MAX_SHAPE_MASKS] =
59    mask_combine_names!(0, 1, 2, 3, 4, 5, 6, 7);
60
61fn param(name: String, param_type: ParamType, default: ParamValue) -> ParamSpec {
62    ParamSpec {
63        name,
64        param_type,
65        default,
66        min: None,
67        max: None,
68        step: None,
69        unit: match param_type {
70            ParamType::Bool => ParamUnit::Bool,
71            ParamType::Enum => ParamUnit::Enum,
72            _ => ParamUnit::Normalized,
73        },
74        enum_values: if param_type == ParamType::Enum {
75            MASK_MODES.map(str::to_owned).to_vec()
76        } else {
77            Vec::new()
78        },
79    }
80}
81
82/// The pass combining `count` masks (1 to [`MAX_SHAPE_MASKS`]) into its source's coverage.
83///
84/// Mask `i` is input `m{i}` (coverage in alpha), inverted when `invert{i}` is set, and combined
85/// by `mode{i}`: coverage starts empty when the first mask adds and full otherwise; add is
86/// `c + m - c m`, subtract `c (1 - m)`, intersect `c m`. The source is multiplied by the result.
87///
88/// # Panics
89/// When `count` is zero or more than [`MAX_SHAPE_MASKS`].
90#[must_use]
91pub fn mask_combine_definition(count: usize) -> EffectDef {
92    assert!(
93        (1..=MAX_SHAPE_MASKS).contains(&count),
94        "a mask-combine pass takes 1 to {MAX_SHAPE_MASKS} masks"
95    );
96    let names = &MASK_COMBINE_NAMES[..count];
97    let mut params = Vec::with_capacity(count * 2);
98    let mut steps = String::new();
99    for names in names {
100        params.push(param(
101            names.invert.into(),
102            ParamType::Bool,
103            ParamValue::Bool(false),
104        ));
105        params.push(param(
106            names.mode.into(),
107            ParamType::Enum,
108            ParamValue::Enum(0),
109        ));
110        writeln!(
111            steps,
112            "    coverage = builtin_mask_step(coverage, sample_{input}(uv).a, params.{invert}, params.{mode});",
113            input = names.input,
114            invert = names.invert,
115            mode = names.mode,
116        )
117        .expect("writing to a String cannot fail");
118    }
119    EffectDef {
120        id: MASK_COMBINE_EFFECT_IDS[count - 1].into(),
121        version: 1,
122        kind: EffectKind::Effect,
123        inputs: names
124            .iter()
125            .map(|names| InputSpec {
126                name: names.input.into(),
127            })
128            .collect(),
129        params,
130        resources: Vec::new(),
131        wgsl_body: format!(
132            r"fn builtin_mask_step(coverage: f32, mask: f32, invert: u32, mode: u32) -> f32 {{
133    var m = clamp(mask, 0.0, 1.0);
134    if (invert != 0u) {{ m = 1.0 - m; }}
135    if (mode == 1u) {{ return coverage * (1.0 - m); }}
136    if (mode == 2u) {{ return coverage * m; }}
137    return coverage + m - coverage * m;
138}}
139
140fn effect(uv: vec2f, src: vec4f) -> vec4f {{
141    // Empty when the first mask adds, full otherwise.
142    var coverage = select(1.0, 0.0, params.mode0 == 0u);
143{steps}    return src * clamp(coverage, 0.0, 1.0);
144}}
145"
146        ),
147        tier_min: CapabilityTier::TierB,
148        license: Some(crate::builtins::ALPHA2_EFFECT_LICENSE.into()),
149    }
150}
151
152/// Every mask-combine pass, for one to [`MAX_SHAPE_MASKS`] masks.
153#[must_use]
154pub fn mask_combine_definitions() -> Vec<EffectDef> {
155    (1..=MAX_SHAPE_MASKS).map(mask_combine_definition).collect()
156}
157
158/// The track-matte pass: keeps the first input where the second's alpha (or luminance over
159/// black, BT.709 weights on linear light times alpha) says, optionally inverted.
160///
161/// A two-input transition that ignores progress; pass a value inside `(0, 1)` so its body runs.
162#[must_use]
163pub fn track_matte_definition() -> EffectDef {
164    EffectDef {
165        id: TRACK_MATTE_EFFECT_ID.into(),
166        version: 1,
167        kind: EffectKind::Transition,
168        inputs: Vec::new(),
169        params: vec![
170            ParamSpec {
171                unit: ParamUnit::Bool,
172                ..param("luma".into(), ParamType::Bool, ParamValue::Bool(false))
173            },
174            ParamSpec {
175                unit: ParamUnit::Bool,
176                ..param("inverted".into(), ParamType::Bool, ParamValue::Bool(false))
177            },
178        ],
179        resources: Vec::new(),
180        wgsl_body: r"fn builtin_matte_linear(value: f32) -> f32 {
181    if (value <= 0.04045) { return value / 12.92; }
182    return pow((value + 0.055) / 1.055, 2.4);
183}
184
185fn transition(uv: vec2f) -> vec4f {
186    let picture = from_color(uv);
187    let matte = to_color(uv);
188    var coverage = matte.a;
189    if (params.luma != 0u) {
190        let straight = clamp(matte.rgb / max(matte.a, 0.000001), vec3f(0.0), vec3f(1.0));
191        let linear = vec3f(
192            builtin_matte_linear(straight.r),
193            builtin_matte_linear(straight.g),
194            builtin_matte_linear(straight.b),
195        );
196        coverage = dot(linear, vec3f(0.2126, 0.7152, 0.0722)) * matte.a;
197    }
198    if (params.inverted != 0u) { coverage = 1.0 - coverage; }
199    return picture * clamp(coverage, 0.0, 1.0);
200}
201"
202        .into(),
203        tier_min: CapabilityTier::TierB,
204        license: Some(crate::builtins::ALPHA2_EFFECT_LICENSE.into()),
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::{
211        MASK_COMBINE_EFFECT_IDS, MASK_COMBINE_NAMES, MAX_SHAPE_MASKS, mask_combine_definition,
212    };
213
214    #[test]
215    fn each_masks_static_names_are_the_ones_the_pass_declares() {
216        for count in 1..=MAX_SHAPE_MASKS {
217            let definition = mask_combine_definition(count);
218            assert_eq!(definition.id, MASK_COMBINE_EFFECT_IDS[count - 1]);
219            let params = definition
220                .params
221                .iter()
222                .map(|spec| spec.name.clone())
223                .collect::<Vec<_>>();
224            let inputs = definition
225                .inputs
226                .iter()
227                .map(|input| input.name.clone())
228                .collect::<Vec<_>>();
229            let mut expected_params = Vec::new();
230            let mut expected_inputs = Vec::new();
231            for (index, names) in MASK_COMBINE_NAMES[..count].iter().enumerate() {
232                assert_eq!(names.input, format!("m{index}"));
233                assert_eq!(names.invert, format!("invert{index}"));
234                assert_eq!(names.mode, format!("mode{index}"));
235                expected_inputs.push(names.input.to_owned());
236                expected_params.push(names.invert.to_owned());
237                expected_params.push(names.mode.to_owned());
238            }
239            assert_eq!(inputs, expected_inputs);
240            assert_eq!(params, expected_params);
241        }
242    }
243}