concinnity_core/gfx/auto_exposure/
state.rs1use super::AutoExposureSettings;
7use crate::math::exp;
8
9pub const LUM_LOG2_MIN: f32 = -10.0;
12
13pub const LUM_LOG2_MAX: f32 = 12.0;
18
19pub const HISTOGRAM_BINS: usize = 256;
23
24#[derive(Debug, Clone, Copy)]
30pub struct AutoExposureState {
31 pub current_ev: f32,
35}
36
37impl AutoExposureState {
38 pub fn new(settings: &AutoExposureSettings) -> Self {
42 Self {
43 current_ev: (settings.min_ev + settings.max_ev) * 0.5,
44 }
45 }
46
47 pub fn update(
55 &mut self,
56 avg_log_lum: f32,
57 ev_bias: f32,
58 settings: &AutoExposureSettings,
59 dt: f32,
60 ) -> f32 {
61 let target = (settings.target_log_lum - avg_log_lum + ev_bias)
67 .clamp(settings.min_ev, settings.max_ev);
68 if !dt.is_finite() || dt <= 0.0 || !target.is_finite() {
69 self.current_ev = self.current_ev.clamp(settings.min_ev, settings.max_ev);
70 return self.current_ev;
71 }
72 let blend = 1.0 - exp(-settings.speed * dt);
73 self.current_ev = (self.current_ev + (target - self.current_ev) * blend)
74 .clamp(settings.min_ev, settings.max_ev);
75 self.current_ev
76 }
77}
78
79#[cfg(test)]
87pub(crate) fn average_log_luminance(histogram: &[u32; HISTOGRAM_BINS]) -> f32 {
88 let step = (LUM_LOG2_MAX - LUM_LOG2_MIN) / HISTOGRAM_BINS as f32;
89 let mut weighted_sum = 0.0f64;
90 let mut count = 0u64;
91 for (i, &n) in histogram.iter().enumerate().skip(1) {
92 if n == 0 {
93 continue;
94 }
95 let centre = LUM_LOG2_MIN + (i as f32 + 0.5) * step;
96 weighted_sum += centre as f64 * n as f64;
97 count += n as u64;
98 }
99 if count == 0 {
100 LUM_LOG2_MIN
101 } else {
102 (weighted_sum / count as f64) as f32
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 use crate::gfx::auto_exposure::HDR_MIDDLE_GREY_LOG2;
110
111 #[test]
112 fn update_pulls_current_ev_toward_target() {
113 let settings = AutoExposureSettings::resolve(-8.0, 8.0, 4.0, false);
114 let mut state = AutoExposureState { current_ev: 0.0 };
115 let ev = state.update(2.0, 0.0, &settings, 1.0);
117 assert!(ev < 0.0, "ev should move toward target_ev = -2.0, got {ev}");
118 assert!(ev > -2.0, "ev should not overshoot in one step, got {ev}");
119
120 for _ in 0..200 {
122 state.update(2.0, 0.0, &settings, 1.0 / 60.0);
123 }
124 assert!((state.current_ev + 2.0).abs() < 1.0e-3);
125 }
126
127 #[test]
128 fn update_clamps_to_settings_bounds() {
129 let settings = AutoExposureSettings::resolve(-1.0, 1.0, 5.0, false);
130 let mut state = AutoExposureState { current_ev: 0.0 };
131 for _ in 0..100 {
133 state.update(-10.0, 0.0, &settings, 1.0 / 60.0);
134 }
135 assert!((state.current_ev - 1.0).abs() < 1.0e-3);
136 }
137
138 #[test]
139 fn update_short_circuits_non_finite_dt() {
140 let settings = AutoExposureSettings::resolve(-2.0, 2.0, 1.0, false);
141 let mut state = AutoExposureState { current_ev: 0.5 };
142 let ev = state.update(5.0, 0.0, &settings, f32::NAN);
143 assert_eq!(ev, 0.5);
144 let ev = state.update(5.0, 0.0, &settings, -1.0);
145 assert_eq!(ev, 0.5);
146 }
147
148 #[test]
149 fn update_honours_ev_bias() {
150 let settings = AutoExposureSettings::resolve(-8.0, 8.0, 10.0, false);
151 let mut state = AutoExposureState { current_ev: 0.0 };
152 for _ in 0..200 {
154 state.update(0.0, 1.0, &settings, 1.0 / 60.0);
155 }
156 assert!((state.current_ev - 1.0).abs() < 1.0e-3);
157 }
158
159 #[test]
160 fn resolve_defaults_to_scene_white_target_on_sdr() {
161 let s = AutoExposureSettings::resolve(-8.0, 8.0, 1.5, false);
165 assert_eq!(s.target_log_lum, 0.0);
166 }
167
168 #[test]
169 fn resolve_shifts_to_middle_grey_on_hdr() {
170 let s = AutoExposureSettings::resolve(-8.0, 8.0, 1.5, true);
174 assert!((s.target_log_lum - HDR_MIDDLE_GREY_LOG2).abs() < 1.0e-6);
175 }
176
177 #[test]
178 fn update_shifts_target_by_target_log_lum_on_hdr() {
179 let sdr = AutoExposureSettings::resolve(-8.0, 8.0, 10.0, false);
184 let hdr = AutoExposureSettings::resolve(-8.0, 8.0, 10.0, true);
185 let mut state_sdr = AutoExposureState { current_ev: 0.0 };
186 let mut state_hdr = AutoExposureState { current_ev: 0.0 };
187 for _ in 0..500 {
188 state_sdr.update(0.0, 0.0, &sdr, 1.0 / 60.0);
189 state_hdr.update(0.0, 0.0, &hdr, 1.0 / 60.0);
190 }
191 assert!(state_sdr.current_ev.abs() < 1.0e-3);
193 assert!((state_hdr.current_ev - HDR_MIDDLE_GREY_LOG2).abs() < 1.0e-3);
194 }
195
196 #[test]
197 fn average_log_luminance_empties_to_floor() {
198 let histogram = [0u32; HISTOGRAM_BINS];
199 assert_eq!(average_log_luminance(&histogram), LUM_LOG2_MIN);
200 }
201
202 #[test]
203 fn average_log_luminance_single_bin_returns_bin_centre() {
204 let mut histogram = [0u32; HISTOGRAM_BINS];
205 histogram[128] = 10;
206 let avg = average_log_luminance(&histogram);
207 let step = (LUM_LOG2_MAX - LUM_LOG2_MIN) / HISTOGRAM_BINS as f32;
208 let expected = LUM_LOG2_MIN + (128.5) * step;
209 assert!(
210 (avg - expected).abs() < 1.0e-3,
211 "avg={avg} expected={expected}"
212 );
213 }
214
215 #[test]
216 fn average_log_luminance_ignores_underflow_bin() {
217 let mut histogram = [0u32; HISTOGRAM_BINS];
220 histogram[0] = 10_000;
221 histogram[100] = 1;
222 let avg = average_log_luminance(&histogram);
223 let step = (LUM_LOG2_MAX - LUM_LOG2_MIN) / HISTOGRAM_BINS as f32;
224 let expected = LUM_LOG2_MIN + (100.5) * step;
225 assert!(
226 (avg - expected).abs() < 1.0e-3,
227 "avg={avg} expected={expected}"
228 );
229 }
230}