Skip to main content

alpha2_render/builtins/
mod.rs

1//! Built-in effects and transitions as data, one module per catalog group.
2
3use alpha2_gpu::CapabilityTier;
4use alpha2_shaders::{
5    CHROMA_KEY_BODY, EFFECT_HELPERS, EXPANDED_EFFECT, FOCUS_COMBINE, GAUSSIAN_BLUR,
6    GL_TRANSITION_ENTRY, GLOW_COMBINE, GLOW_THRESHOLD, LUT_3D_BODY, SHADOW_COMBINE, SHADOW_OFFSET,
7    TRANSITION_HELPERS, UNSHARP_COMBINE,
8};
9
10use crate::{
11    EffectDef, EffectId, EffectKind, EffectResourceKind, InputSpec, ParamSpec, ParamType,
12    ParamUnit, ParamValue, Renderer, ResourceSpec, ShaderDiagnostics,
13};
14
15mod effects;
16mod transitions;
17
18pub(crate) use effects::blur::{
19    directional_blur_definition, tilt_shift_definition, unsharp_mask_definition,
20};
21pub(crate) use effects::stylize::drop_shadow_definition;
22
23/// Stable identifier of the built-in chroma-key effect.
24pub const CHROMA_KEY_ID: &str = "alpha2.effect.chroma-key";
25
26/// Stable identifier of the atomic Gaussian-blur effect.
27pub const GAUSSIAN_BLUR_ID: &str = "alpha2.effect.gaussian-blur";
28
29/// Internal stable identifier of one separable Gaussian-blur pass.
30pub(crate) const GAUSSIAN_BLUR_PASS_ID: &str = "alpha2.internal.gaussian-blur-pass";
31
32/// Stable identifier of the built-in glow effect.
33pub const GLOW_ID: &str = "alpha2.effect.glow";
34
35/// Internal stable identifier of glow's threshold pass.
36pub(crate) const GLOW_THRESHOLD_ID: &str = "alpha2.internal.glow-threshold";
37
38/// Internal stable identifier of glow's two-input combining pass.
39pub(crate) const GLOW_COMBINE_ID: &str = "alpha2.internal.glow-combine";
40
41/// Stable identifier of the built-in directional (one-axis Gaussian) blur.
42pub(crate) const DIRECTIONAL_BLUR_ID: &str = "alpha2.effect.directional-blur";
43
44/// Stable identifier of the built-in unsharp mask.
45pub(crate) const UNSHARP_MASK_ID: &str = "alpha2.effect.unsharp-mask";
46
47/// Internal stable identifier of unsharp mask's two-input combining pass.
48pub(crate) const UNSHARP_COMBINE_ID: &str = "alpha2.internal.unsharp-combine";
49
50/// Stable identifier of the built-in tilt-shift.
51pub(crate) const TILT_SHIFT_ID: &str = "alpha2.effect.tilt-shift";
52
53/// Internal identifier of tilt-shift's pass mixing the source and its two blurs.
54pub(crate) const FOCUS_COMBINE_ID: &str = "alpha2.internal.focus-combine";
55
56/// Stable identifier of the built-in drop shadow.
57pub(crate) const DROP_SHADOW_ID: &str = "alpha2.effect.drop-shadow";
58
59/// Internal stable identifier of drop shadow's offset pass.
60pub(crate) const SHADOW_OFFSET_ID: &str = "alpha2.internal.shadow-offset";
61
62/// Internal stable identifier of drop shadow's two-input combining pass.
63pub(crate) const SHADOW_COMBINE_ID: &str = "alpha2.internal.shadow-combine";
64
65/// `EffectDef::license` of the effects Alpha2 itself wrote.
66pub(crate) const ALPHA2_EFFECT_LICENSE: &str = "MIT; Copyright Alpha2 contributors";
67
68/// Stable identifier of the resource-backed 3D LUT effect.
69pub const LUT_3D_ID: &str = "alpha2.effect.lut-3d";
70
71/// The gl-transitions commit the ports come from.
72pub(crate) const GL_TRANSITIONS_BASE: &str = "https://github.com/gl-transitions/gl-transitions/blob/902218a1b63773ac0d0d9f491951da3392365bfe/transitions/";
73
74/// Returns fresh definitions for every public built-in clip effect.
75#[must_use]
76pub fn builtin_effect_definitions() -> Vec<EffectDef> {
77    builtin_effect_groups()
78        .into_iter()
79        .flat_map(|(_, definitions)| definitions)
80        .collect()
81}
82
83/// The public built-in effects by catalog group, in catalog order.
84pub(crate) fn builtin_effect_groups() -> Vec<(&'static str, Vec<EffectDef>)> {
85    vec![
86        ("Color", effects::color::definitions()),
87        ("Keying", effects::keying::definitions()),
88        ("Blur & Sharpen", effects::blur::definitions()),
89        ("Stylize", effects::stylize::definitions()),
90        ("Distort", effects::distort::definitions()),
91    ]
92}
93
94/// Returns fresh, data-only definitions for all built-in transitions.
95#[must_use]
96pub fn builtin_transition_definitions() -> Vec<EffectDef> {
97    builtin_transition_groups()
98        .into_iter()
99        .flat_map(|(_, definitions)| definitions)
100        .collect()
101}
102
103/// The built-in transitions by catalog group, in catalog order.
104pub(crate) fn builtin_transition_groups() -> Vec<(&'static str, Vec<EffectDef>)> {
105    vec![
106        ("Classic", transitions::classic::definitions()),
107        ("Wipes", transitions::wipes::definitions()),
108        ("Stylized", transitions::stylized::definitions()),
109        ("Shapes", transitions::shapes::definitions()),
110        ("3D", transitions::three_d::definitions()),
111        ("Zoom & Blur", transitions::zoom_blur::definitions()),
112        ("Glitch", transitions::glitch::definitions()),
113    ]
114}
115
116/// Returns the data-only definition of the built-in chroma-key effect.
117///
118/// `key_color` is compared against straight sRGB-encoded color, as a color picker shows the
119/// screen. With `distance_mode` `rgb`, pixels within `threshold` are removed, a `softness` band
120/// is partly transparent and `spill_suppression` desaturates it; `chroma` compares only chroma,
121/// so the key survives shadows.
122#[must_use]
123pub fn chroma_key_definition() -> EffectDef {
124    EffectDef {
125        id: CHROMA_KEY_ID.into(),
126        version: 1,
127        kind: EffectKind::Effect,
128        inputs: Vec::new(),
129        params: vec![
130            color("key_color", [0.0, 1.0, 0.0, 1.0]),
131            float("threshold", 0.35, 0.0, 3.0_f32.sqrt()),
132            float("softness", 0.1, f32::EPSILON, 1.0),
133            float("spill_suppression", 0.2, 0.0, 1.0),
134            choice("distance_mode", 0, &["rgb", "chroma"]),
135        ],
136        resources: Vec::new(),
137        wgsl_body: effect_body(CHROMA_KEY_BODY),
138        tier_min: CapabilityTier::TierB,
139        license: Some(ALPHA2_EFFECT_LICENSE.into()),
140    }
141}
142
143/// Returns the data-only definition of the built-in Gaussian blur.
144///
145/// Standard deviation is half the `radius`, a fraction of the shorter side. The graph draws it
146/// as separable passes ([`crate::RenderGraph::add_effect`]); its own body is never drawn.
147#[must_use]
148pub fn gaussian_blur_definition() -> EffectDef {
149    EffectDef {
150        id: GAUSSIAN_BLUR_ID.into(),
151        version: 1,
152        kind: EffectKind::Effect,
153        inputs: Vec::new(),
154        params: vec![float_step("radius", 0.012, 0.0, 1.0, 0.001)],
155        resources: Vec::new(),
156        wgsl_body: effect_body(EXPANDED_EFFECT),
157        tier_min: CapabilityTier::TierB,
158        license: Some(ALPHA2_EFFECT_LICENSE.into()),
159    }
160}
161
162pub(crate) fn gaussian_blur_pass_definition() -> EffectDef {
163    EffectDef {
164        id: GAUSSIAN_BLUR_PASS_ID.into(),
165        version: 1,
166        kind: EffectKind::Effect,
167        inputs: Vec::new(),
168        params: vec![
169            vec2("step", [0.0, 0.0]),
170            float_step(
171                "sigma",
172                1.0,
173                0.0,
174                crate::gaussian_blur::MAX_PASS_SIGMA_TAPS,
175                0.001,
176            ),
177        ],
178        resources: Vec::new(),
179        wgsl_body: effect_body(GAUSSIAN_BLUR),
180        tier_min: CapabilityTier::TierB,
181        license: Some(ALPHA2_EFFECT_LICENSE.into()),
182    }
183}
184
185/// Returns the data-only definition of the built-in glow.
186///
187/// Straight color above `threshold` is blurred with `radius` and added back scaled by
188/// `intensity`. Drawn as internal passes; its own body is never drawn.
189#[must_use]
190pub fn glow_definition() -> EffectDef {
191    EffectDef {
192        id: GLOW_ID.into(),
193        version: 1,
194        kind: EffectKind::Effect,
195        inputs: Vec::new(),
196        params: vec![
197            float("threshold", 0.6, 0.0, 1.0),
198            float("intensity", 0.8, 0.0, 3.0),
199            float("radius", 0.012, 0.0, 0.06),
200        ],
201        resources: Vec::new(),
202        wgsl_body: effect_body(EXPANDED_EFFECT),
203        tier_min: CapabilityTier::TierB,
204        license: Some(ALPHA2_EFFECT_LICENSE.into()),
205    }
206}
207
208fn glow_threshold_definition() -> EffectDef {
209    EffectDef {
210        id: GLOW_THRESHOLD_ID.into(),
211        ..simple_effect(
212            "glow-threshold",
213            GLOW_THRESHOLD,
214            vec![float("threshold", 0.6, 0.0, 1.0)],
215        )
216    }
217}
218
219// Two inputs make it a transition to the registry; drawn at progress 0.5, which it ignores.
220fn glow_combine_definition() -> EffectDef {
221    EffectDef {
222        id: GLOW_COMBINE_ID.into(),
223        version: 1,
224        kind: EffectKind::Transition,
225        inputs: Vec::new(),
226        params: vec![float("intensity", 0.8, 0.0, 3.0)],
227        resources: Vec::new(),
228        wgsl_body: effect_body(GLOW_COMBINE),
229        tier_min: CapabilityTier::TierB,
230        license: Some(ALPHA2_EFFECT_LICENSE.into()),
231    }
232}
233
234fn unsharp_combine_definition() -> EffectDef {
235    EffectDef {
236        id: UNSHARP_COMBINE_ID.into(),
237        version: 1,
238        kind: EffectKind::Transition,
239        inputs: Vec::new(),
240        params: vec![
241            float("amount", 1.0, 0.0, 5.0),
242            float("threshold", 0.0, 0.0, 1.0),
243        ],
244        resources: Vec::new(),
245        wgsl_body: effect_body(UNSHARP_COMBINE),
246        tier_min: CapabilityTier::TierB,
247        license: Some(ALPHA2_EFFECT_LICENSE.into()),
248    }
249}
250
251fn focus_combine_definition() -> EffectDef {
252    EffectDef {
253        id: FOCUS_COMBINE_ID.into(),
254        version: 1,
255        kind: EffectKind::Effect,
256        inputs: vec![
257            InputSpec {
258                name: "soft".into(),
259            },
260            InputSpec {
261                name: "blurred".into(),
262            },
263        ],
264        params: vec![
265            float("position", 0.5, 0.0, 1.0),
266            degrees("angle", 0.0, -90.0, 90.0),
267            float("focus", 0.15, 0.0, 1.0),
268            float_step("falloff", 0.25, 0.001, 1.0, 0.005),
269        ],
270        resources: Vec::new(),
271        wgsl_body: effect_body(FOCUS_COMBINE),
272        tier_min: CapabilityTier::TierB,
273        license: Some(ALPHA2_EFFECT_LICENSE.into()),
274    }
275}
276
277fn shadow_offset_definition() -> EffectDef {
278    EffectDef {
279        id: SHADOW_OFFSET_ID.into(),
280        ..simple_effect(
281            "shadow-offset",
282            SHADOW_OFFSET,
283            vec![
284                vec2("offset", [0.0, 0.0]),
285                color("color", [0.0, 0.0, 0.0, 1.0]),
286                float("opacity", 0.6, 0.0, 1.0),
287            ],
288        )
289    }
290}
291
292fn shadow_combine_definition() -> EffectDef {
293    EffectDef {
294        id: SHADOW_COMBINE_ID.into(),
295        version: 1,
296        kind: EffectKind::Transition,
297        inputs: Vec::new(),
298        params: vec![boolean("shadow_only", false)],
299        resources: Vec::new(),
300        wgsl_body: effect_body(SHADOW_COMBINE),
301        tier_min: CapabilityTier::TierB,
302        license: Some(ALPHA2_EFFECT_LICENSE.into()),
303    }
304}
305
306/// Returns the data-only definition of the resource-backed 3D LUT effect.
307#[must_use]
308pub fn lut_3d_definition() -> EffectDef {
309    EffectDef {
310        id: LUT_3D_ID.into(),
311        version: 1,
312        kind: EffectKind::Effect,
313        inputs: Vec::new(),
314        params: vec![float("strength", 1.0, 0.0, 1.0)],
315        resources: vec![ResourceSpec {
316            name: "lut".into(),
317            kind: EffectResourceKind::Lut3d,
318        }],
319        wgsl_body: effect_body(LUT_3D_BODY),
320        tier_min: CapabilityTier::TierB,
321        license: Some(ALPHA2_EFFECT_LICENSE.into()),
322    }
323}
324
325/// The passes the engine inserts itself, hidden from the effect catalog.
326pub(crate) fn internal_effect_definitions() -> Vec<EffectDef> {
327    let mut definitions = vec![
328        gaussian_blur_pass_definition(),
329        glow_threshold_definition(),
330        glow_combine_definition(),
331        shadow_offset_definition(),
332        shadow_combine_definition(),
333        unsharp_combine_definition(),
334        focus_combine_definition(),
335        crate::masking::track_matte_definition(),
336        crate::generator::generator_definition(),
337    ];
338    definitions.extend(crate::masking::mask_combine_definitions());
339    definitions
340}
341
342impl Renderer {
343    fn register_builtin_definition(
344        &mut self,
345        definition: EffectDef,
346    ) -> Result<EffectId, ShaderDiagnostics> {
347        let id = self.register_lazily(definition)?;
348        self.effects.mark_builtin(id.clone());
349        Ok(id)
350    }
351
352    /// Registers the full built-in catalog; pipelines are made on first draw or [`Self::warmup`].
353    /// Built-ins cannot be removed through the host-effect methods.
354    ///
355    /// # Errors
356    /// Returns diagnostics from the first definition that fails.
357    pub fn register_builtins(&mut self) -> Result<Vec<EffectId>, ShaderDiagnostics> {
358        for definition in internal_effect_definitions() {
359            let internal = self.register_builtin_definition(definition)?;
360            self.effects.mark_internal(internal);
361        }
362        let mut definitions = builtin_effect_definitions();
363        definitions.extend(builtin_transition_definitions());
364        let mut ids = Vec::with_capacity(definitions.len());
365        for definition in definitions {
366            ids.push(self.register_builtin_definition(definition)?);
367        }
368        Ok(ids)
369    }
370
371    /// Registers the built-in chroma-key effect.
372    ///
373    /// # Errors
374    /// Returns schema diagnostics when registration fails.
375    pub fn register_builtin_chroma_key(&mut self) -> Result<EffectId, ShaderDiagnostics> {
376        self.register_builtin_definition(chroma_key_definition())
377    }
378
379    /// Registers the public Gaussian blur and its internal pass.
380    ///
381    /// # Errors
382    /// Returns schema diagnostics when registration fails.
383    pub fn register_builtin_gaussian_blur(&mut self) -> Result<EffectId, ShaderDiagnostics> {
384        let internal = self.register_builtin_definition(gaussian_blur_pass_definition())?;
385        self.effects.mark_internal(internal);
386        self.register_builtin_definition(gaussian_blur_definition())
387    }
388
389    /// Registers the built-in 3D LUT effect.
390    ///
391    /// # Errors
392    /// Returns schema diagnostics when registration fails.
393    pub fn register_builtin_lut_3d(&mut self) -> Result<EffectId, ShaderDiagnostics> {
394        self.register_builtin_definition(lut_3d_definition())
395    }
396
397    /// Registers all built-in transition definitions.
398    ///
399    /// # Errors
400    /// Diagnostics from the first failure; earlier definitions stay registered.
401    pub fn register_builtin_transitions(&mut self) -> Result<Vec<EffectId>, ShaderDiagnostics> {
402        let definitions = builtin_transition_definitions();
403        let mut ids = Vec::with_capacity(definitions.len());
404        for definition in definitions {
405            ids.push(self.register_builtin_definition(definition)?);
406        }
407        Ok(ids)
408    }
409}
410
411pub(crate) fn effect_body(body: &str) -> String {
412    format!("{EFFECT_HELPERS}\n{body}")
413}
414
415/// A public effect Alpha2 wrote, `alpha2.effect.<name>`.
416pub(crate) fn simple_effect(name: &str, body: &str, params: Vec<ParamSpec>) -> EffectDef {
417    EffectDef {
418        id: format!("alpha2.effect.{name}"),
419        version: 1,
420        kind: EffectKind::Effect,
421        inputs: Vec::new(),
422        params,
423        resources: Vec::new(),
424        wgsl_body: effect_body(body),
425        tier_min: CapabilityTier::TierB,
426        license: Some(ALPHA2_EFFECT_LICENSE.into()),
427    }
428}
429
430/// A port of gl-transitions file `source` by `author`, `alpha2.transition.<name>`.
431pub(crate) fn transition(
432    name: &str,
433    body: &str,
434    source: &str,
435    author: &str,
436    params: Vec<ParamSpec>,
437) -> EffectDef {
438    EffectDef {
439        id: format!("alpha2.transition.{name}"),
440        version: 1,
441        kind: EffectKind::Transition,
442        inputs: Vec::new(),
443        params,
444        resources: Vec::new(),
445        wgsl_body: format!("{TRANSITION_HELPERS}\n{GL_TRANSITION_ENTRY}\n{body}"),
446        tier_min: CapabilityTier::TierB,
447        license: Some(format!(
448            "MIT; Author: {author}; Source: {GL_TRANSITIONS_BASE}{source}"
449        )),
450    }
451}
452
453/// A transition Alpha2 wrote, `alpha2.transition.<name>`; its body defines `transition` over
454/// top-left uv.
455pub(crate) fn original_transition(name: &str, body: &str, params: Vec<ParamSpec>) -> EffectDef {
456    EffectDef {
457        id: format!("alpha2.transition.{name}"),
458        version: 1,
459        kind: EffectKind::Transition,
460        inputs: Vec::new(),
461        params,
462        resources: Vec::new(),
463        wgsl_body: format!("{TRANSITION_HELPERS}\n{body}"),
464        tier_min: CapabilityTier::TierB,
465        license: Some(ALPHA2_EFFECT_LICENSE.into()),
466    }
467}
468
469pub(crate) fn float(name: &str, default: f32, min: f32, max: f32) -> ParamSpec {
470    let span = max - min;
471    let step = if span > 50.0 {
472        1.0
473    } else if span <= 0.1 {
474        0.001
475    } else {
476        0.01
477    };
478    float_step(name, default, min, max, step)
479}
480
481pub(crate) fn float_step(name: &str, default: f32, min: f32, max: f32, step: f32) -> ParamSpec {
482    ParamSpec {
483        name: name.into(),
484        param_type: ParamType::Float,
485        default: ParamValue::Float(default),
486        min: Some(min),
487        max: Some(max),
488        step: Some(step),
489        unit: ParamUnit::Normalized,
490        enum_values: Vec::new(),
491    }
492}
493
494pub(crate) fn degrees(name: &str, default: f32, min: f32, max: f32) -> ParamSpec {
495    ParamSpec {
496        unit: ParamUnit::Degrees,
497        step: Some(1.0),
498        ..float(name, default, min, max)
499    }
500}
501
502pub(crate) fn vec2(name: &str, default: [f32; 2]) -> ParamSpec {
503    ParamSpec {
504        name: name.into(),
505        param_type: ParamType::Vec2,
506        default: ParamValue::Vec2(default),
507        min: None,
508        max: None,
509        step: None,
510        unit: ParamUnit::Normalized,
511        enum_values: Vec::new(),
512    }
513}
514
515pub(crate) fn vec2_range(
516    name: &str,
517    default: [f32; 2],
518    min: f32,
519    max: f32,
520    step: f32,
521) -> ParamSpec {
522    ParamSpec {
523        min: Some(min),
524        max: Some(max),
525        step: Some(step),
526        ..vec2(name, default)
527    }
528}
529
530pub(crate) fn color(name: &str, default: [f32; 4]) -> ParamSpec {
531    ParamSpec {
532        name: name.into(),
533        param_type: ParamType::Color,
534        default: ParamValue::Color(default),
535        min: None,
536        max: None,
537        step: None,
538        unit: ParamUnit::Color,
539        enum_values: Vec::new(),
540    }
541}
542
543/// Four independent progress thresholds, one per channel, in `0..=1`.
544pub(crate) fn thresholds(name: &str, default: [f32; 4]) -> ParamSpec {
545    ParamSpec {
546        name: name.into(),
547        param_type: ParamType::Vec4,
548        default: ParamValue::Vec4(default),
549        min: Some(0.0),
550        max: Some(1.0),
551        step: Some(0.01),
552        unit: ParamUnit::Normalized,
553        enum_values: Vec::new(),
554    }
555}
556
557pub(crate) fn choice(name: &str, default: u32, values: &[&str]) -> ParamSpec {
558    ParamSpec {
559        name: name.into(),
560        param_type: ParamType::Enum,
561        default: ParamValue::Enum(default),
562        min: None,
563        max: None,
564        step: None,
565        unit: ParamUnit::Enum,
566        enum_values: values.iter().map(|value| (*value).into()).collect(),
567    }
568}
569
570pub(crate) fn boolean(name: &str, default: bool) -> ParamSpec {
571    ParamSpec {
572        name: name.into(),
573        param_type: ParamType::Bool,
574        default: ParamValue::Bool(default),
575        min: None,
576        max: None,
577        step: None,
578        unit: ParamUnit::Bool,
579        enum_values: Vec::new(),
580    }
581}