use super::AutoExposureSettings;
use crate::math::exp;
pub const LUM_LOG2_MIN: f32 = -10.0;
pub const LUM_LOG2_MAX: f32 = 12.0;
pub const HISTOGRAM_BINS: usize = 256;
#[derive(Debug, Clone, Copy)]
pub struct AutoExposureState {
pub current_ev: f32,
}
impl AutoExposureState {
pub fn new(settings: &AutoExposureSettings) -> Self {
Self {
current_ev: (settings.min_ev + settings.max_ev) * 0.5,
}
}
pub fn update(
&mut self,
avg_log_lum: f32,
ev_bias: f32,
settings: &AutoExposureSettings,
dt: f32,
) -> f32 {
let target = (settings.target_log_lum - avg_log_lum + ev_bias)
.clamp(settings.min_ev, settings.max_ev);
if !dt.is_finite() || dt <= 0.0 || !target.is_finite() {
self.current_ev = self.current_ev.clamp(settings.min_ev, settings.max_ev);
return self.current_ev;
}
let blend = 1.0 - exp(-settings.speed * dt);
self.current_ev = (self.current_ev + (target - self.current_ev) * blend)
.clamp(settings.min_ev, settings.max_ev);
self.current_ev
}
}
#[cfg(test)]
pub(crate) fn average_log_luminance(histogram: &[u32; HISTOGRAM_BINS]) -> f32 {
let step = (LUM_LOG2_MAX - LUM_LOG2_MIN) / HISTOGRAM_BINS as f32;
let mut weighted_sum = 0.0f64;
let mut count = 0u64;
for (i, &n) in histogram.iter().enumerate().skip(1) {
if n == 0 {
continue;
}
let centre = LUM_LOG2_MIN + (i as f32 + 0.5) * step;
weighted_sum += centre as f64 * n as f64;
count += n as u64;
}
if count == 0 {
LUM_LOG2_MIN
} else {
(weighted_sum / count as f64) as f32
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::gfx::auto_exposure::HDR_MIDDLE_GREY_LOG2;
#[test]
fn update_pulls_current_ev_toward_target() {
let settings = AutoExposureSettings::resolve(-8.0, 8.0, 4.0, false);
let mut state = AutoExposureState { current_ev: 0.0 };
let ev = state.update(2.0, 0.0, &settings, 1.0);
assert!(ev < 0.0, "ev should move toward target_ev = -2.0, got {ev}");
assert!(ev > -2.0, "ev should not overshoot in one step, got {ev}");
for _ in 0..200 {
state.update(2.0, 0.0, &settings, 1.0 / 60.0);
}
assert!((state.current_ev + 2.0).abs() < 1.0e-3);
}
#[test]
fn update_clamps_to_settings_bounds() {
let settings = AutoExposureSettings::resolve(-1.0, 1.0, 5.0, false);
let mut state = AutoExposureState { current_ev: 0.0 };
for _ in 0..100 {
state.update(-10.0, 0.0, &settings, 1.0 / 60.0);
}
assert!((state.current_ev - 1.0).abs() < 1.0e-3);
}
#[test]
fn update_short_circuits_non_finite_dt() {
let settings = AutoExposureSettings::resolve(-2.0, 2.0, 1.0, false);
let mut state = AutoExposureState { current_ev: 0.5 };
let ev = state.update(5.0, 0.0, &settings, f32::NAN);
assert_eq!(ev, 0.5);
let ev = state.update(5.0, 0.0, &settings, -1.0);
assert_eq!(ev, 0.5);
}
#[test]
fn update_honours_ev_bias() {
let settings = AutoExposureSettings::resolve(-8.0, 8.0, 10.0, false);
let mut state = AutoExposureState { current_ev: 0.0 };
for _ in 0..200 {
state.update(0.0, 1.0, &settings, 1.0 / 60.0);
}
assert!((state.current_ev - 1.0).abs() < 1.0e-3);
}
#[test]
fn resolve_defaults_to_scene_white_target_on_sdr() {
let s = AutoExposureSettings::resolve(-8.0, 8.0, 1.5, false);
assert_eq!(s.target_log_lum, 0.0);
}
#[test]
fn resolve_shifts_to_middle_grey_on_hdr() {
let s = AutoExposureSettings::resolve(-8.0, 8.0, 1.5, true);
assert!((s.target_log_lum - HDR_MIDDLE_GREY_LOG2).abs() < 1.0e-6);
}
#[test]
fn update_shifts_target_by_target_log_lum_on_hdr() {
let sdr = AutoExposureSettings::resolve(-8.0, 8.0, 10.0, false);
let hdr = AutoExposureSettings::resolve(-8.0, 8.0, 10.0, true);
let mut state_sdr = AutoExposureState { current_ev: 0.0 };
let mut state_hdr = AutoExposureState { current_ev: 0.0 };
for _ in 0..500 {
state_sdr.update(0.0, 0.0, &sdr, 1.0 / 60.0);
state_hdr.update(0.0, 0.0, &hdr, 1.0 / 60.0);
}
assert!(state_sdr.current_ev.abs() < 1.0e-3);
assert!((state_hdr.current_ev - HDR_MIDDLE_GREY_LOG2).abs() < 1.0e-3);
}
#[test]
fn average_log_luminance_empties_to_floor() {
let histogram = [0u32; HISTOGRAM_BINS];
assert_eq!(average_log_luminance(&histogram), LUM_LOG2_MIN);
}
#[test]
fn average_log_luminance_single_bin_returns_bin_centre() {
let mut histogram = [0u32; HISTOGRAM_BINS];
histogram[128] = 10;
let avg = average_log_luminance(&histogram);
let step = (LUM_LOG2_MAX - LUM_LOG2_MIN) / HISTOGRAM_BINS as f32;
let expected = LUM_LOG2_MIN + (128.5) * step;
assert!(
(avg - expected).abs() < 1.0e-3,
"avg={avg} expected={expected}"
);
}
#[test]
fn average_log_luminance_ignores_underflow_bin() {
let mut histogram = [0u32; HISTOGRAM_BINS];
histogram[0] = 10_000;
histogram[100] = 1;
let avg = average_log_luminance(&histogram);
let step = (LUM_LOG2_MAX - LUM_LOG2_MIN) / HISTOGRAM_BINS as f32;
let expected = LUM_LOG2_MIN + (100.5) * step;
assert!(
(avg - expected).abs() < 1.0e-3,
"avg={avg} expected={expected}"
);
}
}