primase_core/atmosphere/
post.rs1use serde::Serialize;
3
4#[derive(Clone, Debug, PartialEq, Serialize)]
6pub struct ToneMap {
7 pub curve: ReinhardCurve,
8 pub colour_matrix: Option<ColourMatrix>,
10 pub vignette: Option<[f32; 4]>,
13 pub lut: Option<ColourLut>,
16}
17
18#[derive(Clone, Debug, PartialEq, Serialize)]
21pub enum ColourMatrix {
22 BeforeCurve([[f32; 4]; 3]),
24 AfterCurve([[f32; 4]; 3]),
26}
27
28impl ColourMatrix {
29 pub fn rows(&self) -> &[[f32; 4]; 3] {
31 match self {
32 Self::BeforeCurve(rows) | Self::AfterCurve(rows) => rows,
33 }
34 }
35}
36
37#[derive(Clone, Debug, PartialEq, Serialize)]
41pub struct ReinhardCurve {
42 pub contrast: f32,
43 pub white_balance: f32,
44 pub gamma: f32,
45}
46
47impl ReinhardCurve {
48 pub fn table(&self) -> [u16; 1024] {
51 std::array::from_fn(|i| {
52 let u = (i as f32 + 0.5) / 1024.;
53 let x = u * 0.2 / (1. - u) * self.white_balance;
54 let z = (x.log2() * self.contrast).exp2();
55 let mapped = z / (z + 1.);
56 let value = (mapped.log2() * (1. / self.gamma)).exp2();
57 let rounded = half::f16::from_f32(value);
58 rounded.to_bits() - u16::from(rounded.to_f32() > value)
59 })
60 }
61}
62
63#[derive(Clone, Debug, PartialEq, Serialize)]
67pub struct ColourLut {
68 pub side: u32,
69 pub values: Vec<[f32; 3]>,
70 pub input_power: f32,
71}
72
73impl ToneMap {
74 pub(super) fn check(&self) -> Result<(), String> {
75 for (name, value) in [
76 ("contrast", self.curve.contrast),
77 ("white balance", self.curve.white_balance),
78 ("gamma", self.curve.gamma),
79 ] {
80 if !(value.is_finite() && value > 0.) {
81 return Err(format!("tone map {name} {value} must be finite and positive"));
82 }
83 }
84 if !self
85 .colour_matrix
86 .iter()
87 .flat_map(|matrix| matrix.rows())
88 .flatten()
89 .chain(self.vignette.iter().flatten())
90 .all(|x| x.is_finite())
91 {
92 return Err("tone map colour matrix or vignette is not finite".into());
93 }
94 let Some(lut) = &self.lut else { return Ok(()) };
95 if !(lut.input_power.is_finite() && lut.input_power > 0.) {
96 return Err("tone map LUT input power must be finite and positive".into());
97 }
98 let count = u64::from(lut.side).checked_pow(3);
99 if lut.side < 2 || count != Some(lut.values.len() as u64) {
100 return Err("tone map LUT requires side >= 2 and side cubed RGB values".into());
101 }
102 if !lut.values.iter().flatten().all(|x| x.is_finite()) {
103 return Err("tone map LUT contains a nonfinite value".into());
104 }
105 Ok(())
106 }
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112 use crate::{Atmosphere, World};
113
114 #[test]
115 fn display_processing_survives_world_serialization_and_rejects_invalid_lut_sizes() {
116 let tone = ToneMap {
117 curve: ReinhardCurve {
118 contrast: 1.45,
119 white_balance: 1.7,
120 gamma: 1.,
121 },
122 colour_matrix: Some(ColourMatrix::AfterCurve([
123 [1., 0., 0., 0.],
124 [0., 1., 0., 0.],
125 [0., 0., 1., 0.],
126 ])),
127 vignette: Some([0.; 4]),
128 lut: Some(ColourLut {
129 side: 2,
130 values: vec![[0.; 3]; 8],
131 input_power: 1. / 2.2,
132 }),
133 };
134 let mut world = World::new();
135 world.insert_resource(Atmosphere {
136 tone_map: Some(tone.clone()),
137 ..Default::default()
138 });
139 assert!(world.verify_resources().is_empty());
140 use crate::Resource;
141 let json = world
142 .resource::<Atmosphere>()
143 .unwrap()
144 .to_json()
145 .unwrap()
146 .unwrap();
147 assert_eq!(json["tone_map"]["lut"]["side"], 2);
148 assert!(json["tone_map"]["colour_matrix"]["AfterCurve"].is_array());
149 let mut before = tone.clone();
150 before.colour_matrix = Some(ColourMatrix::BeforeCurve(
151 *tone.colour_matrix.as_ref().unwrap().rows(),
152 ));
153 assert!(before.check().is_ok());
154 assert!(serde_json::to_value(&before).unwrap()["colour_matrix"]["BeforeCurve"].is_array());
155 if let Some(ColourMatrix::BeforeCurve(rows)) = &mut before.colour_matrix {
156 rows[0][0] = f32::NAN;
157 }
158 assert!(before.check().unwrap_err().contains("matrix"));
159 let mut invalid = tone.clone();
160 invalid.lut.as_mut().unwrap().values.pop();
161 assert_eq!(
162 invalid.check().unwrap_err(),
163 "tone map LUT requires side >= 2 and side cubed RGB values"
164 );
165 let mut invalid = tone;
166 invalid.curve.gamma = 0.;
167 assert!(invalid.check().unwrap_err().contains("gamma"));
168 }
169
170 #[test]
171 fn independent_absent_stages_are_preserved_without_identity_data() {
172 let tone = ToneMap {
173 curve: ReinhardCurve {
174 contrast: 1.45,
175 white_balance: 1.7,
176 gamma: 1.,
177 },
178 colour_matrix: None,
179 vignette: None,
180 lut: None,
181 };
182 assert!(tone.check().is_ok());
183 let json = serde_json::to_value(&tone).unwrap();
184 for stage in ["colour_matrix", "vignette", "lut"] {
185 assert!(json[stage].is_null(), "{stage}");
186 }
187 let mut invalid = tone;
188 invalid.vignette = Some([f32::NAN; 4]);
189 assert!(invalid.check().is_err());
190 }
191}