#![allow(dead_code)]
use std::f32::consts::LOG2_E;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[allow(dead_code)]
pub enum FilmToneOp {
Linear,
Reinhard,
AcesFilmic,
Hable,
Logarithmic,
}
#[derive(Debug, Clone, PartialEq)]
#[allow(dead_code)]
pub struct FilmToneConfig {
pub operator: FilmToneOp,
pub exposure: f32,
pub gamma: f32,
pub white_point: f32,
pub enabled: bool,
}
impl Default for FilmToneConfig {
fn default() -> Self {
Self {
operator: FilmToneOp::AcesFilmic,
exposure: 1.0,
gamma: 2.2,
white_point: 11.2,
enabled: true,
}
}
}
#[allow(dead_code)]
pub fn default_film_tone_config() -> FilmToneConfig {
FilmToneConfig::default()
}
#[allow(dead_code)]
pub fn tone_linear(x: f32, exposure: f32) -> f32 {
(x * exposure).clamp(0.0, 1.0)
}
#[allow(dead_code)]
pub fn tone_reinhard(x: f32, exposure: f32, white: f32) -> f32 {
let v = x * exposure;
v * (1.0 + v / (white * white)) / (1.0 + v)
}
#[allow(dead_code)]
pub fn tone_aces_filmic(x: f32) -> f32 {
let a = 2.51;
let b = 0.03;
let c = 2.43;
let d = 0.59;
let e = 0.14;
((x * (a * x + b)) / (x * (c * x + d) + e)).clamp(0.0, 1.0)
}
#[allow(dead_code)]
fn hable_partial(x: f32) -> f32 {
let a = 0.15;
let b = 0.50;
let c = 0.10;
let d = 0.20;
let e = 0.02;
let f = 0.30;
((x * (a * x + c * b) + d * e) / (x * (a * x + b) + d * f)) - e / f
}
#[allow(dead_code)]
pub fn tone_hable(x: f32, exposure: f32, white: f32) -> f32 {
let curr = hable_partial(x * exposure * 2.0);
let white_scale = 1.0 / hable_partial(white);
(curr * white_scale).clamp(0.0, 1.0)
}
#[allow(dead_code)]
pub fn tone_logarithmic(x: f32, exposure: f32) -> f32 {
let v = (x * exposure).max(0.0);
((v + 1.0).ln() * LOG2_E * 0.1).clamp(0.0, 1.0)
}
#[allow(dead_code)]
pub fn apply_gamma(x: f32, gamma: f32) -> f32 {
x.max(0.0).powf(1.0 / gamma)
}
#[allow(dead_code)]
pub fn apply_film_tone(x: f32, cfg: &FilmToneConfig) -> f32 {
if !cfg.enabled {
return x;
}
let mapped = match cfg.operator {
FilmToneOp::Linear => tone_linear(x, cfg.exposure),
FilmToneOp::Reinhard => tone_reinhard(x, cfg.exposure, cfg.white_point),
FilmToneOp::AcesFilmic => tone_aces_filmic(x * cfg.exposure),
FilmToneOp::Hable => tone_hable(x, cfg.exposure, cfg.white_point),
FilmToneOp::Logarithmic => tone_logarithmic(x, cfg.exposure),
};
apply_gamma(mapped, cfg.gamma)
}
#[allow(dead_code)]
pub fn film_tone_to_json(cfg: &FilmToneConfig) -> String {
format!(
r#"{{"exposure":{:.4},"gamma":{:.4},"enabled":{}}}"#,
cfg.exposure, cfg.gamma, cfg.enabled
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn linear_zero_in_zero_out() {
assert!((tone_linear(0.0, 1.0)).abs() < 1e-6);
}
#[test]
fn linear_clamps_at_one() {
assert!((tone_linear(100.0, 1.0) - 1.0).abs() < 1e-6);
}
#[test]
fn reinhard_positive() {
let v = tone_reinhard(1.0, 1.0, 11.2);
assert!(v > 0.0 && v <= 1.0);
}
#[test]
fn aces_clamps() {
assert!((0.0..=1.0).contains(&tone_aces_filmic(5.0)));
}
#[test]
fn log_uses_log2e() {
let v = tone_logarithmic(1.0, 1.0);
assert!((0.0..=1.0).contains(&v));
}
#[test]
fn gamma_identity_at_one() {
assert!((apply_gamma(1.0, 1.0) - 1.0).abs() < 1e-6);
}
#[test]
fn disabled_returns_input() {
let cfg = FilmToneConfig {
enabled: false,
..Default::default()
};
assert!((apply_film_tone(0.5, &cfg) - 0.5).abs() < 1e-6);
}
#[test]
fn apply_aces_nonzero_input() {
let cfg = FilmToneConfig::default();
let v = apply_film_tone(1.0, &cfg);
assert!(v > 0.0);
}
#[test]
fn json_contains_exposure() {
assert!(film_tone_to_json(&FilmToneConfig::default()).contains("exposure"));
}
#[test]
fn hable_clamps() {
let v = tone_hable(10.0, 1.0, 11.2);
assert!((0.0..=1.0).contains(&v));
}
}