From 6e37930de612639c45795bed93d0f880f2184e68 Mon Sep 17 00:00:00 2001 From: ldubos Date: Fri, 3 Feb 2023 05:20:01 +0100 Subject: [PATCH 1/4] add lch colorspace --- crates/bevy_render/src/color/colorspace.rs | 203 +++++++++++++ crates/bevy_render/src/color/mod.rs | 315 ++++++++++++++++++++- 2 files changed, 516 insertions(+), 2 deletions(-) diff --git a/crates/bevy_render/src/color/colorspace.rs b/crates/bevy_render/src/color/colorspace.rs index 9ad0f63e72d43..b7bbb6066a11b 100644 --- a/crates/bevy_render/src/color/colorspace.rs +++ b/crates/bevy_render/src/color/colorspace.rs @@ -102,6 +102,123 @@ impl HslRepresentation { } } +pub struct LchRepresentation; +impl LchRepresentation { + const CIE_EPSILON: f32 = 216.0 / 24389.0; + const CIE_KAPPA: f32 = 24389.0 / 27.0; + const D65_WHITE_X: f32 = 0.95047; + const D65_WHITE_Y: f32 = 1.0; + const D65_WHITE_Z: f32 = 1.08883; + + /// converts a color in LCH space to sRGB space + #[inline] + pub fn lch_to_nonlinear_srgb(lightness: f32, chroma: f32, hue: f32) -> [f32; 3] { + let lightness = lightness * 100.0; + let chroma = chroma * 100.0; + + // convert LCH to Lab http://www.brucelindbloom.com/index.html?Eqn_LCH_to_Lab.html + let l = lightness; + let a = chroma * hue.to_radians().cos(); + let b = chroma * hue.to_radians().sin(); + + // convert Lab to XYZ http://www.brucelindbloom.com/index.html?Eqn_Lab_to_XYZ.html + let fy = (l + 16.0) / 116.0; + let fx = a / 500.0 + fy; + let fz = fy - b / 200.0; + let xr = { + let fx3 = fx.powf(3.0); + + if fx3 > Self::CIE_EPSILON { + fx3 + } else { + (116.0 * fx - 16.0) / Self::CIE_KAPPA + } + }; + let yr = if l > Self::CIE_EPSILON * Self::CIE_KAPPA { + ((l + 16.0) / 116.0).powf(3.0) + } else { + l / Self::CIE_KAPPA + }; + let zr = { + let fz3 = fz.powf(3.0); + + if fz3 > Self::CIE_EPSILON { + fz3 + } else { + (116.0 * fz - 16.0) / Self::CIE_KAPPA + } + }; + let x = xr * Self::D65_WHITE_X; + let y = yr * Self::D65_WHITE_Y; + let z = zr * Self::D65_WHITE_Z; + + // XYZ to sRGB http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_RGB.html + let red = x * 3.2404541621141045 + y * -1.5371385127977166 + z * -0.498531409556016; + let green = x * -0.9692660305051868 + y * 1.8760108454466942 + z * 0.041556017530349834; + let blue = x * 0.055643430959114726 + y * -0.2040259135167538 + z * 1.0572251882231791; + + [ + red.linear_to_nonlinear_srgb().max(0.0).min(1.0), + green.linear_to_nonlinear_srgb().max(0.0).min(1.0), + blue.linear_to_nonlinear_srgb().max(0.0).min(1.0), + ] + } + + /// converts a color in sRGB space to LCH space + #[inline] + pub fn nonlinear_srgb_to_lch([red, green, blue]: [f32; 3]) -> (f32, f32, f32) { + // RGB to XYZ http://www.brucelindbloom.com/index.html?Eqn_RGB_to_XYZ.html + let red = red.nonlinear_to_linear_srgb(); + let green = green.nonlinear_to_linear_srgb(); + let blue = blue.nonlinear_to_linear_srgb(); + + let x = red * 0.4124564390896922 + green * 0.357576077643909 + blue * 0.18043748326639894; + let y = red * 0.21267285140562253 + green * 0.715152155287818 + blue * 0.07217499330655958; + let z = red * 0.0193338955823293 + green * 0.11919202588130297 + blue * 0.9503040785363679; + + // XYZ to Lab http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_Lab.html + let xr = x / Self::D65_WHITE_X; + let yr = y / Self::D65_WHITE_Y; + let zr = z / Self::D65_WHITE_Z; + let fx = if xr > Self::CIE_EPSILON { + xr.cbrt() + } else { + (Self::CIE_KAPPA * xr + 16.0) / 116.0 + }; + let fy = if yr > Self::CIE_EPSILON { + yr.cbrt() + } else { + (Self::CIE_KAPPA * yr + 16.0) / 116.0 + }; + let fz = if yr > Self::CIE_EPSILON { + zr.cbrt() + } else { + (Self::CIE_KAPPA * zr + 16.0) / 116.0 + }; + let l = 116.0 * fy - 16.0; + let a = 500.0 * (fx - fy); + let b = 200.0 * (fy - fz); + + // Lab to LCH http://www.brucelindbloom.com/index.html?Eqn_Lab_to_LCH.html + let c = (a.powf(2.0) + b.powf(2.0)).sqrt(); + let h = { + let h = b.to_radians().atan2(a.to_radians()).to_degrees(); + + if h < 0.0 { + h + 360.0 + } else { + h + } + }; + + ( + (l / 100.0).max(0.0).min(1.5), + (c / 100.0).max(0.0).min(1.5), + h, + ) + } +} + #[cfg(test)] mod test { use super::*; @@ -214,4 +331,90 @@ mod test { assert_eq!((saturation * 100.0).round() as u32, 83); assert_eq!((lightness * 100.0).round() as u32, 51); } + + #[test] + fn lch_to_srgb() { + // black + let (lightness, chroma, hue) = (0.0, 0.0, 0.0); + let [r, g, b] = LchRepresentation::lch_to_nonlinear_srgb(lightness, chroma, hue); + assert_eq!((r * 100.0).round() as u32, 0); + assert_eq!((g * 100.0).round() as u32, 0); + assert_eq!((b * 100.0).round() as u32, 0); + + // white + let (lightness, chroma, hue) = (1.0, 0.0, 0.0); + let [r, g, b] = LchRepresentation::lch_to_nonlinear_srgb(lightness, chroma, hue); + assert_eq!((r * 100.0).round() as u32, 100); + assert_eq!((g * 100.0).round() as u32, 100); + assert_eq!((b * 100.0).round() as u32, 100); + + let (lightness, chroma, hue) = (0.501236, 0.777514, 327.6608); + let [r, g, b] = LchRepresentation::lch_to_nonlinear_srgb(lightness, chroma, hue); + assert_eq!((r * 100.0).round() as u32, 75); + assert_eq!((g * 100.0).round() as u32, 25); + assert_eq!((b * 100.0).round() as u32, 75); + + // a red + let (lightness, chroma, hue) = (0.487122, 0.999531, 318.7684); + let [r, g, b] = LchRepresentation::lch_to_nonlinear_srgb(lightness, chroma, hue); + assert_eq!((r * 100.0).round() as u32, 70); + assert_eq!((g * 100.0).round() as u32, 19); + assert_eq!((b * 100.0).round() as u32, 90); + + // a green + let (lightness, chroma, hue) = (0.732929, 0.560925, 164.3216); + let [r, g, b] = LchRepresentation::lch_to_nonlinear_srgb(lightness, chroma, hue); + assert_eq!((r * 100.0).round() as u32, 10); + assert_eq!((g * 100.0).round() as u32, 80); + assert_eq!((b * 100.0).round() as u32, 59); + + // a blue + let (lightness, chroma, hue) = (0.335030, 1.176923, 306.7828); + let [r, g, b] = LchRepresentation::lch_to_nonlinear_srgb(lightness, chroma, hue); + assert_eq!((r * 100.0).round() as u32, 25); + assert_eq!((g * 100.0).round() as u32, 10); + assert_eq!((b * 100.0).round() as u32, 92); + } + + #[test] + fn srgb_to_lch() { + // black + let (lightness, chroma, hue) = LchRepresentation::nonlinear_srgb_to_lch([0.0, 0.0, 0.0]); + assert_eq!((lightness * 100.0).round() as u32, 0); + assert_eq!((chroma * 100.0).round() as u32, 0); + assert_eq!(hue.round() as u32, 0); + + // white + let (lightness, chroma, hue) = LchRepresentation::nonlinear_srgb_to_lch([1.0, 1.0, 1.0]); + assert_eq!((lightness * 100.0).round() as u32, 100); + assert_eq!((chroma * 100.0).round() as u32, 0); + assert_eq!(hue.round() as u32, 0); + + let (lightness, chroma, hue) = LchRepresentation::nonlinear_srgb_to_lch([0.75, 0.25, 0.75]); + println!("{} {} {}", lightness, chroma, hue); + assert_eq!((lightness * 100.0).round() as u32, 50); + assert_eq!((chroma * 100.0).round() as u32, 78); + assert_eq!(hue.round() as u32, 328); + + // a red + let (lightness, chroma, hue) = + LchRepresentation::nonlinear_srgb_to_lch([0.70, 0.19, 0.90]); + assert_eq!((lightness * 100.0).round() as u32, 49); + assert_eq!((chroma * 100.0).round() as u32, 100); + assert_eq!(hue.round() as u32, 319); + + // a green + let (lightness, chroma, hue) = + LchRepresentation::nonlinear_srgb_to_lch([0.10, 0.80, 0.59]); + assert_eq!((lightness * 100.0).round() as u32, 73); + assert_eq!((chroma * 100.0).round() as u32, 56); + assert_eq!(hue.round() as u32, 164); + + // a blue + let (lightness, chroma, hue) = + LchRepresentation::nonlinear_srgb_to_lch([0.25, 0.10, 0.92]); + assert_eq!((lightness * 100.0).round() as u32, 34); + assert_eq!((chroma * 100.0).round() as u32, 118); + assert_eq!(hue.round() as u32, 307); + } } diff --git a/crates/bevy_render/src/color/mod.rs b/crates/bevy_render/src/color/mod.rs index 535cbe410125e..43746a242a134 100644 --- a/crates/bevy_render/src/color/mod.rs +++ b/crates/bevy_render/src/color/mod.rs @@ -44,6 +44,17 @@ pub enum Color { /// Alpha channel. [0.0, 1.0] alpha: f32, }, + /// LCH(ab) (lightness, chroma, hue) color with an alpha channel + Lcha { + /// Lightness channel. [0.0, 1.5] + lightness: f32, + /// Chroma channel. [0.0, 1.5] + chroma: f32, + /// Hue channel. [0.0, 360.0] + hue: f32, + /// Alpha channel. [0.0, 1.0] + alpha: f32, + }, } impl Color { @@ -401,7 +412,8 @@ impl Color { match self { Color::Rgba { alpha, .. } | Color::RgbaLinear { alpha, .. } - | Color::Hsla { alpha, .. } => *alpha, + | Color::Hsla { alpha, .. } + | Color::Lcha { alpha, .. } => *alpha, } } @@ -410,7 +422,8 @@ impl Color { match self { Color::Rgba { alpha, .. } | Color::RgbaLinear { alpha, .. } - | Color::Hsla { alpha, .. } => { + | Color::Hsla { alpha, .. } + | Color::Lcha { alpha, .. } => { *alpha = a; } } @@ -454,6 +467,22 @@ impl Color { alpha: *alpha, } } + Color::Lcha { + lightness, + chroma, + hue, + alpha, + } => { + let [red, green, blue] = + LchRepresentation::lch_to_nonlinear_srgb(*lightness, *chroma, *hue); + + Color::Rgba { + red, + green, + blue, + alpha: *alpha, + } + } } } @@ -487,6 +516,22 @@ impl Color { alpha: *alpha, } } + Color::Lcha { + lightness, + chroma, + hue, + alpha, + } => { + let [red, green, blue] = + LchRepresentation::lch_to_nonlinear_srgb(*lightness, *chroma, *hue); + + Color::Rgba { + red: red.nonlinear_to_linear_srgb(), + green: green.nonlinear_to_linear_srgb(), + blue: blue.nonlinear_to_linear_srgb(), + alpha: *alpha, + } + } } } @@ -527,6 +572,22 @@ impl Color { } } Color::Hsla { .. } => *self, + Color::Lcha { + lightness, + chroma, + hue, + alpha, + } => { + let rgb = LchRepresentation::lch_to_nonlinear_srgb(*lightness, *chroma, *hue); + let (hue, saturation, lightness) = HslRepresentation::nonlinear_srgb_to_hsl(rgb); + + Color::Hsla { + hue, + saturation, + lightness, + alpha: *alpha, + } + } } } @@ -560,6 +621,17 @@ impl Color { HslRepresentation::hsl_to_nonlinear_srgb(hue, saturation, lightness); [red, green, blue, alpha] } + Color::Lcha { + lightness, + chroma, + hue, + alpha, + } => { + let [red, green, blue] = + LchRepresentation::lch_to_nonlinear_srgb(lightness, chroma, hue); + + [red, green, blue, alpha] + } } } @@ -599,6 +671,22 @@ impl Color { alpha, ] } + Color::Lcha { + lightness, + chroma, + hue, + alpha, + } => { + let [red, green, blue] = + LchRepresentation::lch_to_nonlinear_srgb(lightness, chroma, hue); + + [ + red.nonlinear_to_linear_srgb(), + green.nonlinear_to_linear_srgb(), + blue.nonlinear_to_linear_srgb(), + alpha, + ] + } } } @@ -634,6 +722,63 @@ impl Color { lightness, alpha, } => [hue, saturation, lightness, alpha], + Color::Lcha { + lightness, + chroma, + hue, + alpha, + } => { + let rgb = LchRepresentation::lch_to_nonlinear_srgb(lightness, chroma, hue); + let (hue, saturation, lightness) = HslRepresentation::nonlinear_srgb_to_hsl(rgb); + + [hue, saturation, lightness, alpha] + } + } + } + + /// Converts a `Color` to a `[f32; 4]` from LCH colorspace + pub fn as_lch_f32(self: Color) -> [f32; 4] { + match self { + Color::Rgba { + red, + green, + blue, + alpha, + } => { + let (lightness, chroma, hue) = + LchRepresentation::nonlinear_srgb_to_lch([red, green, blue]); + [lightness, chroma, hue, alpha] + } + Color::RgbaLinear { + red, + green, + blue, + alpha, + } => { + let (lightness, chroma, hue) = LchRepresentation::nonlinear_srgb_to_lch([ + red.linear_to_nonlinear_srgb(), + green.linear_to_nonlinear_srgb(), + blue.linear_to_nonlinear_srgb(), + ]); + [lightness, chroma, hue, alpha] + } + Color::Hsla { + hue, + saturation, + lightness, + alpha, + } => { + let rgb = HslRepresentation::hsl_to_nonlinear_srgb(hue, saturation, lightness); + let (lightness, chroma, hue) = LchRepresentation::nonlinear_srgb_to_lch(rgb); + + [lightness, chroma, hue, alpha] + } + Color::Lcha { + lightness, + chroma, + hue, + alpha, + } => [lightness, chroma, hue, alpha], } } @@ -680,6 +825,22 @@ impl Color { (alpha * 255.0) as u8, ]) } + Color::Lcha { + lightness, + chroma, + hue, + alpha, + } => { + let [red, green, blue] = + LchRepresentation::lch_to_nonlinear_srgb(lightness, chroma, hue); + + u32::from_le_bytes([ + (red * 255.0) as u8, + (green * 255.0) as u8, + (blue * 255.0) as u8, + (alpha * 255.0) as u8, + ]) + } } } @@ -726,6 +887,22 @@ impl Color { (alpha * 255.0) as u8, ]) } + Color::Lcha { + lightness, + chroma, + hue, + alpha, + } => { + let [red, green, blue] = + LchRepresentation::lch_to_nonlinear_srgb(lightness, chroma, hue); + + u32::from_le_bytes([ + (red.nonlinear_to_linear_srgb() * 255.0) as u8, + (green.nonlinear_to_linear_srgb() * 255.0) as u8, + (blue.nonlinear_to_linear_srgb() * 255.0) as u8, + (alpha * 255.0) as u8, + ]) + } } } } @@ -775,6 +952,18 @@ impl AddAssign for Color { *lightness += rhs[2]; *alpha += rhs[3]; } + Color::Lcha { + lightness, + chroma, + hue, + alpha, + } => { + let rhs = rhs.as_lch_f32(); + *lightness += rhs[0]; + *chroma += rhs[1]; + *hue += rhs[2]; + *alpha += rhs[3]; + } } } } @@ -826,6 +1015,21 @@ impl Add for Color { alpha: alpha + rhs[3], } } + Color::Lcha { + lightness, + chroma, + hue, + alpha, + } => { + let rhs = rhs.as_lch_f32(); + + Color::Lcha { + lightness: lightness + rhs[0], + chroma: chroma + rhs[1], + hue: hue + rhs[2], + alpha: alpha + rhs[3], + } + } } } } @@ -936,6 +1140,17 @@ impl Mul for Color { lightness: lightness * rhs, alpha, }, + Color::Lcha { + lightness, + chroma, + hue, + alpha, + } => Color::Lcha { + lightness: lightness * rhs, + chroma: chroma * rhs, + hue: hue * rhs, + alpha, + }, } } } @@ -963,6 +1178,16 @@ impl MulAssign for Color { *saturation *= rhs; *lightness *= rhs; } + Color::Lcha { + lightness, + chroma, + hue, + .. + } => { + *lightness *= rhs; + *chroma *= rhs; + *hue *= rhs; + } } } } @@ -1005,6 +1230,17 @@ impl Mul for Color { lightness: lightness * rhs.z, alpha: alpha * rhs.w, }, + Color::Lcha { + lightness, + chroma, + hue, + alpha, + } => Color::Lcha { + lightness: lightness * rhs.x, + chroma: chroma * rhs.y, + hue: hue * rhs.z, + alpha: alpha * rhs.w, + }, } } } @@ -1040,6 +1276,17 @@ impl MulAssign for Color { *lightness *= rhs.z; *alpha *= rhs.w; } + Color::Lcha { + lightness, + chroma, + hue, + alpha, + } => { + *lightness *= rhs.x; + *chroma *= rhs.y; + *hue *= rhs.z; + *alpha *= rhs.w; + } } } } @@ -1082,6 +1329,17 @@ impl Mul for Color { lightness: lightness * rhs.z, alpha, }, + Color::Lcha { + lightness, + chroma, + hue, + alpha, + } => Color::Lcha { + lightness: lightness * rhs.x, + chroma: chroma * rhs.y, + hue: hue * rhs.z, + alpha, + }, } } } @@ -1109,6 +1367,16 @@ impl MulAssign for Color { *saturation *= rhs.y; *lightness *= rhs.z; } + Color::Lcha { + lightness, + chroma, + hue, + .. + } => { + *lightness *= rhs.x; + *chroma *= rhs.y; + *hue *= rhs.z; + } } } } @@ -1151,6 +1419,17 @@ impl Mul<[f32; 4]> for Color { lightness: lightness * rhs[2], alpha: alpha * rhs[3], }, + Color::Lcha { + lightness, + chroma, + hue, + alpha, + } => Color::Lcha { + lightness: lightness * rhs[0], + chroma: chroma * rhs[1], + hue: hue * rhs[2], + alpha: alpha * rhs[3], + }, } } } @@ -1186,6 +1465,17 @@ impl MulAssign<[f32; 4]> for Color { *lightness *= rhs[2]; *alpha *= rhs[3]; } + Color::Lcha { + lightness, + chroma, + hue, + alpha, + } => { + *lightness *= rhs[0]; + *chroma *= rhs[1]; + *hue *= rhs[2]; + *alpha *= rhs[3]; + } } } } @@ -1228,6 +1518,17 @@ impl Mul<[f32; 3]> for Color { lightness: lightness * rhs[2], alpha, }, + Color::Lcha { + lightness, + chroma, + hue, + alpha, + } => Color::Lcha { + lightness: lightness * rhs[0], + chroma: chroma * rhs[1], + hue: hue * rhs[2], + alpha, + }, } } } @@ -1255,6 +1556,16 @@ impl MulAssign<[f32; 3]> for Color { *saturation *= rhs[1]; *lightness *= rhs[2]; } + Color::Lcha { + lightness, + chroma, + hue, + .. + } => { + *lightness *= rhs[0]; + *chroma *= rhs[1]; + *hue *= rhs[2]; + } } } } From 3f47c7b4be13077161b639b4b7832af46c8c355a Mon Sep 17 00:00:00 2001 From: ldubos Date: Fri, 3 Feb 2023 09:29:09 +0100 Subject: [PATCH 2/4] fix fmt --- crates/bevy_render/src/color/colorspace.rs | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/crates/bevy_render/src/color/colorspace.rs b/crates/bevy_render/src/color/colorspace.rs index b7bbb6066a11b..8917879fa8be1 100644 --- a/crates/bevy_render/src/color/colorspace.rs +++ b/crates/bevy_render/src/color/colorspace.rs @@ -391,28 +391,24 @@ mod test { assert_eq!(hue.round() as u32, 0); let (lightness, chroma, hue) = LchRepresentation::nonlinear_srgb_to_lch([0.75, 0.25, 0.75]); - println!("{} {} {}", lightness, chroma, hue); assert_eq!((lightness * 100.0).round() as u32, 50); assert_eq!((chroma * 100.0).round() as u32, 78); assert_eq!(hue.round() as u32, 328); // a red - let (lightness, chroma, hue) = - LchRepresentation::nonlinear_srgb_to_lch([0.70, 0.19, 0.90]); + let (lightness, chroma, hue) = LchRepresentation::nonlinear_srgb_to_lch([0.70, 0.19, 0.90]); assert_eq!((lightness * 100.0).round() as u32, 49); assert_eq!((chroma * 100.0).round() as u32, 100); assert_eq!(hue.round() as u32, 319); // a green - let (lightness, chroma, hue) = - LchRepresentation::nonlinear_srgb_to_lch([0.10, 0.80, 0.59]); + let (lightness, chroma, hue) = LchRepresentation::nonlinear_srgb_to_lch([0.10, 0.80, 0.59]); assert_eq!((lightness * 100.0).round() as u32, 73); assert_eq!((chroma * 100.0).round() as u32, 56); assert_eq!(hue.round() as u32, 164); // a blue - let (lightness, chroma, hue) = - LchRepresentation::nonlinear_srgb_to_lch([0.25, 0.10, 0.92]); + let (lightness, chroma, hue) = LchRepresentation::nonlinear_srgb_to_lch([0.25, 0.10, 0.92]); assert_eq!((lightness * 100.0).round() as u32, 34); assert_eq!((chroma * 100.0).round() as u32, 118); assert_eq!(hue.round() as u32, 307); From 6741801be2c74e625ad8ff020321bfd94c15cebd Mon Sep 17 00:00:00 2001 From: ldubos Date: Fri, 3 Feb 2023 09:38:32 +0100 Subject: [PATCH 3/4] fix excessive precision --- crates/bevy_render/src/color/colorspace.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/bevy_render/src/color/colorspace.rs b/crates/bevy_render/src/color/colorspace.rs index 8917879fa8be1..6b7782fb9db27 100644 --- a/crates/bevy_render/src/color/colorspace.rs +++ b/crates/bevy_render/src/color/colorspace.rs @@ -153,9 +153,9 @@ impl LchRepresentation { let z = zr * Self::D65_WHITE_Z; // XYZ to sRGB http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_RGB.html - let red = x * 3.2404541621141045 + y * -1.5371385127977166 + z * -0.498531409556016; - let green = x * -0.9692660305051868 + y * 1.8760108454466942 + z * 0.041556017530349834; - let blue = x * 0.055643430959114726 + y * -0.2040259135167538 + z * 1.0572251882231791; + let red = x * 3.2404542 + y * -1.5371385 + z * -0.4985314; + let green = x * -0.969266 + y * 1.8760108 + z * 0.041556; + let blue = x * 0.0556434 + y * -0.2040259 + z * 1.0572252; [ red.linear_to_nonlinear_srgb().max(0.0).min(1.0), @@ -172,9 +172,9 @@ impl LchRepresentation { let green = green.nonlinear_to_linear_srgb(); let blue = blue.nonlinear_to_linear_srgb(); - let x = red * 0.4124564390896922 + green * 0.357576077643909 + blue * 0.18043748326639894; - let y = red * 0.21267285140562253 + green * 0.715152155287818 + blue * 0.07217499330655958; - let z = red * 0.0193338955823293 + green * 0.11919202588130297 + blue * 0.9503040785363679; + let x = red * 0.4124564 + green * 0.3575761 + blue * 0.1804375; + let y = red * 0.2126729 + green * 0.7151522 + blue * 0.072175; + let z = red * 0.0193339 + green * 0.119192 + blue * 0.9503041; // XYZ to Lab http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_Lab.html let xr = x / Self::D65_WHITE_X; From 333fe9a06e76b6553ee896fc19167826a8f6e8c6 Mon Sep 17 00:00:00 2001 From: ldubos Date: Fri, 3 Feb 2023 18:31:11 +0100 Subject: [PATCH 4/4] add docs --- crates/bevy_render/src/color/colorspace.rs | 30 +++++++++++++++++----- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/crates/bevy_render/src/color/colorspace.rs b/crates/bevy_render/src/color/colorspace.rs index 6b7782fb9db27..8f06134dc1b2d 100644 --- a/crates/bevy_render/src/color/colorspace.rs +++ b/crates/bevy_render/src/color/colorspace.rs @@ -104,8 +104,14 @@ impl HslRepresentation { pub struct LchRepresentation; impl LchRepresentation { + // References available at http://brucelindbloom.com/ in the "Math" section + + // CIE Constants + // http://brucelindbloom.com/index.html?LContinuity.html (16) (17) const CIE_EPSILON: f32 = 216.0 / 24389.0; const CIE_KAPPA: f32 = 24389.0 / 27.0; + // D65 White Reference: + // https://en.wikipedia.org/wiki/Illuminant_D65#Definition const D65_WHITE_X: f32 = 0.95047; const D65_WHITE_Y: f32 = 1.0; const D65_WHITE_Z: f32 = 1.08883; @@ -116,12 +122,14 @@ impl LchRepresentation { let lightness = lightness * 100.0; let chroma = chroma * 100.0; - // convert LCH to Lab http://www.brucelindbloom.com/index.html?Eqn_LCH_to_Lab.html + // convert LCH to Lab + // http://www.brucelindbloom.com/index.html?Eqn_LCH_to_Lab.html let l = lightness; let a = chroma * hue.to_radians().cos(); let b = chroma * hue.to_radians().sin(); - // convert Lab to XYZ http://www.brucelindbloom.com/index.html?Eqn_Lab_to_XYZ.html + // convert Lab to XYZ + // http://www.brucelindbloom.com/index.html?Eqn_Lab_to_XYZ.html let fy = (l + 16.0) / 116.0; let fx = a / 500.0 + fy; let fz = fy - b / 200.0; @@ -152,7 +160,9 @@ impl LchRepresentation { let y = yr * Self::D65_WHITE_Y; let z = zr * Self::D65_WHITE_Z; - // XYZ to sRGB http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_RGB.html + // XYZ to sRGB + // http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_RGB.html + // http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html (sRGB, XYZ to RGB [M]-1) let red = x * 3.2404542 + y * -1.5371385 + z * -0.4985314; let green = x * -0.969266 + y * 1.8760108 + z * 0.041556; let blue = x * 0.0556434 + y * -0.2040259 + z * 1.0572252; @@ -167,16 +177,19 @@ impl LchRepresentation { /// converts a color in sRGB space to LCH space #[inline] pub fn nonlinear_srgb_to_lch([red, green, blue]: [f32; 3]) -> (f32, f32, f32) { - // RGB to XYZ http://www.brucelindbloom.com/index.html?Eqn_RGB_to_XYZ.html + // RGB to XYZ + // http://www.brucelindbloom.com/index.html?Eqn_RGB_to_XYZ.html let red = red.nonlinear_to_linear_srgb(); let green = green.nonlinear_to_linear_srgb(); let blue = blue.nonlinear_to_linear_srgb(); + // http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html (sRGB, RGB to XYZ [M]) let x = red * 0.4124564 + green * 0.3575761 + blue * 0.1804375; let y = red * 0.2126729 + green * 0.7151522 + blue * 0.072175; let z = red * 0.0193339 + green * 0.119192 + blue * 0.9503041; - // XYZ to Lab http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_Lab.html + // XYZ to Lab + // http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_Lab.html let xr = x / Self::D65_WHITE_X; let yr = y / Self::D65_WHITE_Y; let zr = z / Self::D65_WHITE_Z; @@ -199,7 +212,8 @@ impl LchRepresentation { let a = 500.0 * (fx - fy); let b = 200.0 * (fy - fz); - // Lab to LCH http://www.brucelindbloom.com/index.html?Eqn_Lab_to_LCH.html + // Lab to LCH + // http://www.brucelindbloom.com/index.html?Eqn_Lab_to_LCH.html let c = (a.powf(2.0) + b.powf(2.0)).sqrt(); let h = { let h = b.to_radians().atan2(a.to_radians()).to_degrees(); @@ -334,6 +348,8 @@ mod test { #[test] fn lch_to_srgb() { + // "truth" from http://www.brucelindbloom.com/ColorCalculator.html + // black let (lightness, chroma, hue) = (0.0, 0.0, 0.0); let [r, g, b] = LchRepresentation::lch_to_nonlinear_srgb(lightness, chroma, hue); @@ -378,6 +394,8 @@ mod test { #[test] fn srgb_to_lch() { + // "truth" from http://www.brucelindbloom.com/ColorCalculator.html + // black let (lightness, chroma, hue) = LchRepresentation::nonlinear_srgb_to_lch([0.0, 0.0, 0.0]); assert_eq!((lightness * 100.0).round() as u32, 0);