Add LCH(ab) color space to bevy_render::color::Color (#7483)

# Objective

- Fixes #766

## Solution

- Add a new `Lcha` member to `bevy_render::color::Color` enum

---

## Changelog

- Add a new `Lcha` member to `bevy_render::color::Color` enum
- Add `bevy_render::color::LchRepresentation` struct
This commit is contained in:
Ludwig DUBOS 2023-02-06 17:51:40 +00:00
parent 5b930c8486
commit 7b7b34f635
2 changed files with 530 additions and 2 deletions

View File

@ -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);
}
}

View File

@ -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<Color> 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<Color> 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<f32> 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<f32> for Color {
*saturation *= rhs;
*lightness *= rhs;
}
Color::Lcha {
lightness,
chroma,
hue,
..
} => {
*lightness *= rhs;
*chroma *= rhs;
*hue *= rhs;
}
}
}
}
@ -1005,6 +1230,17 @@ impl Mul<Vec4> 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<Vec4> 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<Vec3> 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<Vec3> 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];
}
}
}
}