Skip to content

Commit 7b7b34f

Browse files
author
Ludwig DUBOS
committed
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
1 parent 5b930c8 commit 7b7b34f

File tree

2 files changed

+530
-2
lines changed

2 files changed

+530
-2
lines changed

crates/bevy_render/src/color/colorspace.rs

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,137 @@ impl HslRepresentation {
102102
}
103103
}
104104

105+
pub struct LchRepresentation;
106+
impl LchRepresentation {
107+
// References available at http://brucelindbloom.com/ in the "Math" section
108+
109+
// CIE Constants
110+
// http://brucelindbloom.com/index.html?LContinuity.html (16) (17)
111+
const CIE_EPSILON: f32 = 216.0 / 24389.0;
112+
const CIE_KAPPA: f32 = 24389.0 / 27.0;
113+
// D65 White Reference:
114+
// https://en.wikipedia.org/wiki/Illuminant_D65#Definition
115+
const D65_WHITE_X: f32 = 0.95047;
116+
const D65_WHITE_Y: f32 = 1.0;
117+
const D65_WHITE_Z: f32 = 1.08883;
118+
119+
/// converts a color in LCH space to sRGB space
120+
#[inline]
121+
pub fn lch_to_nonlinear_srgb(lightness: f32, chroma: f32, hue: f32) -> [f32; 3] {
122+
let lightness = lightness * 100.0;
123+
let chroma = chroma * 100.0;
124+
125+
// convert LCH to Lab
126+
// http://www.brucelindbloom.com/index.html?Eqn_LCH_to_Lab.html
127+
let l = lightness;
128+
let a = chroma * hue.to_radians().cos();
129+
let b = chroma * hue.to_radians().sin();
130+
131+
// convert Lab to XYZ
132+
// http://www.brucelindbloom.com/index.html?Eqn_Lab_to_XYZ.html
133+
let fy = (l + 16.0) / 116.0;
134+
let fx = a / 500.0 + fy;
135+
let fz = fy - b / 200.0;
136+
let xr = {
137+
let fx3 = fx.powf(3.0);
138+
139+
if fx3 > Self::CIE_EPSILON {
140+
fx3
141+
} else {
142+
(116.0 * fx - 16.0) / Self::CIE_KAPPA
143+
}
144+
};
145+
let yr = if l > Self::CIE_EPSILON * Self::CIE_KAPPA {
146+
((l + 16.0) / 116.0).powf(3.0)
147+
} else {
148+
l / Self::CIE_KAPPA
149+
};
150+
let zr = {
151+
let fz3 = fz.powf(3.0);
152+
153+
if fz3 > Self::CIE_EPSILON {
154+
fz3
155+
} else {
156+
(116.0 * fz - 16.0) / Self::CIE_KAPPA
157+
}
158+
};
159+
let x = xr * Self::D65_WHITE_X;
160+
let y = yr * Self::D65_WHITE_Y;
161+
let z = zr * Self::D65_WHITE_Z;
162+
163+
// XYZ to sRGB
164+
// http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_RGB.html
165+
// http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html (sRGB, XYZ to RGB [M]-1)
166+
let red = x * 3.2404542 + y * -1.5371385 + z * -0.4985314;
167+
let green = x * -0.969266 + y * 1.8760108 + z * 0.041556;
168+
let blue = x * 0.0556434 + y * -0.2040259 + z * 1.0572252;
169+
170+
[
171+
red.linear_to_nonlinear_srgb().max(0.0).min(1.0),
172+
green.linear_to_nonlinear_srgb().max(0.0).min(1.0),
173+
blue.linear_to_nonlinear_srgb().max(0.0).min(1.0),
174+
]
175+
}
176+
177+
/// converts a color in sRGB space to LCH space
178+
#[inline]
179+
pub fn nonlinear_srgb_to_lch([red, green, blue]: [f32; 3]) -> (f32, f32, f32) {
180+
// RGB to XYZ
181+
// http://www.brucelindbloom.com/index.html?Eqn_RGB_to_XYZ.html
182+
let red = red.nonlinear_to_linear_srgb();
183+
let green = green.nonlinear_to_linear_srgb();
184+
let blue = blue.nonlinear_to_linear_srgb();
185+
186+
// http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html (sRGB, RGB to XYZ [M])
187+
let x = red * 0.4124564 + green * 0.3575761 + blue * 0.1804375;
188+
let y = red * 0.2126729 + green * 0.7151522 + blue * 0.072175;
189+
let z = red * 0.0193339 + green * 0.119192 + blue * 0.9503041;
190+
191+
// XYZ to Lab
192+
// http://www.brucelindbloom.com/index.html?Eqn_XYZ_to_Lab.html
193+
let xr = x / Self::D65_WHITE_X;
194+
let yr = y / Self::D65_WHITE_Y;
195+
let zr = z / Self::D65_WHITE_Z;
196+
let fx = if xr > Self::CIE_EPSILON {
197+
xr.cbrt()
198+
} else {
199+
(Self::CIE_KAPPA * xr + 16.0) / 116.0
200+
};
201+
let fy = if yr > Self::CIE_EPSILON {
202+
yr.cbrt()
203+
} else {
204+
(Self::CIE_KAPPA * yr + 16.0) / 116.0
205+
};
206+
let fz = if yr > Self::CIE_EPSILON {
207+
zr.cbrt()
208+
} else {
209+
(Self::CIE_KAPPA * zr + 16.0) / 116.0
210+
};
211+
let l = 116.0 * fy - 16.0;
212+
let a = 500.0 * (fx - fy);
213+
let b = 200.0 * (fy - fz);
214+
215+
// Lab to LCH
216+
// http://www.brucelindbloom.com/index.html?Eqn_Lab_to_LCH.html
217+
let c = (a.powf(2.0) + b.powf(2.0)).sqrt();
218+
let h = {
219+
let h = b.to_radians().atan2(a.to_radians()).to_degrees();
220+
221+
if h < 0.0 {
222+
h + 360.0
223+
} else {
224+
h
225+
}
226+
};
227+
228+
(
229+
(l / 100.0).max(0.0).min(1.5),
230+
(c / 100.0).max(0.0).min(1.5),
231+
h,
232+
)
233+
}
234+
}
235+
105236
#[cfg(test)]
106237
mod test {
107238
use super::*;
@@ -214,4 +345,90 @@ mod test {
214345
assert_eq!((saturation * 100.0).round() as u32, 83);
215346
assert_eq!((lightness * 100.0).round() as u32, 51);
216347
}
348+
349+
#[test]
350+
fn lch_to_srgb() {
351+
// "truth" from http://www.brucelindbloom.com/ColorCalculator.html
352+
353+
// black
354+
let (lightness, chroma, hue) = (0.0, 0.0, 0.0);
355+
let [r, g, b] = LchRepresentation::lch_to_nonlinear_srgb(lightness, chroma, hue);
356+
assert_eq!((r * 100.0).round() as u32, 0);
357+
assert_eq!((g * 100.0).round() as u32, 0);
358+
assert_eq!((b * 100.0).round() as u32, 0);
359+
360+
// white
361+
let (lightness, chroma, hue) = (1.0, 0.0, 0.0);
362+
let [r, g, b] = LchRepresentation::lch_to_nonlinear_srgb(lightness, chroma, hue);
363+
assert_eq!((r * 100.0).round() as u32, 100);
364+
assert_eq!((g * 100.0).round() as u32, 100);
365+
assert_eq!((b * 100.0).round() as u32, 100);
366+
367+
let (lightness, chroma, hue) = (0.501236, 0.777514, 327.6608);
368+
let [r, g, b] = LchRepresentation::lch_to_nonlinear_srgb(lightness, chroma, hue);
369+
assert_eq!((r * 100.0).round() as u32, 75);
370+
assert_eq!((g * 100.0).round() as u32, 25);
371+
assert_eq!((b * 100.0).round() as u32, 75);
372+
373+
// a red
374+
let (lightness, chroma, hue) = (0.487122, 0.999531, 318.7684);
375+
let [r, g, b] = LchRepresentation::lch_to_nonlinear_srgb(lightness, chroma, hue);
376+
assert_eq!((r * 100.0).round() as u32, 70);
377+
assert_eq!((g * 100.0).round() as u32, 19);
378+
assert_eq!((b * 100.0).round() as u32, 90);
379+
380+
// a green
381+
let (lightness, chroma, hue) = (0.732929, 0.560925, 164.3216);
382+
let [r, g, b] = LchRepresentation::lch_to_nonlinear_srgb(lightness, chroma, hue);
383+
assert_eq!((r * 100.0).round() as u32, 10);
384+
assert_eq!((g * 100.0).round() as u32, 80);
385+
assert_eq!((b * 100.0).round() as u32, 59);
386+
387+
// a blue
388+
let (lightness, chroma, hue) = (0.335030, 1.176923, 306.7828);
389+
let [r, g, b] = LchRepresentation::lch_to_nonlinear_srgb(lightness, chroma, hue);
390+
assert_eq!((r * 100.0).round() as u32, 25);
391+
assert_eq!((g * 100.0).round() as u32, 10);
392+
assert_eq!((b * 100.0).round() as u32, 92);
393+
}
394+
395+
#[test]
396+
fn srgb_to_lch() {
397+
// "truth" from http://www.brucelindbloom.com/ColorCalculator.html
398+
399+
// black
400+
let (lightness, chroma, hue) = LchRepresentation::nonlinear_srgb_to_lch([0.0, 0.0, 0.0]);
401+
assert_eq!((lightness * 100.0).round() as u32, 0);
402+
assert_eq!((chroma * 100.0).round() as u32, 0);
403+
assert_eq!(hue.round() as u32, 0);
404+
405+
// white
406+
let (lightness, chroma, hue) = LchRepresentation::nonlinear_srgb_to_lch([1.0, 1.0, 1.0]);
407+
assert_eq!((lightness * 100.0).round() as u32, 100);
408+
assert_eq!((chroma * 100.0).round() as u32, 0);
409+
assert_eq!(hue.round() as u32, 0);
410+
411+
let (lightness, chroma, hue) = LchRepresentation::nonlinear_srgb_to_lch([0.75, 0.25, 0.75]);
412+
assert_eq!((lightness * 100.0).round() as u32, 50);
413+
assert_eq!((chroma * 100.0).round() as u32, 78);
414+
assert_eq!(hue.round() as u32, 328);
415+
416+
// a red
417+
let (lightness, chroma, hue) = LchRepresentation::nonlinear_srgb_to_lch([0.70, 0.19, 0.90]);
418+
assert_eq!((lightness * 100.0).round() as u32, 49);
419+
assert_eq!((chroma * 100.0).round() as u32, 100);
420+
assert_eq!(hue.round() as u32, 319);
421+
422+
// a green
423+
let (lightness, chroma, hue) = LchRepresentation::nonlinear_srgb_to_lch([0.10, 0.80, 0.59]);
424+
assert_eq!((lightness * 100.0).round() as u32, 73);
425+
assert_eq!((chroma * 100.0).round() as u32, 56);
426+
assert_eq!(hue.round() as u32, 164);
427+
428+
// a blue
429+
let (lightness, chroma, hue) = LchRepresentation::nonlinear_srgb_to_lch([0.25, 0.10, 0.92]);
430+
assert_eq!((lightness * 100.0).round() as u32, 34);
431+
assert_eq!((chroma * 100.0).round() as u32, 118);
432+
assert_eq!(hue.round() as u32, 307);
433+
}
217434
}

0 commit comments

Comments
 (0)