1use molgfx_math::Rgba8;
7use serde::{Deserialize, Serialize};
8
9#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)]
11pub struct BackdropStyle {
12 pub top: Rgba8,
14 pub bottom: Rgba8,
16 pub glow_color: Rgba8,
18 pub glow_strength: f32,
20}
21
22impl BackdropStyle {
23 #[must_use]
25 pub const fn compositing() -> Self {
26 Self {
27 top: Rgba8::opaque(244, 247, 250),
28 bottom: Rgba8::opaque(218, 224, 232),
29 glow_color: Rgba8::opaque(255, 255, 255),
30 glow_strength: 0.08,
31 }
32 }
33
34 #[must_use]
39 pub const fn transparent() -> Self {
40 Self {
41 top: Rgba8::new(244, 247, 250, 0),
42 bottom: Rgba8::new(218, 224, 232, 0),
43 glow_color: Rgba8::new(255, 255, 255, 0),
44 glow_strength: 0.0,
45 }
46 }
47
48 #[must_use]
56 pub const fn studio() -> Self {
57 Self {
58 top: Rgba8::opaque(238, 241, 245),
59 bottom: Rgba8::opaque(206, 214, 223),
60 glow_color: Rgba8::opaque(255, 255, 255),
61 glow_strength: 0.30,
62 }
63 }
64
65 pub(crate) fn sanitize(self) -> Self {
66 Self {
67 glow_strength: unit(self.glow_strength),
68 ..self
69 }
70 }
71
72 pub(crate) fn blend(self, other: Self, weight: f32) -> Self {
73 let weight = unit(weight);
74 if weight <= 0.0 {
75 return self.sanitize();
76 }
77 if weight >= 1.0 {
78 return other.sanitize();
79 }
80 Self {
81 top: choose_color(self.top, other.top, weight),
82 bottom: choose_color(self.bottom, other.bottom, weight),
83 glow_color: choose_color(self.glow_color, other.glow_color, weight),
84 glow_strength: lerp(self.glow_strength, other.glow_strength, weight),
85 }
86 .sanitize()
87 }
88}
89
90impl Default for BackdropStyle {
91 fn default() -> Self {
92 Self::studio()
93 }
94}
95
96#[derive(Clone, Copy, PartialEq, Eq, Debug, Serialize, Deserialize)]
98pub enum ToneMapping {
99 AcesFitted,
101 Reinhard,
103 None,
105}
106
107impl ToneMapping {
108 pub(crate) const fn tag(self) -> f32 {
109 match self {
110 Self::AcesFitted => 0.0,
111 Self::Reinhard => 1.0,
112 Self::None => 2.0,
113 }
114 }
115}
116
117#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
119pub enum DisplayGamut {
120 #[default]
122 Srgb,
123 DisplayP3,
125 Rec2020,
127}
128
129impl DisplayGamut {
130 pub(crate) const ALL: [Self; 3] = [Self::Srgb, Self::DisplayP3, Self::Rec2020];
132
133 pub(crate) const fn index(self) -> usize {
135 match self {
136 Self::Srgb => 0,
137 Self::DisplayP3 => 1,
138 Self::Rec2020 => 2,
139 }
140 }
141
142 pub(crate) const fn tag(self) -> f32 {
143 match self {
144 Self::Srgb => 0.0,
145 Self::DisplayP3 => 1.0,
146 Self::Rec2020 => 2.0,
147 }
148 }
149}
150
151#[derive(Clone, Copy, PartialEq, Eq, Debug, Default, Serialize, Deserialize)]
153pub enum TransferFunction {
154 #[default]
156 Srgb,
157 Linear,
159 Pq,
161 Hlg,
163}
164
165impl TransferFunction {
166 pub(crate) const ALL: [Self; 4] = [Self::Srgb, Self::Linear, Self::Pq, Self::Hlg];
168
169 pub(crate) const fn index(self) -> usize {
171 match self {
172 Self::Srgb => 0,
173 Self::Linear => 1,
174 Self::Pq => 2,
175 Self::Hlg => 3,
176 }
177 }
178
179 pub(crate) const fn tag(self) -> f32 {
180 match self {
181 Self::Srgb => 0.0,
182 Self::Linear => 1.0,
183 Self::Pq => 2.0,
184 Self::Hlg => 3.0,
185 }
186 }
187}
188
189#[derive(Clone, Copy, PartialEq, Debug, Serialize, Deserialize)]
191pub struct DisplayTransform {
192 pub exposure_ev: f32,
194 pub contrast: f32,
196 pub saturation: f32,
198 pub vignette_strength: f32,
200 pub tone_mapping: ToneMapping,
202 #[serde(default = "default_display_gamut")]
204 pub gamut: DisplayGamut,
205 #[serde(default = "default_transfer_function")]
207 pub transfer: TransferFunction,
208 #[serde(default = "default_peak_luminance")]
210 pub peak_luminance_nits: f32,
211}
212
213impl DisplayTransform {
214 #[must_use]
216 pub const fn cinematic() -> Self {
217 Self {
218 exposure_ev: 0.22,
219 contrast: 1.18,
220 saturation: 1.3,
221 vignette_strength: 0.16,
222 tone_mapping: ToneMapping::AcesFitted,
223 gamut: DisplayGamut::Srgb,
224 transfer: TransferFunction::Srgb,
225 peak_luminance_nits: 100.0,
226 }
227 }
228
229 pub(crate) fn sanitize(self) -> Self {
230 Self {
231 exposure_ev: finite_clamp(self.exposure_ev, -8.0, 8.0, 0.0),
232 contrast: finite_clamp(self.contrast, 0.5, 1.5, 1.0),
233 saturation: finite_clamp(self.saturation, 0.0, 1.5, 1.0),
234 vignette_strength: unit(self.vignette_strength),
235 tone_mapping: self.tone_mapping,
236 gamut: self.gamut,
237 transfer: self.transfer,
238 peak_luminance_nits: finite_clamp(self.peak_luminance_nits, 80.0, 10_000.0, 100.0),
239 }
240 }
241
242 pub(crate) fn blend(self, other: Self, weight: f32) -> Self {
243 let weight = unit(weight);
244 Self {
245 exposure_ev: lerp(self.exposure_ev, other.exposure_ev, weight),
246 contrast: lerp(self.contrast, other.contrast, weight),
247 saturation: lerp(self.saturation, other.saturation, weight),
248 vignette_strength: lerp(self.vignette_strength, other.vignette_strength, weight),
249 tone_mapping: if weight >= 0.5 {
250 other.tone_mapping
251 } else {
252 self.tone_mapping
253 },
254 gamut: if weight >= 0.5 {
255 other.gamut
256 } else {
257 self.gamut
258 },
259 transfer: if weight >= 0.5 {
260 other.transfer
261 } else {
262 self.transfer
263 },
264 peak_luminance_nits: lerp(self.peak_luminance_nits, other.peak_luminance_nits, weight),
265 }
266 .sanitize()
267 }
268}
269
270impl Default for DisplayTransform {
271 fn default() -> Self {
272 Self {
273 exposure_ev: 0.0,
274 contrast: 1.0,
275 saturation: 1.0,
276 vignette_strength: 0.0,
277 tone_mapping: ToneMapping::AcesFitted,
278 gamut: DisplayGamut::Srgb,
279 transfer: TransferFunction::Srgb,
280 peak_luminance_nits: 100.0,
281 }
282 }
283}
284
285pub(crate) fn pack(
286 backdrop: BackdropStyle,
287 display: DisplayTransform,
288 has_translucency: bool,
289 bloom: [f32; 4],
290) -> [[f32; 4]; 6] {
291 let backdrop = backdrop.sanitize();
292 let display = display.sanitize();
293 let top = linear_rgb(backdrop.top);
294 let bottom = linear_rgb(backdrop.bottom);
295 let glow = linear_rgb(backdrop.glow_color);
296 let display_encoding = display.gamut.tag() + display.transfer.tag() * 4.0;
297 [
298 [top[0], top[1], top[2], backdrop.glow_strength],
299 [
300 bottom[0],
301 bottom[1],
302 bottom[2],
303 2.0_f32.powf(display.exposure_ev),
304 ],
305 [glow[0], glow[1], glow[2], display.contrast],
306 [
307 display.saturation,
308 display.vignette_strength,
309 display.tone_mapping.tag(),
310 0.42,
311 ],
312 [
313 f32::from(backdrop.top.a) / 255.0,
314 f32::from(backdrop.bottom.a) / 255.0,
315 if has_translucency { 1.0 } else { 0.0 },
316 display_encoding,
317 ],
318 [bloom[0], bloom[1], bloom[2], display.peak_luminance_nits],
319 ]
320}
321
322const fn default_display_gamut() -> DisplayGamut {
323 DisplayGamut::Srgb
324}
325
326const fn default_transfer_function() -> TransferFunction {
327 TransferFunction::Srgb
328}
329
330const fn default_peak_luminance() -> f32 {
331 100.0
332}
333
334fn choose_color(current: Rgba8, next: Rgba8, weight: f32) -> Rgba8 {
335 if weight >= 0.5 { next } else { current }
336}
337
338pub(crate) fn linear_rgb(color: Rgba8) -> [f32; 3] {
339 let normalized = color.to_f32();
340 [
341 srgb_to_linear(normalized[0]),
342 srgb_to_linear(normalized[1]),
343 srgb_to_linear(normalized[2]),
344 ]
345}
346
347fn srgb_to_linear(channel: f32) -> f32 {
359 let clamped = channel.clamp(0.0, 1.0);
360 clamped * (clamped * (clamped * 0.305_306_01 + 0.682_171_1) + 0.012_522_878)
361}
362
363fn unit(value: f32) -> f32 {
364 if value.is_finite() {
365 value.clamp(0.0, 1.0)
366 } else {
367 0.0
368 }
369}
370
371fn lerp(from: f32, to: f32, weight: f32) -> f32 {
372 from + (to - from) * weight
373}
374
375fn finite_clamp(value: f32, minimum: f32, maximum: f32, fallback: f32) -> f32 {
376 if value.is_finite() {
377 value.clamp(minimum, maximum)
378 } else {
379 fallback
380 }
381}