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