UI borders and outlines clipping fix (#16044)

# Objective

fixes #15502

Clipped borders and outlines aren't drawn correctly.

### Borders aren't clipped

Spawn two nodes with the same dimensions and border thickness, but clip
on of the nodes so that only its top left quarter is visible:

<img width="194" alt="clip"
src="https://github.com/user-attachments/assets/2d3f6d28-aa20-44df-967a-677725828294">

You can see that instead of clipping the border, instead the border is
scaled to fit inside of the unclipped section.

```rust
use bevy::color::palettes::css::BLUE;
use bevy::prelude::*;
use bevy::winit::WinitSettings;

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .insert_resource(WinitSettings::desktop_app())
        .add_systems(Startup, setup)
        .run();
}

fn setup(mut commands: Commands) {
    commands.spawn(Camera2d);
    commands
        .spawn(Node {
            width: Val::Percent(100.),
            height: Val::Percent(100.),
            justify_content: JustifyContent::Center,
            align_items: AlignItems::Center,
            ..Default::default()
        })
        .with_children(|commands| {
            commands
                .spawn(Node {
                    column_gap: Val::Px(10.),
                    ..Default::default()
                })
                .with_children(|commands| {
                    commands
                        .spawn(Node {
                            width: Val::Px(100.),
                            height: Val::Px(100.),
                            overflow: Overflow::clip(),
                            ..Default::default()
                        })
                        .with_child((
                            Node {
                                position_type: PositionType::Absolute,
                                width: Val::Px(100.),
                                height: Val::Px(100.),
                                border: UiRect::all(Val::Px(10.)),
                                ..Default::default()
                            },
                            BackgroundColor(Color::WHITE),
                            BorderColor(BLUE.into()),
                        ));

                    commands
                        .spawn(Node {
                            width: Val::Px(50.),
                            height: Val::Px(50.),
                            overflow: Overflow::clip(),
                            ..Default::default()
                        })
                        .with_child((
                            Node {
                                position_type: PositionType::Absolute,
                                width: Val::Px(100.),
                                height: Val::Px(100.),
                                border: UiRect::all(Val::Px(10.)),
                                ..Default::default()
                            },
                            BackgroundColor(Color::WHITE),
                            BorderColor(BLUE.into()),
                        ));
                });
        });
}
```

You can also see this problem in the `overflow` example. If you hover
over any of the clipped nodes you'll see that the outline only wraps the
visible section of the node

### Outlines are clipped incorrectly

A UI nodes Outline's are drawn outside of its bounds, so applying the
local clipping rect to the outline doesn't make any sense.
Instead an `Outline` should be clipped using its parent's clipping rect.

## Solution

* Pass the `point` value into the vertex shader instead of calculating
it in the shader.
* In `extract_uinode_borders` use the parents clipping rect when
clipping outlines.

The extra parameter isn't a great solution I think, but I wanted to fix
borders for the 0.15 release and this is the most minimal approach I
could think of without replacing the whole shader and prepare function.

 ## Showcase

<img width="149" alt="clipp"
src="https://github.com/user-attachments/assets/19fbd3cc-e7cd-42e1-a5e0-fd92aad04dcd">

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
This commit is contained in:
ickshonpe 2024-10-21 23:54:09 +01:00 committed by GitHub
parent d0af199249
commit 9930df83ed
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 23 additions and 8 deletions

View file

@ -16,6 +16,7 @@ use bevy_core_pipeline::core_3d::graph::{Core3d, Node3d};
use bevy_core_pipeline::{core_2d::Camera2d, core_3d::Camera3d}; use bevy_core_pipeline::{core_2d::Camera2d, core_3d::Camera3d};
use bevy_ecs::entity::{EntityHashMap, EntityHashSet}; use bevy_ecs::entity::{EntityHashMap, EntityHashSet};
use bevy_ecs::prelude::*; use bevy_ecs::prelude::*;
use bevy_hierarchy::Parent;
use bevy_math::{FloatOrd, Mat4, Rect, URect, UVec4, Vec2, Vec3, Vec3Swizzles, Vec4Swizzles}; use bevy_math::{FloatOrd, Mat4, Rect, URect, UVec4, Vec2, Vec3, Vec3Swizzles, Vec4Swizzles};
use bevy_render::render_phase::ViewSortedRenderPhases; use bevy_render::render_phase::ViewSortedRenderPhases;
use bevy_render::sync_world::MainEntity; use bevy_render::sync_world::MainEntity;
@ -404,8 +405,10 @@ pub fn extract_uinode_borders(
Option<&CalculatedClip>, Option<&CalculatedClip>,
Option<&TargetCamera>, Option<&TargetCamera>,
AnyOf<(&BorderColor, &Outline)>, AnyOf<(&BorderColor, &Outline)>,
Option<&Parent>,
)>, )>,
>, >,
parent_clip_query: Extract<Query<&CalculatedClip>>,
mapping: Extract<Query<RenderEntity>>, mapping: Extract<Query<RenderEntity>>,
) { ) {
let image = AssetId::<Image>::default(); let image = AssetId::<Image>::default();
@ -418,6 +421,7 @@ pub fn extract_uinode_borders(
maybe_clip, maybe_clip,
maybe_camera, maybe_camera,
(maybe_border_color, maybe_outline), (maybe_border_color, maybe_outline),
maybe_parent,
) in &uinode_query ) in &uinode_query
{ {
let Some(camera_entity) = maybe_camera let Some(camera_entity) = maybe_camera
@ -471,6 +475,9 @@ pub fn extract_uinode_borders(
if let Some(outline) = maybe_outline { if let Some(outline) = maybe_outline {
let outline_size = uinode.outlined_node_size(); let outline_size = uinode.outlined_node_size();
let parent_clip =
maybe_parent.and_then(|parent| parent_clip_query.get(parent.get()).ok());
extracted_uinodes.uinodes.insert( extracted_uinodes.uinodes.insert(
commands.spawn(TemporaryRenderEntity).id(), commands.spawn(TemporaryRenderEntity).id(),
ExtractedUiNode { ExtractedUiNode {
@ -481,7 +488,7 @@ pub fn extract_uinode_borders(
..Default::default() ..Default::default()
}, },
image, image,
clip: maybe_clip.map(|clip| clip.clip), clip: parent_clip.map(|clip| clip.clip),
camera_entity: render_camera_entity, camera_entity: render_camera_entity,
item: ExtractedUiItem::Node { item: ExtractedUiItem::Node {
transform: global_transform.compute_matrix(), transform: global_transform.compute_matrix(),
@ -768,6 +775,8 @@ struct UiVertex {
pub border: [f32; 4], pub border: [f32; 4],
/// Size of the UI node. /// Size of the UI node.
pub size: [f32; 2], pub size: [f32; 2],
/// Position relative to the center of the UI node.
pub point: [f32; 2],
} }
#[derive(Resource)] #[derive(Resource)]
@ -998,6 +1007,7 @@ pub fn prepare_uinodes(
// Specify the corners of the node // Specify the corners of the node
let positions = QUAD_VERTEX_POSITIONS let positions = QUAD_VERTEX_POSITIONS
.map(|pos| (*transform * (pos * rect_size).extend(1.)).xyz()); .map(|pos| (*transform * (pos * rect_size).extend(1.)).xyz());
let points = QUAD_VERTEX_POSITIONS.map(|pos| pos.xy() * rect_size.xy());
// Calculate the effect of clipping // Calculate the effect of clipping
// Note: this won't work with rotation/scaling, but that's much more complex (may need more that 2 quads) // Note: this won't work with rotation/scaling, but that's much more complex (may need more that 2 quads)
@ -1031,6 +1041,13 @@ pub fn prepare_uinodes(
positions[3] + positions_diff[3].extend(0.), positions[3] + positions_diff[3].extend(0.),
]; ];
let points = [
points[0] + positions_diff[0],
points[1] + positions_diff[1],
points[2] + positions_diff[2],
points[3] + positions_diff[3],
];
let transformed_rect_size = transform.transform_vector3(rect_size); let transformed_rect_size = transform.transform_vector3(rect_size);
// Don't try to cull nodes that have a rotation // Don't try to cull nodes that have a rotation
@ -1113,6 +1130,7 @@ pub fn prepare_uinodes(
], ],
border: [border.left, border.top, border.right, border.bottom], border: [border.left, border.top, border.right, border.bottom],
size: rect_size.xy().into(), size: rect_size.xy().into(),
point: points[i].into(),
}); });
} }
@ -1215,6 +1233,7 @@ pub fn prepare_uinodes(
radius: [0.0; 4], radius: [0.0; 4],
border: [0.0; 4], border: [0.0; 4],
size: size.into(), size: size.into(),
point: [0.0; 2],
}); });
} }

View file

@ -72,6 +72,8 @@ impl SpecializedRenderPipeline for UiPipeline {
VertexFormat::Float32x4, VertexFormat::Float32x4,
// border size // border size
VertexFormat::Float32x2, VertexFormat::Float32x2,
// position relative to the center
VertexFormat::Float32x2,
], ],
); );
let shader_defs = if key.anti_alias { let shader_defs = if key.anti_alias {

View file

@ -38,6 +38,7 @@ fn vertex(
// x: left, y: top, z: right, w: bottom. // x: left, y: top, z: right, w: bottom.
@location(5) border: vec4<f32>, @location(5) border: vec4<f32>,
@location(6) size: vec2<f32>, @location(6) size: vec2<f32>,
@location(7) point: vec2<f32>,
) -> VertexOutput { ) -> VertexOutput {
var out: VertexOutput; var out: VertexOutput;
out.uv = vertex_uv; out.uv = vertex_uv;
@ -47,13 +48,6 @@ fn vertex(
out.radius = radius; out.radius = radius;
out.size = size; out.size = size;
out.border = border; 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; out.point = point;
return out; return out;