1use std::sync::LazyLock;
12
13use crate::{
14 EffectDef, EffectId, EffectInputs, EffectParams, EffectResources, EffectRuntime, GraphError,
15 ParamValue, RenderError, RenderGraph, RenderNodeId,
16 builtins::{
17 DIRECTIONAL_BLUR_ID, DROP_SHADOW_ID, FOCUS_COMBINE_ID, GAUSSIAN_BLUR_ID,
18 GAUSSIAN_BLUR_PASS_ID, GLOW_COMBINE_ID, GLOW_ID, GLOW_THRESHOLD_ID, SHADOW_COMBINE_ID,
19 SHADOW_OFFSET_ID, TILT_SHIFT_ID, UNSHARP_COMBINE_ID, UNSHARP_MASK_ID,
20 directional_blur_definition, drop_shadow_definition, gaussian_blur_definition,
21 glow_definition, tilt_shift_definition, unsharp_mask_definition,
22 },
23};
24
25const PASS_REACH: f32 = 12.0;
27
28pub(crate) const MAX_PASS_SIGMA_TAPS: f32 = PASS_REACH / 3.0;
30
31const MAX_PASSES_PER_AXIS: usize = 16;
33
34static GAUSSIAN_BLUR: LazyLock<EffectDef> = LazyLock::new(gaussian_blur_definition);
37static GLOW: LazyLock<EffectDef> = LazyLock::new(glow_definition);
38static DIRECTIONAL_BLUR: LazyLock<EffectDef> = LazyLock::new(directional_blur_definition);
39static UNSHARP_MASK: LazyLock<EffectDef> = LazyLock::new(unsharp_mask_definition);
40static TILT_SHIFT: LazyLock<EffectDef> = LazyLock::new(tilt_shift_definition);
41static DROP_SHADOW: LazyLock<EffectDef> = LazyLock::new(drop_shadow_definition);
42
43#[must_use]
47#[allow(clippy::cast_precision_loss)]
48pub fn blur_sigma_pixels(radius: f32, size: crate::RenderSize) -> f32 {
49 radius * 0.5 * size.width.min(size.height) as f32
50}
51
52pub(crate) fn expanded_passes(effect: &str) -> Option<&'static [&'static str]> {
55 match effect {
56 GAUSSIAN_BLUR_ID | DIRECTIONAL_BLUR_ID => Some(&[GAUSSIAN_BLUR_PASS_ID]),
57 GLOW_ID => Some(&[GLOW_THRESHOLD_ID, GAUSSIAN_BLUR_PASS_ID, GLOW_COMBINE_ID]),
58 UNSHARP_MASK_ID => Some(&[GAUSSIAN_BLUR_PASS_ID, UNSHARP_COMBINE_ID]),
59 TILT_SHIFT_ID => Some(&[GAUSSIAN_BLUR_PASS_ID, FOCUS_COMBINE_ID]),
60 DROP_SHADOW_ID => Some(&[SHADOW_OFFSET_ID, GAUSSIAN_BLUR_PASS_ID, SHADOW_COMBINE_ID]),
61 _ => None,
62 }
63}
64
65impl<'a> RenderGraph<'a> {
66 pub(crate) fn add_expanded_effect(
69 &mut self,
70 effect: &EffectId,
71 source: RenderNodeId,
72 params: &EffectParams,
73 runtime: EffectRuntime,
74 ) -> Option<Result<RenderNodeId, GraphError>> {
75 match effect.as_str() {
76 GAUSSIAN_BLUR_ID => Some(self.add_gaussian_blur(source, params, runtime)),
77 GLOW_ID => Some(self.add_glow(source, params, runtime)),
78 DIRECTIONAL_BLUR_ID => Some(self.add_directional_blur(source, params, runtime)),
79 UNSHARP_MASK_ID => Some(self.add_unsharp_mask(source, params, runtime)),
80 TILT_SHIFT_ID => Some(self.add_tilt_shift(source, params, runtime)),
81 DROP_SHADOW_ID => Some(self.add_drop_shadow(source, params, runtime)),
82 _ => None,
83 }
84 }
85
86 fn add_gaussian_blur(
87 &mut self,
88 source: RenderNodeId,
89 params: &EffectParams,
90 runtime: EffectRuntime,
91 ) -> Result<RenderNodeId, GraphError> {
92 let [radius] = checked_params(&GAUSSIAN_BLUR, params, ["radius"])?;
93 self.add_gaussian_cascade(source, blur_sigma_pixels(radius, self.size), runtime)
94 }
95
96 #[allow(clippy::cast_precision_loss)]
97 fn add_drop_shadow(
98 &mut self,
99 source: RenderNodeId,
100 params: &EffectParams,
101 runtime: EffectRuntime,
102 ) -> Result<RenderNodeId, GraphError> {
103 let [color, opacity, angle, distance, softness, shadow_only] = checked_values(
104 &DROP_SHADOW,
105 params,
106 [
107 "color",
108 "opacity",
109 "angle",
110 "distance",
111 "softness",
112 "shadow_only",
113 ],
114 )?;
115 let (
116 ParamValue::Float(opacity),
117 ParamValue::Float(angle),
118 ParamValue::Float(distance),
119 ParamValue::Float(softness),
120 ) = (opacity, angle, distance, softness)
121 else {
122 return Err(RenderError::InvalidEffectParams.into());
123 };
124 let radians = angle.to_radians();
126 let unit = self.unit_pixels();
127 let (unit_over_width, unit_over_height) = (
128 unit / self.size.width as f32,
129 unit / self.size.height as f32,
130 );
131 let offset = [
132 libm::cosf(radians) * distance * unit_over_width,
133 -libm::sinf(radians) * distance * unit_over_height,
134 ];
135 let shadow = self.add_internal_effect(
136 SHADOW_OFFSET_ID,
137 source,
138 EffectParams::new()
139 .with("offset", ParamValue::Vec2(offset))
140 .with("color", color)
141 .with("opacity", ParamValue::Float(opacity)),
142 runtime,
143 )?;
144 let soft = self.add_gaussian(shadow, softness * 0.5, runtime)?;
145 self.add_transition(
146 EffectId::from_static(SHADOW_COMBINE_ID, 1),
147 source,
148 soft,
149 EffectParams::new().with("shadow_only", shadow_only),
150 EffectRuntime {
151 progress: 0.5,
152 ..runtime
153 },
154 )
155 }
156
157 fn add_glow(
158 &mut self,
159 source: RenderNodeId,
160 params: &EffectParams,
161 runtime: EffectRuntime,
162 ) -> Result<RenderNodeId, GraphError> {
163 let [threshold, intensity, radius] =
164 checked_params(&GLOW, params, ["threshold", "intensity", "radius"])?;
165 let bright = self.add_internal_effect(
166 GLOW_THRESHOLD_ID,
167 source,
168 EffectParams::new().with("threshold", ParamValue::Float(threshold)),
169 runtime,
170 )?;
171 let bloom = self.add_gaussian(bright, radius * 0.5, runtime)?;
172 self.add_transition(
173 EffectId::from_static(GLOW_COMBINE_ID, 1),
174 source,
175 bloom,
176 EffectParams::new().with("intensity", ParamValue::Float(intensity)),
177 EffectRuntime {
178 progress: 0.5,
179 ..runtime
180 },
181 )
182 }
183
184 #[allow(clippy::cast_precision_loss)]
186 fn add_directional_blur(
187 &mut self,
188 source: RenderNodeId,
189 params: &EffectParams,
190 runtime: EffectRuntime,
191 ) -> Result<RenderNodeId, GraphError> {
192 let [length, angle] = checked_params(&DIRECTIONAL_BLUR, params, ["length", "angle"])?;
193 let radians = angle.to_radians();
195 let direction = [libm::cosf(radians), -libm::sinf(radians)];
196 let sigma_pixels = length * 0.5 * self.unit_pixels();
197 self.add_gaussian_axis(source, sigma_pixels, direction, runtime)
198 }
199
200 fn add_unsharp_mask(
201 &mut self,
202 source: RenderNodeId,
203 params: &EffectParams,
204 runtime: EffectRuntime,
205 ) -> Result<RenderNodeId, GraphError> {
206 let [amount, radius, threshold] =
207 checked_params(&UNSHARP_MASK, params, ["amount", "radius", "threshold"])?;
208 let blurred = self.add_gaussian(source, radius * 0.5, runtime)?;
209 self.add_transition(
210 EffectId::from_static(UNSHARP_COMBINE_ID, 1),
211 source,
212 blurred,
213 EffectParams::new()
214 .with("amount", ParamValue::Float(amount))
215 .with("threshold", ParamValue::Float(threshold)),
216 EffectRuntime {
217 progress: 0.5,
218 ..runtime
219 },
220 )
221 }
222
223 fn add_tilt_shift(
227 &mut self,
228 source: RenderNodeId,
229 params: &EffectParams,
230 runtime: EffectRuntime,
231 ) -> Result<RenderNodeId, GraphError> {
232 let [blur, position, angle, focus, falloff] = checked_params(
233 &TILT_SHIFT,
234 params,
235 ["blur", "position", "angle", "focus", "falloff"],
236 )?;
237 let (narrow, wide) = (blur * 0.25, blur * 0.5);
238 let soft = self.add_gaussian(source, narrow, runtime)?;
239 let rest = (wide * wide - narrow * narrow).max(0.0).sqrt();
240 let blurred = self.add_gaussian(soft, rest, runtime)?;
241 self.add_effect_with_inputs(
242 EffectId::from_static(FOCUS_COMBINE_ID, 1),
243 source,
244 EffectInputs::new()
245 .with("soft", soft)
246 .with("blurred", blurred),
247 EffectParams::new()
248 .with("position", ParamValue::Float(position))
249 .with("angle", ParamValue::Float(angle))
250 .with("focus", ParamValue::Float(focus))
251 .with("falloff", ParamValue::Float(falloff)),
252 EffectResources::new(),
253 runtime,
254 )
255 }
256
257 #[allow(clippy::cast_precision_loss)]
261 fn unit_pixels(&self) -> f32 {
262 self.size.width.min(self.size.height) as f32
263 }
264
265 fn add_gaussian(
268 &mut self,
269 source: RenderNodeId,
270 sigma: f32,
271 runtime: EffectRuntime,
272 ) -> Result<RenderNodeId, GraphError> {
273 self.add_gaussian_cascade(source, sigma * self.unit_pixels(), runtime)
274 }
275
276 #[allow(clippy::cast_precision_loss)]
285 pub fn add_gaussian_pixels(
286 &mut self,
287 source: RenderNodeId,
288 sigma_pixels: f32,
289 runtime: EffectRuntime,
290 ) -> Result<RenderNodeId, GraphError> {
291 if source.graph != self.id || source.index >= self.nodes.len() {
292 return Err(GraphError::InvalidDependency);
293 }
294 if sigma_pixels.is_nan() || sigma_pixels <= 0.0 || !sigma_pixels.is_finite() {
295 return Ok(source);
296 }
297 self.add_gaussian_cascade(source, sigma_pixels, runtime)
298 }
299
300 fn add_gaussian_cascade(
303 &mut self,
304 source: RenderNodeId,
305 sigma_pixels: f32,
306 runtime: EffectRuntime,
307 ) -> Result<RenderNodeId, GraphError> {
308 let node = self.add_gaussian_axis(source, sigma_pixels, [1.0, 0.0], runtime)?;
309 self.add_gaussian_axis(node, sigma_pixels, [0.0, 1.0], runtime)
310 }
311
312 #[allow(clippy::cast_precision_loss)]
317 pub(crate) fn add_gaussian_axis(
318 &mut self,
319 source: RenderNodeId,
320 sigma_pixels: f32,
321 direction: [f32; 2],
322 runtime: EffectRuntime,
323 ) -> Result<RenderNodeId, GraphError> {
324 let extent = [self.size.width as f32, self.size.height as f32];
325 let mut node = source;
326 let mut blurred = 0.0_f32;
327 let mut spacing = 1.0_f32;
328 for _ in 0..MAX_PASSES_PER_AXIS {
329 let remaining = (sigma_pixels * sigma_pixels - blurred * blurred)
330 .max(0.0)
331 .sqrt();
332 let pass = remaining.min(MAX_PASS_SIGMA_TAPS * spacing);
333 let step = [
334 direction[0] * spacing / extent[0],
335 direction[1] * spacing / extent[1],
336 ];
337 node = self.add_internal_effect(
338 GAUSSIAN_BLUR_PASS_ID,
339 node,
340 EffectParams::new()
341 .with("step", ParamValue::Vec2(step))
342 .with("sigma", ParamValue::Float(pass / spacing)),
343 runtime,
344 )?;
345 blurred = blurred.hypot(pass);
346 if pass < MAX_PASS_SIGMA_TAPS * spacing {
347 break;
348 }
349 spacing = blurred;
350 }
351 Ok(node)
352 }
353
354 pub(crate) fn add_internal_effect(
355 &mut self,
356 id: &'static str,
357 source: RenderNodeId,
358 params: EffectParams<'a>,
359 runtime: EffectRuntime,
360 ) -> Result<RenderNodeId, GraphError> {
361 self.add_effect_with_resources(
362 EffectId::from_static(id, 1),
363 source,
364 params,
365 EffectResources::new(),
366 runtime,
367 )
368 }
369}
370
371pub(crate) fn checked_values<const N: usize>(
375 definition: &EffectDef,
376 params: &EffectParams,
377 names: [&str; N],
378) -> Result<[ParamValue; N], GraphError> {
379 crate::runtime_effect::encode_params(&definition.params, params)?;
380 let mut values = std::array::from_fn(|_| ParamValue::Float(0.0));
381 for (value, name) in values.iter_mut().zip(names) {
382 let spec = definition
383 .params
384 .iter()
385 .find(|spec| spec.name == name)
386 .ok_or(RenderError::InvalidEffectParams)?;
387 *value = params.get(name).unwrap_or(&spec.default).clone();
388 }
389 Ok(values)
390}
391
392pub(crate) fn checked_params<const N: usize>(
395 definition: &EffectDef,
396 params: &EffectParams,
397 names: [&str; N],
398) -> Result<[f32; N], GraphError> {
399 crate::runtime_effect::encode_params(&definition.params, params)?;
400 let mut values = [0.0; N];
401 for (value, name) in values.iter_mut().zip(names) {
402 let spec = definition
403 .params
404 .iter()
405 .find(|spec| spec.name == name)
406 .ok_or(RenderError::InvalidEffectParams)?;
407 let ParamValue::Float(supplied) = params.get(name).unwrap_or(&spec.default) else {
408 return Err(RenderError::InvalidEffectParams.into());
409 };
410 *value = *supplied;
411 }
412 Ok(values)
413}
414
415#[cfg(test)]
416mod tests {
417 use super::checked_values;
418 use crate::{EffectParams, ParamValue, chroma_key_definition};
419
420 #[test]
421 fn checked_values_fill_defaults_and_refuse_a_wrong_type() {
422 let definition = chroma_key_definition();
423 let supplied = EffectParams::new()
424 .with("key_color", ParamValue::Color([0.2, 0.4, 0.6, 1.0]))
425 .with("distance_mode", ParamValue::Enum(1));
426 let [key, mode, softness] = checked_values(
427 &definition,
428 &supplied,
429 ["key_color", "distance_mode", "softness"],
430 )
431 .expect("valid parameters");
432 assert_eq!(key, ParamValue::Color([0.2, 0.4, 0.6, 1.0]));
433 assert_eq!(mode, ParamValue::Enum(1));
434 assert_eq!(softness, ParamValue::Float(0.1));
435 let wrong = EffectParams::new().with("key_color", ParamValue::Float(1.0));
436 assert!(checked_values(&definition, &wrong, ["key_color"]).is_err());
437 assert!(checked_values(&definition, &EffectParams::new(), ["missing"]).is_err());
438 }
439}