diff --git a/crates/bevy_ui/src/node_bundles.rs b/crates/bevy_ui/src/node_bundles.rs index 964fd61c514ca0..24f12f4562078c 100644 --- a/crates/bevy_ui/src/node_bundles.rs +++ b/crates/bevy_ui/src/node_bundles.rs @@ -1,11 +1,10 @@ //! This module contains basic node bundles used to build UIs #[cfg(feature = "bevy_text")] -use crate::widget::TextFlags; use crate::{ - widget::{Button, UiImageSize}, - BackgroundColor, BorderColor, ContentSize, FocusPolicy, Interaction, Node, Style, UiImage, - UiMaterial, ZIndex, + widget::{Button, TextFlags, UiImageSize}, + BackgroundColor, Border, BorderColor, ContentSize, CornerRadius, FocusPolicy, Interaction, + Node, Style, UiImage, UiMaterial, ZIndex, }; use bevy_asset::Handle; use bevy_ecs::bundle::Bundle; @@ -50,6 +49,10 @@ pub struct NodeBundle { pub visibility: Visibility, /// Inherited visibility of an entity. pub inherited_visibility: InheritedVisibility, + /// Describes the radius of corners for the node + pub corner_radius: CornerRadius, + /// Describes the visual properties of the node's border + pub border: Border, /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering pub view_visibility: ViewVisibility, /// Indicates the depth at which the node should appear in the UI @@ -70,6 +73,8 @@ impl Default for NodeBundle { visibility: Default::default(), inherited_visibility: Default::default(), view_visibility: Default::default(), + corner_radius: Default::default(), + border: Default::default(), z_index: Default::default(), } } @@ -115,6 +120,10 @@ pub struct ImageBundle { pub visibility: Visibility, /// Inherited visibility of an entity. pub inherited_visibility: InheritedVisibility, + /// Describes the radius of corners for the node + pub corner_radius: CornerRadius, + /// Describes the visual properties of the node's border + pub border: Border, /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering pub view_visibility: ViewVisibility, /// Indicates the depth at which the node should appear in the UI @@ -330,6 +339,10 @@ pub struct ButtonBundle { pub visibility: Visibility, /// Inherited visibility of an entity. pub inherited_visibility: InheritedVisibility, + /// Describes the radius of corners for the node + pub corner_radius: CornerRadius, + /// Describes the visual properties of the node's border + pub border: Border, /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering pub view_visibility: ViewVisibility, /// Indicates the depth at which the node should appear in the UI @@ -352,6 +365,8 @@ impl Default for ButtonBundle { visibility: Default::default(), inherited_visibility: Default::default(), view_visibility: Default::default(), + corner_radius: Default::default(), + border: Default::default(), z_index: Default::default(), } } diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index cede7b2f1e5f2c..1acdd94ba60bd2 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -2,42 +2,47 @@ mod pipeline; mod render_pass; mod ui_material_pipeline; -use bevy_core_pipeline::core_2d::graph::{Labels2d, SubGraph2d}; -use bevy_core_pipeline::core_3d::graph::{Labels3d, SubGraph3d}; -use bevy_core_pipeline::{core_2d::Camera2d, core_3d::Camera3d}; -use bevy_hierarchy::Parent; -use bevy_render::{ - render_phase::PhaseItem, render_resource::BindGroupEntries, view::ViewVisibility, - ExtractSchedule, Render, -}; -use bevy_sprite::{SpriteAssetEvents, TextureAtlas}; pub use pipeline::*; pub use render_pass::*; pub use ui_material_pipeline::*; -use crate::graph::{LabelsUi, SubGraphUi}; use crate::{ - texture_slice::ComputedTextureSlices, BackgroundColor, BorderColor, CalculatedClip, - ContentSize, DefaultUiCamera, Node, Outline, Style, TargetCamera, UiImage, UiScale, Val, + graph::{LabelsUi, SubGraphUi}, + texture_slice::ComputedTextureSlices, + BackgroundColor, Border, BorderColor, CalculatedClip, ContentSize, CornerRadius, + DefaultUiCamera, Node, Outline, Style, TargetCamera, UiImage, UiScale, Val, }; use bevy_app::prelude::*; use bevy_asset::{load_internal_asset, AssetEvent, AssetId, Assets, Handle}; +use bevy_core_pipeline::{ + core_2d::{ + graph::{Labels2d, SubGraph2d}, + Camera2d, + }, + core_3d::{ + graph::{Labels3d, SubGraph3d}, + Camera3d, + }, +}; use bevy_ecs::prelude::*; -use bevy_math::{Mat4, Rect, URect, UVec4, Vec2, Vec3, Vec4Swizzles}; +use bevy_hierarchy::Parent; +use bevy_math::{Mat4, Rect, URect, UVec4, Vec2, Vec3, Vec4, Vec4Swizzles}; use bevy_render::{ camera::Camera, color::Color, render_asset::RenderAssets, render_graph::{RenderGraph, RunGraphOnViewNode}, - render_phase::{sort_phase_system, AddRenderCommand, DrawFunctions, RenderPhase}, - render_resource::*, + render_phase::{sort_phase_system, AddRenderCommand, DrawFunctions, PhaseItem, RenderPhase}, + render_resource::{BindGroupEntries, *}, renderer::{RenderDevice, RenderQueue}, texture::Image, - view::{ExtractedView, ViewUniforms}, - Extract, RenderApp, RenderSet, + view::{ExtractedView, ViewUniforms, ViewVisibility}, + Extract, ExtractSchedule, Render, RenderApp, RenderSet, +}; +use bevy_sprite::{ + TextureAtlasLayout, {SpriteAssetEvents, TextureAtlas}, }; -use bevy_sprite::TextureAtlasLayout; #[cfg(feature = "bevy_text")] use bevy_text::{PositionedGlyph, Text, TextLayoutInfo}; use bevy_transform::components::GlobalTransform; @@ -139,6 +144,9 @@ pub struct ExtractedUiNode { pub clip: Option, pub flip_x: bool, pub flip_y: bool, + pub border_color: Option, + pub border_width: Option, + pub corner_radius: Option<[f32; 4]>, // Camera to render this UI node to. By the time it is extracted, // it is defaulted to a single camera if only one exists. // Nodes with ambiguous camera will be ignored. @@ -277,6 +285,9 @@ pub fn extract_uinode_borders( flip_x: false, flip_y: false, camera_entity, + border_color: Default::default(), + border_width: Default::default(), + corner_radius: Default::default(), }, ); } @@ -368,6 +379,9 @@ pub fn extract_uinode_outlines( flip_x: false, flip_y: false, camera_entity, + border_color: Default::default(), + border_width: Default::default(), + corner_radius: Default::default(), }, ); } @@ -392,6 +406,8 @@ pub fn extract_uinodes( Option<&TextureAtlas>, Option<&TargetCamera>, Option<&ComputedTextureSlices>, + Option<&CornerRadius>, + Option<&Border>, )>, >, ) { @@ -406,6 +422,8 @@ pub fn extract_uinodes( atlas, camera, slices, + corner_radius, + border, ) in uinode_query.iter() { let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get()) @@ -420,7 +438,16 @@ pub fn extract_uinodes( if let Some((image, slices)) = maybe_image.zip(slices) { extracted_uinodes.uinodes.extend( slices - .extract_ui_nodes(transform, uinode, color, image, clip, camera_entity) + .extract_ui_nodes( + transform, + uinode, + color, + image, + clip, + camera_entity, + *corner_radius.unwrap(), + *border.unwrap(), + ) .map(|e| (commands.spawn_empty().id(), e)), ); continue; @@ -468,6 +495,9 @@ pub fn extract_uinodes( flip_x, flip_y, camera_entity, + border_color: border.map(|border| border.color), + border_width: border.map(|border| border.width), + corner_radius: corner_radius.map(|corner_radius| corner_radius.to_array()), }, ); } @@ -564,11 +594,22 @@ pub fn extract_text_uinodes( &ViewVisibility, Option<&CalculatedClip>, Option<&TargetCamera>, + Option<&CornerRadius>, + Option<&Border>, )>, >, ) { - for (uinode, global_transform, text, text_layout_info, view_visibility, clip, camera) in - uinode_query.iter() + for ( + uinode, + global_transform, + text, + text_layout_info, + view_visibility, + clip, + camera, + corner_radius, + border, + ) in uinode_query.iter() { let Some(camera_entity) = camera.map(TargetCamera::entity).or(default_ui_camera.get()) else { @@ -631,6 +672,9 @@ pub fn extract_text_uinodes( clip: clip.map(|clip| clip.clip), flip_x: false, flip_y: false, + border_color: border.map(|border| border.color), + border_width: border.map(|border| border.width), + corner_radius: corner_radius.map(|corner_radius| corner_radius.to_array()), camera_entity, }, ); @@ -643,6 +687,26 @@ pub fn extract_text_uinodes( struct UiVertex { pub position: [f32; 3], pub uv: [f32; 2], + pub uniform_index: u32, +} + +const MAX_UI_UNIFORM_ENTRIES: usize = 256; + +#[repr(C)] +#[derive(Copy, Clone, Debug)] +pub struct UiUniform { + entries: [UiUniformEntry; MAX_UI_UNIFORM_ENTRIES], +} + +#[repr(C)] +#[derive(Copy, Clone, Debug, Default)] +pub struct UiUniformEntry { + pub size: Vec2, + pub center: Vec2, + pub border_color: u32, + pub border_width: f32, + /// NOTE: This is a Vec4 because using [f32; 4] with AsStd140 results in a 16-bytes alignment. + pub corner_radius: Vec4, pub color: [f32; 4], pub mode: u32, } @@ -651,6 +715,8 @@ struct UiVertex { pub struct UiMeta { vertices: BufferVec, view_bind_group: Option, + ui_uniforms: UiUniform, + ui_uniform_bind_group: Option, } impl Default for UiMeta { @@ -658,6 +724,8 @@ impl Default for UiMeta { Self { vertices: BufferVec::new(BufferUsages::VERTEX), view_bind_group: None, + ui_uniforms: Default::default(), + ui_uniform_bind_group: None, } } } @@ -671,10 +739,12 @@ pub(crate) const QUAD_VERTEX_POSITIONS: [Vec3; 4] = [ pub(crate) const QUAD_INDICES: [usize; 6] = [0, 2, 3, 0, 1, 2]; -#[derive(Component)] +#[derive(Component, Debug)] pub struct UiBatch { pub range: Range, pub image: AssetId, + pub ui_uniform_offset: u32, + pub z: f32, pub camera: Entity, } @@ -787,6 +857,8 @@ pub fn prepare_uinodes( range: index..index, image: extracted_uinode.image, camera: extracted_uinode.camera_entity, + ui_uniform_offset: 0, + z: 0.0, }; batches.push((item.entity, new_batch)); @@ -937,13 +1009,11 @@ pub fn prepare_uinodes( .map(|pos| pos / atlas_extent) }; - let color = extracted_uinode.color.as_linear_rgba_f32(); for i in QUAD_INDICES { ui_meta.vertices.push(UiVertex { position: positions_clipped[i].into(), uv: uvs[i].into(), - color, - mode, + uniform_index: 0, }); } index += QUAD_INDICES.len() as u32; diff --git a/crates/bevy_ui/src/render/pipeline.rs b/crates/bevy_ui/src/render/pipeline.rs index 6dad2b104c3bb9..9b28dbdf0f39ee 100644 --- a/crates/bevy_ui/src/render/pipeline.rs +++ b/crates/bevy_ui/src/render/pipeline.rs @@ -13,6 +13,7 @@ use bevy_render::{ pub struct UiPipeline { pub view_layout: BindGroupLayout, pub image_layout: BindGroupLayout, + pub ui_uniform_layout: BindGroupLayout, } impl FromWorld for UiPipeline { @@ -38,9 +39,25 @@ impl FromWorld for UiPipeline { ), ); + let ui_uniform_layout = render_device.create_bind_group_layout( + Some("ui_uniform_layout"), + &[BindGroupLayoutEntry { + binding: 0, + visibility: ShaderStages::VERTEX, + ty: BindingType::Buffer { + ty: BufferBindingType::Uniform, + has_dynamic_offset: true, + min_binding_size: BufferSize::new(0u64), + }, + + count: None, + }], + ); + UiPipeline { view_layout, image_layout, + ui_uniform_layout, } } } @@ -57,11 +74,21 @@ impl SpecializedRenderPipeline for UiPipeline { let vertex_layout = VertexBufferLayout::from_vertex_formats( VertexStepMode::Vertex, vec![ - // position + // Position VertexFormat::Float32x3, - // uv + // UV + VertexFormat::Float32x2, + // UiUniform Node Index + VertexFormat::Uint32, + // Size + VertexFormat::Float32x2, + // Center VertexFormat::Float32x2, - // color + // Border Color + VertexFormat::Uint32, + // Border Width + VertexFormat::Float32, + // Corner Radius VertexFormat::Float32x4, // mode VertexFormat::Uint32, @@ -90,7 +117,11 @@ impl SpecializedRenderPipeline for UiPipeline { write_mask: ColorWrites::ALL, })], }), - layout: vec![self.view_layout.clone(), self.image_layout.clone()], + layout: vec![ + self.view_layout.clone(), + self.image_layout.clone(), + self.ui_uniform_layout.clone(), + ], push_constant_ranges: Vec::new(), primitive: PrimitiveState { front_face: FrontFace::Ccw, diff --git a/crates/bevy_ui/src/render/render_pass.rs b/crates/bevy_ui/src/render/render_pass.rs index e0119509d7b270..e10a49b57af804 100644 --- a/crates/bevy_ui/src/render/render_pass.rs +++ b/crates/bevy_ui/src/render/render_pass.rs @@ -149,6 +149,7 @@ pub type DrawUi = ( SetItemPipeline, SetUiViewBindGroup<0>, SetUiTextureBindGroup<1>, + SetUiUniformBindGroup<2>, DrawUiNode, ); @@ -196,6 +197,26 @@ impl RenderCommand

for SetUiTextureBindGroup RenderCommandResult::Success } } +pub struct SetUiUniformBindGroup; +impl EntityRenderCommand for SetUiUniformBindGroup { + type Param = (SRes, SQuery>); + + fn render<'w>( + _view: Entity, + item: Entity, + (ui_meta, query_batch): SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let batch = query_batch.get(item).unwrap(); + + pass.set_bind_group( + I, + ui_meta.into_inner().ui_uniform_bind_group.as_ref().unwrap(), + &[batch.ui_uniform_offset], + ); + RenderCommandResult::Success + } +} pub struct DrawUiNode; impl RenderCommand

for DrawUiNode { type Param = SRes; diff --git a/crates/bevy_ui/src/render/ui.wgsl b/crates/bevy_ui/src/render/ui.wgsl index aeb57aad81358e..f7810d10c377ea 100644 --- a/crates/bevy_ui/src/render/ui.wgsl +++ b/crates/bevy_ui/src/render/ui.wgsl @@ -4,31 +4,76 @@ const TEXTURED_QUAD: u32 = 0u; @group(0) @binding(0) var view: View; +struct UiUniformEntry { + color: u32, + size: vec2, + center: vec2, + border_color: u32, + border_width: f32, + corner_radius: vec4, +}; + +struct UiUniform { + // NOTE: this array size must be kept in sync with the constants defined bevy_ui/src/render/mod.rs + entries: array, +}; + +@group(2) @binding(0) +var ui_uniform: UiUniform; + struct VertexOutput { @location(0) uv: vec2, @location(1) color: vec4, - @location(3) @interpolate(flat) mode: u32, + @location(2) @interpolate(flat) mode: u32, + @location(3) size: vec2, + @location(4) start_point: vec2, + @location(5) border_color: vec4, + @location(6) border_width: f32, + @location(7) corner_radius: f32, @builtin(position) position: vec4, }; +fn unpack_color_from_u32(color: u32) -> vec4 { + return vec4((vec4(color) >> vec4(0u, 8u, 16u, 24u)) & vec4(255u)) / 255.0; +} + @vertex fn vertex( @location(0) vertex_position: vec3, @location(1) vertex_uv: vec2, @location(2) vertex_color: vec4, @location(3) mode: u32, + @location(4) ui_uniform_index: u32, ) -> VertexOutput { var out: VertexOutput; + var node = ui_uniform.entries[ui_uniform_index]; out.uv = vertex_uv; out.position = view.view_proj * vec4(vertex_position, 1.0); out.color = vertex_color; out.mode = mode; + out.size = node.size; + out.point = vertex_position.xy - node.center; + out.border_width = node.border_width; + out.border_color = unpack_color_from_u32(node.border_color); + + // get radius for this specific corner + var corner_index = select(0, 1, out.start_point.y > 0.0) + select(0, 2, out.start_point.x > 0.0); + out.corner_radius = node.corner_radius[corner_index]; + + // clamp radius between (0.0) and (shortest side / 2.0) + out.radius = clamp(out.radius, 0.0, min(out.size.x, out.size.y) / 2.0); + return out; } @group(1) @binding(0) var sprite_texture: texture_2d; @group(1) @binding(1) var sprite_sampler: sampler; +fn distance_round_border(start_point: vec2, size: vec2, radius: f32) -> f32 { + var q = abs(start_point) - size + radius; + return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0))) - radius; +} + @fragment fn fragment(in: VertexOutput) -> @location(0) vec4 { // textureSample can only be called in unform control flow, not inside an if branch. @@ -38,5 +83,19 @@ fn fragment(in: VertexOutput) -> @location(0) vec4 { } else { color = in.color; } + + if (in.radius > 0.0 || in.border_width > 0.0) { + var distance = distance_round_border(in.point, in.size * 0.5, in.radius); + + // clamp radius between (0.0) and (shortest side / 2.0) + var radius = clamp(in.radius, 0.0, min(in.size.x, in.size.y) / 2.0); + + var distance = distance_round_border(in.point, in.size * 0.5, radius); + + var inner_alpha = 1.0 - smoothStep(0.0, edge_softness, distance + edge_softness); + var border_alpha = 1.0 - smoothStep(in.border_width - border_softness, in.border_width, abs(distance)); + color = mix(vec4(0.0), mix(color, in.border_color, border_alpha), inner_alpha); + } + return color; } diff --git a/crates/bevy_ui/src/texture_slice.rs b/crates/bevy_ui/src/texture_slice.rs index 2c77f43499cf8b..48e2489a5065b3 100644 --- a/crates/bevy_ui/src/texture_slice.rs +++ b/crates/bevy_ui/src/texture_slice.rs @@ -10,7 +10,10 @@ use bevy_sprite::{ImageScaleMode, TextureSlice}; use bevy_transform::prelude::*; use bevy_utils::HashSet; -use crate::{widget::UiImageSize, BackgroundColor, CalculatedClip, ExtractedUiNode, Node, UiImage}; +use crate::{ + widget::UiImageSize, BackgroundColor, Border, CalculatedClip, CornerRadius, ExtractedUiNode, + Node, UiImage, +}; /// Component storing texture slices for image nodes entities with a tiled or sliced [`ImageScaleMode`] /// @@ -39,6 +42,8 @@ impl ComputedTextureSlices { image: &'a UiImage, clip: Option<&'a CalculatedClip>, camera_entity: Entity, + corner_radius: CornerRadius, + border: Border, ) -> impl ExactSizeIterator + 'a { let mut flip = Vec2::new(1.0, -1.0); let [mut flip_x, mut flip_y] = [false; 2]; @@ -69,6 +74,9 @@ impl ComputedTextureSlices { atlas_size, clip: clip.map(|clip| clip.clip), camera_entity, + border_color: Some(border.color), + border_width: Some(border.width), + corner_radius: Some(corner_radius.to_array()), } }) } diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index ddbe439e92db02..627b551aea29d4 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -1803,6 +1803,54 @@ impl Default for ZIndex { } } +/// The corner radius of the node +/// +/// This describes a radius value for each corner of a node, even if they have no [`Border`]. +#[derive(Component, Default, Copy, Clone, Debug, Reflect)] +#[reflect(Component)] +pub struct CornerRadius { + pub top_left: f32, + pub bottom_left: f32, + pub top_right: f32, + pub bottom_right: f32, +} + +impl CornerRadius { + /// Creates a [`CornerRadius`] instance with all corners set to the specified radius. + pub fn all(corner_radius: f32) -> Self { + Self { + top_left: corner_radius, + bottom_left: corner_radius, + top_right: corner_radius, + bottom_right: corner_radius, + } + } + + /// Creates an array with the values for all corners in this order: + /// top-left, bottom-left, top-right, bottom-right + pub fn to_array(&self) -> [f32; 4] { + [ + self.top_left, + self.bottom_left, + self.top_right, + self.bottom_right, + ] + } +} + +/// The visual properties of the node's border +#[derive(Component, Default, Copy, Clone, Debug, Reflect)] +#[reflect(Component)] +pub struct Border { + /// The width of the border + /// + /// This is different from [`Style`] border and it will not cause any displacement inside the node. + pub width: f32, + + /// The color of the border + pub color: Color, +} + #[cfg(test)] mod tests { use crate::GridPlacement; diff --git a/examples/ui/button.rs b/examples/ui/button.rs index 27ab4a19a12d59..8eb1c4b447c957 100644 --- a/examples/ui/button.rs +++ b/examples/ui/button.rs @@ -63,6 +63,11 @@ fn setup(mut commands: Commands, asset_server: Res) { justify_content: JustifyContent::Center, ..default() }, + corner_radius: CornerRadius::all(5.0), + border: Border { + color: Color::rgb(0.05, 0.05, 0.05), + width: 5.0, + }, ..default() }) .with_children(|parent| { diff --git a/examples/ui/ui.rs b/examples/ui/ui.rs index 21817b62e7f76d..f3839b5b17e4fd 100644 --- a/examples/ui/ui.rs +++ b/examples/ui/ui.rs @@ -42,43 +42,35 @@ fn setup(mut commands: Commands, asset_server: Res) { style: Style { width: Val::Px(200.), border: UiRect::all(Val::Px(2.)), + align_items: AlignItems::FlexEnd, ..default() }, - background_color: Color::rgb(0.65, 0.65, 0.65).into(), + background_color: Color::rgb(0.15, 0.15, 0.15).into(), + border: Border { + color: Color::rgb(0.65, 0.65, 0.65), + width: 2.0, + }, ..default() }) .with_children(|parent| { - // left vertical fill (content) - parent - .spawn(NodeBundle { - style: Style { - width: Val::Percent(100.), - ..default() + // text + parent.spawn(( + TextBundle::from_section( + "Text Example", + TextStyle { + font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font_size: 30.0, }, - background_color: Color::rgb(0.15, 0.15, 0.15).into(), + ) + .with_style(Style { + margin: UiRect::all(Val::Px(5.)), ..default() - }) - .with_children(|parent| { - // text - parent.spawn(( - TextBundle::from_section( - "Text Example", - TextStyle { - font: asset_server.load("fonts/FiraSans-Bold.ttf"), - font_size: 30.0, - ..default() - }, - ) - .with_style(Style { - margin: UiRect::all(Val::Px(5.)), - ..default() - }), - // Because this is a distinct label widget and - // not button/list item text, this is necessary - // for accessibility to treat the text accordingly. - Label, - )); - }); + }), + // Because this is a distinct label widget and + // not button/list item text, this is necessary + // for accessibility to treat the text accordingly. + Label, + )); }); // right vertical fill parent