Skip to main content

concinnity_core/gfx/
ssao.rs

1//! Screen-space ambient occlusion (GTAO) configuration. Backend-agnostic
2//! resolve of the authored `PostProcessConfig` SSAO fields into clamped
3//! settings, plus the per-frame GPU uniform. The horizon-search arc integral
4//! itself lives in each backend's shader; this module owns only the parameter
5//! math so it can be unit-tested without a GPU.
6
7use crate::gfx::camera::view_ray_scale;
8
9use crate::gfx::render_types::SsaoParams;
10
11// Upper bound on `intensity` so a stray asset value cannot drive the ambient
12// term fully black across the whole frame.
13const MAX_INTENSITY: f32 = 4.0;
14
15// Smallest usable radius. A zero or negative radius would make every horizon
16// search degenerate, so the authored value is floored here.
17const MIN_RADIUS: f32 = 1.0e-3;
18
19/// Clamped SSAO tunables resolved from the authored asset fields. Held by the
20/// backend and turned into a per-frame [`SsaoParams`] once the camera is known.
21#[derive(Debug, Clone, Copy, PartialEq)]
22pub struct SsaoSettings {
23    /// World-space hemisphere radius the horizon search covers.
24    pub radius: f32,
25    /// Occlusion strength multiplier applied to the integrated visibility.
26    pub intensity: f32,
27}
28
29impl SsaoSettings {
30    /// Clamp the authored radius / intensity into a safe range.
31    pub fn resolve(radius: f32, intensity: f32) -> Self {
32        Self {
33            radius: radius.max(MIN_RADIUS),
34            intensity: intensity.clamp(0.0, MAX_INTENSITY),
35        }
36    }
37
38    /// Build the per-frame GPU uniform from these settings and the active
39    /// camera. `fov_y_radians` is the vertical field of view and `aspect` the
40    /// viewport width / height ratio: together they give the view-ray scale
41    /// the kernel needs to rebuild view-space positions from linear depth.
42    pub fn params(&self, fov_y_radians: f32, aspect: f32) -> SsaoParams {
43        let (tan_half_fov_y, aspect) = view_ray_scale(fov_y_radians, aspect);
44        SsaoParams {
45            radius: self.radius,
46            intensity: self.intensity,
47            tan_half_fov_y,
48            aspect,
49        }
50    }
51}
52
53#[cfg(test)]
54mod tests {
55    use super::*;
56
57    #[test]
58    fn resolve_floors_radius_and_clamps_intensity() {
59        let s = SsaoSettings::resolve(-1.0, 100.0);
60        assert!(s.radius >= MIN_RADIUS);
61        assert_eq!(s.intensity, MAX_INTENSITY);
62
63        let s = SsaoSettings::resolve(0.75, -2.0);
64        assert_eq!(s.radius, 0.75);
65        assert_eq!(s.intensity, 0.0);
66    }
67
68    #[test]
69    fn resolve_passes_through_in_range_values() {
70        let s = SsaoSettings::resolve(0.5, 1.0);
71        assert_eq!(s.radius, 0.5);
72        assert_eq!(s.intensity, 1.0);
73    }
74
75    #[test]
76    fn params_compute_tan_half_fov() {
77        let s = SsaoSettings::resolve(0.5, 1.0);
78        // A 90-degree vertical FOV has tan(45 deg) == 1.
79        let p = s.params(core::f32::consts::FRAC_PI_2, 1.6);
80        assert!((p.tan_half_fov_y - 1.0).abs() < 1.0e-5);
81        assert_eq!(p.aspect, 1.6);
82        assert_eq!(p.radius, 0.5);
83        assert_eq!(p.intensity, 1.0);
84    }
85
86    #[test]
87    fn params_floor_a_degenerate_aspect() {
88        let s = SsaoSettings::resolve(0.5, 1.0);
89        let p = s.params(core::f32::consts::FRAC_PI_2, 0.0);
90        assert!(p.aspect >= MIN_RADIUS);
91    }
92}