diff --git a/crates/bevy_ui_render/src/gradient.rs b/crates/bevy_ui_render/src/gradient.rs index b5b199edf2f87..705ed5c14b522 100644 --- a/crates/bevy_ui_render/src/gradient.rs +++ b/crates/bevy_ui_render/src/gradient.rs @@ -665,7 +665,8 @@ fn convert_color_to_space(color: LinearRgba, space: InterpolationColorSpace) -> [ oklcha.lightness, oklcha.chroma, - oklcha.hue.to_radians(), + // The shader expects normalized hues + oklcha.hue / 360., oklcha.alpha, ] } @@ -676,18 +677,13 @@ fn convert_color_to_space(color: LinearRgba, space: InterpolationColorSpace) -> 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, - ] + // The shader expects normalized hues + [hsla.hue / 360., 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] + // The shader expects normalized hues + [hsva.hue / 360., hsva.saturation, hsva.value, hsva.alpha] } } } diff --git a/crates/bevy_ui_render/src/gradient.wgsl b/crates/bevy_ui_render/src/gradient.wgsl index 54a3639dee43e..c36db3abb5895 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,22 +115,62 @@ fn fragment(in: GradientVertexOutput) -> @location(0) vec4 { } } -fn oklaba_to_linear_rgba(c: vec4) -> vec4 { +// https://en.wikipedia.org/wiki/SRGB +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 + } +} + +// https://en.wikipedia.org/wiki/SRGB +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) + ); +} + +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 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; @@ -153,10 +194,10 @@ 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 srgb_to_linear_rgb(vec3(r + m, g + m, b + m)); } -fn hsva_to_linear_rgba(hsva: vec4) -> vec4 { +fn hsv_to_linear_rgb(hsva: vec3) -> vec3 { let h = hsva.x * 6.0; let s = hsva.y; let v = hsva.z; @@ -179,13 +220,12 @@ 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 srgb_to_linear_rgb(vec3(r + m, g + m, b + m)); } -fn oklcha_to_linear_rgba(c: vec4) -> vec4 { - 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)); +fn oklch_to_linear_rgb(c: vec3) -> vec3 { + let hue = c.z * TAU; + return oklab_to_linear_rgb(vec3(c.x, c.y * cos(hue), c.y * sin(hue))); } fn rem_euclid(a: f32, b: f32) -> f32 { @@ -223,102 +263,149 @@ 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 { +fn mix_oklch(a: vec3, b: vec3, t: f32) -> vec3 { + // 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. + var h = a.z; + var g = b.z; + if a.y < HUE_GUARD { + h = g; + } else if b.y < HUE_GUARD { + g = h; + } + + let hue_diff = g - h; + if abs(hue_diff) > 0.5 { if hue_diff > 0.0 { - adjusted_hue = a.z + (hue_diff - TAU) * t; + h += (hue_diff - 1.) * t; } else { - adjusted_hue = a.z + (hue_diff + TAU) * t; + h += (hue_diff + 1.) * t; } } else { - adjusted_hue = a.z + hue_diff * t; + h += hue_diff * t; } - return vec4( + return vec3( mix(a.x, b.x, t), mix(a.y, b.y, t), - rem_euclid(adjusted_hue, TAU), - mix(a.w, b.w, t) + fract(h), ); } -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 { +fn mix_oklch_long(a: vec3, b: vec3, t: f32) -> vec3 { + var h = a.z; + var g = b.z; + if a.y < HUE_GUARD { + h = g; + } else if b.y < HUE_GUARD { + g = h; + } + + let hue_diff = g - h; + if abs(hue_diff) < 0.5 { if hue_diff >= 0.0 { - adjusted_hue = a.z + (hue_diff - TAU) * t; + h += (hue_diff - 1.) * t; } else { - adjusted_hue = a.z + (hue_diff + TAU) * t; + h += (hue_diff + 1.) * t; } } else { - adjusted_hue = a.z + hue_diff * t; + h += hue_diff * t; } - return vec4( + return vec3( mix(a.x, b.x, t), mix(a.y, b.y, t), - rem_euclid(adjusted_hue, TAU), - mix(a.w, b.w, t) + fract(h), ); } -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), +fn mix_hsl(a: vec3, b: vec3, t: f32) -> vec3 { + // If the saturation 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. + var h = a.x; + var g = b.x; + if a.y < HUE_GUARD { + h = g; + } else if b.y < HUE_GUARD { + g = h; + } + + return vec3( + fract(h + (fract(g - h + 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), +fn mix_hsl_long(a: vec3, b: vec3, t: f32) -> vec3 { + var h = a.x; + var g = b.x; + if a.y < HUE_GUARD { + h = g; + } else if b.y < HUE_GUARD { + g = h; + } + + let d = fract(g - h + 0.5) - 0.5; + return vec3( + fract(h + (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; +fn mix_hsv(a: vec3, b: vec3, t: f32) -> vec3 { + // If the saturation 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. + var h = a.x; + var g = b.x; + if a.y < HUE_GUARD { + h = g; + } else if b.y < HUE_GUARD { + g = h; + } + + let hue_diff = g - h; if abs(hue_diff) > 0.5 { if hue_diff > 0.0 { - adjusted_hue = a.x + (hue_diff - 1.0) * t; + h += (hue_diff - 1.0) * t; } else { - adjusted_hue = a.x + (hue_diff + 1.0) * t; + h += (hue_diff + 1.0) * t; } } else { - adjusted_hue = a.x + hue_diff * t; + h += hue_diff * t; } - return vec4( - fract(adjusted_hue), + return vec3( + fract(h), 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; +fn mix_hsv_long(a: vec3, b: vec3, t: f32) -> vec3 { + var h = a.x; + var g = b.x; + if a.y < HUE_GUARD { + h = g; + } else if b.y < HUE_GUARD { + g = h; + } + + let hue_diff = g - h; if abs(hue_diff) < 0.5 { if hue_diff >= 0.0 { - adjusted_hue = a.x + (hue_diff - 1.0) * t; + h += (hue_diff - 1.0) * t; } else { - adjusted_hue = a.x + (hue_diff + 1.0) * t; + h += (hue_diff + 1.0) * t; } } else { - adjusted_hue = a.x + hue_diff * t; + h += hue_diff * t; } - return vec4( - fract(adjusted_hue), + return vec3( + fract(h), mix(a.y, b.y, t), mix(a.z, b.z, t), - mix(a.w, b.w, t) ); } @@ -363,27 +450,27 @@ fn interpolate_gradient( t = 0.5 * (1 + (t - hint) / (1.0 - hint)); } - return convert_to_linear_rgba(mix_colors(start_color, end_color, t)); + return convert_to_linear_rgba(vec4(mix_colors(start_color.xyz, end_color.xyz, t), mix(start_color.a, end_color.a, t))); } // Mix the colors, choosing the appropriate interpolation method for the given color space fn mix_colors( - start_color: vec4, - end_color: vec4, + start_color: vec3, + end_color: vec3, t: f32, -) -> vec4 { +) -> vec3 { #ifdef IN_OKLCH - return mix_oklcha(start_color, end_color, t); + return mix_oklch(start_color, end_color, t); #else ifdef IN_OKLCH_LONG - return mix_oklcha_long(start_color, end_color, t); + return mix_oklch_long(start_color, end_color, t); #else ifdef IN_HSV - return mix_hsva(start_color, end_color, t); + return mix_hsv(start_color, end_color, t); #else ifdef IN_HSV_LONG - return mix_hsva_long(start_color, end_color, t); + return mix_hsv_long(start_color, end_color, t); #else ifdef IN_HSL - return mix_hsla(start_color, end_color, t); + return mix_hsl(start_color, end_color, t); #else ifdef IN_HSL_LONG - return mix_hsla_long(start_color, end_color, t); + return mix_hsl_long(start_color, end_color, t); #else // Just lerp in linear RGBA, OkLab and SRGBA spaces return mix(start_color, end_color, t); @@ -395,23 +482,24 @@ fn convert_to_linear_rgba( color: vec4, ) -> vec4 { #ifdef IN_OKLCH - return oklcha_to_linear_rgba(color); + let rgb = oklch_to_linear_rgb(color.xyz); #else ifdef IN_OKLCH_LONG - return oklcha_to_linear_rgba(color); + let rgb = oklch_to_linear_rgb(color.xyz); #else ifdef IN_HSV - return hsva_to_linear_rgba(color); + let rgb = hsv_to_linear_rgb(color.xyz); #else ifdef IN_HSV_LONG - return hsva_to_linear_rgba(color); + let rgb = hsv_to_linear_rgb(color.xyz); #else ifdef IN_HSL - return hsla_to_linear_rgba(color); + let rgb = hsl_to_linear_rgb(color.xyz); #else ifdef IN_HSL_LONG - return hsla_to_linear_rgba(color); + let rgb = hsl_to_linear_rgb(color.xyz); #else ifdef IN_OKLAB - return oklaba_to_linear_rgba(color); + let rgb = oklab_to_linear_rgb(color.xyz); #else ifdef IN_SRGB - return vec4(pow(color.rgb, vec3(2.2)), color.a); + let rgb = pow(color.xyz, vec3(2.2)); #else // Color is already in linear rgba space - return color; + let rgb = color.rgb; #endif + return vec4(rgb, color.a); } diff --git a/examples/testbed/ui.rs b/examples/testbed/ui.rs index 5d23ba9cf57ee..5961b67834b3a 100644 --- a/examples/testbed/ui.rs +++ b/examples/testbed/ui.rs @@ -555,15 +555,19 @@ mod layout_rounding { } mod linear_gradient { + use bevy::color::palettes::css::BLUE; + use bevy::color::palettes::css::LIME; use bevy::color::palettes::css::RED; use bevy::color::palettes::css::YELLOW; use bevy::color::Color; use bevy::ecs::prelude::*; use bevy::render::camera::Camera2d; use bevy::state::state_scoped::DespawnOnExitState; + use bevy::text::TextFont; use bevy::ui::AlignItems; use bevy::ui::BackgroundGradient; use bevy::ui::ColorStop; + use bevy::ui::GridPlacement; use bevy::ui::InterpolationColorSpace; use bevy::ui::JustifyContent; use bevy::ui::LinearGradient; @@ -588,52 +592,92 @@ mod linear_gradient { 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::LinearRgba, - InterpolationColorSpace::Srgba, - InterpolationColorSpace::Oklaba, - InterpolationColorSpace::Oklcha, - InterpolationColorSpace::OklchaLong, - InterpolationColorSpace::Hsla, - InterpolationColorSpace::HslaLong, - InterpolationColorSpace::Hsva, - InterpolationColorSpace::HsvaLong, - ] { - commands.spawn(( - Node { - justify_content: JustifyContent::SpaceEvenly, - ..Default::default() - }, - children![( - Node { - height: Val::Px(30.), - width: Val::Px(300.), - ..Default::default() - }, - BackgroundGradient::from(LinearGradient { - color_space, - angle: LinearGradient::TO_RIGHT, - stops: stops.clone(), - }), - children![ + let mut i = 0; + commands + .spawn(Node { + display: bevy::ui::Display::Grid, + row_gap: Val::Px(4.), + column_gap: Val::Px(4.), + ..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![ + Color::hsl(180.71191, 0.0, 0.3137255).into(), + Color::hsl(180.71191, 0.5, 0.3137255).into(), + Color::hsl(180.71191, 1.0, 0.3137255).into(), + ], + vec![ + Color::hsl(180.71191, 0.825, 0.0).into(), + Color::hsl(180.71191, 0.825, 0.5).into(), + Color::hsl(180.71191, 0.825, 1.0).into(), + ], + vec![ + Color::hsl(0.0 + 0.0001, 1.0, 0.5).into(), + Color::hsl(180.0, 1.0, 0.5).into(), + Color::hsl(360.0 - 0.0001, 1.0, 0.5).into(), + ], + vec![ + Color::WHITE.into(), + RED.into(), + LIME.into(), + BLUE.into(), + Color::BLACK.into(), + ], + ] { + for color_space in [ + InterpolationColorSpace::LinearRgba, + InterpolationColorSpace::Srgba, + InterpolationColorSpace::Oklaba, + InterpolationColorSpace::Oklcha, + InterpolationColorSpace::OklchaLong, + InterpolationColorSpace::Hsla, + InterpolationColorSpace::HslaLong, + InterpolationColorSpace::Hsva, + InterpolationColorSpace::HsvaLong, + ] { + let row = i % 18 + 1; + let column = i / 18 + 1; + i += 1; + + commands.spawn(( Node { - position_type: PositionType::Absolute, - ..default() + grid_row: GridPlacement::start(row as i16 + 1), + grid_column: GridPlacement::start(column as i16 + 1), + justify_content: JustifyContent::SpaceEvenly, + ..Default::default() }, - bevy::ui::widget::Text(format!("{color_space:?}")), - ] - )], - )); - } - } + children![( + Node { + height: Val::Px(30.), + width: Val::Px(300.), + justify_content: JustifyContent::Center, + ..Default::default() + }, + BackgroundGradient::from(LinearGradient { + color_space, + angle: LinearGradient::TO_RIGHT, + stops: stops.clone(), + }), + children![ + Node { + position_type: PositionType::Absolute, + ..default() + }, + TextFont::from_font_size(10.), + bevy::ui::widget::Text(format!("{color_space:?}")), + ] + )], + )); + } + } + }); }); } }