diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d56cb46f13cd..2caa067898624 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,16 @@ - [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 +[616]: https://github.com/bevyengine/bevy/pull/616 ## Version 0.2.1 (2020-9-20) diff --git a/crates/bevy_render/src/color.rs b/crates/bevy_render/src/color.rs index 6c626231d953a..5e1ff1e0bf742 100644 --- a/crates/bevy_render/src/color.rs +++ b/crates/bevy_render/src/color.rs @@ -1,5 +1,6 @@ use super::texture::Texture; use crate::{ + colorspace::*, impl_render_resource_bytes, renderer::{RenderResource, RenderResourceType}, }; @@ -10,32 +11,68 @@ use bevy_property::Property; use serde::{Deserialize, Serialize}; 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)] #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Property)] pub struct Color { - pub r: f32, - pub g: f32, - pub b: f32, - pub a: f32, + red: f32, + green: f32, + blue: f32, + alpha: f32, } unsafe impl Byteable for Color {} impl Color { - pub const BLACK: Color = Color::rgb(0.0, 0.0, 0.0); - pub const BLUE: Color = Color::rgb(0.0, 0.0, 1.0); - pub const GREEN: Color = Color::rgb(0.0, 1.0, 0.0); - pub const NONE: Color = Color::rgba(0.0, 0.0, 0.0, 0.0); - pub const RED: Color = Color::rgb(1.0, 0.0, 0.0); - pub const WHITE: Color = Color::rgb(1.0, 1.0, 1.0); + pub const BLACK: Color = Color::rgb_linear(0.0, 0.0, 0.0); + pub const BLUE: Color = Color::rgb_linear(0.0, 0.0, 1.0); + pub const GREEN: Color = Color::rgb_linear(0.0, 1.0, 0.0); + pub const NONE: Color = Color::rgba_linear(0.0, 0.0, 0.0, 0.0); + pub const RED: Color = Color::rgb_linear(1.0, 0.0, 0.0); + pub const WHITE: Color = Color::rgb_linear(1.0, 1.0, 1.0); + + // TODO: cant make rgb and rgba const due traits not allowed in const functions + // 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() + } + + /// New ``Color`` from sRGB colorspace. + 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() + } - pub const fn rgb(r: f32, g: f32, b: f32) -> Color { - Color { r, g, b, a: 1.0 } + /// 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, + } } - pub const fn rgba(r: f32, g: f32, b: f32, a: f32) -> Color { - Color { r, g, b, a } + /// 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>(hex: T) -> Result { @@ -74,12 +111,14 @@ impl Color { Err(HexColorError::Length) } + /// New ``Color`` from sRGB colorspace. pub fn rgb_u8(r: u8, g: u8, b: u8) -> Color { Color::rgba_u8(r, g, b, u8::MAX) } // Float operations in const fn are not stable yet // 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 { Color::rgba( r as f32 / u8::MAX as f32, @@ -88,6 +127,82 @@ impl Color { 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 { @@ -99,10 +214,10 @@ impl Default for Color { impl AddAssign for Color { fn add_assign(&mut self, rhs: Color) { *self = Color { - r: self.r + rhs.r, - g: self.g + rhs.g, - b: self.b + rhs.b, - a: self.a + rhs.a, + red: self.red + rhs.red, + green: self.green + rhs.green, + blue: self.blue + rhs.blue, + alpha: self.alpha + rhs.alpha, } } } @@ -112,10 +227,10 @@ impl Add for Color { fn add(self, rhs: Color) -> Self::Output { Color { - r: self.r + rhs.r, - g: self.g + rhs.g, - b: self.b + rhs.b, - a: self.a + rhs.a, + red: self.red + rhs.red, + green: self.green + rhs.green, + blue: self.blue + rhs.blue, + alpha: self.alpha + rhs.alpha, } } } @@ -125,10 +240,10 @@ impl Add for Color { fn add(self, rhs: Vec4) -> Self::Output { Color { - r: self.r + rhs.x(), - g: self.g + rhs.y(), - b: self.b + rhs.z(), - a: self.a + rhs.w(), + red: self.red + rhs.x(), + green: self.green + rhs.y(), + blue: self.blue + rhs.z(), + alpha: self.alpha + rhs.w(), } } } @@ -136,17 +251,17 @@ impl Add for Color { impl From for Color { fn from(vec4: Vec4) -> Self { Color { - r: vec4.x(), - g: vec4.y(), - b: vec4.z(), - a: vec4.w(), + red: vec4.x(), + green: vec4.y(), + blue: vec4.z(), + alpha: vec4.w(), } } } impl Into<[f32; 4]> for Color { fn into(self) -> [f32; 4] { - [self.r, self.g, self.b, self.a] + [self.red, self.green, self.blue, self.alpha] } } impl Mul for Color { @@ -154,20 +269,20 @@ impl Mul for Color { fn mul(self, rhs: f32) -> Self::Output { Color { - r: self.r * rhs, - g: self.g * rhs, - b: self.b * rhs, - a: self.a * rhs, + red: self.red * rhs, + green: self.green * rhs, + blue: self.blue * rhs, + alpha: self.alpha * rhs, } } } impl MulAssign for Color { fn mul_assign(&mut self, rhs: f32) { - self.r *= rhs; - self.g *= rhs; - self.b *= rhs; - self.a *= rhs; + self.red *= rhs; + self.green *= rhs; + self.blue *= rhs; + self.alpha *= rhs; } } @@ -176,20 +291,20 @@ impl Mul for Color { fn mul(self, rhs: Vec4) -> Self::Output { Color { - r: self.r * rhs.x(), - g: self.g * rhs.y(), - b: self.b * rhs.z(), - a: self.a * rhs.w(), + red: self.red * rhs.x(), + green: self.green * rhs.y(), + blue: self.blue * rhs.z(), + alpha: self.alpha * rhs.w(), } } } impl MulAssign for Color { fn mul_assign(&mut self, rhs: Vec4) { - self.r *= rhs.x(); - self.g *= rhs.y(); - self.b *= rhs.z(); - self.a *= rhs.w(); + self.red *= rhs.x(); + self.green *= rhs.y(); + self.blue *= rhs.z(); + self.alpha *= rhs.w(); } } @@ -198,19 +313,19 @@ impl Mul for Color { fn mul(self, rhs: Vec3) -> Self::Output { Color { - r: self.r * rhs.x(), - g: self.g * rhs.y(), - b: self.b * rhs.z(), - a: self.a, + red: self.red * rhs.x(), + green: self.green * rhs.y(), + blue: self.blue * rhs.z(), + alpha: self.alpha, } } } impl MulAssign for Color { fn mul_assign(&mut self, rhs: Vec3) { - self.r *= rhs.x(); - self.g *= rhs.y(); - self.b *= rhs.z(); + self.red *= rhs.x(); + self.green *= rhs.y(); + self.blue *= rhs.z(); } } @@ -289,6 +404,17 @@ fn decode_rgba(data: &[u8]) -> Result { } } +#[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] fn test_hex_color() { assert_eq!(Color::hex("FFF").unwrap(), Color::rgb(1.0, 1.0, 1.0)); diff --git a/crates/bevy_render/src/colorspace.rs b/crates/bevy_render/src/colorspace.rs new file mode 100644 index 0000000000000..3f789b25d4b9c --- /dev/null +++ b/crates/bevy_render/src/colorspace.rs @@ -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 + ); + } +} +//================================================================================================== diff --git a/crates/bevy_render/src/lib.rs b/crates/bevy_render/src/lib.rs index 0bdcf8a7bbde1..ebcdf04c4f9a6 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -1,6 +1,7 @@ pub mod batch; pub mod camera; pub mod color; +pub mod colorspace; pub mod draw; pub mod entity; pub mod mesh; diff --git a/crates/bevy_text/src/font.rs b/crates/bevy_text/src/font.rs index 93e61248acdd5..a98acd0076a4c 100644 --- a/crates/bevy_text/src/font.rs +++ b/crates/bevy_text/src/font.rs @@ -30,9 +30,9 @@ impl Font { // TODO: make this texture grayscale let color = Color::WHITE; let color_u8 = [ - (color.r * 255.0) as u8, - (color.g * 255.0) as u8, - (color.b * 255.0) as u8, + (color.r() * 255.0) as u8, + (color.g() * 255.0) as u8, + (color.b() * 255.0) as u8, ]; Texture::new( Vec2::new(width as f32, height as f32), @@ -43,7 +43,7 @@ impl Font { color_u8[0], color_u8[1], color_u8[2], - (color.a * a * 255.0) as u8, + (color.a() * a * 255.0) as u8, ] }) .flatten() @@ -75,9 +75,9 @@ impl Font { ); let color_u8 = [ - (color.r * 255.0) as u8, - (color.g * 255.0) as u8, - (color.b * 255.0) as u8, + (color.r() * 255.0) as u8, + (color.g() * 255.0) as u8, + (color.b() * 255.0) as u8, ]; // TODO: this offset is a bit hackey @@ -108,7 +108,7 @@ impl Font { color_u8[0], color_u8[1], color_u8[2], - (color.a * a * 255.0) as u8, + (color.a() * a * 255.0) as u8, ] }) .flatten() diff --git a/crates/bevy_wgpu/src/wgpu_type_converter.rs b/crates/bevy_wgpu/src/wgpu_type_converter.rs index 083c48a158c85..7a27620eadc34 100644 --- a/crates/bevy_wgpu/src/wgpu_type_converter.rs +++ b/crates/bevy_wgpu/src/wgpu_type_converter.rs @@ -124,10 +124,10 @@ impl<'a> From<&'a OwnedWgpuVertexBufferDescriptor> for wgpu::VertexBufferDescrip impl WgpuFrom for wgpu::Color { fn from(color: Color) -> Self { wgpu::Color { - r: color.r as f64, - g: color.g as f64, - b: color.b as f64, - a: color.a as f64, + r: color.r_linear() as f64, + g: color.g_linear() as f64, + b: color.b_linear() as f64, + a: color.a() as f64, } } }