Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_ui_render/src/gradient.wgsl
6596 views
#import bevy_render::view::View
#import bevy_ui::ui_node::{
    draw_uinode_background,
    draw_uinode_border,
}

const PI: f32 = 3.14159265358979323846;
const TAU: f32 = 2. * PI;
const HUE_GUARD: f32 = 0.0001;

const TEXTURED = 1u;
const RIGHT_VERTEX = 2u;
const BOTTOM_VERTEX = 4u;
// must align with BORDER_* shader_flags from bevy_ui/render/mod.rs
const RADIAL: u32 = 16u;
const FILL_START: u32 = 32u;
const FILL_END: u32 = 64u;
const CONIC: u32 = 128u;
const BORDER_LEFT: u32 = 256u;
const BORDER_TOP: u32 = 512u;
const BORDER_RIGHT: u32 = 1024u;
const BORDER_BOTTOM: u32 = 2048u;
const BORDER_ANY: u32 = BORDER_LEFT + BORDER_TOP + BORDER_RIGHT + BORDER_BOTTOM;

fn enabled(flags: u32, mask: u32) -> bool {
    return (flags & mask) != 0u;
}

@group(0) @binding(0) var<uniform> view: View;

struct GradientVertexOutput {
    @location(0) uv: vec2<f32>,
    @location(1) @interpolate(flat) size: vec2<f32>,
    @location(2) @interpolate(flat) flags: u32,
    @location(3) @interpolate(flat) radius: vec4<f32>,
    @location(4) @interpolate(flat) border: vec4<f32>,    

    // Position relative to the center of the rectangle.
    @location(5) point: vec2<f32>,
    @location(6) @interpolate(flat) g_start: vec2<f32>,
    @location(7) @interpolate(flat) dir: vec2<f32>,
    @location(8) @interpolate(flat) start_color: vec4<f32>,
    @location(9) @interpolate(flat) start_len: f32,
    @location(10) @interpolate(flat) end_len: f32,
    @location(11) @interpolate(flat) end_color: vec4<f32>,
    @location(12) @interpolate(flat) hint: f32,
    @builtin(position) position: vec4<f32>,
};

@vertex
fn vertex(
    @location(0) vertex_position: vec3<f32>,
    @location(1) vertex_uv: vec2<f32>,
    @location(2) flags: u32,

    // x: top left, y: top right, z: bottom right, w: bottom left.
    @location(3) radius: vec4<f32>,

    // x: left, y: top, z: right, w: bottom.
    @location(4) border: vec4<f32>,
    @location(5) size: vec2<f32>,
    @location(6) point: vec2<f32>,
    @location(7) @interpolate(flat) g_start: vec2<f32>,
    @location(8) @interpolate(flat) dir: vec2<f32>,
    @location(9) @interpolate(flat) start_color: vec4<f32>,
    @location(10) @interpolate(flat) start_len: f32,
    @location(11) @interpolate(flat) end_len: f32,
    @location(12) @interpolate(flat) end_color: vec4<f32>,
    @location(13) @interpolate(flat) hint: f32
) -> GradientVertexOutput {
    var out: GradientVertexOutput;
    out.position = view.clip_from_world * vec4(vertex_position, 1.0);
    out.uv = vertex_uv;
    out.size = size;
    out.flags = flags;
    out.radius = radius;
    out.border = border;
    out.point = point;
    out.dir = dir;
    out.start_color = start_color;
    out.start_len = start_len;
    out.end_len = end_len;
    out.end_color = end_color;
    out.g_start = g_start;
    out.hint = hint;

    return out;
}

@fragment
fn fragment(in: GradientVertexOutput) -> @location(0) vec4<f32> {
    var g_distance: f32;
    if enabled(in.flags, RADIAL) {
        g_distance = radial_distance(in.point, in.g_start, in.dir.x);
    } else if enabled(in.flags, CONIC) {
        g_distance = conic_distance(in.dir.x, in.point, in.g_start);
    } else {
        g_distance = linear_distance(in.point, in.g_start, in.dir);
    }

    let gradient_color = interpolate_gradient(
        g_distance,
        in.start_color,
        in.start_len,
        in.end_color,
        in.end_len,
        in.hint,
        in.flags
    );

    if enabled(in.flags, BORDER_ANY) {
        return draw_uinode_border(gradient_color, in.point, in.size, in.radius, in.border, in.flags);
    } else {
        return draw_uinode_background(gradient_color, in.point, in.size, in.radius, in.border);
    }
}

// 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<f32>) -> vec3<f32> {
    return vec3(
        gamma(color.x),
        gamma(color.y),
        gamma(color.z)
    );
}

fn linear_rgb_to_srgb(color: vec3<f32>) -> vec3<f32> {
    return vec3(
        inverse_gamma(color.x),
        inverse_gamma(color.y),
        inverse_gamma(color.z)
    );
}

fn oklab_to_linear_rgb(c: vec3<f32>) -> vec3<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;
    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 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,
    );
}

fn hsl_to_linear_rgb(hsl: vec3<f32>) -> vec3<f32> {
    let h = hsl.x;
    let s = hsl.y;
    let l = hsl.z;
    let c = (1.0 - abs(2.0 * l - 1.0)) * s;
    let hp = h * 6.0;
    let x = c * (1.0 - abs(hp % 2.0 - 1.0));
    var r: f32 = 0.0;
    var g: f32 = 0.0;
    var b: f32 = 0.0;
    if 0.0 <= hp && hp < 1.0 {
        r = c; g = x; b = 0.0;
    } else if 1.0 <= hp && hp < 2.0 {
        r = x; g = c; b = 0.0;
    } else if 2.0 <= hp && hp < 3.0 {
        r = 0.0; g = c; b = x;
    } else if 3.0 <= hp && hp < 4.0 {
        r = 0.0; g = x; b = c;
    } else if 4.0 <= hp && hp < 5.0 {
        r = x; g = 0.0; b = c;
    } else if 5.0 <= hp && hp < 6.0 {
        r = c; g = 0.0; b = x;
    }
    let m = l - 0.5 * c;
    return srgb_to_linear_rgb(vec3(r + m, g + m, b + m));
}

fn hsv_to_linear_rgb(hsva: vec3<f32>) -> vec3<f32> {
    let h = hsva.x * 6.0;
    let s = hsva.y;
    let v = hsva.z;
    let c = v * s;
    let x = c * (1.0 - abs(h % 2.0 - 1.0));
    let m = v - c;
    var r: f32 = 0.0;
    var g: f32 = 0.0;
    var b: f32 = 0.0;
    if 0.0 <= h && h < 1.0 {
        r = c; g = x; b = 0.0;
    } else if 1.0 <= h && h < 2.0 {
        r = x; g = c; b = 0.0;
    } else if 2.0 <= h && h < 3.0 {
        r = 0.0; g = c; b = x;
    } else if 3.0 <= h && h < 4.0 {
        r = 0.0; g = x; b = c;
    } else if 4.0 <= h && h < 5.0 {
        r = x; g = 0.0; b = c;
    } else if 5.0 <= h && h < 6.0 {
        r = c; g = 0.0; b = x;
    }
    return srgb_to_linear_rgb(vec3(r + m, g + m, b + m));
}

fn oklch_to_linear_rgb(c: vec3<f32>) -> vec3<f32> {
    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 {
    return ((a % b) + b) % b;
}


// 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.

fn linear_distance(
    point: vec2<f32>,
    g_start: vec2<f32>,
    g_dir: vec2<f32>,
) -> f32 {
    return dot(point - g_start, g_dir);
}

fn radial_distance(
    point: vec2<f32>,
    center: vec2<f32>,
    ratio: f32,
) -> f32 {
    let d = point - center;
    return length(vec2(d.x, d.y * ratio));
}

fn conic_distance(
    start: f32,
    point: vec2<f32>,
    center: vec2<f32>,
) -> f32 {
    let d = point - center;
    let angle = atan2(-d.x, d.y) + PI;
    return (((angle - start) % TAU) + TAU) % TAU;
}

fn mix_oklch(a: vec3<f32>, b: vec3<f32>, t: f32) -> vec3<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.
    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 {
            h += (hue_diff - 1.) * t;
        } else {
            h += (hue_diff + 1.) * t;
        }
    } else {
        h += hue_diff * t;
    }
    return vec3(
        mix(a.x, b.x, t),
        mix(a.y, b.y, t),
        fract(h),
    );
}

fn mix_oklch_long(a: vec3<f32>, b: vec3<f32>, t: f32) -> vec3<f32> {
    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 {
            h += (hue_diff - 1.) * t;
        } else {
            h += (hue_diff + 1.) * t;
        }
    } else {
        h += hue_diff * t;
    }
    return vec3(
        mix(a.x, b.x, t),
        mix(a.y, b.y, t),
        fract(h),
    );
}

fn mix_hsl(a: vec3<f32>, b: vec3<f32>, t: f32) -> vec3<f32> {
    // 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),
    );
}

fn mix_hsl_long(a: vec3<f32>, b: vec3<f32>, t: f32) -> vec3<f32> {
    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),
    );
}

fn mix_hsv(a: vec3<f32>, b: vec3<f32>, t: f32) -> vec3<f32> {
    // 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 {
            h += (hue_diff - 1.0) * t;
        } else {
            h += (hue_diff + 1.0) * t;
        }
    } else {
        h += hue_diff * t;
    }
    return vec3(
        fract(h),
        mix(a.y, b.y, t),
        mix(a.z, b.z, t),
    );
}

fn mix_hsv_long(a: vec3<f32>, b: vec3<f32>, t: f32) -> vec3<f32> {
    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 {
            h += (hue_diff - 1.0) * t;
        } else {
            h += (hue_diff + 1.0) * t;
        }
    } else {
        h += hue_diff * t;
    }
    return vec3(
        fract(h),
        mix(a.y, b.y, t),
        mix(a.z, b.z, t),
    );
}

fn interpolate_gradient(
    distance: f32,
    start_color: vec4<f32>,
    start_distance: f32,
    end_color: vec4<f32>,
    end_distance: f32,
    hint: f32,
    flags: u32,
) -> vec4<f32> {
    if start_distance == end_distance {
        if distance <= start_distance && enabled(flags, FILL_START) {
            return convert_to_linear_rgba(start_color);
        }
        if start_distance <= distance && enabled(flags, FILL_END) {
            return convert_to_linear_rgba(end_color);
        }
        return vec4(0.);
    }

    var t = (distance - start_distance) / (end_distance - start_distance);

    if t < 0.0 {
        if enabled(flags, FILL_START) {
            return convert_to_linear_rgba(start_color);
        }
        return vec4(0.0);
    }

    if 1. < t {
        if enabled(flags, FILL_END) {
            return convert_to_linear_rgba(end_color);
        }
        return vec4(0.0);
    }

    if t < hint {
        t = 0.5 * t / hint;
    } else {
        t = 0.5 * (1 + (t - hint) / (1.0 - hint));
    }

    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: vec3<f32>,
    end_color: vec3<f32>,
    t: f32,
) -> vec3<f32> {
#ifdef IN_OKLCH
    return mix_oklch(start_color, end_color, t);
#else ifdef IN_OKLCH_LONG
    return mix_oklch_long(start_color, end_color, t);
#else ifdef IN_HSV
    return mix_hsv(start_color, end_color, t);
#else ifdef IN_HSV_LONG
    return mix_hsv_long(start_color, end_color, t);
#else ifdef IN_HSL
    return mix_hsl(start_color, end_color, t);
#else ifdef IN_HSL_LONG
    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);
#endif
}

// Convert a color from the interpolation color space to linear rgba
fn convert_to_linear_rgba(
    color: vec4<f32>,
) -> vec4<f32> {
#ifdef IN_OKLCH
    let rgb = oklch_to_linear_rgb(color.xyz);
#else ifdef IN_OKLCH_LONG
    let rgb = oklch_to_linear_rgb(color.xyz);
#else ifdef IN_HSV
    let rgb = hsv_to_linear_rgb(color.xyz);
#else ifdef IN_HSV_LONG
    let rgb = hsv_to_linear_rgb(color.xyz);
#else ifdef IN_HSL
    let rgb = hsl_to_linear_rgb(color.xyz);
#else ifdef IN_HSL_LONG
    let rgb = hsl_to_linear_rgb(color.xyz);
#else ifdef IN_OKLAB
    let rgb = oklab_to_linear_rgb(color.xyz);
#else ifdef IN_SRGB
    let rgb = pow(color.xyz, vec3(2.2));
#else
    // Color is already in linear rgba space
    let rgb = color.rgb;
#endif
    return vec4(rgb, color.a);
}