1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub enum ToneMapOperator {
23 None,
25 Reinhard,
27 ReinhardExtended,
29 Aces,
31 Uncharted2,
33 NeutralFilmic,
35}
36
37impl Default for ToneMapOperator {
38 fn default() -> Self { Self::Aces }
39}
40
41impl ToneMapOperator {
42 pub fn apply(&self, color: glam::Vec3, exposure: f32) -> glam::Vec3 {
44 let c = color * exposure;
45 match self {
46 Self::None => clamp01(c),
47 Self::Reinhard => reinhard(c),
48 Self::ReinhardExtended => reinhard_extended(c, 4.0),
49 Self::Aces => aces_filmic(c),
50 Self::Uncharted2 => uncharted2(c),
51 Self::NeutralFilmic => neutral_filmic(c),
52 }
53 }
54
55 pub fn glsl_func_name(&self) -> &'static str {
57 match self {
58 Self::None => "tonemap_none",
59 Self::Reinhard => "tonemap_reinhard",
60 Self::ReinhardExtended => "tonemap_reinhard_ext",
61 Self::Aces => "tonemap_aces",
62 Self::Uncharted2 => "tonemap_uncharted2",
63 Self::NeutralFilmic => "tonemap_neutral",
64 }
65 }
66}
67
68fn clamp01(c: glam::Vec3) -> glam::Vec3 {
71 glam::Vec3::new(c.x.clamp(0.0, 1.0), c.y.clamp(0.0, 1.0), c.z.clamp(0.0, 1.0))
72}
73
74fn reinhard(c: glam::Vec3) -> glam::Vec3 {
76 glam::Vec3::new(
77 c.x / (1.0 + c.x),
78 c.y / (1.0 + c.y),
79 c.z / (1.0 + c.z),
80 )
81}
82
83fn reinhard_extended(c: glam::Vec3, white_point: f32) -> glam::Vec3 {
85 let wp2 = white_point * white_point;
86 glam::Vec3::new(
87 c.x * (1.0 + c.x / wp2) / (1.0 + c.x),
88 c.y * (1.0 + c.y / wp2) / (1.0 + c.y),
89 c.z * (1.0 + c.z / wp2) / (1.0 + c.z),
90 )
91}
92
93fn aces_filmic(c: glam::Vec3) -> glam::Vec3 {
95 let a = 2.51;
96 let b = 0.03;
97 let cc = 2.43;
98 let d = 0.59;
99 let e = 0.14;
100 let f = |x: f32| -> f32 {
101 ((x * (a * x + b)) / (x * (cc * x + d) + e)).clamp(0.0, 1.0)
102 };
103 glam::Vec3::new(f(c.x), f(c.y), f(c.z))
104}
105
106fn uncharted2(c: glam::Vec3) -> glam::Vec3 {
108 let f = |x: f32| -> f32 {
109 let a = 0.15;
110 let b = 0.50;
111 let cc = 0.10;
112 let d = 0.20;
113 let e = 0.02;
114 let ff = 0.30;
115 ((x * (a * x + cc * b) + d * e) / (x * (a * x + b) + d * ff)) - e / ff
116 };
117 let white_scale = 1.0 / f(11.2);
118 glam::Vec3::new(
119 f(c.x) * white_scale,
120 f(c.y) * white_scale,
121 f(c.z) * white_scale,
122 )
123}
124
125fn neutral_filmic(c: glam::Vec3) -> glam::Vec3 {
127 let f = |x: f32| -> f32 {
128 let a = x.max(0.0);
129 (a * (a * 6.2 + 0.5)) / (a * (a * 6.2 + 1.7) + 0.06)
130 };
131 glam::Vec3::new(f(c.x), f(c.y), f(c.z))
132}
133
134#[derive(Debug, Clone)]
138pub struct HdrParams {
139 pub enabled: bool,
141 pub tone_map: ToneMapOperator,
143 pub exposure_mode: ExposureMode,
145 pub manual_exposure: f32,
147 pub adaptation_speed: f32,
149 pub min_ev: f32,
151 pub max_ev: f32,
153 pub compensation: f32,
155}
156
157impl Default for HdrParams {
158 fn default() -> Self {
159 Self {
160 enabled: true,
161 tone_map: ToneMapOperator::Aces,
162 exposure_mode: ExposureMode::Auto,
163 manual_exposure: 1.0,
164 adaptation_speed: 1.5,
165 min_ev: -2.0,
166 max_ev: 4.0,
167 compensation: 0.0,
168 }
169 }
170}
171
172impl HdrParams {
173 pub fn disabled() -> Self {
174 Self { enabled: false, ..Default::default() }
175 }
176
177 pub fn manual(exposure: f32) -> Self {
178 Self {
179 exposure_mode: ExposureMode::Manual,
180 manual_exposure: exposure,
181 ..Default::default()
182 }
183 }
184}
185
186#[derive(Debug, Clone, Copy, PartialEq, Eq)]
188pub enum ExposureMode {
189 Auto,
191 Manual,
193 AutoClamped,
195}
196
197pub struct AutoExposure {
201 current_lum: f32,
203 pub exposure: f32,
205 pub avg_luminance: f32,
207}
208
209impl AutoExposure {
210 pub fn new() -> Self {
211 Self {
212 current_lum: 0.5,
213 exposure: 1.0,
214 avg_luminance: 0.5,
215 }
216 }
217
218 pub fn update(&mut self, avg_lum: f32, dt: f32, params: &HdrParams) {
224 match params.exposure_mode {
225 ExposureMode::Manual => {
226 self.exposure = params.manual_exposure;
227 self.avg_luminance = avg_lum;
228 }
229 ExposureMode::Auto | ExposureMode::AutoClamped => {
230 let speed = 1.0 - (-dt / params.adaptation_speed.max(0.01)).exp();
232 self.current_lum += (avg_lum.max(0.001) - self.current_lum) * speed;
233
234 let ev = (self.current_lum / 0.18).log2();
236 let clamped_ev = ev.clamp(params.min_ev, params.max_ev);
237
238 self.exposure = 1.0 / (2.0_f32.powf(clamped_ev) * 1.2);
240 self.exposure *= 2.0_f32.powf(params.compensation);
241
242 self.avg_luminance = self.current_lum;
243 }
244 }
245 }
246
247 pub fn reset(&mut self) {
249 self.current_lum = 0.5;
250 self.exposure = 1.0;
251 }
252}
253
254impl Default for AutoExposure {
255 fn default() -> Self { Self::new() }
256}
257
258pub struct ExposurePresets;
262
263impl ExposurePresets {
264 pub fn bright_scene() -> HdrParams {
266 HdrParams {
267 exposure_mode: ExposureMode::Auto,
268 min_ev: 0.0,
269 max_ev: 3.0,
270 compensation: -0.5,
271 ..Default::default()
272 }
273 }
274
275 pub fn dark_scene() -> HdrParams {
277 HdrParams {
278 exposure_mode: ExposureMode::Auto,
279 min_ev: -2.0,
280 max_ev: 1.0,
281 compensation: 0.5,
282 ..Default::default()
283 }
284 }
285
286 pub fn death_sequence(progress: f32) -> HdrParams {
288 HdrParams {
289 exposure_mode: ExposureMode::Manual,
290 manual_exposure: (1.0 - progress * 0.95).max(0.05),
291 ..Default::default()
292 }
293 }
294
295 pub fn boss_entrance() -> HdrParams {
297 HdrParams {
298 exposure_mode: ExposureMode::Manual,
299 manual_exposure: 2.0,
300 ..Default::default()
301 }
302 }
303
304 pub fn victory() -> HdrParams {
306 HdrParams {
307 exposure_mode: ExposureMode::Manual,
308 manual_exposure: 1.3,
309 ..Default::default()
310 }
311 }
312
313 pub fn normal() -> HdrParams {
315 HdrParams::default()
316 }
317
318 pub fn shrine() -> HdrParams {
320 HdrParams {
321 exposure_mode: ExposureMode::Auto,
322 compensation: 0.3,
323 ..Default::default()
324 }
325 }
326}
327
328pub const HDR_TONEMAP_FRAG: &str = r#"
333#version 330 core
334
335in vec2 f_uv;
336out vec4 frag_color;
337
338uniform sampler2D u_hdr_scene;
339uniform float u_exposure;
340uniform int u_tonemap_op; // 0=none, 1=reinhard, 2=aces, 3=uncharted2
341
342// Reinhard
343vec3 tonemap_reinhard(vec3 c) {
344 return c / (vec3(1.0) + c);
345}
346
347// ACES (Narkowicz approximation)
348vec3 tonemap_aces(vec3 c) {
349 float a = 2.51;
350 float b = 0.03;
351 float cc = 2.43;
352 float d = 0.59;
353 float e = 0.14;
354 return clamp((c * (a * c + b)) / (c * (cc * c + d) + e), 0.0, 1.0);
355}
356
357// Uncharted 2 (Hable)
358vec3 uc2_curve(vec3 x) {
359 float A = 0.15; float B = 0.50; float C = 0.10;
360 float D = 0.20; float E = 0.02; float F = 0.30;
361 return ((x * (A * x + C * B) + D * E) / (x * (A * x + B) + D * F)) - E / F;
362}
363
364vec3 tonemap_uncharted2(vec3 c) {
365 float W = 11.2;
366 return uc2_curve(c) / uc2_curve(vec3(W));
367}
368
369void main() {
370 vec3 hdr = texture(u_hdr_scene, f_uv).rgb;
371
372 // Apply exposure
373 vec3 exposed = hdr * u_exposure;
374
375 // Tone map
376 vec3 mapped;
377 if (u_tonemap_op == 0) mapped = clamp(exposed, 0.0, 1.0);
378 else if (u_tonemap_op == 1) mapped = tonemap_reinhard(exposed);
379 else if (u_tonemap_op == 2) mapped = tonemap_aces(exposed);
380 else if (u_tonemap_op == 3) mapped = tonemap_uncharted2(exposed);
381 else mapped = tonemap_aces(exposed);
382
383 // Gamma correction (linear → sRGB)
384 mapped = pow(mapped, vec3(1.0 / 2.2));
385
386 frag_color = vec4(mapped, 1.0);
387}
388"#;
389
390pub const LUMINANCE_FRAG: &str = r#"
393#version 330 core
394
395in vec2 f_uv;
396out vec4 frag_color;
397
398uniform sampler2D u_scene;
399
400void main() {
401 vec3 color = texture(u_scene, f_uv).rgb;
402 float lum = dot(color, vec3(0.2126, 0.7152, 0.0722));
403 // Output log luminance for averaging
404 float logLum = log(max(lum, 0.0001));
405 frag_color = vec4(logLum, lum, 0.0, 1.0);
406}
407"#;
408
409#[derive(Debug, Clone, Default)]
413pub struct HdrStats {
414 pub enabled: bool,
415 pub tone_map: &'static str,
416 pub exposure: f32,
417 pub avg_luminance: f32,
418 pub exposure_mode: &'static str,
419}
420
421impl HdrStats {
422 pub fn from_state(params: &HdrParams, auto_exp: &AutoExposure) -> Self {
423 Self {
424 enabled: params.enabled,
425 tone_map: match params.tone_map {
426 ToneMapOperator::None => "None",
427 ToneMapOperator::Reinhard => "Reinhard",
428 ToneMapOperator::ReinhardExtended => "Reinhard Ext",
429 ToneMapOperator::Aces => "ACES",
430 ToneMapOperator::Uncharted2 => "Uncharted2",
431 ToneMapOperator::NeutralFilmic => "Neutral",
432 },
433 exposure: auto_exp.exposure,
434 avg_luminance: auto_exp.avg_luminance,
435 exposure_mode: match params.exposure_mode {
436 ExposureMode::Auto => "Auto",
437 ExposureMode::Manual => "Manual",
438 ExposureMode::AutoClamped => "Auto (Clamped)",
439 },
440 }
441 }
442}
443
444#[cfg(test)]
447mod tests {
448 use super::*;
449 use glam::Vec3;
450
451 #[test]
452 fn reinhard_preserves_zero() {
453 let result = ToneMapOperator::Reinhard.apply(Vec3::ZERO, 1.0);
454 assert!(result.length() < 1e-6);
455 }
456
457 #[test]
458 fn reinhard_maps_bright_below_one() {
459 let result = ToneMapOperator::Reinhard.apply(Vec3::new(10.0, 10.0, 10.0), 1.0);
460 assert!(result.x < 1.0);
461 assert!(result.x > 0.9);
462 }
463
464 #[test]
465 fn aces_maps_to_unit_range() {
466 let result = ToneMapOperator::Aces.apply(Vec3::new(5.0, 3.0, 1.0), 1.0);
467 assert!(result.x >= 0.0 && result.x <= 1.0);
468 assert!(result.y >= 0.0 && result.y <= 1.0);
469 assert!(result.z >= 0.0 && result.z <= 1.0);
470 }
471
472 #[test]
473 fn uncharted2_maps_to_unit_range() {
474 let result = ToneMapOperator::Uncharted2.apply(Vec3::new(5.0, 3.0, 1.0), 1.0);
475 assert!(result.x >= 0.0 && result.x <= 1.0);
476 assert!(result.y >= 0.0 && result.y <= 1.0);
477 }
478
479 #[test]
480 fn exposure_scales_output() {
481 let low = ToneMapOperator::Aces.apply(Vec3::ONE, 0.5);
482 let high = ToneMapOperator::Aces.apply(Vec3::ONE, 2.0);
483 assert!(high.x > low.x);
484 }
485
486 #[test]
487 fn auto_exposure_adapts() {
488 let params = HdrParams::default();
489 let mut ae = AutoExposure::new();
490
491 for _ in 0..60 {
493 ae.update(2.0, 0.016, ¶ms);
494 }
495 let bright_exp = ae.exposure;
496
497 ae.reset();
499 for _ in 0..60 {
500 ae.update(0.01, 0.016, ¶ms);
501 }
502 let dark_exp = ae.exposure;
503
504 assert!(dark_exp > bright_exp, "dark={dark_exp} should > bright={bright_exp}");
506 }
507
508 #[test]
509 fn manual_exposure_fixed() {
510 let params = HdrParams::manual(2.5);
511 let mut ae = AutoExposure::new();
512 ae.update(0.5, 0.016, ¶ms);
513 assert_eq!(ae.exposure, 2.5);
514 }
515
516 #[test]
517 fn death_exposure_dims() {
518 let start = ExposurePresets::death_sequence(0.0);
519 let end = ExposurePresets::death_sequence(1.0);
520 assert!(end.manual_exposure < start.manual_exposure);
521 assert!(end.manual_exposure > 0.0);
522 }
523
524 #[test]
525 fn none_tonemap_clamps() {
526 let result = ToneMapOperator::None.apply(Vec3::new(2.0, -0.5, 0.5), 1.0);
527 assert_eq!(result.x, 1.0);
528 assert_eq!(result.y, 0.0);
529 assert_eq!(result.z, 0.5);
530 }
531}