1#![allow(dead_code)]
8#![allow(clippy::cast_precision_loss)]
9
10#[derive(Debug, Clone, Copy, PartialEq)]
12pub enum TonemapAlgorithm {
13 Reinhard,
15 HableFilimic,
17 Aces,
19 DragoLog,
21}
22
23#[derive(Debug, Clone)]
25pub struct TonemapParams {
26 pub algorithm: TonemapAlgorithm,
28 pub exposure: f32,
30 pub gamma: f32,
32 pub peak_luminance: f32,
34}
35
36impl Default for TonemapParams {
37 fn default() -> Self {
38 Self {
39 algorithm: TonemapAlgorithm::Reinhard,
40 exposure: 1.0,
41 gamma: 2.2,
42 peak_luminance: 1000.0,
43 }
44 }
45}
46
47#[must_use]
55pub fn reinhard_tonemap(r: f32, g: f32, b: f32) -> (f32, f32, f32) {
56 (r / (1.0 + r), g / (1.0 + g), b / (1.0 + b))
57}
58
59#[must_use]
63pub fn hable_tonemap(r: f32, g: f32, b: f32, exposure: f32) -> (f32, f32, f32) {
64 let scale = exposure;
65 let hable = |x: f32| -> f32 {
66 let (a, b_c, c, d, e, f) = (0.15_f32, 0.50, 0.10, 0.20, 0.02, 0.30);
68 (x * (a * x + c * b_c) + d * e) / (x * (a * x + b_c) + d * f) - e / f
69 };
70 let white = hable(11.2);
71 let map = |v: f32| hable(v * scale) / white;
72 (map(r), map(g), map(b))
73}
74
75#[must_use]
77pub fn aces_tonemap(r: f32, g: f32, b: f32) -> (f32, f32, f32) {
78 let aces = |x: f32| -> f32 {
79 let (a, b_c, c, d, e) = (2.51_f32, 0.03, 2.43, 0.59, 0.14);
80 ((x * (a * x + b_c)) / (x * (c * x + d) + e)).clamp(0.0, 1.0)
81 };
82 (aces(r), aces(g), aces(b))
83}
84
85#[must_use]
90pub fn drago_log_tonemap(r: f32, g: f32, b: f32, peak_luminance: f32) -> (f32, f32, f32) {
91 let map = |v: f32| -> f32 {
92 if v <= 0.0 || peak_luminance <= 0.0 {
93 return 0.0;
94 }
95 let l = v / peak_luminance;
96 (1.0 + (l * std::f32::consts::E).ln()) / (1.0 + (std::f32::consts::E).ln())
97 };
98 (map(r), map(g), map(b))
99}
100
101#[must_use]
107pub fn apply_gamma(value: f32, gamma: f32) -> f32 {
108 if gamma <= 0.0 {
109 return value.clamp(0.0, 1.0);
110 }
111 value.clamp(0.0, 1.0).powf(1.0 / gamma)
112}
113
114pub fn apply_tonemap_frame(pixels: &mut [f32], width: u32, height: u32, params: &TonemapParams) {
124 let n = (width * height) as usize;
125 let stride = if pixels.len() == n * 4 { 4 } else { 3 };
126 assert_eq!(
127 pixels.len(),
128 n * stride,
129 "pixels.len() must equal width*height*stride"
130 );
131
132 for i in 0..n {
133 let base = i * stride;
134 let r = pixels[base] * params.exposure;
135 let g = pixels[base + 1] * params.exposure;
136 let b = pixels[base + 2] * params.exposure;
137
138 let (tr, tg, tb) = match params.algorithm {
139 TonemapAlgorithm::Reinhard => reinhard_tonemap(r, g, b),
140 TonemapAlgorithm::HableFilimic => hable_tonemap(r, g, b, 1.0),
141 TonemapAlgorithm::Aces => aces_tonemap(r, g, b),
142 TonemapAlgorithm::DragoLog => drago_log_tonemap(r, g, b, params.peak_luminance),
143 };
144
145 pixels[base] = apply_gamma(tr, params.gamma);
146 pixels[base + 1] = apply_gamma(tg, params.gamma);
147 pixels[base + 2] = apply_gamma(tb, params.gamma);
148 }
150}
151
152#[cfg(test)]
156mod tests {
157 use super::*;
158
159 const EPS: f32 = 1e-5;
160
161 #[test]
162 fn test_reinhard_zero() {
163 let (r, g, b) = reinhard_tonemap(0.0, 0.0, 0.0);
164 assert!(r.abs() < EPS && g.abs() < EPS && b.abs() < EPS);
165 }
166
167 #[test]
168 fn test_reinhard_large_input_approaches_one() {
169 let (r, g, b) = reinhard_tonemap(1e6, 1e6, 1e6);
170 assert!((1.0 - r).abs() < 1e-4);
171 assert!((1.0 - g).abs() < 1e-4);
172 assert!((1.0 - b).abs() < 1e-4);
173 }
174
175 #[test]
176 fn test_reinhard_half_input() {
177 let (r, _, _) = reinhard_tonemap(0.5, 0.0, 0.0);
179 assert!((r - 1.0 / 3.0).abs() < EPS);
180 }
181
182 #[test]
183 fn test_hable_unity_exposure() {
184 let (r, g, b) = hable_tonemap(1.0, 1.0, 1.0, 1.0);
185 assert!((0.0..=1.0).contains(&r));
187 assert!((0.0..=1.0).contains(&g));
188 assert!((0.0..=1.0).contains(&b));
189 }
190
191 #[test]
192 fn test_aces_zero() {
193 let (r, g, b) = aces_tonemap(0.0, 0.0, 0.0);
194 assert!(r < EPS && g < EPS && b < EPS);
196 }
197
198 #[test]
199 fn test_aces_output_clamped() {
200 let (r, g, b) = aces_tonemap(1e6, 1e6, 1e6);
201 assert!(r <= 1.0 && g <= 1.0 && b <= 1.0);
202 }
203
204 #[test]
205 fn test_drago_zero_input() {
206 let (r, g, b) = drago_log_tonemap(0.0, 0.0, 0.0, 1000.0);
207 assert!(r.abs() < EPS && g.abs() < EPS && b.abs() < EPS);
208 }
209
210 #[test]
211 fn test_drago_output_range() {
212 let (r, g, b) = drago_log_tonemap(500.0, 500.0, 500.0, 1000.0);
213 assert!(r >= 0.0 && r <= 1.0);
214 assert!(g >= 0.0 && g <= 1.0);
215 assert!(b >= 0.0 && b <= 1.0);
216 }
217
218 #[test]
219 fn test_apply_gamma_identity_at_one() {
220 let v = apply_gamma(0.5, 1.0);
222 assert!((v - 0.5).abs() < EPS);
223 }
224
225 #[test]
226 fn test_apply_gamma_clamps_above_one() {
227 let v = apply_gamma(2.0, 2.2);
228 assert!((v - 1.0).abs() < EPS);
229 }
230
231 #[test]
232 fn test_apply_gamma_clamps_below_zero() {
233 let v = apply_gamma(-1.0, 2.2);
234 assert!(v.abs() < EPS);
235 }
236
237 #[test]
238 fn test_apply_tonemap_frame_reinhard_rgb() {
239 let mut pixels = vec![1.0_f32; 4 * 3]; let params = TonemapParams {
241 algorithm: TonemapAlgorithm::Reinhard,
242 exposure: 1.0,
243 gamma: 1.0,
244 peak_luminance: 1000.0,
245 };
246 apply_tonemap_frame(&mut pixels, 2, 2, ¶ms);
247 for i in 0..4 {
249 let base = i * 3;
250 assert!((pixels[base] - 0.5).abs() < EPS, "pixel {i} r");
251 }
252 }
253
254 #[test]
255 fn test_apply_tonemap_frame_rgba_alpha_preserved() {
256 let mut pixels = vec![1.0_f32, 1.0, 1.0, 0.75];
258 let params = TonemapParams::default();
259 apply_tonemap_frame(&mut pixels, 1, 1, ¶ms);
260 assert!((pixels[3] - 0.75).abs() < EPS);
262 }
263}