Skip to main content

mirage_engine/
color.rs

1use bytemuck::{Pod, Zeroable};
2
3/// A red, green, blue, and opacity color, each a linear value the engine
4/// encodes to sRGB on its way to the window.
5///
6/// Nothing clamps a channel: a value past `1.0` is light the frame holds
7/// and the tone map brings down, which is what an emissive material or a
8/// bright light is written with. `0.0..=1.0` is what the window can show.
9///
10/// Values are linear; the engine writes sRGB-encoded values to the screen.
11/// A color channel past `1.0` holds light no screen draws, which
12/// [`FrameContext::set_bloom`](crate::FrameContext::set_bloom) spreads.
13#[repr(C)]
14#[derive(Clone, Copy, Debug, Default, PartialEq, Pod, Zeroable)]
15pub struct Color {
16    /// Red.
17    pub red: f32,
18    /// Green.
19    pub green: f32,
20    /// Blue.
21    pub blue: f32,
22    /// Opacity: `1.0` is fully opaque, `0.0` is empty.
23    pub alpha: f32,
24}
25
26impl Color {
27    /// Opaque black.
28    pub const BLACK: Self = Self::rgb(0.0, 0.0, 0.0);
29
30    /// Opaque white.
31    pub const WHITE: Self = Self::rgb(1.0, 1.0, 1.0);
32
33    /// An opaque color, each channel a fraction of `1.0`.
34    pub const fn rgb(red: f32, green: f32, blue: f32) -> Self {
35        Self::rgba(red, green, blue, 1.0)
36    }
37
38    /// A color with opacity `alpha`, each channel and `alpha` a fraction of
39    /// `1.0`.
40    pub const fn rgba(red: f32, green: f32, blue: f32, alpha: f32) -> Self {
41        Self {
42            red,
43            green,
44            blue,
45            alpha,
46        }
47    }
48
49    /// The same channels at opacity `alpha`, a fraction of `1.0`.
50    pub const fn with_alpha(self, alpha: f32) -> Self {
51        Self { alpha, ..self }
52    }
53
54    /// The color three sRGB-encoded bytes hold, red, green and blue, fully
55    /// opaque: what a texture's own texels are read back as.
56    pub(crate) fn of_srgb(texel: [u8; 3]) -> Self {
57        let linear = |byte: u8| {
58            let encoded = f32::from(byte) / 255.0;
59            match encoded <= 0.040_45 {
60                true => encoded / 12.92,
61                false => ((encoded + 0.055) / 1.055).powf(2.4),
62            }
63        };
64
65        Self::rgb(linear(texel[0]), linear(texel[1]), linear(texel[2]))
66    }
67
68    /// Its red, green, and blue scaled by `factor`, a fraction of each
69    /// channel's own value, keeping its opacity.
70    pub const fn dimmed(self, factor: f32) -> Self {
71        Self {
72            red: self.red * factor,
73            green: self.green * factor,
74            blue: self.blue * factor,
75            alpha: self.alpha,
76        }
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83
84    #[test]
85    fn a_color_keeps_its_channels_through_an_opacity_change_and_its_opacity_through_a_dim() {
86        let color = Color::rgba(0.4, 0.6, 0.8, 0.5);
87
88        assert_eq!(color.with_alpha(0.25), Color::rgba(0.4, 0.6, 0.8, 0.25));
89        assert_eq!(color.dimmed(0.5), Color::rgba(0.2, 0.3, 0.4, 0.5));
90    }
91}