diff --git a/Cargo.toml b/Cargo.toml index b1cfe08c05054..0b64a82ed330a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/crates/bevy_ui_render/src/gradient.rs b/crates/bevy_ui_render/src/gradient.rs index a2ff3a0dee795..b5b199edf2f87 100644 --- a/crates/bevy_ui_render/src/gradient.rs +++ b/crates/bevy_ui_render/src/gradient.rs @@ -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,44 @@ struct UiGradientVertex { hint: f32, } +fn convert_color_to_space(color: LinearRgba, space: InterpolationColorSpace) -> [f32; 4] { + match space { + InterpolationColorSpace::Oklaba => { + let oklaba: Oklaba = color.into(); + [oklaba.lightness, oklaba.a, oklaba.b, oklaba.alpha] + } + InterpolationColorSpace::Oklcha | InterpolationColorSpace::OklchaLong => { + let oklcha: Oklcha = color.into(); + [ + oklcha.lightness, + oklcha.chroma, + oklcha.hue.to_radians(), + oklcha.alpha, + ] + } + InterpolationColorSpace::Srgba => { + let srgba: Srgba = color.into(); + [srgba.red, srgba.green, srgba.blue, srgba.alpha] + } + InterpolationColorSpace::LinearRgba => color.to_f32_array(), + InterpolationColorSpace::Hsla | InterpolationColorSpace::HslaLong => { + let hsla: Hsla = color.into(); + // Normalize hue to 0..1 range for shader + [ + hsla.hue / 360.0, + hsla.saturation, + hsla.lightness, + hsla.alpha, + ] + } + InterpolationColorSpace::Hsva | InterpolationColorSpace::HsvaLong => { + 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, @@ -804,8 +842,9 @@ 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) diff --git a/crates/bevy_ui_render/src/gradient.wgsl b/crates/bevy_ui_render/src/gradient.wgsl index 54bc35eb146b8..54a3639dee43e 100644 --- a/crates/bevy_ui_render/src/gradient.wgsl +++ b/crates/bevy_ui_render/src/gradient.wgsl @@ -114,26 +114,6 @@ fn fragment(in: GradientVertexOutput) -> @location(0) vec4 { } } -// 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, b: vec4, t: f32) -> vec4 { - 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) -> vec4 { - 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) -> vec4 { 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) -> vec4 { ); } -fn mix_linear_rgba_in_oklaba_space(a: vec4, b: vec4, t: f32) -> vec4 { - return oklaba_to_linear_rgba(mix(linear_rgba_to_oklaba(a), linear_rgba_to_oklaba(b), t)); -} - -fn linear_rgba_to_hsla(c: vec4) -> vec4 { - 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(h, s, l, c.a); - } -} - - fn hsla_to_linear_rgba(hsl: vec4) -> vec4 { let h = hsl.x; let s = hsl.y; @@ -203,30 +156,6 @@ fn hsla_to_linear_rgba(hsl: vec4) -> vec4 { return vec4(r + m, g + m, b + m, hsl.a); } -fn linear_rgba_to_hsva(c: vec4) -> vec4 { - 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(h, s, v, c.a); -} - fn hsva_to_linear_rgba(hsva: vec4) -> vec4 { let h = hsva.x * 6.0; let s = hsva.y; @@ -253,14 +182,6 @@ fn hsva_to_linear_rgba(hsva: vec4) -> vec4 { return vec4(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) -> vec4 { - 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) -> vec4 { 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, b: vec4, t: f32) -> vec4 { - 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, b: vec4, t: f32) -> vec4 { - 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, b: vec4, t: f32) -> vec4 { - 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, b: vec4, t: f32) -> vec4 { - 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, b: vec4, t: f32) -> vec4 { - 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(h, s, v, a_alpha)); -} - -fn mix_linear_rgba_in_hsva_space_long(a: vec4, b: vec4, t: f32) -> vec4 { - 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(h, s, v, a_alpha)); -} - -fn mix_linear_rgba_in_hsla_space(a: vec4, b: vec4, t: f32) -> vec4 { - 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(h, s, l, a_alpha)); -} - -fn mix_linear_rgba_in_hsla_space_long(a: vec4, b: vec4, t: f32) -> vec4 { - 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(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,105 @@ fn conic_distance( return (((angle - start) % TAU) + TAU) % TAU; } +fn mix_oklcha(a: vec4, b: vec4, t: f32) -> vec4 { + 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, b: vec4, t: f32) -> vec4 { + 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, b: vec4, t: f32) -> vec4 { + return vec4( + fract(a.x + (fract(b.x - a.x + 0.5) - 0.5) * t), + mix(a.y, b.y, t), + mix(a.z, b.z, t), + mix(a.w, b.w, t) + ); +} + +fn mix_hsla_long(a: vec4, b: vec4, t: f32) -> vec4 { + let d = fract(b.x - a.x + 0.5) - 0.5; + return vec4( + fract(a.x + (d + select(1., -1., 0. < d)) * t), + mix(a.y, b.y, t), + mix(a.z, b.z, t), + mix(a.w, b.w, t) + ); +} + +fn mix_hsva(a: vec4, b: vec4, t: f32) -> vec4 { + 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, b: vec4, t: f32) -> vec4 { + 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, @@ -397,10 +333,10 @@ fn interpolate_gradient( ) -> vec4 { if start_distance == end_distance { if distance <= start_distance && enabled(flags, FILL_START) { - return start_color; + return convert_to_linear_rgba(start_color); } if start_distance <= distance && enabled(flags, FILL_END) { - return end_color; + return convert_to_linear_rgba(end_color); } return vec4(0.); } @@ -409,14 +345,14 @@ fn interpolate_gradient( if t < 0.0 { if enabled(flags, FILL_START) { - return start_color; + return convert_to_linear_rgba(start_color); } return vec4(0.0); } if 1. < t { if enabled(flags, FILL_END) { - return end_color; + return convert_to_linear_rgba(end_color); } return vec4(0.0); } @@ -426,24 +362,56 @@ 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); + + return convert_to_linear_rgba(mix_colors(start_color, end_color, t)); +} + +// Mix the colors, choosing the appropriate interpolation method for the given color space +fn mix_colors( + start_color: vec4, + end_color: vec4, + t: f32, +) -> vec4 { +#ifdef IN_OKLCH + return 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 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 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 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 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 mix_hsla_long(start_color, end_color, t); #else + // Just lerp in linear RGBA, OkLab and SRGBA spaces return mix(start_color, end_color, t); #endif } + +// Convert a color from the interpolation color space to linear rgba +fn convert_to_linear_rgba( + color: vec4, +) -> vec4 { +#ifdef IN_OKLCH + return oklcha_to_linear_rgba(color); +#else ifdef IN_OKLCH_LONG + return oklcha_to_linear_rgba(color); +#else ifdef IN_HSV + return hsva_to_linear_rgba(color); +#else ifdef IN_HSV_LONG + return hsva_to_linear_rgba(color); +#else ifdef IN_HSL + return hsla_to_linear_rgba(color); +#else ifdef IN_HSL_LONG + return hsla_to_linear_rgba(color); +#else ifdef IN_OKLAB + return oklaba_to_linear_rgba(color); +#else ifdef IN_SRGB + return vec4(pow(color.rgb, vec3(2.2)), color.a); +#else + // Color is already in linear rgba space + return color; +#endif +} diff --git a/examples/README.md b/examples/README.md index aabe0199054b2..29420e66c53ac 100644 --- a/examples/README.md +++ b/examples/README.md @@ -510,6 +510,7 @@ Example | Description [Many Foxes](../examples/stress_tests/many_foxes.rs) | Loads an animated fox model and spawns lots of them. Good for testing skinned mesh performance. Takes an unsigned integer argument for the number of foxes to spawn. Defaults to 1000 [Many Gizmos](../examples/stress_tests/many_gizmos.rs) | Test rendering of many gizmos [Many Glyphs](../examples/stress_tests/many_glyphs.rs) | Simple benchmark to test text rendering. +[Many Gradients](../examples/stress_tests/many_gradients.rs) | Stress test for gradient rendering performance [Many Lights](../examples/stress_tests/many_lights.rs) | Simple benchmark to test rendering many point lights. Run with `WGPU_SETTINGS_PRIO=webgl2` to restrict to uniform buffers and max 256 lights [Many Sprites](../examples/stress_tests/many_sprites.rs) | Displays many sprites in a grid arrangement! Used for performance testing. Use `--colored` to enable color tinted sprites. [Many Text2d](../examples/stress_tests/many_text2d.rs) | Displays many Text2d! Used for performance testing. diff --git a/examples/stress_tests/many_gradients.rs b/examples/stress_tests/many_gradients.rs new file mode 100644 index 0000000000000..1c96ac6b12373 --- /dev/null +++ b/examples/stress_tests/many_gradients.rs @@ -0,0 +1,180 @@ +//! 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}, + math::ops::sin, + prelude::*, + ui::{ + BackgroundGradient, ColorStop, Display, Gradient, InterpolationColorSpace, LinearGradient, + RepeatedGridTrack, + }, + 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 {total_gradients} 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) { + commands.spawn(Camera2d); + + let rows_to_spawn = args.gradient_count.div_ceil(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)), + ColorStop::new(GREEN, Val::Percent(20.0)), + ColorStop::new(YELLOW, Val::Percent(40.0)), + ColorStop::new(ORANGE, Val::Percent(60.0)), + ColorStop::new(LIME, Val::Percent(80.0)), + ColorStop::new(DARK_CYAN, Val::Percent(90.0)), + ], + ); + + gradient.color_space = if args.srgb { + InterpolationColorSpace::Srgba + } else if args.hsl { + InterpolationColorSpace::Hsla + } else { + InterpolationColorSpace::Oklaba + }; + + 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, + time: Res