Skip to main content

alpha2_render/
gaussian_blur.rs

1//! The built-in effects a render graph draws as several internal passes.
2//!
3//! A Gaussian is separable and the sum of Gaussians is a Gaussian, so a blur
4//! of any width is a chain of narrow one-axis passes. Each pass reads at most
5//! [`PASS_REACH`] taps either side, `MAX_PASS_SIGMA_TAPS` standard deviations
6//! wide. The first pass samples every texel; every later pass samples taps as
7//! far apart as the blur accumulated so far, a picture already smooth at that
8//! scale, so no pass leaves gaps between its taps. The pass count grows with
9//! the logarithm of the width: about four times wider per pass.
10
11use std::sync::LazyLock;
12
13use crate::{
14    EffectDef, EffectId, EffectParams, EffectResources, EffectRuntime, GraphError, ParamValue,
15    RenderError, RenderGraph, RenderNodeId,
16    builtins::{
17        GAUSSIAN_BLUR_ID, GAUSSIAN_BLUR_PASS_ID, GLOW_COMBINE_ID, GLOW_ID, GLOW_THRESHOLD_ID,
18        gaussian_blur_definition, glow_definition,
19    },
20};
21
22/// Taps one blur pass reads on either side of its centre.
23const PASS_REACH: f32 = 12.0;
24
25/// Widest standard deviation one pass draws, in taps: its reach covers three.
26pub(crate) const MAX_PASS_SIGMA_TAPS: f32 = PASS_REACH / 3.0;
27
28/// More passes than any canvas needs: a 4320-pixel deviation takes six.
29const MAX_PASSES_PER_AXIS: usize = 16;
30
31/// The definitions whose parameters an expanded built-in is checked
32/// against, made once rather than for every graph that draws one.
33static GAUSSIAN_BLUR: LazyLock<EffectDef> = LazyLock::new(gaussian_blur_definition);
34static GLOW: LazyLock<EffectDef> = LazyLock::new(glow_definition);
35
36/// The internal effects a built-in drawn as several passes draws with, for
37/// [`RenderGraph::add_expanded_effect`]'s effects; `None` for every other.
38pub(crate) fn expanded_passes(effect: &str) -> Option<&'static [&'static str]> {
39    match effect {
40        GAUSSIAN_BLUR_ID => Some(&[GAUSSIAN_BLUR_PASS_ID]),
41        GLOW_ID => Some(&[GLOW_THRESHOLD_ID, GAUSSIAN_BLUR_PASS_ID, GLOW_COMBINE_ID]),
42        _ => None,
43    }
44}
45
46impl<'a> RenderGraph<'a> {
47    /// Adds `effect` as the internal passes that draw it, when it is a
48    /// built-in drawn that way; `None` for every other effect.
49    pub(crate) fn add_expanded_effect(
50        &mut self,
51        effect: &EffectId,
52        source: RenderNodeId,
53        params: &EffectParams,
54        runtime: EffectRuntime,
55    ) -> Option<Result<RenderNodeId, GraphError>> {
56        match effect.as_str() {
57            GAUSSIAN_BLUR_ID => Some(self.add_gaussian_blur(source, params, runtime)),
58            GLOW_ID => Some(self.add_glow(source, params, runtime)),
59            _ => None,
60        }
61    }
62
63    fn add_gaussian_blur(
64        &mut self,
65        source: RenderNodeId,
66        params: &EffectParams,
67        runtime: EffectRuntime,
68    ) -> Result<RenderNodeId, GraphError> {
69        let [radius] = checked_params(&GAUSSIAN_BLUR, params, ["radius"])?;
70        self.add_gaussian(source, radius * 0.5, runtime)
71    }
72
73    fn add_glow(
74        &mut self,
75        source: RenderNodeId,
76        params: &EffectParams,
77        runtime: EffectRuntime,
78    ) -> Result<RenderNodeId, GraphError> {
79        let [threshold, intensity, radius] =
80            checked_params(&GLOW, params, ["threshold", "intensity", "radius"])?;
81        let bright = self.add_internal_effect(
82            GLOW_THRESHOLD_ID,
83            source,
84            EffectParams::new().with("threshold", ParamValue::Float(threshold)),
85            runtime,
86        )?;
87        let bloom = self.add_gaussian(bright, radius * 0.5, runtime)?;
88        self.add_transition(
89            EffectId::from_static(GLOW_COMBINE_ID, 1),
90            source,
91            bloom,
92            EffectParams::new().with("intensity", ParamValue::Float(intensity)),
93            EffectRuntime {
94                progress: 0.5,
95                ..runtime
96            },
97        )
98    }
99
100    /// A Gaussian with a standard deviation of `sigma` output heights, as
101    /// horizontal then vertical pass cascades.
102    #[allow(clippy::cast_precision_loss)]
103    fn add_gaussian(
104        &mut self,
105        source: RenderNodeId,
106        sigma: f32,
107        runtime: EffectRuntime,
108    ) -> Result<RenderNodeId, GraphError> {
109        self.add_gaussian_cascade(source, sigma * self.size.height as f32, runtime)
110    }
111
112    /// Adds a Gaussian blur of `source` whose standard deviation is
113    /// `sigma_pixels` of the graph's pixels on both axes, drawn as the
114    /// built-in blur draws: narrow one-axis passes, within the renderer's
115    /// filter-sample budget. A mask's feather softens its coverage with it.
116    /// A deviation that is not finite and positive returns `source` itself.
117    ///
118    /// # Errors
119    /// Returns [`GraphError::InvalidDependency`] unless `source` is earlier.
120    #[allow(clippy::cast_precision_loss)]
121    pub fn add_gaussian_pixels(
122        &mut self,
123        source: RenderNodeId,
124        sigma_pixels: f32,
125        runtime: EffectRuntime,
126    ) -> Result<RenderNodeId, GraphError> {
127        if source.graph != self.id || source.index >= self.nodes.len() {
128            return Err(GraphError::InvalidDependency);
129        }
130        if sigma_pixels.is_nan() || sigma_pixels <= 0.0 || !sigma_pixels.is_finite() {
131            return Ok(source);
132        }
133        self.add_gaussian_cascade(source, sigma_pixels, runtime)
134    }
135
136    /// The pass cascades of a Gaussian of `sigma_pixels`; one identity pass
137    /// per axis at zero.
138    #[allow(clippy::cast_precision_loss)]
139    fn add_gaussian_cascade(
140        &mut self,
141        source: RenderNodeId,
142        sigma_pixels: f32,
143        runtime: EffectRuntime,
144    ) -> Result<RenderNodeId, GraphError> {
145        let mut node = source;
146        for (axis, extent) in [(0, self.size.width), (1, self.size.height)] {
147            let mut blurred = 0.0_f32;
148            let mut spacing = 1.0_f32;
149            for _ in 0..MAX_PASSES_PER_AXIS {
150                let remaining = (sigma_pixels * sigma_pixels - blurred * blurred)
151                    .max(0.0)
152                    .sqrt();
153                let pass = remaining.min(MAX_PASS_SIGMA_TAPS * spacing);
154                let mut step = [0.0; 2];
155                step[axis] = spacing / extent as f32;
156                node = self.add_internal_effect(
157                    GAUSSIAN_BLUR_PASS_ID,
158                    node,
159                    EffectParams::new()
160                        .with("step", ParamValue::Vec2(step))
161                        .with("sigma", ParamValue::Float(pass / spacing)),
162                    runtime,
163                )?;
164                blurred = blurred.hypot(pass);
165                if pass < MAX_PASS_SIGMA_TAPS * spacing {
166                    break;
167                }
168                spacing = blurred;
169            }
170        }
171        Ok(node)
172    }
173
174    fn add_internal_effect(
175        &mut self,
176        id: &'static str,
177        source: RenderNodeId,
178        params: EffectParams<'a>,
179        runtime: EffectRuntime,
180    ) -> Result<RenderNodeId, GraphError> {
181        self.add_effect_with_resources(
182            EffectId::from_static(id, 1),
183            source,
184            params,
185            EffectResources::new(),
186            runtime,
187        )
188    }
189}
190
191/// The named float parameters of `definition` as `params` gives them, its
192/// defaults filling the rest, checked as a pass would check them.
193fn checked_params<const N: usize>(
194    definition: &EffectDef,
195    params: &EffectParams,
196    names: [&str; N],
197) -> Result<[f32; N], GraphError> {
198    crate::runtime_effect::encode_params(&definition.params, params)?;
199    let mut values = [0.0; N];
200    for (value, name) in values.iter_mut().zip(names) {
201        let spec = definition
202            .params
203            .iter()
204            .find(|spec| spec.name == name)
205            .ok_or(RenderError::InvalidEffectParams)?;
206        let ParamValue::Float(supplied) = params.get(name).unwrap_or(&spec.default) else {
207            return Err(RenderError::InvalidEffectParams.into());
208        };
209        *value = *supplied;
210    }
211    Ok(values)
212}