diff --git a/crates/bevy_render/src/color/colorspace.rs b/crates/bevy_render/src/color/colorspace.rs index 9ad0f63e72d43..8f06134dc1b2d 100644 --- a/crates/bevy_render/src/color/colorspace.rs +++ b/crates/bevy_render/src/color/colorspace.rs @@ -102,6 +102,137 @@ 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; + + /// 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 + // 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; + + [ + 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(); + + // 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 + 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 +345,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() { + // "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); + 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() { + // "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); + 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]); + 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]; + } } } }