1use 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
22const PASS_REACH: f32 = 12.0;
24
25pub(crate) const MAX_PASS_SIGMA_TAPS: f32 = PASS_REACH / 3.0;
27
28const MAX_PASSES_PER_AXIS: usize = 16;
30
31static GAUSSIAN_BLUR: LazyLock<EffectDef> = LazyLock::new(gaussian_blur_definition);
34static GLOW: LazyLock<EffectDef> = LazyLock::new(glow_definition);
35
36pub(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 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 #[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 #[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 #[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
191fn 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}