Bevy: Convert colors from srgb to linear srgb automatically

Created on 27 Sep 2020  路  7Comments  路  Source: bevyengine/bevy

As per this Discord conversation and https://github.com/bevyengine/bevy/issues/419#issuecomment-691532199, assume people are providing raw color values in the _non-linear_ srgb colorspace. Bevy should then convert the provided colors to linear srgb for internal use, for example in the constructors in bevy_render/src/color.rs), and automatically convert the colors to linear srgb when it makes sense under the hood.

We could have constructors for both non-linear and linear sources as long as we document them wherever there could be ambiguity.

Workaround

Right now this can be worked around by manually converting the colors to linear srgb colorspace before using them with the engine like this:

// # Cargo.toml
// [dependencies]
// palette = "0.5.0"

use palette::Srgb;
use bevy::prelude::Color;

fn some_function() {
    // (0.592, 0.337, 0.157) is a dark brown in the non-linear srgb colorspace
    // Use `palette` to convert from non-linear to linear
    let color_intermediate = Srgb::new(0.592, 0.337, 0.157).into_linear();  
    // Convert to Bevy's `Color` struct
    let color_linear = Color::rgb(color_intermediate.red, color_intermediate.green, color_intermediate.blue) ;
    // Now use `color_linear` with Bevy
}
enhancement rendering

Most helpful comment

Oh good, to be clear I didn't _want_ an additional dependency -- but I also didn't want to figure out the math myself when I was tired. 馃槃 I'm glad the math ended up being so simple.

All 7 comments

Here's some helper workaround functions I made for my little project to make things more ergonomic:

use bevy::prelude::Color;
use palette::Srgb;

/// Take a color in non-linear srgb and convert it to the linear srgb format Bevy currently needs
pub fn color_from_f32(r: f32, g: f32, b: f32) -> Color {
    let l = Srgb::new(r, g, b).into_linear();
    Color::rgb(l.red, l.green, l.blue)
}

/// Take a color in non-linear srgb and convert it to the linear srgb format Bevy currently needs
pub fn color_from_u8(r: u8, g: u8, b: u8) -> Color {
    color_from_f32(r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0)
}

I would say we make the default color constructors sRGB aware, and offer additonal linear color constructors.

I'm not sure on adding a palette dependency, as this would mean +10k lines (according to https://lib.rs/crates/palette) of addtional dependencies. The features it offers don't come across as something you need in every day game-dev, at least from my experience. 馃

The formulas for linear to sRGB and vice-versa aren't much code. A full dependency on palette isn't needed.

Here's an (untested) transcription of the formulas from the Wikipedia article about sRGB:

fn linear_to_srgb(u: f32) -> f32 {
    if u <= 0.0031308 {
        12.92 * u
    } else {
        1.055 * f32::powf(u,0.416666) - 0.055
    }
}

fn srgb_to_linear(u: f32) -> f32 {
    if u <= 0.04045 {
        u / 12.92
    } else {
        f32::powf((u + 0.055) / 1.055, 2.4)
    }
}

I agree that the needed formula is simple enough to not warrant a dependency on an external crate.

Here follows an example solution with a macro (using the same formula from wikipedia). I like this solution because the needed code change is very clear and minimal.

use bevy::prelude::*;
use bevy::render::pass::ClearColor;

macro_rules! color_rgb {
    ( $( $x:expr ),* ) => {
        Color::rgb( $(
            // Without converting srgb to linear
            //$x
            // With converting srgb to linear
            if $x <= 0.04045 {
                $x / 12.92
            } else {
                ((($x as f32) + 0.055) / 1.055).powf(2.4)
            }
        ),* )
    };
}

fn main() {
    App::build()
        .add_default_plugins()
        //.add_resource(ClearColor(Color::rgb(0.83, 0.474, 0.206)))
        .add_resource(ClearColor(color_rgb!(0.83, 0.474, 0.206)))
        .run();
}

Oh good, to be clear I didn't _want_ an additional dependency -- but I also didn't want to figure out the math myself when I was tired. 馃槃 I'm glad the math ended up being so simple.

I found one possible problem with my solution above. Using GIMP colorpicker on the resulting color gives me the equivalent of 0.831, 0.475, 0.208, with differ slightly from the values in the code: 0.83, 0.474, 0.206.

Maybe this is a precision/rounding problem? Or maybe hardware specific? For reference I'm using an Nvidia graphics card in Linux.

I think we can close this out now that #616 landed

Was this page helpful?
0 / 5 - 0 ratings

Related issues

cart picture cart  路  22Comments

coolit picture coolit  路  22Comments

kakoeimon picture kakoeimon  路  13Comments

fopsdev picture fopsdev  路  14Comments

NickelCoating picture NickelCoating  路  15Comments