#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct HdrConfig {
pub auto_exposure: bool,
pub target_luminance: f32,
pub exposure_speed: f32,
pub min_exposure: f32,
pub max_exposure: f32,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct HdrBuffer {
pub width: u32,
pub height: u32,
pub pixels: Vec<[f32; 4]>,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct HdrPipelineState {
pub config: HdrConfig,
pub current_exposure: f32,
pub avg_luminance: f32,
}
#[allow(dead_code)]
#[derive(Debug, Clone)]
pub struct HdrResult {
pub output: HdrBuffer,
pub applied_exposure: f32,
pub histogram_peak: f32,
}
#[allow(dead_code)]
pub fn default_hdr_config() -> HdrConfig {
HdrConfig {
auto_exposure: true,
target_luminance: 0.18,
exposure_speed: 1.0,
min_exposure: 0.1,
max_exposure: 10.0,
}
}
#[allow(dead_code)]
pub fn new_hdr_buffer(w: u32, h: u32) -> HdrBuffer {
HdrBuffer {
width: w,
height: h,
pixels: vec![[0.0, 0.0, 0.0, 1.0]; (w * h) as usize],
}
}
#[allow(dead_code)]
pub fn new_hdr_pipeline(cfg: HdrConfig) -> HdrPipelineState {
HdrPipelineState {
config: cfg,
current_exposure: 1.0,
avg_luminance: 0.18,
}
}
#[allow(dead_code)]
pub fn compute_avg_luminance(buf: &HdrBuffer) -> f32 {
if buf.pixels.is_empty() {
return 0.0;
}
let sum: f32 = buf
.pixels
.iter()
.map(|p| 0.2126 * p[0] + 0.7152 * p[1] + 0.0722 * p[2])
.sum();
sum / buf.pixels.len() as f32
}
#[allow(dead_code)]
pub fn update_exposure(state: &mut HdrPipelineState, scene_luminance: f32, dt: f32) {
let alpha = (state.config.exposure_speed * dt).clamp(0.0, 1.0);
state.avg_luminance = state.avg_luminance + alpha * (scene_luminance - state.avg_luminance);
if state.config.auto_exposure && state.avg_luminance > 1e-6 {
let desired = state.config.target_luminance / state.avg_luminance;
let target_ev = desired.clamp(state.config.min_exposure, state.config.max_exposure);
state.current_exposure =
state.current_exposure + alpha * (target_ev - state.current_exposure);
}
}
#[allow(dead_code)]
pub fn apply_exposure(buf: &HdrBuffer, exposure: f32) -> HdrBuffer {
let pixels = buf
.pixels
.iter()
.map(|p| [p[0] * exposure, p[1] * exposure, p[2] * exposure, p[3]])
.collect();
HdrBuffer {
width: buf.width,
height: buf.height,
pixels,
}
}
#[allow(dead_code)]
pub fn hdr_pixel_at(buf: &HdrBuffer, x: u32, y: u32) -> [f32; 4] {
if x >= buf.width || y >= buf.height {
return [0.0, 0.0, 0.0, 0.0];
}
buf.pixels[(y * buf.width + x) as usize]
}
#[allow(dead_code)]
pub fn write_hdr_pixel(buf: &mut HdrBuffer, x: u32, y: u32, p: [f32; 4]) {
if x < buf.width && y < buf.height {
buf.pixels[(y * buf.width + x) as usize] = p;
}
}
#[allow(dead_code)]
pub fn process_hdr(
state: &mut HdrPipelineState,
input: &HdrBuffer,
dt: f32,
) -> HdrResult {
let scene_lum = compute_avg_luminance(input);
update_exposure(state, scene_lum, dt);
let output = apply_exposure(input, state.current_exposure);
let histogram_peak = output
.pixels
.iter()
.map(|p| 0.2126 * p[0] + 0.7152 * p[1] + 0.0722 * p[2])
.fold(0.0_f32, f32::max);
HdrResult {
applied_exposure: state.current_exposure,
histogram_peak,
output,
}
}
#[allow(dead_code)]
pub fn hdr_config_to_json(cfg: &HdrConfig) -> String {
format!(
r#"{{"auto_exposure":{},"target_luminance":{:.4},"exposure_speed":{:.4},"min_exposure":{:.4},"max_exposure":{:.4}}}"#,
cfg.auto_exposure,
cfg.target_luminance,
cfg.exposure_speed,
cfg.min_exposure,
cfg.max_exposure
)
}
#[allow(dead_code)]
pub fn hdr_state_to_json(state: &HdrPipelineState) -> String {
format!(
r#"{{"current_exposure":{:.4},"avg_luminance":{:.4},"config":{}}}"#,
state.current_exposure,
state.avg_luminance,
hdr_config_to_json(&state.config)
)
}
#[cfg(test)]
mod tests {
use super::*;
fn white_buf(w: u32, h: u32) -> HdrBuffer {
HdrBuffer {
width: w,
height: h,
pixels: vec![[1.0, 1.0, 1.0, 1.0]; (w * h) as usize],
}
}
#[test]
fn default_config_auto_exposure_enabled() {
let cfg = default_hdr_config();
assert!(cfg.auto_exposure);
assert!((cfg.target_luminance - 0.18).abs() < 1e-5);
}
#[test]
fn new_hdr_buffer_correct_size() {
let buf = new_hdr_buffer(4, 4);
assert_eq!(buf.pixels.len(), 16);
assert_eq!(buf.width, 4);
assert_eq!(buf.height, 4);
}
#[test]
fn compute_avg_luminance_white_image() {
let buf = white_buf(2, 2);
let lum = compute_avg_luminance(&buf);
assert!((lum - 1.0).abs() < 1e-4, "white image luminance ≈ 1.0, got {}", lum);
}
#[test]
fn compute_avg_luminance_black_image() {
let buf = new_hdr_buffer(2, 2);
let lum = compute_avg_luminance(&buf);
assert!((lum).abs() < 1e-6);
}
#[test]
fn apply_exposure_scales_rgb() {
let buf = white_buf(1, 1);
let out = apply_exposure(&buf, 2.0);
assert!((out.pixels[0][0] - 2.0).abs() < 1e-5);
assert!((out.pixels[0][3] - 1.0).abs() < 1e-5); }
#[test]
fn hdr_pixel_at_bounds() {
let buf = new_hdr_buffer(4, 4);
let p = hdr_pixel_at(&buf, 10, 10); assert_eq!(p, [0.0, 0.0, 0.0, 0.0]);
}
#[test]
fn write_hdr_pixel_round_trip() {
let mut buf = new_hdr_buffer(4, 4);
write_hdr_pixel(&mut buf, 1, 2, [0.5, 0.3, 0.1, 1.0]);
let p = hdr_pixel_at(&buf, 1, 2);
assert!((p[0] - 0.5).abs() < 1e-5);
assert!((p[1] - 0.3).abs() < 1e-5);
}
#[test]
fn process_hdr_returns_result_with_exposure() {
let cfg = default_hdr_config();
let mut state = new_hdr_pipeline(cfg);
let input = white_buf(2, 2);
let result = process_hdr(&mut state, &input, 0.016);
assert!(result.applied_exposure > 0.0);
assert_eq!(result.output.pixels.len(), 4);
}
#[test]
fn hdr_config_to_json_contains_auto_exposure() {
let cfg = default_hdr_config();
let json = hdr_config_to_json(&cfg);
assert!(json.contains("\"auto_exposure\":true"));
}
#[test]
fn hdr_state_to_json_contains_current_exposure() {
let cfg = default_hdr_config();
let state = new_hdr_pipeline(cfg);
let json = hdr_state_to_json(&state);
assert!(json.contains("\"current_exposure\""));
}
}