Preconvert colors to sRGB

This commit is contained in:
Tyler Critchlow 2025-07-12 19:16:33 -04:00
parent f1eace62f0
commit fff060d638
4 changed files with 321 additions and 178 deletions

View File

@ -3068,6 +3068,17 @@ description = "Test rendering of many UI elements"
category = "Stress Tests"
wasm = true
[[example]]
name = "many_gradients"
path = "examples/stress_tests/many_gradients.rs"
doc-scrape-examples = true
[package.metadata.example.many_gradients]
name = "Many Gradients"
description = "Stress test for gradient rendering performance"
category = "Stress Tests"
wasm = true
[[example]]
name = "many_cameras_lights"
path = "examples/stress_tests/many_cameras_lights.rs"

View File

@ -7,7 +7,7 @@ use core::{
use super::shader_flags::BORDER_ALL;
use crate::*;
use bevy_asset::*;
use bevy_color::{ColorToComponents, LinearRgba};
use bevy_color::{ColorToComponents, Hsla, Hsva, LinearRgba, Oklaba, Oklcha, Srgba};
use bevy_ecs::{
prelude::Component,
system::{
@ -654,6 +654,36 @@ struct UiGradientVertex {
hint: f32,
}
fn convert_color_to_space(color: LinearRgba, space: InterpolationColorSpace) -> [f32; 4] {
match space {
InterpolationColorSpace::OkLab => {
let oklaba: Oklaba = color.into();
[oklaba.lightness, oklaba.a, oklaba.b, oklaba.alpha]
}
InterpolationColorSpace::OkLch | InterpolationColorSpace::OkLchLong => {
let oklcha: Oklcha = color.into();
[oklcha.lightness, oklcha.chroma, oklcha.hue.to_radians(), oklcha.alpha]
}
InterpolationColorSpace::Srgb => {
let srgba: Srgba = color.into();
[srgba.red, srgba.green, srgba.blue, srgba.alpha]
}
InterpolationColorSpace::LinearRgb => {
color.to_f32_array()
}
InterpolationColorSpace::Hsl | InterpolationColorSpace::HslLong => {
let hsla: Hsla = color.into();
// Normalize hue to 0..1 range for shader
[hsla.hue / 360.0, hsla.saturation, hsla.lightness, hsla.alpha]
}
InterpolationColorSpace::Hsv | InterpolationColorSpace::HsvLong => {
let hsva: Hsva = color.into();
// Normalize hue to 0..1 range for shader
[hsva.hue / 360.0, hsva.saturation, hsva.value, hsva.alpha]
}
}
}
pub fn prepare_gradient(
mut commands: Commands,
render_device: Res<RenderDevice>,
@ -804,8 +834,8 @@ pub fn prepare_gradient(
continue;
}
}
let start_color = start_stop.0.to_f32_array();
let end_color = end_stop.0.to_f32_array();
let start_color = convert_color_to_space(start_stop.0, gradient.color_space);
let end_color = convert_color_to_space(end_stop.0, gradient.color_space);
let mut stop_flags = flags;
if 0. < start_stop.1
&& (stop_index == gradient.stops_range.start || segment_count == 0)

View File

@ -114,26 +114,6 @@ fn fragment(in: GradientVertexOutput) -> @location(0) vec4<f32> {
}
}
// This function converts two linear rgba colors to srgba space, mixes them, and then converts the result back to linear rgb space.
fn mix_linear_rgba_in_srgba_space(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
let a_srgb = pow(a.rgb, vec3(1. / 2.2));
let b_srgb = pow(b.rgb, vec3(1. / 2.2));
let mixed_srgb = mix(a_srgb, b_srgb, t);
return vec4(pow(mixed_srgb, vec3(2.2)), mix(a.a, b.a, t));
}
fn linear_rgba_to_oklaba(c: vec4<f32>) -> vec4<f32> {
let l = pow(0.41222146 * c.x + 0.53633255 * c.y + 0.051445995 * c.z, 1. / 3.);
let m = pow(0.2119035 * c.x + 0.6806995 * c.y + 0.10739696 * c.z, 1. / 3.);
let s = pow(0.08830246 * c.x + 0.28171885 * c.y + 0.6299787 * c.z, 1. / 3.);
return vec4(
0.21045426 * l + 0.7936178 * m - 0.004072047 * s,
1.9779985 * l - 2.4285922 * m + 0.4505937 * s,
0.025904037 * l + 0.78277177 * m - 0.80867577 * s,
c.a
);
}
fn oklaba_to_linear_rgba(c: vec4<f32>) -> vec4<f32> {
let l_ = c.x + 0.39633778 * c.y + 0.21580376 * c.z;
let m_ = c.x - 0.105561346 * c.y - 0.06385417 * c.z;
@ -149,33 +129,6 @@ fn oklaba_to_linear_rgba(c: vec4<f32>) -> vec4<f32> {
);
}
fn mix_linear_rgba_in_oklaba_space(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
return oklaba_to_linear_rgba(mix(linear_rgba_to_oklaba(a), linear_rgba_to_oklaba(b), t));
}
fn linear_rgba_to_hsla(c: vec4<f32>) -> vec4<f32> {
let max = max(max(c.r, c.g), c.b);
let min = min(min(c.r, c.g), c.b);
let l = (max + min) * 0.5;
if max == min {
return vec4(0., 0., l, c.a);
} else {
let delta = max - min;
let s = delta / (1. - abs(2. * l - 1.));
var h = 0.;
if max == c.r {
h = ((c.g - c.b) / delta) % 6.;
} else if max == c.g {
h = ((c.b - c.r) / delta) + 2.;
} else {
h = ((c.r - c.g) / delta) + 4.;
}
h = h / 6.;
return vec4<f32>(h, s, l, c.a);
}
}
fn hsla_to_linear_rgba(hsl: vec4<f32>) -> vec4<f32> {
let h = hsl.x;
let s = hsl.y;
@ -203,30 +156,6 @@ fn hsla_to_linear_rgba(hsl: vec4<f32>) -> vec4<f32> {
return vec4<f32>(r + m, g + m, b + m, hsl.a);
}
fn linear_rgba_to_hsva(c: vec4<f32>) -> vec4<f32> {
let maxc = max(max(c.r, c.g), c.b);
let minc = min(min(c.r, c.g), c.b);
let delta = maxc - minc;
var h: f32 = 0.0;
var s: f32 = 0.0;
let v: f32 = maxc;
if delta != 0.0 {
s = delta / maxc;
if maxc == c.r {
h = ((c.g - c.b) / delta) % 6.0;
} else if maxc == c.g {
h = ((c.b - c.r) / delta) + 2.0;
} else {
h = ((c.r - c.g) / delta) + 4.0;
}
h = h / 6.0;
if h < 0.0 {
h = h + 1.0;
}
}
return vec4<f32>(h, s, v, c.a);
}
fn hsva_to_linear_rgba(hsva: vec4<f32>) -> vec4<f32> {
let h = hsva.x * 6.0;
let s = hsva.y;
@ -253,14 +182,6 @@ fn hsva_to_linear_rgba(hsva: vec4<f32>) -> vec4<f32> {
return vec4<f32>(r + m, g + m, b + m, hsva.a);
}
/// hue is left in radians and not converted to degrees
fn linear_rgba_to_oklcha(c: vec4<f32>) -> vec4<f32> {
let o = linear_rgba_to_oklaba(c);
let chroma = sqrt(o.y * o.y + o.z * o.z);
let hue = atan2(o.z, o.y);
return vec4(o.x, chroma, rem_euclid(hue, TAU), o.a);
}
fn oklcha_to_linear_rgba(c: vec4<f32>) -> vec4<f32> {
let a = c.y * cos(c.z);
let b = c.y * sin(c.z);
@ -271,90 +192,6 @@ fn rem_euclid(a: f32, b: f32) -> f32 {
return ((a % b) + b) % b;
}
fn lerp_hue(a: f32, b: f32, t: f32) -> f32 {
let diff = rem_euclid(b - a + PI, TAU) - PI;
return rem_euclid(a + diff * t, TAU);
}
fn lerp_hue_long(a: f32, b: f32, t: f32) -> f32 {
let diff = rem_euclid(b - a + PI, TAU) - PI;
return rem_euclid(a + (diff + select(TAU, -TAU, 0. < diff)) * t, TAU);
}
fn mix_oklcha(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
let ah = select(a.z, b.z, a.y == 0.);
let bh = select(b.z, a.z, b.y == 0.);
return vec4(
mix(a.xy, b.xy, t),
lerp_hue(ah, bh, t),
mix(a.a, b.a, t)
);
}
fn mix_oklcha_long(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
let ah = select(a.z, b.z, a.y == 0.);
let bh = select(b.z, a.z, b.y == 0.);
return vec4(
mix(a.xy, b.xy, t),
lerp_hue_long(ah, bh, t),
mix(a.w, b.w, t)
);
}
fn mix_linear_rgba_in_oklcha_space(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
return oklcha_to_linear_rgba(mix_oklcha(linear_rgba_to_oklcha(a), linear_rgba_to_oklcha(b), t));
}
fn mix_linear_rgba_in_oklcha_space_long(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
return oklcha_to_linear_rgba(mix_oklcha_long(linear_rgba_to_oklcha(a), linear_rgba_to_oklcha(b), t));
}
fn mix_linear_rgba_in_hsva_space(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
let ha = linear_rgba_to_hsva(a);
let hb = linear_rgba_to_hsva(b);
var h: f32;
if ha.y == 0. {
h = hb.x;
} else if hb.y == 0. {
h = ha.x;
} else {
h = lerp_hue(ha.x * TAU, hb.x * TAU, t) / TAU;
}
let s = mix(ha.y, hb.y, t);
let v = mix(ha.z, hb.z, t);
let a_alpha = mix(ha.a, hb.a, t);
return hsva_to_linear_rgba(vec4<f32>(h, s, v, a_alpha));
}
fn mix_linear_rgba_in_hsva_space_long(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
let ha = linear_rgba_to_hsva(a);
let hb = linear_rgba_to_hsva(b);
let h = lerp_hue_long(ha.x * TAU, hb.x * TAU, t) / TAU;
let s = mix(ha.y, hb.y, t);
let v = mix(ha.z, hb.z, t);
let a_alpha = mix(ha.a, hb.a, t);
return hsva_to_linear_rgba(vec4<f32>(h, s, v, a_alpha));
}
fn mix_linear_rgba_in_hsla_space(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
let ha = linear_rgba_to_hsla(a);
let hb = linear_rgba_to_hsla(b);
let h = lerp_hue(ha.x * TAU, hb.x * TAU, t) / TAU;
let s = mix(ha.y, hb.y, t);
let l = mix(ha.z, hb.z, t);
let a_alpha = mix(ha.a, hb.a, t);
return hsla_to_linear_rgba(vec4<f32>(h, s, l, a_alpha));
}
fn mix_linear_rgba_in_hsla_space_long(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
let ha = linear_rgba_to_hsla(a);
let hb = linear_rgba_to_hsla(b);
let h = lerp_hue_long(ha.x * TAU, hb.x * TAU, t) / TAU;
let s = mix(ha.y, hb.y, t);
let l = mix(ha.z, hb.z, t);
let a_alpha = mix(ha.a, hb.a, t);
return hsla_to_linear_rgba(vec4<f32>(h, s, l, a_alpha));
}
// These functions are used to calculate the distance in gradient space from the start of the gradient to the point.
// The distance in gradient space is then used to interpolate between the start and end colors.
@ -386,6 +223,126 @@ fn conic_distance(
return (((angle - start) % TAU) + TAU) % TAU;
}
fn mix_oklcha(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
let hue_diff = b.z - a.z;
var adjusted_hue = a.z;
if abs(hue_diff) > PI {
if hue_diff > 0.0 {
adjusted_hue = a.z + (hue_diff - TAU) * t;
} else {
adjusted_hue = a.z + (hue_diff + TAU) * t;
}
} else {
adjusted_hue = a.z + hue_diff * t;
}
return vec4(
mix(a.x, b.x, t),
mix(a.y, b.y, t),
rem_euclid(adjusted_hue, TAU),
mix(a.w, b.w, t)
);
}
fn mix_oklcha_long(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
let hue_diff = b.z - a.z;
var adjusted_hue = a.z;
if abs(hue_diff) < PI {
if hue_diff >= 0.0 {
adjusted_hue = a.z + (hue_diff - TAU) * t;
} else {
adjusted_hue = a.z + (hue_diff + TAU) * t;
}
} else {
adjusted_hue = a.z + hue_diff * t;
}
return vec4(
mix(a.x, b.x, t),
mix(a.y, b.y, t),
rem_euclid(adjusted_hue, TAU),
mix(a.w, b.w, t)
);
}
fn mix_hsla(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
let hue_diff = b.x - a.x;
var adjusted_hue = a.x;
if abs(hue_diff) > 0.5 {
if hue_diff > 0.0 {
adjusted_hue = a.x + (hue_diff - 1.0) * t;
} else {
adjusted_hue = a.x + (hue_diff + 1.0) * t;
}
} else {
adjusted_hue = a.x + hue_diff * t;
}
return vec4(
fract(adjusted_hue),
mix(a.y, b.y, t),
mix(a.z, b.z, t),
mix(a.w, b.w, t)
);
}
fn mix_hsla_long(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
let hue_diff = b.x - a.x;
var adjusted_hue = a.x;
if abs(hue_diff) < 0.5 {
if hue_diff >= 0.0 {
adjusted_hue = a.x + (hue_diff - 1.0) * t;
} else {
adjusted_hue = a.x + (hue_diff + 1.0) * t;
}
} else {
adjusted_hue = a.x + hue_diff * t;
}
return vec4(
fract(adjusted_hue),
mix(a.y, b.y, t),
mix(a.z, b.z, t),
mix(a.w, b.w, t)
);
}
fn mix_hsva(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
let hue_diff = b.x - a.x;
var adjusted_hue = a.x;
if abs(hue_diff) > 0.5 {
if hue_diff > 0.0 {
adjusted_hue = a.x + (hue_diff - 1.0) * t;
} else {
adjusted_hue = a.x + (hue_diff + 1.0) * t;
}
} else {
adjusted_hue = a.x + hue_diff * t;
}
return vec4(
fract(adjusted_hue),
mix(a.y, b.y, t),
mix(a.z, b.z, t),
mix(a.w, b.w, t)
);
}
fn mix_hsva_long(a: vec4<f32>, b: vec4<f32>, t: f32) -> vec4<f32> {
let hue_diff = b.x - a.x;
var adjusted_hue = a.x;
if abs(hue_diff) < 0.5 {
if hue_diff >= 0.0 {
adjusted_hue = a.x + (hue_diff - 1.0) * t;
} else {
adjusted_hue = a.x + (hue_diff + 1.0) * t;
}
} else {
adjusted_hue = a.x + hue_diff * t;
}
return vec4(
fract(adjusted_hue),
mix(a.y, b.y, t),
mix(a.z, b.z, t),
mix(a.w, b.w, t)
);
}
fn interpolate_gradient(
distance: f32,
start_color: vec4<f32>,
@ -426,23 +383,23 @@ fn interpolate_gradient(
} else {
t = 0.5 * (1 + (t - hint) / (1.0 - hint));
}
#ifdef IN_SRGB
return mix_linear_rgba_in_srgba_space(start_color, end_color, t);
#else ifdef IN_OKLAB
return mix_linear_rgba_in_oklaba_space(start_color, end_color, t);
#else ifdef IN_OKLCH
return mix_linear_rgba_in_oklcha_space(start_color, end_color, t);
#ifdef IN_OKLCH
return oklcha_to_linear_rgba(mix_oklcha(start_color, end_color, t));
#else ifdef IN_OKLCH_LONG
return mix_linear_rgba_in_oklcha_space_long(start_color, end_color, t);
return oklcha_to_linear_rgba(mix_oklcha_long(start_color, end_color, t));
#else ifdef IN_HSV
return mix_linear_rgba_in_hsva_space(start_color, end_color, t);
return hsva_to_linear_rgba(mix_hsva(start_color, end_color, t));
#else ifdef IN_HSV_LONG
return mix_linear_rgba_in_hsva_space_long(start_color, end_color, t);
return hsva_to_linear_rgba(mix_hsva_long(start_color, end_color, t));
#else ifdef IN_HSL
return mix_linear_rgba_in_hsla_space(start_color, end_color, t);
return hsla_to_linear_rgba(mix_hsla(start_color, end_color, t));
#else ifdef IN_HSL_LONG
return mix_linear_rgba_in_hsla_space_long(start_color, end_color, t);
return hsla_to_linear_rgba(mix_hsla_long(start_color, end_color, t));
#else ifdef IN_OKLAB
return oklaba_to_linear_rgba(mix(start_color, end_color, t));
#else ifdef IN_SRGB
let mixed_srgb = mix(start_color, end_color, t);
return vec4(pow(mixed_srgb.rgb, vec3(2.2)), mixed_srgb.a);
#else
return mix(start_color, end_color, t);
#endif

View File

@ -0,0 +1,145 @@
//! Stress test demonstrating gradient performance improvements.
//!
//! This example creates many UI nodes with gradients to measure the performance
//! impact of pre-converting colors to the target color space on the CPU.
use argh::FromArgs;
use bevy::{
color::palettes::css::*,
diagnostic::{FrameTimeDiagnosticsPlugin, LogDiagnosticsPlugin},
prelude::*,
ui::{BackgroundGradient, LinearGradient, ColorStop, Gradient, RepeatedGridTrack, Display, InterpolationColorSpace},
window::{PresentMode, WindowResolution},
};
const COLS: usize = 30;
#[derive(FromArgs, Resource, Debug)]
/// Gradient stress test
struct Args {
/// how many gradients per group (default: 900)
#[argh(option, default = "900")]
gradient_count: usize,
/// whether to animate gradients by changing colors
#[argh(switch)]
animate: bool,
/// use sRGB interpolation
#[argh(switch)]
srgb: bool,
/// use HSL interpolation
#[argh(switch)]
hsl: bool,
}
fn main() {
let args: Args = argh::from_env();
let total_gradients = args.gradient_count;
println!("Gradient stress test with {} gradients", total_gradients);
println!("Color space: {}", if args.srgb {
"sRGB"
} else if args.hsl {
"HSL"
} else {
"OkLab (default)"
});
App::new()
.add_plugins((
LogDiagnosticsPlugin::default(),
FrameTimeDiagnosticsPlugin::default(),
DefaultPlugins.set(WindowPlugin {
primary_window: Some(Window {
title: "Gradient Stress Test".to_string(),
resolution: WindowResolution::new(1920.0, 1080.0),
present_mode: PresentMode::AutoNoVsync,
..default()
}),
..default()
}),
))
.insert_resource(args)
.add_systems(Startup, setup)
.add_systems(Update, animate_gradients)
.run();
}
fn setup(mut commands: Commands, args: Res<Args>) {
commands.spawn(Camera2d);
let rows_to_spawn = (args.gradient_count + COLS - 1) / COLS;
// Create a grid of gradients
commands
.spawn(Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
display: Display::Grid,
grid_template_columns: RepeatedGridTrack::flex(COLS as u16, 1.0),
grid_template_rows: RepeatedGridTrack::flex(rows_to_spawn as u16, 1.0),
..default()
})
.with_children(|parent| {
for i in 0..args.gradient_count {
let angle = (i as f32 * 10.0) % 360.0;
let mut gradient = LinearGradient::new(angle, vec![
ColorStop::new(RED, Val::Percent(0.0)),
ColorStop::new(BLUE, Val::Percent(100.0)),
]);
gradient.color_space = if args.srgb {
InterpolationColorSpace::Srgb
} else if args.hsl {
InterpolationColorSpace::Hsl
} else {
InterpolationColorSpace::OkLab
};
parent.spawn((
Node {
width: Val::Percent(100.0),
height: Val::Percent(100.0),
..default()
},
BackgroundGradient(vec![Gradient::Linear(gradient)]),
GradientNode { index: i },
));
}
});
}
#[derive(Component)]
struct GradientNode {
index: usize,
}
fn animate_gradients(
mut gradients: Query<(&mut BackgroundGradient, &GradientNode)>,
args: Res<Args>,
time: Res<Time>,
) {
if !args.animate {
return;
}
let t = time.elapsed_secs();
for (mut bg_gradient, node) in &mut gradients {
let offset = node.index as f32 * 0.01;
let hue_shift = (t + offset).sin() * 0.5 + 0.5;
if let Some(Gradient::Linear(gradient)) = bg_gradient.0.get_mut(0) {
let color1 = Color::hsl(hue_shift * 360.0, 1.0, 0.5);
let color2 = Color::hsl((hue_shift + 0.3) * 360.0 % 360.0, 1.0, 0.5);
gradient.stops = vec![
ColorStop::new(color1, Val::Percent(0.0)),
ColorStop::new(color2, Val::Percent(100.0)),
];
}
}
}