1use crate::Xyza;
8use bevy_math::{DMat3, DVec3, Mat3};
9#[cfg(feature = "bevy_reflect")]
10use bevy_reflect::prelude::*;
11
12#[derive(Debug, Clone, Copy, PartialEq)]
15#[cfg_attr(
16 feature = "bevy_reflect",
17 derive(Reflect),
18 reflect(Clone, PartialEq, Default)
19)]
20#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
21#[cfg_attr(
22 all(feature = "serialize", feature = "bevy_reflect"),
23 reflect(Serialize, Deserialize)
24)]
25pub struct Chromaticity {
26 pub x: f32,
28 pub y: f32,
30}
31
32impl Chromaticity {
33 pub const fn new(x: f32, y: f32) -> Self {
35 Self { x, y }
36 }
37
38 pub const D65: Self = Self::new(0.3127, 0.3290);
41
42 pub const D60: Self = Self::new(0.32168, 0.33767);
45
46 pub const fn to_xyza(self, luminance: f32) -> Xyza {
50 let xyz = self.to_dxyz();
51 Xyza::new(
52 (xyz.x * luminance as f64) as f32,
53 luminance,
54 (xyz.z * luminance as f64) as f32,
55 1.0,
56 )
57 }
58
59 const fn to_dxyz(self) -> DVec3 {
62 let (x, y) = (self.x as f64, self.y as f64);
63 DVec3::new(x / y, 1.0, (1.0 - x - y) / y)
64 }
65}
66
67impl Default for Chromaticity {
68 fn default() -> Self {
69 Self::D65
70 }
71}
72
73#[derive(Debug, Clone, Copy, PartialEq)]
77#[cfg_attr(
78 feature = "bevy_reflect",
79 derive(Reflect),
80 reflect(Clone, PartialEq, Default)
81)]
82#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))]
83#[cfg_attr(
84 all(feature = "serialize", feature = "bevy_reflect"),
85 reflect(Serialize, Deserialize)
86)]
87pub struct RgbPrimaries {
88 pub red: Chromaticity,
90 pub green: Chromaticity,
92 pub blue: Chromaticity,
94 pub white: Chromaticity,
96}
97
98impl RgbPrimaries {
99 pub const BT709: Self = Self {
104 red: Chromaticity::new(0.640, 0.330),
105 green: Chromaticity::new(0.300, 0.600),
106 blue: Chromaticity::new(0.150, 0.060),
107 white: Chromaticity::D65,
108 };
109
110 pub const BT2020: Self = Self {
113 red: Chromaticity::new(0.708, 0.292),
114 green: Chromaticity::new(0.170, 0.797),
115 blue: Chromaticity::new(0.131, 0.046),
116 white: Chromaticity::D65,
117 };
118
119 pub const DISPLAY_P3: Self = Self {
122 red: Chromaticity::new(0.680, 0.320),
123 green: Chromaticity::new(0.265, 0.690),
124 blue: Chromaticity::new(0.150, 0.060),
125 white: Chromaticity::D65,
126 };
127
128 pub const ACES_CG: Self = Self {
131 red: Chromaticity::new(0.713, 0.293),
132 green: Chromaticity::new(0.165, 0.830),
133 blue: Chromaticity::new(0.128, 0.044),
134 white: Chromaticity::D60,
135 };
136
137 pub fn matrix_to(self, dst: Self) -> Mat3 {
144 (dst.rgb_to_xyz_dmat3().inverse() * self.rgb_to_xyz_dmat3()).as_mat3()
145 }
146
147 fn rgb_to_xyz_dmat3(&self) -> DMat3 {
150 let primaries = DMat3::from_cols(
151 self.red.to_dxyz(),
152 self.green.to_dxyz(),
153 self.blue.to_dxyz(),
154 );
155 let scale = primaries.inverse() * self.white.to_dxyz();
157 primaries * DMat3::from_diagonal(scale)
158 }
159}
160
161impl Default for RgbPrimaries {
162 fn default() -> Self {
163 Self::BT709
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170 use crate::testing::{assert_approx_eq, assert_mat3_approx_eq};
171 use bevy_math::Vec3;
172
173 #[test]
174 fn bt709_to_bt2020_matches_bt2087() {
175 let expected = Mat3::from_cols_array_2d(&[
177 [0.6274, 0.0691, 0.0164],
178 [0.3293, 0.9195, 0.0880],
179 [0.0433, 0.0114, 0.8956],
180 ]);
181 let m = RgbPrimaries::BT709.matrix_to(RgbPrimaries::BT2020);
182 assert_mat3_approx_eq(m, expected, 1e-4);
183 }
184
185 #[test]
186 fn rgb_to_rgb_round_trip_is_identity() {
187 let sets = [
188 RgbPrimaries::BT709,
189 RgbPrimaries::BT2020,
190 RgbPrimaries::DISPLAY_P3,
191 RgbPrimaries::ACES_CG,
192 ];
193 for src in sets {
194 for dst in sets {
195 let round_trip = dst.matrix_to(src) * src.matrix_to(dst);
196 assert_mat3_approx_eq(round_trip, Mat3::IDENTITY, 1e-6);
197 }
198 }
199 }
200
201 #[test]
202 fn white_maps_to_white_point() {
203 for primaries in [
204 RgbPrimaries::BT709,
205 RgbPrimaries::BT2020,
206 RgbPrimaries::DISPLAY_P3,
207 RgbPrimaries::ACES_CG,
208 ] {
209 let white_xyz = primaries.rgb_to_xyz_dmat3().as_mat3() * Vec3::ONE;
210 let expected = primaries.white.to_xyza(1.0);
211 assert_approx_eq!(white_xyz.x, expected.x, 1e-6);
212 assert_approx_eq!(white_xyz.y, expected.y, 1e-6);
213 assert_approx_eq!(white_xyz.z, expected.z, 1e-6);
214 }
215 }
216
217 #[test]
218 fn bt709_matrix_matches_crate_srgb_matrix() {
219 let expected = Mat3::from_cols_array_2d(&[
223 [0.4124564, 0.2126729, 0.0193339],
224 [0.3575761, 0.7151522, 0.119192],
225 [0.1804375, 0.072175, 0.9503041],
226 ]);
227 let m = RgbPrimaries::BT709.rgb_to_xyz_dmat3().as_mat3();
228 assert_mat3_approx_eq(m, expected, 5e-4);
229 }
230
231 #[test]
232 fn chromaticity_to_xyza() {
233 let xyz = Chromaticity::D65.to_xyza(1.0);
234 assert_approx_eq!(xyz.x, 0.9504559, 1e-6);
235 assert_approx_eq!(xyz.y, 1.0, 1e-6);
236 assert_approx_eq!(xyz.z, 1.0890578, 1e-6);
237 assert_approx_eq!(xyz.alpha, 1.0, 1e-6);
238
239 let xyz = Chromaticity::D65.to_xyza(2.0);
240 assert_approx_eq!(xyz.y, 2.0, 1e-6);
241 assert_approx_eq!(xyz.x, 2.0 * 0.9504559, 1e-5);
242 }
243}