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}