use std::sync::LazyLock;
use crate::{
EffectDef, EffectId, EffectParams, EffectResources, EffectRuntime, GraphError, ParamValue,
RenderError, RenderGraph, RenderNodeId,
builtins::{
GAUSSIAN_BLUR_ID, GAUSSIAN_BLUR_PASS_ID, GLOW_COMBINE_ID, GLOW_ID, GLOW_THRESHOLD_ID,
gaussian_blur_definition, glow_definition,
},
};
const PASS_REACH: f32 = 12.0;
pub(crate) const MAX_PASS_SIGMA_TAPS: f32 = PASS_REACH / 3.0;
const MAX_PASSES_PER_AXIS: usize = 16;
static GAUSSIAN_BLUR: LazyLock<EffectDef> = LazyLock::new(gaussian_blur_definition);
static GLOW: LazyLock<EffectDef> = LazyLock::new(glow_definition);
pub(crate) fn expanded_passes(effect: &str) -> Option<&'static [&'static str]> {
match effect {
GAUSSIAN_BLUR_ID => Some(&[GAUSSIAN_BLUR_PASS_ID]),
GLOW_ID => Some(&[GLOW_THRESHOLD_ID, GAUSSIAN_BLUR_PASS_ID, GLOW_COMBINE_ID]),
_ => None,
}
}
impl<'a> RenderGraph<'a> {
pub(crate) fn add_expanded_effect(
&mut self,
effect: &EffectId,
source: RenderNodeId,
params: &EffectParams,
runtime: EffectRuntime,
) -> Option<Result<RenderNodeId, GraphError>> {
match effect.as_str() {
GAUSSIAN_BLUR_ID => Some(self.add_gaussian_blur(source, params, runtime)),
GLOW_ID => Some(self.add_glow(source, params, runtime)),
_ => None,
}
}
fn add_gaussian_blur(
&mut self,
source: RenderNodeId,
params: &EffectParams,
runtime: EffectRuntime,
) -> Result<RenderNodeId, GraphError> {
let [radius] = checked_params(&GAUSSIAN_BLUR, params, ["radius"])?;
self.add_gaussian(source, radius * 0.5, runtime)
}
fn add_glow(
&mut self,
source: RenderNodeId,
params: &EffectParams,
runtime: EffectRuntime,
) -> Result<RenderNodeId, GraphError> {
let [threshold, intensity, radius] =
checked_params(&GLOW, params, ["threshold", "intensity", "radius"])?;
let bright = self.add_internal_effect(
GLOW_THRESHOLD_ID,
source,
EffectParams::new().with("threshold", ParamValue::Float(threshold)),
runtime,
)?;
let bloom = self.add_gaussian(bright, radius * 0.5, runtime)?;
self.add_transition(
EffectId::from_static(GLOW_COMBINE_ID, 1),
source,
bloom,
EffectParams::new().with("intensity", ParamValue::Float(intensity)),
EffectRuntime {
progress: 0.5,
..runtime
},
)
}
#[allow(clippy::cast_precision_loss)]
fn add_gaussian(
&mut self,
source: RenderNodeId,
sigma: f32,
runtime: EffectRuntime,
) -> Result<RenderNodeId, GraphError> {
self.add_gaussian_cascade(source, sigma * self.size.height as f32, runtime)
}
#[allow(clippy::cast_precision_loss)]
pub fn add_gaussian_pixels(
&mut self,
source: RenderNodeId,
sigma_pixels: f32,
runtime: EffectRuntime,
) -> Result<RenderNodeId, GraphError> {
if source.graph != self.id || source.index >= self.nodes.len() {
return Err(GraphError::InvalidDependency);
}
if sigma_pixels.is_nan() || sigma_pixels <= 0.0 || !sigma_pixels.is_finite() {
return Ok(source);
}
self.add_gaussian_cascade(source, sigma_pixels, runtime)
}
#[allow(clippy::cast_precision_loss)]
fn add_gaussian_cascade(
&mut self,
source: RenderNodeId,
sigma_pixels: f32,
runtime: EffectRuntime,
) -> Result<RenderNodeId, GraphError> {
let mut node = source;
for (axis, extent) in [(0, self.size.width), (1, self.size.height)] {
let mut blurred = 0.0_f32;
let mut spacing = 1.0_f32;
for _ in 0..MAX_PASSES_PER_AXIS {
let remaining = (sigma_pixels * sigma_pixels - blurred * blurred)
.max(0.0)
.sqrt();
let pass = remaining.min(MAX_PASS_SIGMA_TAPS * spacing);
let mut step = [0.0; 2];
step[axis] = spacing / extent as f32;
node = self.add_internal_effect(
GAUSSIAN_BLUR_PASS_ID,
node,
EffectParams::new()
.with("step", ParamValue::Vec2(step))
.with("sigma", ParamValue::Float(pass / spacing)),
runtime,
)?;
blurred = blurred.hypot(pass);
if pass < MAX_PASS_SIGMA_TAPS * spacing {
break;
}
spacing = blurred;
}
}
Ok(node)
}
fn add_internal_effect(
&mut self,
id: &'static str,
source: RenderNodeId,
params: EffectParams<'a>,
runtime: EffectRuntime,
) -> Result<RenderNodeId, GraphError> {
self.add_effect_with_resources(
EffectId::from_static(id, 1),
source,
params,
EffectResources::new(),
runtime,
)
}
}
fn checked_params<const N: usize>(
definition: &EffectDef,
params: &EffectParams,
names: [&str; N],
) -> Result<[f32; N], GraphError> {
crate::runtime_effect::encode_params(&definition.params, params)?;
let mut values = [0.0; N];
for (value, name) in values.iter_mut().zip(names) {
let spec = definition
.params
.iter()
.find(|spec| spec.name == name)
.ok_or(RenderError::InvalidEffectParams)?;
let ParamValue::Float(supplied) = params.get(name).unwrap_or(&spec.default) else {
return Err(RenderError::InvalidEffectParams.into());
};
*value = *supplied;
}
Ok(values)
}