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