sRGB awareness for Color (#616)

Color is now sRGB aware, added SrgbColorSpace trait for f32
This commit is contained in:
Julian Heinken 2020-10-08 19:30:23 +02:00 committed by GitHub
parent e89301ad29
commit a92790c011
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 253 additions and 67 deletions

View File

@ -6,7 +6,16 @@
- [Another fast compile flag for macOS][552] - [Another fast compile flag for macOS][552]
### Changed
- Breaking Change: [sRGB awareness for `Color`][616]
- Color is now assumed to be provided in the non-linear sRGB colorspace, and constructors such as `Color::rgb` and `Color::rgba` will be converted to linear sRGB under-the-hood.
- This allows drop-in use of colors from most applications.
- New methods `Color::rgb_linear` and `Color::rgba_linear` will accept colors already in linear sRGB (the old behavior)
- Individual color-components must now be accessed through setters and getters: `.r`, `.g`, `.b`, `.a`, `.set_r`, `.set_g`, `.set_b`, `.set_a`, and the corresponding methods with the `*_linear` suffix.
[552]: https://github.com/bevyengine/bevy/pull/552 [552]: https://github.com/bevyengine/bevy/pull/552
[616]: https://github.com/bevyengine/bevy/pull/616
## Version 0.2.1 (2020-9-20) ## Version 0.2.1 (2020-9-20)

View File

@ -1,5 +1,6 @@
use super::texture::Texture; use super::texture::Texture;
use crate::{ use crate::{
colorspace::*,
impl_render_resource_bytes, impl_render_resource_bytes,
renderer::{RenderResource, RenderResourceType}, renderer::{RenderResource, RenderResourceType},
}; };
@ -10,32 +11,68 @@ use bevy_property::Property;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::ops::{Add, AddAssign, Mul, MulAssign}; use std::ops::{Add, AddAssign, Mul, MulAssign};
/// A RGBA color /// RGBA color in the Linear sRGB colorspace (often colloquially referred to as "linear", "RGB", or "linear RGB").
#[repr(C)] #[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Property)] #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Property)]
pub struct Color { pub struct Color {
pub r: f32, red: f32,
pub g: f32, green: f32,
pub b: f32, blue: f32,
pub a: f32, alpha: f32,
} }
unsafe impl Byteable for Color {} unsafe impl Byteable for Color {}
impl Color { impl Color {
pub const BLACK: Color = Color::rgb(0.0, 0.0, 0.0); pub const BLACK: Color = Color::rgb_linear(0.0, 0.0, 0.0);
pub const BLUE: Color = Color::rgb(0.0, 0.0, 1.0); pub const BLUE: Color = Color::rgb_linear(0.0, 0.0, 1.0);
pub const GREEN: Color = Color::rgb(0.0, 1.0, 0.0); pub const GREEN: Color = Color::rgb_linear(0.0, 1.0, 0.0);
pub const NONE: Color = Color::rgba(0.0, 0.0, 0.0, 0.0); pub const NONE: Color = Color::rgba_linear(0.0, 0.0, 0.0, 0.0);
pub const RED: Color = Color::rgb(1.0, 0.0, 0.0); pub const RED: Color = Color::rgb_linear(1.0, 0.0, 0.0);
pub const WHITE: Color = Color::rgb(1.0, 1.0, 1.0); pub const WHITE: Color = Color::rgb_linear(1.0, 1.0, 1.0);
pub const fn rgb(r: f32, g: f32, b: f32) -> Color { // TODO: cant make rgb and rgba const due traits not allowed in const functions
Color { r, g, b, a: 1.0 } // see issue #57563 https://github.com/rust-lang/rust/issues/57563
/// New ``Color`` from sRGB colorspace.
pub fn rgb(r: f32, g: f32, b: f32) -> Color {
Color {
red: r,
green: g,
blue: b,
alpha: 1.0,
}
.as_nonlinear_srgb_to_linear_srgb()
} }
pub const fn rgba(r: f32, g: f32, b: f32, a: f32) -> Color { /// New ``Color`` from sRGB colorspace.
Color { r, g, b, a } pub fn rgba(r: f32, g: f32, b: f32, a: f32) -> Color {
Color {
red: r,
green: g,
blue: b,
alpha: a,
}
.as_nonlinear_srgb_to_linear_srgb()
}
/// New ``Color`` from linear colorspace.
pub const fn rgb_linear(r: f32, g: f32, b: f32) -> Color {
Color {
red: r,
green: g,
blue: b,
alpha: 1.0,
}
}
/// New ``Color`` from linear colorspace.
pub const fn rgba_linear(r: f32, g: f32, b: f32, a: f32) -> Color {
Color {
red: r,
green: g,
blue: b,
alpha: a,
}
} }
pub fn hex<T: AsRef<str>>(hex: T) -> Result<Color, HexColorError> { pub fn hex<T: AsRef<str>>(hex: T) -> Result<Color, HexColorError> {
@ -74,12 +111,14 @@ impl Color {
Err(HexColorError::Length) Err(HexColorError::Length)
} }
/// New ``Color`` from sRGB colorspace.
pub fn rgb_u8(r: u8, g: u8, b: u8) -> Color { pub fn rgb_u8(r: u8, g: u8, b: u8) -> Color {
Color::rgba_u8(r, g, b, u8::MAX) Color::rgba_u8(r, g, b, u8::MAX)
} }
// Float operations in const fn are not stable yet // Float operations in const fn are not stable yet
// see https://github.com/rust-lang/rust/issues/57241 // see https://github.com/rust-lang/rust/issues/57241
/// New ``Color`` from sRGB colorspace.
pub fn rgba_u8(r: u8, g: u8, b: u8, a: u8) -> Color { pub fn rgba_u8(r: u8, g: u8, b: u8, a: u8) -> Color {
Color::rgba( Color::rgba(
r as f32 / u8::MAX as f32, r as f32 / u8::MAX as f32,
@ -88,6 +127,82 @@ impl Color {
a as f32 / u8::MAX as f32, a as f32 / u8::MAX as f32,
) )
} }
fn as_nonlinear_srgb_to_linear_srgb(self) -> Color {
Color {
red: self.red.nonlinear_to_linear_srgb(),
green: self.green.nonlinear_to_linear_srgb(),
blue: self.blue.nonlinear_to_linear_srgb(),
alpha: self.alpha, //alpha is always linear
}
}
// non-linear-sRGB Component Getter
pub fn r(&self) -> f32 {
self.red.linear_to_nonlinear_srgb()
}
pub fn g(&self) -> f32 {
self.red.linear_to_nonlinear_srgb()
}
pub fn b(&self) -> f32 {
self.red.linear_to_nonlinear_srgb()
}
// linear-sRGB Component Getter
pub fn g_linear(&self) -> f32 {
self.green
}
pub fn r_linear(&self) -> f32 {
self.red
}
pub fn b_linear(&self) -> f32 {
self.blue
}
pub fn a(&self) -> f32 {
self.alpha
}
// non-linear-sRGB Component Setter
pub fn set_r(&mut self, r: f32) -> &mut Self {
self.red = r.nonlinear_to_linear_srgb();
self
}
pub fn set_g(&mut self, g: f32) -> &mut Self {
self.green = g.nonlinear_to_linear_srgb();
self
}
pub fn set_b(&mut self, b: f32) -> &mut Self {
self.blue = b.nonlinear_to_linear_srgb();
self
}
// linear-sRGB Component Setter
pub fn set_r_linear(&mut self, r: f32) -> &mut Self {
self.red = r;
self
}
pub fn set_g_linear(&mut self, g: f32) -> &mut Self {
self.green = g;
self
}
pub fn set_b_linear(&mut self, b: f32) -> &mut Self {
self.blue = b;
self
}
pub fn set_a(&mut self, a: f32) -> &mut Self {
self.alpha = a;
self
}
} }
impl Default for Color { impl Default for Color {
@ -99,10 +214,10 @@ impl Default for Color {
impl AddAssign<Color> for Color { impl AddAssign<Color> for Color {
fn add_assign(&mut self, rhs: Color) { fn add_assign(&mut self, rhs: Color) {
*self = Color { *self = Color {
r: self.r + rhs.r, red: self.red + rhs.red,
g: self.g + rhs.g, green: self.green + rhs.green,
b: self.b + rhs.b, blue: self.blue + rhs.blue,
a: self.a + rhs.a, alpha: self.alpha + rhs.alpha,
} }
} }
} }
@ -112,10 +227,10 @@ impl Add<Color> for Color {
fn add(self, rhs: Color) -> Self::Output { fn add(self, rhs: Color) -> Self::Output {
Color { Color {
r: self.r + rhs.r, red: self.red + rhs.red,
g: self.g + rhs.g, green: self.green + rhs.green,
b: self.b + rhs.b, blue: self.blue + rhs.blue,
a: self.a + rhs.a, alpha: self.alpha + rhs.alpha,
} }
} }
} }
@ -125,10 +240,10 @@ impl Add<Vec4> for Color {
fn add(self, rhs: Vec4) -> Self::Output { fn add(self, rhs: Vec4) -> Self::Output {
Color { Color {
r: self.r + rhs.x(), red: self.red + rhs.x(),
g: self.g + rhs.y(), green: self.green + rhs.y(),
b: self.b + rhs.z(), blue: self.blue + rhs.z(),
a: self.a + rhs.w(), alpha: self.alpha + rhs.w(),
} }
} }
} }
@ -136,17 +251,17 @@ impl Add<Vec4> for Color {
impl From<Vec4> for Color { impl From<Vec4> for Color {
fn from(vec4: Vec4) -> Self { fn from(vec4: Vec4) -> Self {
Color { Color {
r: vec4.x(), red: vec4.x(),
g: vec4.y(), green: vec4.y(),
b: vec4.z(), blue: vec4.z(),
a: vec4.w(), alpha: vec4.w(),
} }
} }
} }
impl Into<[f32; 4]> for Color { impl Into<[f32; 4]> for Color {
fn into(self) -> [f32; 4] { fn into(self) -> [f32; 4] {
[self.r, self.g, self.b, self.a] [self.red, self.green, self.blue, self.alpha]
} }
} }
impl Mul<f32> for Color { impl Mul<f32> for Color {
@ -154,20 +269,20 @@ impl Mul<f32> for Color {
fn mul(self, rhs: f32) -> Self::Output { fn mul(self, rhs: f32) -> Self::Output {
Color { Color {
r: self.r * rhs, red: self.red * rhs,
g: self.g * rhs, green: self.green * rhs,
b: self.b * rhs, blue: self.blue * rhs,
a: self.a * rhs, alpha: self.alpha * rhs,
} }
} }
} }
impl MulAssign<f32> for Color { impl MulAssign<f32> for Color {
fn mul_assign(&mut self, rhs: f32) { fn mul_assign(&mut self, rhs: f32) {
self.r *= rhs; self.red *= rhs;
self.g *= rhs; self.green *= rhs;
self.b *= rhs; self.blue *= rhs;
self.a *= rhs; self.alpha *= rhs;
} }
} }
@ -176,20 +291,20 @@ impl Mul<Vec4> for Color {
fn mul(self, rhs: Vec4) -> Self::Output { fn mul(self, rhs: Vec4) -> Self::Output {
Color { Color {
r: self.r * rhs.x(), red: self.red * rhs.x(),
g: self.g * rhs.y(), green: self.green * rhs.y(),
b: self.b * rhs.z(), blue: self.blue * rhs.z(),
a: self.a * rhs.w(), alpha: self.alpha * rhs.w(),
} }
} }
} }
impl MulAssign<Vec4> for Color { impl MulAssign<Vec4> for Color {
fn mul_assign(&mut self, rhs: Vec4) { fn mul_assign(&mut self, rhs: Vec4) {
self.r *= rhs.x(); self.red *= rhs.x();
self.g *= rhs.y(); self.green *= rhs.y();
self.b *= rhs.z(); self.blue *= rhs.z();
self.a *= rhs.w(); self.alpha *= rhs.w();
} }
} }
@ -198,19 +313,19 @@ impl Mul<Vec3> for Color {
fn mul(self, rhs: Vec3) -> Self::Output { fn mul(self, rhs: Vec3) -> Self::Output {
Color { Color {
r: self.r * rhs.x(), red: self.red * rhs.x(),
g: self.g * rhs.y(), green: self.green * rhs.y(),
b: self.b * rhs.z(), blue: self.blue * rhs.z(),
a: self.a, alpha: self.alpha,
} }
} }
} }
impl MulAssign<Vec3> for Color { impl MulAssign<Vec3> for Color {
fn mul_assign(&mut self, rhs: Vec3) { fn mul_assign(&mut self, rhs: Vec3) {
self.r *= rhs.x(); self.red *= rhs.x();
self.g *= rhs.y(); self.green *= rhs.y();
self.b *= rhs.z(); self.blue *= rhs.z();
} }
} }
@ -289,6 +404,17 @@ fn decode_rgba(data: &[u8]) -> Result<Color, HexColorError> {
} }
} }
#[test]
fn test_color_components_roundtrip() {
let mut color = Color::NONE;
color.set_r(0.5).set_g(0.5).set_b(0.5).set_a(0.5);
const EPS: f32 = 0.001;
assert!((color.r() - 0.5).abs() < EPS);
assert!((color.g() - 0.5).abs() < EPS);
assert!((color.b() - 0.5).abs() < EPS);
assert!((color.a() - 0.5).abs() < EPS);
}
#[test] #[test]
fn test_hex_color() { fn test_hex_color() {
assert_eq!(Color::hex("FFF").unwrap(), Color::rgb(1.0, 1.0, 1.0)); assert_eq!(Color::hex("FFF").unwrap(), Color::rgb(1.0, 1.0, 1.0));

View File

@ -0,0 +1,50 @@
// sRGB
//==================================================================================================
pub trait SrgbColorSpace {
fn linear_to_nonlinear_srgb(self) -> Self;
fn nonlinear_to_linear_srgb(self) -> Self;
}
//source: https://entropymine.com/imageworsener/srgbformula/
impl SrgbColorSpace for f32 {
fn linear_to_nonlinear_srgb(self) -> f32 {
if self <= 0.0 {
return self;
}
if self <= 0.0031308 {
self * 12.92 // linear falloff in dark values
} else {
(1.055 * self.powf(1.0 / 2.4)) - 0.055 //gamma curve in other area
}
}
fn nonlinear_to_linear_srgb(self) -> f32 {
if self <= 0.0 {
return self;
}
if self <= 0.04045 {
self / 12.92 // linear falloff in dark values
} else {
((self + 0.055) / 1.055).powf(2.4) //gamma curve in other area
}
}
}
#[test]
fn test_srgb_full_roundtrip() {
let u8max: f32 = u8::max_value() as f32;
for color in 0..u8::max_value() {
let color01 = color as f32 / u8max;
let color_roundtrip = color01
.linear_to_nonlinear_srgb()
.nonlinear_to_linear_srgb();
// roundtrip is not perfect due to numeric precision, even with f64
// so ensure the error is at least ready for u8 (where sRGB is used)
assert_eq!(
(color01 * u8max).round() as u8,
(color_roundtrip * u8max).round() as u8
);
}
}
//==================================================================================================

View File

@ -1,6 +1,7 @@
pub mod batch; pub mod batch;
pub mod camera; pub mod camera;
pub mod color; pub mod color;
pub mod colorspace;
pub mod draw; pub mod draw;
pub mod entity; pub mod entity;
pub mod mesh; pub mod mesh;

View File

@ -30,9 +30,9 @@ impl Font {
// TODO: make this texture grayscale // TODO: make this texture grayscale
let color = Color::WHITE; let color = Color::WHITE;
let color_u8 = [ let color_u8 = [
(color.r * 255.0) as u8, (color.r() * 255.0) as u8,
(color.g * 255.0) as u8, (color.g() * 255.0) as u8,
(color.b * 255.0) as u8, (color.b() * 255.0) as u8,
]; ];
Texture::new( Texture::new(
Vec2::new(width as f32, height as f32), Vec2::new(width as f32, height as f32),
@ -43,7 +43,7 @@ impl Font {
color_u8[0], color_u8[0],
color_u8[1], color_u8[1],
color_u8[2], color_u8[2],
(color.a * a * 255.0) as u8, (color.a() * a * 255.0) as u8,
] ]
}) })
.flatten() .flatten()
@ -75,9 +75,9 @@ impl Font {
); );
let color_u8 = [ let color_u8 = [
(color.r * 255.0) as u8, (color.r() * 255.0) as u8,
(color.g * 255.0) as u8, (color.g() * 255.0) as u8,
(color.b * 255.0) as u8, (color.b() * 255.0) as u8,
]; ];
// TODO: this offset is a bit hackey // TODO: this offset is a bit hackey
@ -108,7 +108,7 @@ impl Font {
color_u8[0], color_u8[0],
color_u8[1], color_u8[1],
color_u8[2], color_u8[2],
(color.a * a * 255.0) as u8, (color.a() * a * 255.0) as u8,
] ]
}) })
.flatten() .flatten()

View File

@ -124,10 +124,10 @@ impl<'a> From<&'a OwnedWgpuVertexBufferDescriptor> for wgpu::VertexBufferDescrip
impl WgpuFrom<Color> for wgpu::Color { impl WgpuFrom<Color> for wgpu::Color {
fn from(color: Color) -> Self { fn from(color: Color) -> Self {
wgpu::Color { wgpu::Color {
r: color.r as f64, r: color.r_linear() as f64,
g: color.g as f64, g: color.g_linear() as f64,
b: color.b as f64, b: color.b_linear() as f64,
a: color.a as f64, a: color.a() as f64,
} }
} }
} }