Skip to main content

alpha2_render/builtins/
mod.rs

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