Fix various bugs with UI rounded borders (#13523)

# Objective

- Fixes #13503 
- Fix other various bugs I noticed while debugging above issue.

## Solution

- Change the antialiasing(AA) method. It was using fwidth which is the
derivative between pixels, but there were a lot of artifacts being added
from this. So just use the sdf value. This aa method probably isn't as
smooth looking, but better than having artifacts. Below is a
visualization of the fwidth.

![image](https://github.com/bevyengine/bevy/assets/2180432/4e475ad0-c9d0-4a40-af39-5f4422a78392)
- Use the internal sdf for drawing the background instead of the
external sdf and extract the border for these type of nodes. This fixed
2 bugs, one with the background coloring the AA pixels on the edge of
rounded borders. And also allows for the border to use a transparent
color.
- Don't extract borders if all the widths are zero.

## Testing

- played a bunch with the example in the linked issue.
This PR:

![image](https://github.com/bevyengine/bevy/assets/2180432/d7797e0e-e348-4daa-8646-554dc2032523)
Main:

![image](https://github.com/bevyengine/bevy/assets/2180432/4d46c17e-a12d-4e20-aaef-0ffc950cefe2)

- ran the `borders` and `rounded_borders` examples

---

## Changelog

- Fixed various antialiasing issues to do with rounded ui borders.

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: Andreas Weibye <13300393+Weibye@users.noreply.github.com>
This commit is contained in:
Mike 2024-05-27 10:42:13 -07:00 committed by GitHub
parent f67ae29338
commit cef31ffdd9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 89 additions and 25 deletions

View File

@ -199,8 +199,11 @@ pub fn extract_uinode_background_colors(
Option<&TargetCamera>,
&BackgroundColor,
Option<&BorderRadius>,
&Style,
Option<&Parent>,
)>,
>,
node_query: Extract<Query<&Node>>,
) {
for (
entity,
@ -211,6 +214,8 @@ pub fn extract_uinode_background_colors(
camera,
background_color,
border_radius,
style,
parent,
) in &uinode_query
{
let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get())
@ -232,6 +237,23 @@ pub fn extract_uinode_background_colors(
// so we have to divide by `UiScale` to get the size of the UI viewport.
/ ui_scale.0;
// Both vertical and horizontal percentage border values are calculated based on the width of the parent node
// <https://developer.mozilla.org/en-US/docs/Web/CSS/border-width>
let parent_width = parent
.and_then(|parent| node_query.get(parent.get()).ok())
.map(|parent_node| parent_node.size().x)
.unwrap_or(ui_logical_viewport_size.x);
let left =
resolve_border_thickness(style.border.left, parent_width, ui_logical_viewport_size);
let right =
resolve_border_thickness(style.border.right, parent_width, ui_logical_viewport_size);
let top =
resolve_border_thickness(style.border.top, parent_width, ui_logical_viewport_size);
let bottom =
resolve_border_thickness(style.border.bottom, parent_width, ui_logical_viewport_size);
let border = [left, top, right, bottom];
let border_radius = if let Some(border_radius) = border_radius {
resolve_border_radius(
border_radius,
@ -259,7 +281,7 @@ pub fn extract_uinode_background_colors(
flip_x: false,
flip_y: false,
camera_entity,
border: [0.; 4],
border,
border_radius,
node_type: NodeType::Rect,
},
@ -267,6 +289,7 @@ pub fn extract_uinode_background_colors(
}
}
#[allow(clippy::too_many_arguments)]
pub fn extract_uinode_images(
mut commands: Commands,
mut extracted_uinodes: ResMut<ExtractedUiNodes>,
@ -285,11 +308,25 @@ pub fn extract_uinode_images(
Option<&TextureAtlas>,
Option<&ComputedTextureSlices>,
Option<&BorderRadius>,
Option<&Parent>,
&Style,
)>,
>,
node_query: Extract<Query<&Node>>,
) {
for (uinode, transform, view_visibility, clip, camera, image, atlas, slices, border_radius) in
&uinode_query
for (
uinode,
transform,
view_visibility,
clip,
camera,
image,
atlas,
slices,
border_radius,
parent,
style,
) in &uinode_query
{
let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get())
else {
@ -342,6 +379,23 @@ pub fn extract_uinode_images(
// so we have to divide by `UiScale` to get the size of the UI viewport.
/ ui_scale.0;
// Both vertical and horizontal percentage border values are calculated based on the width of the parent node
// <https://developer.mozilla.org/en-US/docs/Web/CSS/border-width>
let parent_width = parent
.and_then(|parent| node_query.get(parent.get()).ok())
.map(|parent_node| parent_node.size().x)
.unwrap_or(ui_logical_viewport_size.x);
let left =
resolve_border_thickness(style.border.left, parent_width, ui_logical_viewport_size);
let right =
resolve_border_thickness(style.border.right, parent_width, ui_logical_viewport_size);
let top =
resolve_border_thickness(style.border.top, parent_width, ui_logical_viewport_size);
let bottom =
resolve_border_thickness(style.border.bottom, parent_width, ui_logical_viewport_size);
let border = [left, top, right, bottom];
let border_radius = if let Some(border_radius) = border_radius {
resolve_border_radius(
border_radius,
@ -366,7 +420,7 @@ pub fn extract_uinode_images(
flip_x: image.flip_x,
flip_y: image.flip_y,
camera_entity,
border: [0.; 4],
border,
border_radius,
node_type: NodeType::Rect,
},
@ -513,6 +567,11 @@ pub fn extract_uinode_borders(
let border = [left, top, right, bottom];
// don't extract border if no border
if left == 0.0 && top == 0.0 && right == 0.0 && bottom == 0.0 {
continue;
}
let border_radius = resolve_border_radius(
border_radius,
node.size(),

View File

@ -79,7 +79,7 @@ fn sd_rounded_box(point: vec2<f32>, size: vec2<f32>, 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
// w and z are swapped above 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.
@ -120,6 +120,12 @@ fn sd_inset_rounded_box(point: vec2<f32>, size: vec2<f32>, radius: vec4<f32>, in
return sd_rounded_box(inner_point, inner_size, r);
}
// get alpha for antialiasing for sdf
fn antialias(distance: f32) -> f32 {
// Using the fwidth(distance) was causing artifacts, so just use the distance.
return clamp(0.0, 1.0, 0.5 - distance);
}
fn draw(in: VertexOutput) -> vec4<f32> {
let texture_color = textureSample(sprite_texture, sprite_sampler, in.uv);
@ -145,32 +151,31 @@ fn draw(in: VertexOutput) -> vec4<f32> {
// 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);
// 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 = select(1.0 - step(0.0, border_distance), antialias(border_distance), external_distance < internal_distance);
if enabled(in.flags, BORDER) {
// The item is a border
// Blend mode ALPHA_BLENDING is used for UI elements, so we don't premultiply alpha here.
return vec4(color.rgb, color.a * t);
}
// 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);
// Blend mode ALPHA_BLENDING is used for UI elements, so we don't premultiply alpha here.
return vec4(color.rgb, color.a * t);
}
// The item is a rectangle, draw normally with anti-aliasing at the edges.
let t = 1. - smoothstep(0.0, fexternal, external_distance);
fn draw_background(in: VertexOutput) -> vec4<f32> {
let texture_color = textureSample(sprite_texture, sprite_sampler, in.uv);
let color = select(in.color, in.color * texture_color, enabled(in.flags, TEXTURED));
// When drawing the background only draw the internal area and not the border.
let internal_distance = sd_inset_rounded_box(in.point, in.size, in.radius, in.border);
let t = antialias(internal_distance);
return vec4(color.rgb, color.a * t);
}
@fragment
fn fragment(in: VertexOutput) -> @location(0) vec4<f32> {
return draw(in);
if enabled(in.flags, BORDER) {
return draw(in);
} else {
return draw_background(in);
}
}