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]
### 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)

View file

@ -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);
pub const fn rgb(r: f32, g: f32, b: f32) -> Color {
Color { r, g, b, a: 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()
}
pub const fn rgba(r: f32, g: f32, b: f32, a: f32) -> Color {
Color { r, g, b, a }
/// 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()
}
/// 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> {
@ -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<Color> 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<Color> 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<Vec4> 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<Vec4> for Color {
impl From<Vec4> 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<f32> for Color {
@ -154,20 +269,20 @@ impl Mul<f32> 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<f32> 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<Vec4> 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<Vec4> 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<Vec3> 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<Vec3> 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<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]
fn test_hex_color() {
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 camera;
pub mod color;
pub mod colorspace;
pub mod draw;
pub mod entity;
pub mod mesh;

View file

@ -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()

View file

@ -124,10 +124,10 @@ impl<'a> From<&'a OwnedWgpuVertexBufferDescriptor> for wgpu::VertexBufferDescrip
impl WgpuFrom<Color> 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,
}
}
}