Skip to content

Commit

Permalink
Fix various bugs with UI rounded borders (#13523)
Browse files Browse the repository at this point in the history
# 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>
  • Loading branch information
3 people authored May 27, 2024
1 parent f67ae29 commit cef31ff
Show file tree
Hide file tree
Showing 2 changed files with 89 additions and 25 deletions.
67 changes: 63 additions & 4 deletions crates/bevy_ui/src/render/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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())
Expand All @@ -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,
Expand Down Expand Up @@ -259,14 +281,15 @@ pub fn extract_uinode_background_colors(
flip_x: false,
flip_y: false,
camera_entity,
border: [0.; 4],
border,
border_radius,
node_type: NodeType::Rect,
},
);
}
}

#[allow(clippy::too_many_arguments)]
pub fn extract_uinode_images(
mut commands: Commands,
mut extracted_uinodes: ResMut<ExtractedUiNodes>,
Expand All @@ -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 {
Expand Down Expand Up @@ -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,
Expand All @@ -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,
},
Expand Down Expand Up @@ -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(),
Expand Down
47 changes: 26 additions & 21 deletions crates/bevy_ui/src/render/ui.wgsl
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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);

Expand All @@ -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);

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 = select(1.0 - step(0.0, border_distance), antialias(border_distance), external_distance < internal_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 = 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);
}
// 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);
}
}

0 comments on commit cef31ff

Please sign in to comment.