diff --git a/crates/bevy_ui_render/src/gradient.wgsl b/crates/bevy_ui_render/src/gradient.wgsl index 54bc35eb146b8..a7062ee561d6a 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,51 +115,90 @@ 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)); +// 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) + ); +} + +// 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 = linear_rgb_to_srgb(a); + let b_srgb = linear_rgb_to_srgb(b); let mixed_srgb = mix(a_srgb, b_srgb, t); - return vec4(pow(mixed_srgb, vec3(2.2)), mix(a.a, b.a, t)); + return srgb_to_linear_rgb(mixed_srgb); } -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 = 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; 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.)); @@ -171,12 +211,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; @@ -200,10 +239,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 srgb_to_linear_rgb(vec3(r + m, g + m, b + m)); } -fn linear_rgba_to_hsva(c: vec4) -> vec4 { +fn linear_rgb_to_hsv(lrgb: vec3) -> vec3 { + 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; @@ -224,13 +264,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; @@ -250,21 +290,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 srgb_to_linear_rgb(vec3(r + m, g + m, b + m)); } /// 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 { @@ -281,79 +321,116 @@ 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 { - let ah = select(a.z, b.z, a.y == 0.); - let bh = select(b.z, a.z, b.y == 0.); - return vec4( +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 { + 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), - mix(a.a, b.a, t) + h, ); } -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( +fn mix_oklch_long(a: vec3, b: vec3, t: f32) -> vec3 { + var h: f32; + if a.y < HUE_GUARD { + h = b.z; + } else if b.y < HUE_GUARD { + h = a.z; + } else { + h = lerp_hue_long(a.z, b.z, t); + } + return vec3( mix(a.xy, b.xy, t), - lerp_hue_long(ah, bh, t), - mix(a.w, b.w, t) + h ); } -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(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 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. + if a.y < HUE_GUARD { + h = b.x; + } else if b.y < HUE_GUARD { + h = a.x; + } else { + h = lerp_hue(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_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 < HUE_GUARD { + h = b.z; + } else if b.y < HUE_GUARD { + 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(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 < HUE_GUARD { + h = b.x; + } else if b.y < HUE_GUARD { + 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(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 < HUE_GUARD { + h = b.x; + } else if b.y < HUE_GUARD { + h = a.x; } else { - h = lerp_hue(ha.x * TAU, hb.x * TAU, t) / TAU; + h = lerp_hue_long(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 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)); + 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)); } // These functions are used to calculate the distance in gradient space from the start of the gradient to the point. @@ -428,22 +505,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)); } diff --git a/examples/testbed/ui.rs b/examples/testbed/ui.rs index 4bbc8e5d770c0..ef2b23b978cbb 100644 --- a/examples/testbed/ui.rs +++ b/examples/testbed/ui.rs @@ -555,6 +555,8 @@ 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; @@ -582,58 +584,119 @@ 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 { + flex_wrap: bevy::ui::FlexWrap::Wrap, + row_gap: Val::Px(5.), + column_gap: Val::Px(5.), + ..Default::default() + }) + .with_children(|commands| { + for stops in [ + vec![ColorStop::auto(BLUE), ColorStop::auto(Color::WHITE)], + vec![ColorStop::auto(RED), ColorStop::auto(YELLOW)], + vec![ + ColorStop::auto(Color::BLACK), + ColorStop::auto(RED), + 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.)), + ColorStop::new(BLUE, Val::Percent(66.)), + ], + vec![ + ColorStop::auto(RED), + ColorStop::auto(LIME), + ColorStop::auto(BLUE), + ], + vec![ColorStop::auto(LIME), ColorStop::auto(BLUE)], + vec![ + 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), + ], + ] { + 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:?}" + )), + ] + )], + )); + } + }); + } + }); }); } }