From d651e12d91f8abdf3de520d83dad15e8e0b1c94a Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 8 Jul 2025 12:10:22 +0100 Subject: [PATCH 01/12] Expanded gradients testbed ui example scene --- examples/testbed/ui.rs | 132 +++++++++++++++++++++++++++-------------- 1 file changed, 86 insertions(+), 46 deletions(-) diff --git a/examples/testbed/ui.rs b/examples/testbed/ui.rs index 4bbc8e5d770c0..e2c4be02f47e0 100644 --- a/examples/testbed/ui.rs +++ b/examples/testbed/ui.rs @@ -555,7 +555,11 @@ mod layout_rounding { } mod linear_gradient { + use bevy::color::palettes::css::BLUE; + use bevy::color::palettes::css::GREEN; + use bevy::color::palettes::css::LIME; use bevy::color::palettes::css::RED; + use bevy::color::palettes::css::WHITE; use bevy::color::palettes::css::YELLOW; use bevy::color::Color; use bevy::ecs::prelude::*; @@ -582,58 +586,94 @@ mod linear_gradient { height: Val::Percent(100.), justify_content: JustifyContent::Center, align_items: AlignItems::Center, - row_gap: Val::Px(5.), + ..default() }, DespawnOnExitState(super::Scene::LinearGradient), )) .with_children(|commands| { - for stops in [ - vec![ColorStop::auto(RED), ColorStop::auto(YELLOW)], - vec![ - ColorStop::auto(Color::BLACK), - ColorStop::auto(RED), - ColorStop::auto(Color::WHITE), - ], - ] { - for color_space in [ - InterpolationColorSpace::LinearRgb, - InterpolationColorSpace::Srgb, - InterpolationColorSpace::OkLab, - InterpolationColorSpace::OkLch, - InterpolationColorSpace::OkLchLong, - InterpolationColorSpace::Hsl, - InterpolationColorSpace::HslLong, - InterpolationColorSpace::Hsv, - InterpolationColorSpace::HsvLong, - ] { - commands.spawn(( - Node { - justify_content: JustifyContent::SpaceEvenly, - ..Default::default() - }, - children![( - Node { - height: Val::Px(30.), - width: Val::Px(300.), + commands + .spawn(Node { + column_gap: Val::Px(5.), + ..Default::default() + }) + .with_children(|commands| { + for stops in [ + vec![ColorStop::auto(RED), ColorStop::auto(YELLOW)], + vec![ + ColorStop::auto(Color::BLACK), + ColorStop::auto(RED), + ColorStop::auto(Color::WHITE), + ], + vec![ + ColorStop::auto(RED), + ColorStop::auto(RED), + ColorStop::auto(RED), + ColorStop::auto(LIME), + ColorStop::auto(LIME), + ColorStop::auto(LIME), + ColorStop::auto(LIME), + ColorStop::auto(LIME), + ColorStop::auto(BLUE), + ColorStop::auto(BLUE), + ColorStop::auto(BLUE), + ], + vec![ + ColorStop::auto(RED), + ColorStop::auto(LIME), + ColorStop::auto(BLUE), + ], + vec![ColorStop::auto(LIME), ColorStop::auto(BLUE)], + ] { + commands + .spawn(Node { + flex_direction: bevy::ui::FlexDirection::Column, + row_gap: Val::Px(5.), ..Default::default() - }, - BackgroundGradient::from(LinearGradient { - color_space, - angle: LinearGradient::TO_RIGHT, - stops: stops.clone(), - }), - children![ - Node { - position_type: PositionType::Absolute, - ..default() - }, - bevy::ui::widget::Text(format!("{color_space:?}")), - ] - )], - )); - } - } + }) + .with_children(|commands| { + for color_space in [ + InterpolationColorSpace::LinearRgb, + InterpolationColorSpace::Srgb, + InterpolationColorSpace::OkLab, + InterpolationColorSpace::OkLch, + InterpolationColorSpace::OkLchLong, + InterpolationColorSpace::Hsl, + InterpolationColorSpace::HslLong, + InterpolationColorSpace::Hsv, + InterpolationColorSpace::HsvLong, + ] { + commands.spawn(( + Node { + justify_content: JustifyContent::SpaceEvenly, + ..Default::default() + }, + children![( + Node { + height: Val::Px(30.), + width: Val::Px(250.), + ..Default::default() + }, + BackgroundGradient::from(LinearGradient { + color_space, + angle: LinearGradient::TO_RIGHT, + stops: stops.clone(), + }), + children![ + Node { + position_type: PositionType::Absolute, + ..default() + }, + bevy::ui::widget::Text(format!( + "{color_space:?}" + )), + ] + )], + )); + } + }); + } + }); }); } } From a34ef60e289d0aa9d3e9ade76d2bfeb3200eb52d Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 8 Jul 2025 12:35:38 +0100 Subject: [PATCH 02/12] Added gamma constants to gradients shader --- crates/bevy_ui_render/src/gradient.wgsl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/bevy_ui_render/src/gradient.wgsl b/crates/bevy_ui_render/src/gradient.wgsl index 54bc35eb146b8..ae3f87b06e5c5 100644 --- a/crates/bevy_ui_render/src/gradient.wgsl +++ b/crates/bevy_ui_render/src/gradient.wgsl @@ -6,6 +6,8 @@ const PI: f32 = 3.14159265358979323846; const TAU: f32 = 2. * PI; +const GAMMA: f32 = 2.2; +const INVERSE_GAMMA: f32 = 1. / GAMMA; const TEXTURED = 1u; const RIGHT_VERTEX = 2u; From 6b896da0e35777e31d7db609c6b436267829df7f Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 8 Jul 2025 13:17:39 +0100 Subject: [PATCH 03/12] Add more example caases --- examples/testbed/ui.rs | 44 ++++++++++++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/examples/testbed/ui.rs b/examples/testbed/ui.rs index e2c4be02f47e0..c4e7b613cc81b 100644 --- a/examples/testbed/ui.rs +++ b/examples/testbed/ui.rs @@ -556,10 +556,8 @@ mod layout_rounding { mod linear_gradient { use bevy::color::palettes::css::BLUE; - use bevy::color::palettes::css::GREEN; use bevy::color::palettes::css::LIME; use bevy::color::palettes::css::RED; - use bevy::color::palettes::css::WHITE; use bevy::color::palettes::css::YELLOW; use bevy::color::Color; use bevy::ecs::prelude::*; @@ -594,36 +592,62 @@ mod linear_gradient { .with_children(|commands| { commands .spawn(Node { + flex_wrap: bevy::ui::FlexWrap::Wrap, column_gap: Val::Px(5.), ..Default::default() }) .with_children(|commands| { for stops in [ + vec![ + ColorStop::new(Color::BLACK, Val::Percent(15.)), + ColorStop::new(Color::WHITE, Val::Percent(85.)), + ], vec![ColorStop::auto(RED), ColorStop::auto(YELLOW)], vec![ ColorStop::auto(Color::BLACK), ColorStop::auto(RED), ColorStop::auto(Color::WHITE), ], + vec![ + ColorStop::new(RED, Val::Percent(33.)), + ColorStop::new(LIME, Val::Percent(33.)), + ColorStop::new(LIME, Val::Percent(66.)), + ColorStop::new(BLUE, Val::Percent(66.)), + ], vec![ ColorStop::auto(RED), - ColorStop::auto(RED), - ColorStop::auto(RED), - ColorStop::auto(LIME), - ColorStop::auto(LIME), - ColorStop::auto(LIME), - ColorStop::auto(LIME), ColorStop::auto(LIME), ColorStop::auto(BLUE), + ], + vec![ColorStop::auto(LIME), ColorStop::auto(BLUE)], + vec![ + ColorStop::auto(RED), + ColorStop::auto(Color::BLACK), + ColorStop::auto(RED), + ], + vec![ ColorStop::auto(BLUE), + ColorStop::auto(Color::WHITE), ColorStop::auto(BLUE), ], vec![ - ColorStop::auto(RED), + ColorStop::auto(Color::WHITE), + ColorStop::auto(BLUE), + ColorStop::auto(Color::WHITE), ColorStop::auto(LIME), + ColorStop::auto(Color::WHITE), + ColorStop::auto(RED), + ColorStop::auto(Color::WHITE), + ], + vec![ + ColorStop::auto(Color::BLACK), ColorStop::auto(BLUE), + ColorStop::auto(Color::BLACK), + ColorStop::auto(LIME), + ColorStop::auto(Color::BLACK), + ColorStop::auto(RED), + ColorStop::auto(Color::BLACK), ], - vec![ColorStop::auto(LIME), ColorStop::auto(BLUE)], ] { commands .spawn(Node { From 5033c675f3e4a06ce381cff847c21ea48910c1ab Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 8 Jul 2025 13:39:55 +0100 Subject: [PATCH 04/12] add gamma correction to hsl and hsv conversion functions --- crates/bevy_ui_render/src/gradient.wgsl | 132 +++++++++++------------- 1 file changed, 63 insertions(+), 69 deletions(-) diff --git a/crates/bevy_ui_render/src/gradient.wgsl b/crates/bevy_ui_render/src/gradient.wgsl index ae3f87b06e5c5..48fd7abb0cbb5 100644 --- a/crates/bevy_ui_render/src/gradient.wgsl +++ b/crates/bevy_ui_render/src/gradient.wgsl @@ -116,51 +116,50 @@ 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 { +// This function converts two linear rgb colors to srgb space, mixes them, and then converts the result back to linear rgb space. +fn mix_linear_rgb_in_srgb_space(a: vec3, b: vec3, t: f32) -> vec3 { 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)); + return pow(mixed_srgb, vec3(2.2)); } -fn linear_rgba_to_oklaba(c: vec4) -> vec4 { +fn linear_rgb_to_oklab(c: vec3) -> vec3 { 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( + return vec3( 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 { +fn oklab_to_linear_rgb(c: vec3) -> vec3 { let l_ = c.x + 0.39633778 * c.y + 0.21580376 * c.z; let m_ = c.x - 0.105561346 * c.y - 0.06385417 * c.z; let s_ = c.x - 0.08948418 * c.y - 1.2914855 * c.z; let l = l_ * l_ * l_; let m = m_ * m_ * m_; let s = s_ * s_ * s_; - return vec4( + return vec3( 4.0767417 * l - 3.3077116 * m + 0.23096994 * s, -1.268438 * l + 2.6097574 * m - 0.34131938 * s, -0.0041960863 * l - 0.7034186 * m + 1.7076147 * s, - c.a ); } -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 mix_linear_rgb_in_oklab_space(a: vec3, b: vec3, t: f32) -> vec3 { + return oklab_to_linear_rgb(mix(linear_rgb_to_oklab(a), linear_rgb_to_oklab(b), t)); } -fn linear_rgba_to_hsla(c: vec4) -> vec4 { +fn linear_rgb_to_hsl(lrgb: vec3) -> vec3 { + let c = pow(lrgb, vec3(INVERSE_GAMMA)); 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); + return vec3(0., 0., l); } else { let delta = max - min; let s = delta / (1. - abs(2. * l - 1.)); @@ -173,12 +172,11 @@ fn linear_rgba_to_hsla(c: vec4) -> vec4 { h = ((c.r - c.g) / delta) + 4.; } h = h / 6.; - return vec4(h, s, l, c.a); + return vec3(h, s, l); } } - -fn hsla_to_linear_rgba(hsl: vec4) -> vec4 { +fn hsl_to_linear_rgb(hsl: vec3) -> vec3 { let h = hsl.x; let s = hsl.y; let l = hsl.z; @@ -202,10 +200,11 @@ fn hsla_to_linear_rgba(hsl: vec4) -> vec4 { r = c; g = 0.0; b = x; } let m = l - 0.5 * c; - return vec4(r + m, g + m, b + m, hsl.a); + return pow(vec3(r + m, g + m, b + m), vec3(GAMMA)); } -fn linear_rgba_to_hsva(c: vec4) -> vec4 { +fn linear_rgb_to_hsv(lrgb: vec3) -> vec3 { + let c = pow(lrgb, vec3(INVERSE_GAMMA)); let maxc = max(max(c.r, c.g), c.b); let minc = min(min(c.r, c.g), c.b); let delta = maxc - minc; @@ -226,13 +225,13 @@ fn linear_rgba_to_hsva(c: vec4) -> vec4 { h = h + 1.0; } } - return vec4(h, s, v, c.a); + return vec3(h, s, v); } -fn hsva_to_linear_rgba(hsva: vec4) -> vec4 { - let h = hsva.x * 6.0; - let s = hsva.y; - let v = hsva.z; +fn hsv_to_linear_rgb(hsv: vec3) -> vec3 { + let h = hsv.x * 6.0; + let s = hsv.y; + let v = hsv.z; let c = v * s; let x = c * (1.0 - abs(h % 2.0 - 1.0)); let m = v - c; @@ -252,21 +251,21 @@ fn hsva_to_linear_rgba(hsva: vec4) -> vec4 { } else if 5.0 <= h && h < 6.0 { r = c; g = 0.0; b = x; } - return vec4(r + m, g + m, b + m, hsva.a); + return pow(vec3(r + m, g + m, b + m), vec3(GAMMA)); } /// 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); +fn linear_rgb_to_oklch(c: vec3) -> vec3 { + let o = linear_rgb_to_oklab(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); + return vec3(o.x, chroma, rem_euclid(hue, TAU)); } -fn oklcha_to_linear_rgba(c: vec4) -> vec4 { +fn oklch_to_linear_rgb(c: vec3) -> vec3 { let a = c.y * cos(c.z); let b = c.y * sin(c.z); - return oklaba_to_linear_rgba(vec4(c.x, a, b, c.a)); + return oklab_to_linear_rgb(vec3(c.x, a, b)); } fn rem_euclid(a: f32, b: f32) -> f32 { @@ -283,37 +282,35 @@ fn lerp_hue_long(a: f32, b: f32, t: f32) -> f32 { return rem_euclid(a + (diff + select(TAU, -TAU, 0. < diff)) * t, TAU); } -fn mix_oklcha(a: vec4, b: vec4, t: f32) -> vec4 { +fn mix_oklch(a: vec3, b: vec3, t: f32) -> vec3 { let ah = select(a.z, b.z, a.y == 0.); let bh = select(b.z, a.z, b.y == 0.); - return vec4( + return vec3( 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 { +fn mix_oklch_long(a: vec3, b: vec3, t: f32) -> vec3 { let ah = select(a.z, b.z, a.y == 0.); let bh = select(b.z, a.z, b.y == 0.); - return vec4( + return vec3( mix(a.xy, b.xy, t), - lerp_hue_long(ah, bh, t), - mix(a.w, b.w, t) + lerp_hue_long(ah, bh, 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_rgb_in_oklch_space(a: vec3, b: vec3, t: f32) -> vec3 { + return oklch_to_linear_rgb(mix_oklch(linear_rgb_to_oklch(a), linear_rgb_to_oklch(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_rgb_in_oklch_space_long(a: vec3, b: vec3, t: f32) -> vec3 { + return oklch_to_linear_rgb(mix_oklch_long(linear_rgb_to_oklch(a), linear_rgb_to_oklch(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); +fn mix_linear_rgb_in_hsv_space(a: vec3, b: vec3, t: f32) -> vec3 { + let ha = linear_rgb_to_hsv(a); + let hb = linear_rgb_to_hsv(b); var h: f32; if ha.y == 0. { h = hb.x; @@ -324,38 +321,34 @@ fn mix_linear_rgba_in_hsva_space(a: vec4, b: vec4, t: f32) -> vec4(h, s, v, a_alpha)); + return hsv_to_linear_rgb(vec3(h, s, v)); } -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); +fn mix_linear_rgb_in_hsv_space_long(a: vec3, b: vec3, t: f32) -> vec3 { + let ha = linear_rgb_to_hsv(a); + let hb = linear_rgb_to_hsv(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)); + return hsv_to_linear_rgb(vec3(h, s, v)); } -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); +fn mix_linear_rgb_in_hsl_space(a: vec3, b: vec3, t: f32) -> vec3 { + let ha = linear_rgb_to_hsl(a); + let hb = linear_rgb_to_hsl(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)); + return hsl_to_linear_rgb(vec3(h, s, l)); } -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); +fn mix_linear_rgb_in_hsl_space_long(a: vec3, b: vec3, t: f32) -> vec3 { + let ha = linear_rgb_to_hsl(a); + let hb = linear_rgb_to_hsl(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)); + return hsl_to_linear_rgb(vec3(h, s, l)); } // These functions are used to calculate the distance in gradient space from the start of the gradient to the point. @@ -430,22 +423,23 @@ fn interpolate_gradient( } #ifdef IN_SRGB - return mix_linear_rgba_in_srgba_space(start_color, end_color, t); + let rgb = mix_linear_rgb_in_srgb_space(start_color.rgb, end_color.rgb, t); #else ifdef IN_OKLAB - return mix_linear_rgba_in_oklaba_space(start_color, end_color, t); + let rgb = mix_linear_rgb_in_oklab_space(start_color.rgb, end_color.rgb, t); #else ifdef IN_OKLCH - return mix_linear_rgba_in_oklcha_space(start_color, end_color, t); + let rgb = mix_linear_rgb_in_oklch_space(start_color.rgb, end_color.rgb, t); #else ifdef IN_OKLCH_LONG - return mix_linear_rgba_in_oklcha_space_long(start_color, end_color, t); + let rgb = mix_linear_rgb_in_oklch_space_long(start_color.rgb, end_color.rgb, t); #else ifdef IN_HSV - return mix_linear_rgba_in_hsva_space(start_color, end_color, t); + let rgb = mix_linear_rgb_in_hsv_space(start_color.rgb, end_color.rgb, t); #else ifdef IN_HSV_LONG - return mix_linear_rgba_in_hsva_space_long(start_color, end_color, t); + let rgb = mix_linear_rgb_in_hsv_space_long(start_color.rgb, end_color.rgb, t); #else ifdef IN_HSL - return mix_linear_rgba_in_hsla_space(start_color, end_color, t); + let rgb = mix_linear_rgb_in_hsl_space(start_color.rgb, end_color.rgb, t); #else ifdef IN_HSL_LONG - return mix_linear_rgba_in_hsla_space_long(start_color, end_color, t); + let rgb = mix_linear_rgb_in_hsl_space_long(start_color.rgb, end_color.rgb, t); #else - return mix(start_color, end_color, t); + let rgb = mix(start_color.rgb, end_color.rgb, t); #endif + return vec4(rgb, mix(start_color.a, end_color.a, t)); } From 517794a0d1bff6b0a2ab6d465fc420b73abff1df Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 8 Jul 2025 14:03:33 +0100 Subject: [PATCH 05/12] more examples --- examples/testbed/ui.rs | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/examples/testbed/ui.rs b/examples/testbed/ui.rs index c4e7b613cc81b..f867c5b31a941 100644 --- a/examples/testbed/ui.rs +++ b/examples/testbed/ui.rs @@ -609,6 +609,17 @@ mod linear_gradient { ColorStop::auto(Color::WHITE), ], vec![ + ColorStop::auto(Color::BLACK), + ColorStop::auto(LIME), + ColorStop::auto(Color::WHITE), + ], + vec![ + ColorStop::auto(Color::BLACK), + ColorStop::auto(BLUE), + ColorStop::auto(Color::WHITE), + ], + vec![ + ColorStop::auto(RED), ColorStop::new(RED, Val::Percent(33.)), ColorStop::new(LIME, Val::Percent(33.)), ColorStop::new(LIME, Val::Percent(66.)), @@ -620,16 +631,6 @@ mod linear_gradient { ColorStop::auto(BLUE), ], vec![ColorStop::auto(LIME), ColorStop::auto(BLUE)], - vec![ - ColorStop::auto(RED), - ColorStop::auto(Color::BLACK), - ColorStop::auto(RED), - ], - vec![ - ColorStop::auto(BLUE), - ColorStop::auto(Color::WHITE), - ColorStop::auto(BLUE), - ], vec![ ColorStop::auto(Color::WHITE), ColorStop::auto(BLUE), From cdd2e777ed08c7878a205eddaf3db540fa55f000 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 8 Jul 2025 14:09:34 +0100 Subject: [PATCH 06/12] use gamma constants in srgbs mix function --- crates/bevy_ui_render/src/gradient.wgsl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/bevy_ui_render/src/gradient.wgsl b/crates/bevy_ui_render/src/gradient.wgsl index 48fd7abb0cbb5..6bf0a1c4af0da 100644 --- a/crates/bevy_ui_render/src/gradient.wgsl +++ b/crates/bevy_ui_render/src/gradient.wgsl @@ -118,10 +118,10 @@ fn fragment(in: GradientVertexOutput) -> @location(0) vec4 { // This function converts two linear rgb colors to srgb space, mixes them, and then converts the result back to linear rgb space. fn mix_linear_rgb_in_srgb_space(a: vec3, b: vec3, t: f32) -> vec3 { - let a_srgb = pow(a.rgb, vec3(1. / 2.2)); - let b_srgb = pow(b.rgb, vec3(1. / 2.2)); + let a_srgb = pow(a.rgb, vec3(INVERSE_GAMMA)); + let b_srgb = pow(b.rgb, vec3(INVERSE_GAMMA)); let mixed_srgb = mix(a_srgb, b_srgb, t); - return pow(mixed_srgb, vec3(2.2)); + return pow(mixed_srgb, vec3(GAMMA)); } fn linear_rgb_to_oklab(c: vec3) -> vec3 { From 204127a5d00b0f7d57cd02e0a00615a3399885b7 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 8 Jul 2025 14:52:40 +0100 Subject: [PATCH 07/12] use more accurate gamma functions --- crates/bevy_ui_render/src/gradient.wgsl | 54 ++++++++++++++++++++----- 1 file changed, 45 insertions(+), 9 deletions(-) diff --git a/crates/bevy_ui_render/src/gradient.wgsl b/crates/bevy_ui_render/src/gradient.wgsl index 6bf0a1c4af0da..fe7b57a96e21a 100644 --- a/crates/bevy_ui_render/src/gradient.wgsl +++ b/crates/bevy_ui_render/src/gradient.wgsl @@ -6,8 +6,6 @@ const PI: f32 = 3.14159265358979323846; const TAU: f32 = 2. * PI; -const GAMMA: f32 = 2.2; -const INVERSE_GAMMA: f32 = 1. / GAMMA; const TEXTURED = 1u; const RIGHT_VERTEX = 2u; @@ -116,12 +114,50 @@ fn fragment(in: GradientVertexOutput) -> @location(0) vec4 { } } +fn gamma(value: f32) -> f32 { + if value <= 0.0 { + return value; + } + if value <= 0.04045 { + return value / 12.92; // linear falloff in dark values + } else { + return pow((value + 0.055) / 1.055, 2.4); // gamma curve in other area + } +} + +fn inverse_gamma(value: f32) -> f32 { + if value <= 0.0 { + return value; + } + + if value <= 0.0031308 { + return value * 12.92; // linear falloff in dark values + } else { + return 1.055 * pow(value, 1.0 / 2.4) - 0.055; // gamma curve in other area + } +} + +fn srgb_to_linear_rgb(color: vec3) -> vec3 { + return vec3( + gamma(color.x), + gamma(color.y), + gamma(color.z) + ); +} +fn linear_rgb_to_srgb(color: vec3) -> vec3 { + return vec3( + inverse_gamma(color.x), + inverse_gamma(color.y), + inverse_gamma(color.z) + ); +} + // This function converts two linear rgb colors to srgb space, mixes them, and then converts the result back to linear rgb space. fn mix_linear_rgb_in_srgb_space(a: vec3, b: vec3, t: f32) -> vec3 { - let a_srgb = pow(a.rgb, vec3(INVERSE_GAMMA)); - let b_srgb = pow(b.rgb, vec3(INVERSE_GAMMA)); + let a_srgb = linear_rgb_to_srgb(a); + let b_srgb = linear_rgb_to_srgb(b); let mixed_srgb = mix(a_srgb, b_srgb, t); - return pow(mixed_srgb, vec3(GAMMA)); + return srgb_to_linear_rgb(mixed_srgb); } fn linear_rgb_to_oklab(c: vec3) -> vec3 { @@ -154,7 +190,7 @@ fn mix_linear_rgb_in_oklab_space(a: vec3, b: vec3, t: f32) -> vec3) -> vec3 { - let c = pow(lrgb, vec3(INVERSE_GAMMA)); + let c = linear_rgb_to_srgb(lrgb); 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; @@ -200,11 +236,11 @@ fn hsl_to_linear_rgb(hsl: vec3) -> vec3 { r = c; g = 0.0; b = x; } let m = l - 0.5 * c; - return pow(vec3(r + m, g + m, b + m), vec3(GAMMA)); + return srgb_to_linear_rgb(vec3(r + m, g + m, b + m)); } fn linear_rgb_to_hsv(lrgb: vec3) -> vec3 { - let c = pow(lrgb, vec3(INVERSE_GAMMA)); + let c = linear_rgb_to_srgb(lrgb); let maxc = max(max(c.r, c.g), c.b); let minc = min(min(c.r, c.g), c.b); let delta = maxc - minc; @@ -251,7 +287,7 @@ fn hsv_to_linear_rgb(hsv: vec3) -> vec3 { } else if 5.0 <= h && h < 6.0 { r = c; g = 0.0; b = x; } - return pow(vec3(r + m, g + m, b + m), vec3(GAMMA)); + return srgb_to_linear_rgb(vec3(r + m, g + m, b + m)); } /// hue is left in radians and not converted to degrees From 898a93e269fdc838cae9083725f5f1716359f1d3 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 8 Jul 2025 17:47:45 +0100 Subject: [PATCH 08/12] Added hue guarding the cylindrical gradients so if the chroma or saturation nears zero the hue is no longer interpolated. --- crates/bevy_ui_render/src/gradient.wgsl | 101 ++++++++++++++++-------- examples/testbed/ui.rs | 5 +- 2 files changed, 68 insertions(+), 38 deletions(-) diff --git a/crates/bevy_ui_render/src/gradient.wgsl b/crates/bevy_ui_render/src/gradient.wgsl index fe7b57a96e21a..54a2b7101ab2d 100644 --- a/crates/bevy_ui_render/src/gradient.wgsl +++ b/crates/bevy_ui_render/src/gradient.wgsl @@ -319,20 +319,32 @@ fn lerp_hue_long(a: f32, b: f32, t: f32) -> f32 { } fn mix_oklch(a: vec3, b: vec3, t: f32) -> vec3 { - let ah = select(a.z, b.z, a.y == 0.); - let bh = select(b.z, a.z, b.y == 0.); + var h: f32; + if a.y < 0.0001 { + h = b.z; + } else if b.y < 0.0001 { + h = a.z; + } else { + h = lerp_hue(a.z, b.z, t); + } return vec3( mix(a.xy, b.xy, t), - lerp_hue(ah, bh, t), + h, ); } fn mix_oklch_long(a: vec3, b: vec3, t: f32) -> vec3 { - let ah = select(a.z, b.z, a.y == 0.); - let bh = select(b.z, a.z, b.y == 0.); + var h: f32; + if a.y < 0.0001 { + h = b.z; + } else if b.y < 0.0001 { + h = a.z; + } else { + h = lerp_hue(a.z, b.z, t); + } return vec3( mix(a.xy, b.xy, t), - lerp_hue_long(ah, bh, t) + h ); } @@ -344,46 +356,67 @@ fn mix_linear_rgb_in_oklch_space_long(a: vec3, b: vec3, t: f32) -> vec return oklch_to_linear_rgb(mix_oklch_long(linear_rgb_to_oklch(a), linear_rgb_to_oklch(b), t)); } -fn mix_linear_rgb_in_hsv_space(a: vec3, b: vec3, t: f32) -> vec3 { - let ha = linear_rgb_to_hsv(a); - let hb = linear_rgb_to_hsv(b); +fn mix_linear_rgb_in_hsv_space(la: vec3, lb: vec3, t: f32) -> vec3 { + let a = linear_rgb_to_hsv(la); + let b = linear_rgb_to_hsv(lb); var h: f32; - if ha.y == 0. { - h = hb.x; - } else if hb.y == 0. { - h = ha.x; + if a.y < 0.0001 { + h = b.x; + } else if b.y < 0.0001 { + h = a.x; } else { - h = lerp_hue(ha.x * TAU, hb.x * TAU, t) / TAU; + h = lerp_hue(a.x * TAU, b.x * TAU, t) / TAU; } - let s = mix(ha.y, hb.y, t); - let v = mix(ha.z, hb.z, t); + let s = mix(a.y, b.y, t); + let v = mix(a.z, b.z, t); return hsv_to_linear_rgb(vec3(h, s, v)); } -fn mix_linear_rgb_in_hsv_space_long(a: vec3, b: vec3, t: f32) -> vec3 { - let ha = linear_rgb_to_hsv(a); - let hb = linear_rgb_to_hsv(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); +fn mix_linear_rgb_in_hsv_space_long(la: vec3, lb: vec3, t: f32) -> vec3 { + let a = linear_rgb_to_hsv(la); + let b = linear_rgb_to_hsv(lb); + var h: f32; + if a.y < 0.0001 { + h = b.z; + } else if b.y < 0.0001 { + h = a.z; + } else { + h = lerp_hue_long(a.x * TAU, b.x * TAU, t) / TAU; + } + let s = mix(a.y, b.y, t); + let v = mix(a.z, b.z, t); return hsv_to_linear_rgb(vec3(h, s, v)); } -fn mix_linear_rgb_in_hsl_space(a: vec3, b: vec3, t: f32) -> vec3 { - let ha = linear_rgb_to_hsl(a); - let hb = linear_rgb_to_hsl(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); +fn mix_linear_rgb_in_hsl_space(la: vec3, lb: vec3, t: f32) -> vec3 { + let a = linear_rgb_to_hsl(la); + let b = linear_rgb_to_hsl(lb); + var h: f32; + if a.y < 0.0001 { + h = b.x; + } else if b.y < 0.0001 { + h = a.x; + } else { + h = lerp_hue(a.x * TAU, b.x * TAU, t) / TAU; + } + let s = mix(a.y, b.y, t); + let l = mix(a.z, b.z, t); return hsl_to_linear_rgb(vec3(h, s, l)); } -fn mix_linear_rgb_in_hsl_space_long(a: vec3, b: vec3, t: f32) -> vec3 { - let ha = linear_rgb_to_hsl(a); - let hb = linear_rgb_to_hsl(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); +fn mix_linear_rgb_in_hsl_space_long(la: vec3, lb: vec3, t: f32) -> vec3 { + let a = linear_rgb_to_hsl(la); + let b = linear_rgb_to_hsl(lb); + var h: f32; + if a.y < 0.0001 { + h = b.x; + } else if b.y < 0.0001 { + h = a.x; + } else { + h = lerp_hue_long(a.x * TAU, b.x * TAU, t) / TAU; + } + let s = mix(a.y, b.y, t); + let l = mix(a.z, b.z, t); return hsl_to_linear_rgb(vec3(h, s, l)); } diff --git a/examples/testbed/ui.rs b/examples/testbed/ui.rs index f867c5b31a941..50c001fca3f72 100644 --- a/examples/testbed/ui.rs +++ b/examples/testbed/ui.rs @@ -598,10 +598,7 @@ mod linear_gradient { }) .with_children(|commands| { for stops in [ - vec![ - ColorStop::new(Color::BLACK, Val::Percent(15.)), - ColorStop::new(Color::WHITE, Val::Percent(85.)), - ], + vec![ColorStop::auto(BLUE), ColorStop::auto(Color::WHITE)], vec![ColorStop::auto(RED), ColorStop::auto(YELLOW)], vec![ ColorStop::auto(Color::BLACK), From 908ccd5e346b5aadd1f9f751fd2b6e3c16648b3e Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 8 Jul 2025 17:59:37 +0100 Subject: [PATCH 09/12] Added row_gap to example layout. --- examples/testbed/ui.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/testbed/ui.rs b/examples/testbed/ui.rs index 50c001fca3f72..ef2b23b978cbb 100644 --- a/examples/testbed/ui.rs +++ b/examples/testbed/ui.rs @@ -593,6 +593,7 @@ mod linear_gradient { commands .spawn(Node { flex_wrap: bevy::ui::FlexWrap::Wrap, + row_gap: Val::Px(5.), column_gap: Val::Px(5.), ..Default::default() }) From 3df4d3f95f4aec43cbb540b88cf8c8b325bc860f Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 8 Jul 2025 18:18:13 +0100 Subject: [PATCH 10/12] Added HUE_GUARD constant --- crates/bevy_ui_render/src/gradient.wgsl | 27 ++++++++++++++----------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/crates/bevy_ui_render/src/gradient.wgsl b/crates/bevy_ui_render/src/gradient.wgsl index 54a2b7101ab2d..0548816097de6 100644 --- a/crates/bevy_ui_render/src/gradient.wgsl +++ b/crates/bevy_ui_render/src/gradient.wgsl @@ -6,6 +6,7 @@ const PI: f32 = 3.14159265358979323846; const TAU: f32 = 2. * PI; +const HUE_GUARD: f32 = 0.0001; const TEXTURED = 1u; const RIGHT_VERTEX = 2u; @@ -114,6 +115,7 @@ fn fragment(in: GradientVertexOutput) -> @location(0) vec4 { } } +// https://en.wikipedia.org/wiki/SRGB fn gamma(value: f32) -> f32 { if value <= 0.0 { return value; @@ -125,6 +127,7 @@ fn gamma(value: f32) -> f32 { } } +// https://en.wikipedia.org/wiki/SRGB fn inverse_gamma(value: f32) -> f32 { if value <= 0.0 { return value; @@ -320,9 +323,9 @@ fn lerp_hue_long(a: f32, b: f32, t: f32) -> f32 { fn mix_oklch(a: vec3, b: vec3, t: f32) -> vec3 { var h: f32; - if a.y < 0.0001 { + if a.y < HUE_GUARD { h = b.z; - } else if b.y < 0.0001 { + } else if b.y < HUE_GUARD { h = a.z; } else { h = lerp_hue(a.z, b.z, t); @@ -335,9 +338,9 @@ fn mix_oklch(a: vec3, b: vec3, t: f32) -> vec3 { fn mix_oklch_long(a: vec3, b: vec3, t: f32) -> vec3 { var h: f32; - if a.y < 0.0001 { + if a.y < HUE_GUARD { h = b.z; - } else if b.y < 0.0001 { + } else if b.y < HUE_GUARD { h = a.z; } else { h = lerp_hue(a.z, b.z, t); @@ -360,9 +363,9 @@ fn mix_linear_rgb_in_hsv_space(la: vec3, lb: vec3, t: f32) -> vec3, lb: vec3, t: f32) -> vec let a = linear_rgb_to_hsv(la); let b = linear_rgb_to_hsv(lb); var h: f32; - if a.y < 0.0001 { + if a.y < HUE_GUARD { h = b.z; - } else if b.y < 0.0001 { + } else if b.y < HUE_GUARD { h = a.z; } else { h = lerp_hue_long(a.x * TAU, b.x * TAU, t) / TAU; @@ -392,9 +395,9 @@ fn mix_linear_rgb_in_hsl_space(la: vec3, lb: vec3, t: f32) -> vec3, lb: vec3, t: f32) -> vec let a = linear_rgb_to_hsl(la); let b = linear_rgb_to_hsl(lb); var h: f32; - if a.y < 0.0001 { + if a.y < HUE_GUARD { h = b.x; - } else if b.y < 0.0001 { + } else if b.y < HUE_GUARD { h = a.x; } else { h = lerp_hue_long(a.x * TAU, b.x * TAU, t) / TAU; From 34d94e0b560c3023ce2438321efc99879f872493 Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 8 Jul 2025 18:31:49 +0100 Subject: [PATCH 11/12] Add hue guard explanation --- crates/bevy_ui_render/src/gradient.wgsl | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/bevy_ui_render/src/gradient.wgsl b/crates/bevy_ui_render/src/gradient.wgsl index 0548816097de6..41d17cf829fdb 100644 --- a/crates/bevy_ui_render/src/gradient.wgsl +++ b/crates/bevy_ui_render/src/gradient.wgsl @@ -323,6 +323,10 @@ fn lerp_hue_long(a: f32, b: f32, t: f32) -> f32 { fn mix_oklch(a: vec3, b: vec3, t: f32) -> vec3 { var h: f32; + + // If the chroma is close to zero for one of the endpoints, don't interpolate + // the hue and instead use the hue of the other endpoint. This allows gradients that smoothly + // transition from black or white to a target color without passing through unrelated hues. if a.y < HUE_GUARD { h = b.z; } else if b.y < HUE_GUARD { @@ -363,6 +367,10 @@ fn mix_linear_rgb_in_hsv_space(la: vec3, lb: vec3, t: f32) -> vec3, lb: vec3, t: f32) -> vec3, lb: vec3, t: f32) -> vec let a = linear_rgb_to_hsl(la); let b = linear_rgb_to_hsl(lb); var h: f32; + if a.y < HUE_GUARD { h = b.x; } else if b.y < HUE_GUARD { From d18dd7a475c29d80ce072188bdad60278ee88ecc Mon Sep 17 00:00:00 2001 From: ickshonpe Date: Tue, 8 Jul 2025 18:58:26 +0100 Subject: [PATCH 12/12] Unbreak oklch long paths --- crates/bevy_ui_render/src/gradient.wgsl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_ui_render/src/gradient.wgsl b/crates/bevy_ui_render/src/gradient.wgsl index 41d17cf829fdb..a7062ee561d6a 100644 --- a/crates/bevy_ui_render/src/gradient.wgsl +++ b/crates/bevy_ui_render/src/gradient.wgsl @@ -347,7 +347,7 @@ fn mix_oklch_long(a: vec3, b: vec3, t: f32) -> vec3 { } else if b.y < HUE_GUARD { h = a.z; } else { - h = lerp_hue(a.z, b.z, t); + h = lerp_hue_long(a.z, b.z, t); } return vec3( mix(a.xy, b.xy, t),