diff --git a/crates/bevy_ui/src/node_bundles.rs b/crates/bevy_ui/src/node_bundles.rs index dbd5f74f057ef..90d29c77d8a95 100644 --- a/crates/bevy_ui/src/node_bundles.rs +++ b/crates/bevy_ui/src/node_bundles.rs @@ -2,7 +2,8 @@ use crate::{ widget::{Button, TextFlags, UiImageSize}, - BackgroundColor, ContentSize, FocusPolicy, Interaction, Node, Style, UiImage, ZIndex, + BackgroundColor, Border, ContentSize, CornerRadius, FocusPolicy, Interaction, + Node, Style, UiImage, ZIndex, }; use bevy_ecs::bundle::Bundle; use bevy_render::{ @@ -43,6 +44,10 @@ pub struct NodeBundle { pub computed_visibility: ComputedVisibility, /// Indicates the depth at which the node should appear in the UI pub z_index: ZIndex, + /// Describes the radius of corners for the node + pub corner_radius: CornerRadius, + /// Describes the visual properties of the node's border + pub border: Border, } impl Default for NodeBundle { @@ -58,6 +63,8 @@ impl Default for NodeBundle { visibility: Default::default(), computed_visibility: Default::default(), z_index: Default::default(), + corner_radius: Default::default(), + border: Default::default(), } } } @@ -103,6 +110,10 @@ pub struct ImageBundle { pub computed_visibility: ComputedVisibility, /// Indicates the depth at which the node should appear in the UI pub z_index: ZIndex, + /// Describes the radius of corners for the node + pub corner_radius: CornerRadius, + /// Describes the visual properties of the node's border + pub border: Border, } #[cfg(feature = "bevy_text")] @@ -243,6 +254,10 @@ pub struct ButtonBundle { pub computed_visibility: ComputedVisibility, /// Indicates the depth at which the node should appear in the UI pub z_index: ZIndex, + /// Describes the radius of corners for the node + pub corner_radius: CornerRadius, + /// Describes the visual properties of the node's border + pub border: Border, } impl Default for ButtonBundle { @@ -260,6 +275,8 @@ impl Default for ButtonBundle { visibility: Default::default(), computed_visibility: Default::default(), z_index: Default::default(), + corner_radius: Default::default(), + border: Default::default(), } } } diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index f203c4b227565..ddc5257c3d57e 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -12,7 +12,10 @@ use crate::{prelude::UiCameraConfig, BackgroundColor, CalculatedClip, Node, UiIm use bevy_app::prelude::*; use bevy_asset::{load_internal_asset, AssetEvent, Assets, Handle, HandleUntyped}; use bevy_ecs::prelude::*; + use bevy_math::{Mat4, Rect, UVec4, Vec2, Vec3, Vec4Swizzles}; + +use bevy_math::{Vec3Swizzles, Vec4}; use bevy_reflect::TypeUuid; use bevy_render::texture::DEFAULT_IMAGE_HANDLE; use bevy_render::{ @@ -38,6 +41,8 @@ use bevy_utils::HashMap; use bytemuck::{Pod, Zeroable}; use std::ops::Range; +use crate::{Border, CornerRadius}; + pub mod node { pub const UI_PASS_DRIVER: &str = "ui_pass_driver"; } @@ -154,6 +159,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]>, } #[derive(Resource, Default)] @@ -173,13 +181,24 @@ pub fn extract_uinodes( Option<&UiImage>, &ComputedVisibility, Option<&CalculatedClip>, + Option<&CornerRadius>, + Option<&Border>, )>, >, ) { extracted_uinodes.uinodes.clear(); + for (stack_index, entity) in ui_stack.uinodes.iter().enumerate() { - if let Ok((uinode, transform, color, maybe_image, visibility, clip)) = - uinode_query.get(*entity) + if let Ok(( + uinode, + transform, + color, + maybe_image, + visibility, + clip, + corner_radius, + border, + )) = uinode_query.get(*entity) { // Skip invisible and completely transparent nodes if !visibility.is_visible() || color.0.a() == 0.0 { @@ -209,6 +228,9 @@ pub fn extract_uinodes( clip: clip.map(|clip| clip.clip), flip_x, flip_y, + 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()), }); } } @@ -337,6 +359,10 @@ pub fn extract_text_uinodes( clip: clip.map(|clip| clip.clip), flip_x: false, flip_y: false, + + border_color: None, + border_width: None, + corner_radius: None, }); } } @@ -349,19 +375,44 @@ struct UiVertex { pub position: [f32; 3], pub uv: [f32; 2], pub color: [f32; 4], + pub uniform_index: u32, +} + +const MAX_UI_UNIFORM_ENTRIES: usize = 256; + +#[repr(C)] +#[derive(Copy, Clone, ShaderType, Debug)] +pub struct UiUniform { + entries: [UiUniformEntry; MAX_UI_UNIFORM_ENTRIES], +} + +#[repr(C)] +#[derive(Copy, Clone, ShaderType, Debug, Default)] +pub struct UiUniformEntry { + pub color: u32, + pub size: Vec2, + pub center: Vec2, + pub border_color: u32, + pub border_width: f32, + /// NOTE: This is a Vec4 because using [f32; 4] with AsShaderType results in a 16-bytes alignment. + pub corner_radius: Vec4, } #[derive(Resource)] pub struct UiMeta { vertices: BufferVec, view_bind_group: Option, + ui_uniforms: DynamicUniformBuffer, + ui_uniform_bind_group: Option, } impl Default for UiMeta { fn default() -> Self { Self { vertices: BufferVec::new(BufferUsages::VERTEX), + ui_uniforms: Default::default(), view_bind_group: None, + ui_uniform_bind_group: None, } } } @@ -375,10 +426,11 @@ const QUAD_VERTEX_POSITIONS: [Vec3; 4] = [ const QUAD_INDICES: [usize; 6] = [0, 2, 3, 0, 1, 2]; -#[derive(Component)] +#[derive(Component, Debug)] pub struct UiBatch { pub range: Range, pub image: Handle, + pub ui_uniform_offset: u32, pub z: f32, } @@ -390,6 +442,7 @@ pub fn prepare_uinodes( mut extracted_uinodes: ResMut, ) { ui_meta.vertices.clear(); + ui_meta.ui_uniforms.clear(); // sort by ui stack index, starting from the deepest node extracted_uinodes @@ -400,14 +453,26 @@ pub fn prepare_uinodes( let mut end = 0; let mut current_batch_handle = Default::default(); let mut last_z = 0.0; + let mut current_batch_uniform: UiUniform = UiUniform { + entries: [UiUniformEntry::default(); MAX_UI_UNIFORM_ENTRIES], + }; + let mut current_uniform_index: u32 = 0; for extracted_uinode in &extracted_uinodes.uinodes { - if current_batch_handle != extracted_uinode.image { + if current_batch_handle != extracted_uinode.image + || current_uniform_index >= MAX_UI_UNIFORM_ENTRIES as u32 + { if start != end { commands.spawn(UiBatch { range: start..end, image: current_batch_handle, + ui_uniform_offset: ui_meta.ui_uniforms.push(current_batch_uniform), z: last_z, }); + + current_uniform_index = 0; + current_batch_uniform = UiUniform { + entries: [UiUniformEntry::default(); MAX_UI_UNIFORM_ENTRIES], + }; start = end; } current_batch_handle = extracted_uinode.image.clone_weak(); @@ -509,28 +574,56 @@ pub fn prepare_uinodes( }; let color = extracted_uinode.color.as_linear_rgba_f32(); + fn encode_color_as_u32(color: Color) -> u32 { + let color = color.as_linear_rgba_f32(); + // encode color as a single u32 to save space + (color[0] * 255.0) as u32 + | ((color[1] * 255.0) as u32) << 8 + | ((color[2] * 255.0) as u32) << 16 + | ((color[3] * 255.0) as u32) << 24 + } + + current_batch_uniform.entries[current_uniform_index as usize] = UiUniformEntry { + color: encode_color_as_u32(extracted_uinode.color), + size: Vec2::new(rect_size.x, rect_size.y), + center: ((positions[0] + positions[2]) / 2.0).xy(), + border_color: extracted_uinode.border_color.map_or(0, encode_color_as_u32), + border_width: extracted_uinode.border_width.unwrap_or(0.0), + corner_radius: extracted_uinode + .corner_radius + .map_or(Vec4::default(), |c| c.into()), + }; + for i in QUAD_INDICES { ui_meta.vertices.push(UiVertex { position: positions_clipped[i].into(), uv: uvs[i].into(), + uniform_index: current_uniform_index, color, }); } + current_uniform_index += 1; last_z = extracted_uinode.transform.w_axis[2]; end += QUAD_INDICES.len() as u32; } // if start != end, there is one last batch to process if start != end { + let offset = ui_meta.ui_uniforms.push(current_batch_uniform); + commands.spawn(UiBatch { range: start..end, image: current_batch_handle, + ui_uniform_offset: offset, z: last_z, }); } ui_meta.vertices.write_buffer(&render_device, &render_queue); + ui_meta + .ui_uniforms + .write_buffer(&render_device, &render_queue); } #[derive(Resource, Default)] @@ -609,4 +702,16 @@ pub fn queue_uinodes( } } } + + if let Some(uniforms_binding) = ui_meta.ui_uniforms.binding() { + ui_meta.ui_uniform_bind_group = + Some(render_device.create_bind_group(&BindGroupDescriptor { + entries: &[BindGroupEntry { + binding: 0, + resource: uniforms_binding, + }], + label: Some("ui_uniforms_bind_group"), + layout: &ui_pipeline.ui_uniform_layout, + })); + } } diff --git a/crates/bevy_ui/src/render/pipeline.rs b/crates/bevy_ui/src/render/pipeline.rs index 84cc74d11ebd7..40c437c6d8a8c 100644 --- a/crates/bevy_ui/src/render/pipeline.rs +++ b/crates/bevy_ui/src/render/pipeline.rs @@ -1,3 +1,5 @@ +use std::mem::size_of; + use bevy_ecs::prelude::*; use bevy_render::{ render_resource::*, @@ -6,10 +8,13 @@ use bevy_render::{ view::{ViewTarget, ViewUniform}, }; +use crate::UiUniform; + #[derive(Resource)] pub struct UiPipeline { pub view_layout: BindGroupLayout, pub image_layout: BindGroupLayout, + pub ui_uniform_layout: BindGroupLayout, } impl FromWorld for UiPipeline { @@ -52,9 +57,28 @@ impl FromWorld for UiPipeline { label: Some("ui_image_layout"), }); + let ui_uniform_layout = + render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { + entries: &[BindGroupLayoutEntry { + binding: 0, + visibility: ShaderStages::VERTEX, + ty: BindingType::Buffer { + ty: BufferBindingType::Uniform, + has_dynamic_offset: true, + min_binding_size: BufferSize::new( + size_of::() as u64 + ), + }, + + count: None, + }], + label: Some("ui_uniform_layout"), + }); + UiPipeline { view_layout, image_layout, + ui_uniform_layout, } } } @@ -66,7 +90,6 @@ pub struct UiPipelineKey { impl SpecializedRenderPipeline for UiPipeline { type Key = UiPipelineKey; - fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { let vertex_layout = VertexBufferLayout::from_vertex_formats( VertexStepMode::Vertex, @@ -77,10 +100,11 @@ impl SpecializedRenderPipeline for UiPipeline { VertexFormat::Float32x2, // color VertexFormat::Float32x4, + // ui_uniform entry index + VertexFormat::Uint32, ], ); let shader_defs = Vec::new(); - RenderPipelineDescriptor { vertex: VertexState { shader: super::UI_SHADER_HANDLE.typed::(), @@ -102,8 +126,14 @@ impl SpecializedRenderPipeline for UiPipeline { write_mask: ColorWrites::ALL, })], }), - layout: vec![self.view_layout.clone(), self.image_layout.clone()], + push_constant_ranges: Vec::new(), + + layout: vec![ + self.view_layout.clone(), + self.image_layout.clone(), + self.ui_uniform_layout.clone(), + ], primitive: PrimitiveState { front_face: FrontFace::Ccw, cull_mode: None, diff --git a/crates/bevy_ui/src/render/render_pass.rs b/crates/bevy_ui/src/render/render_pass.rs index 90e7b6059cecc..d5f51b4dcec55 100644 --- a/crates/bevy_ui/src/render/render_pass.rs +++ b/crates/bevy_ui/src/render/render_pass.rs @@ -122,6 +122,7 @@ pub type DrawUi = ( SetItemPipeline, SetUiViewBindGroup<0>, SetUiTextureBindGroup<1>, + SetUiUniformBindGroup<2>, DrawUiNode, ); @@ -165,6 +166,34 @@ impl RenderCommand

for SetUiTextureBindGroup RenderCommandResult::Success } } +pub struct SetUiUniformBindGroup; +impl RenderCommand

for SetUiUniformBindGroup +where + P: PhaseItem, +{ + type Param = (SRes, SQuery>); + + fn render<'w>( + item: &P, + _view: (), + _entity: (), + (ui_meta, query_batch): SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let batch = query_batch.get(item.entity()).unwrap(); + + pass.set_bind_group( + I, + ui_meta.into_inner().ui_uniform_bind_group.as_ref().unwrap(), + &[batch.ui_uniform_offset], + ); + RenderCommandResult::Success + } + + type ViewWorldQuery = (); + + type ItemWorldQuery = (); +} 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 11ce13aa5468d..306c1e4e0086d 100644 --- a/crates/bevy_ui/src/render/ui.wgsl +++ b/crates/bevy_ui/src/render/ui.wgsl @@ -6,19 +6,64 @@ var view: View; struct VertexOutput { @location(0) uv: vec2, @location(1) color: vec4, + @location(2) size: vec2, + @location(3) point: vec2, + @location(4) border_color: vec4, + @location(5) border_width: f32, + @location(6) radius: f32, @builtin(position) position: vec4, + }; +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; + + +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) ui_uniform_index: u32, ) -> VertexOutput { var out: VertexOutput; + var node = ui_uniform.entries[ui_uniform_index]; + + 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.position.y > 0.0) + select(0, 2, out.position.x > 0.0); + out.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); + + out.uv = vertex_uv; out.position = view.view_proj * vec4(vertex_position, 1.0); - out.color = vertex_color; + out.color = unpack_color_from_u32(node.color); return out; } @@ -27,9 +72,22 @@ var sprite_texture: texture_2d; @group(1) @binding(1) var sprite_sampler: sampler; +fn distance_round_border(point: vec2, size: vec2, radius: f32) -> f32 { + var q = abs(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 { var color = textureSample(sprite_texture, sprite_sampler, in.uv); color = in.color * color; + + if (in.radius > 0.0 || in.border_width > 0.0) { + var distance = distance_round_border(in.point, in.size * 0.5, in.radius); + var inner_alpha = 1.0 - smoothstep(0.0, 0.1, distance); + var border_alpha = 1.0 - smoothstep(in.border_width, 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/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index e9857566e0c83..bc32be2b0fd90 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -1802,3 +1802,55 @@ mod tests { assert_eq!(Val::default(), Val::DEFAULT); } } + +/// The corner radius of the node +/// +/// This describes a radius value for each corner of a node, even if they have no [`Border`]. +/// +/// ## Limitations +/// Currently, every UI nodes that have CornerRadius must have its BackgroundColor's opacity to something bigger than zero.
+/// TODO: Remove this limitation +#[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, +} diff --git a/examples/ui/button.rs b/examples/ui/button.rs index aed15cb1142fe..5b8e871285b60 100644 --- a/examples/ui/button.rs +++ b/examples/ui/button.rs @@ -54,6 +54,10 @@ fn setup(mut commands: Commands, asset_server: Res) { justify_content: JustifyContent::Center, ..default() }, + border: Border { + color: Color::rgb(0.05, 0.05, 0.05), + width: 1.0, + }, ..default() }) .with_children(|parent| { @@ -69,6 +73,7 @@ fn setup(mut commands: Commands, asset_server: Res) { ..default() }, background_color: NORMAL_BUTTON.into(), + corner_radius: CornerRadius::all(25.0), ..default() }) .with_children(|parent| { diff --git a/examples/ui/ui.rs b/examples/ui/ui.rs index d338446f9136b..c647477fe4092 100644 --- a/examples/ui/ui.rs +++ b/examples/ui/ui.rs @@ -40,45 +40,36 @@ fn setup(mut commands: Commands, asset_server: Res) { parent .spawn(NodeBundle { style: Style { - width: Val::Px(200.), + align_items: AlignItems::FlexEnd, + + width: Val::Percent(100.), border: UiRect::all(Val::Px(2.)), ..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() - }, - background_color: Color::rgb(0.15, 0.15, 0.15).into(), + // text + parent.spawn(TextBundle { + style: Style { + margin: UiRect::all(Val::Px(5.0)), ..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, - color: Color::WHITE, - }, - ) - .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, - )); - }); + }, + text: Text::from_section( + "Text Example", + TextStyle { + font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font_size: 30.0, + color: Color::WHITE, + }, + ), + ..default() + }); }); // right vertical fill parent