#import bevy_render::view::View const TEXTURED = 1u; const RIGHT_VERTEX = 2u; const BOTTOM_VERTEX = 4u; const BORDER: u32 = 8u; fn enabled(flags: u32, mask: u32) -> bool { return (flags & mask) != 0u; } @group(0) @binding(0) var view: View; struct VertexOutput { @location(0) uv: vec2, @location(1) color: vec4, @location(2) @interpolate(flat) size: vec2, @location(3) @interpolate(flat) flags: u32, @location(4) @interpolate(flat) radius: vec4, @location(5) @interpolate(flat) border: vec4, // Position relative to the center of the rectangle. @location(6) point: vec2, @builtin(position) position: vec4, }; @vertex fn vertex( @location(0) vertex_position: vec3, @location(1) vertex_uv: vec2, @location(2) vertex_color: vec4, @location(3) flags: u32, // x: top left, y: top right, z: bottom right, w: bottom left. @location(4) radius: vec4, // x: left, y: top, z: right, w: bottom. @location(5) border: vec4, @location(6) size: vec2, ) -> VertexOutput { var out: VertexOutput; out.uv = vertex_uv; out.position = view.view_proj * vec4(vertex_position, 1.0); out.color = vertex_color; out.flags = flags; out.radius = radius; out.size = size; out.border = border; var point = 0.49999 * size; if (flags & RIGHT_VERTEX) == 0u { point.x *= -1.; } if (flags & BOTTOM_VERTEX) == 0u { point.y *= -1.; } out.point = point; return out; } @group(1) @binding(0) var sprite_texture: texture_2d; @group(1) @binding(1) var sprite_sampler: sampler; // The returned value is the shortest distance from the given point to the boundary of the rounded // box. // // Negative values indicate that the point is inside the rounded box, positive values that the point // is outside, and zero is exactly on the boundary. // // Arguments: // - `point` -> The function will return the distance from this point to the closest point on // the boundary. // - `size` -> The maximum width and height of the box. // - `corner_radii` -> The radius of each rounded corner. Ordered counter clockwise starting // top left: // x: top left, y: top right, z: bottom right, w: bottom left. fn sd_rounded_box(point: vec2, size: vec2, corner_radii: vec4) -> f32 { // If 0.0 < y then select bottom left (w) and bottom right corner radius (z). // Else select top left (x) and top right corner radius (y). let rs = select(corner_radii.xy, corner_radii.wz, 0.0 < point.y); // w and z are swapped so that both pairs are in left to right order, otherwise this second // select statement would return the incorrect value for the bottom pair. let radius = select(rs.x, rs.y, 0.0 < point.x); // Vector from the corner closest to the point, to the point. let corner_to_point = abs(point) - 0.5 * size; // Vector from the center of the radius circle to the point. let q = corner_to_point + radius; // Length from center of the radius circle to the point, zeros a component if the point is not // within the quadrant of the radius circle that is part of the curved corner. let l = length(max(q, vec2(0.0))); let m = min(max(q.x, q.y), 0.0); return l + m - radius; } fn sd_inset_rounded_box(point: vec2, size: vec2, radius: vec4, inset: vec4) -> f32 { let inner_size = size - inset.xy - inset.zw; let inner_center = inset.xy + 0.5 * inner_size - 0.5 * size; let inner_point = point - inner_center; var r = radius; // Top left corner. r.x = r.x - max(inset.x, inset.y); // Top right corner. r.y = r.y - max(inset.z, inset.y); // Bottom right corner. r.z = r.z - max(inset.z, inset.w); // Bottom left corner. r.w = r.w - max(inset.x, inset.w); let half_size = inner_size * 0.5; let min = min(half_size.x, half_size.y); r = min(max(r, vec4(0.0)), vec4(min)); return sd_rounded_box(inner_point, inner_size, r); } fn draw(in: VertexOutput) -> vec4 { let texture_color = textureSample(sprite_texture, sprite_sampler, in.uv); // Only use the color sampled from the texture if the `TEXTURED` flag is enabled. // This allows us to draw both textured and untextured shapes together in the same batch. let color = select(in.color, in.color * texture_color, enabled(in.flags, TEXTURED)); // Signed distances. The magnitude is the distance of the point from the edge of the shape. // * Negative values indicate that the point is inside the shape. // * Zero values indicate the point is on on the edge of the shape. // * Positive values indicate the point is outside the shape. // Signed distance from the exterior boundary. let external_distance = sd_rounded_box(in.point, in.size, in.radius); // Signed distance from the border's internal edge (the signed distance is negative if the point // is inside the rect but not on the border). // If the border size is set to zero, this is the same as as the external distance. let internal_distance = sd_inset_rounded_box(in.point, in.size, in.radius, in.border); // Signed distance from the border (the intersection of the rect with its border). // Points inside the border have negative signed distance. Any point outside the border, whether // outside the outside edge, or inside the inner edge have positive signed distance. let border_distance = max(external_distance, -internal_distance); // The `fwidth` function returns an approximation of the rate of change of the signed distance // value that is used to ensure that the smooth alpha transition created by smoothstep occurs // over a range of distance values that is proportional to how quickly the distance is changing. let fborder = fwidth(border_distance); let fexternal = fwidth(external_distance); if enabled(in.flags, BORDER) { // The item is a border // At external edges with no border, `border_distance` is equal to zero. // This select statement ensures we only perform anti-aliasing where a non-zero width border // is present, otherwise an outline about the external boundary would be drawn even without // a border. let t = 1. - select(step(0.0, border_distance), smoothstep(0.0, fborder, border_distance), external_distance < internal_distance); return color.rgba * t; } // The item is a rectangle, draw normally with anti-aliasing at the edges. let t = 1. - smoothstep(0.0, fexternal, external_distance); return color.rgba * t; } @fragment fn fragment(in: VertexOutput) -> @location(0) vec4 { return draw(in); }