diff --git a/crates/bevy_ui/src/entity.rs b/crates/bevy_ui/src/entity.rs index 02aac0c6bc8c2..bd5baefa0d0bc 100644 --- a/crates/bevy_ui/src/entity.rs +++ b/crates/bevy_ui/src/entity.rs @@ -2,7 +2,8 @@ use crate::{ widget::{Button, ImageMode}, - CalculatedSize, FocusPolicy, Interaction, Node, Style, UiColor, UiImage, CAMERA_UI, + Border, CalculatedSize, CornerRadius, FocusPolicy, Interaction, Node, Style, UiColor, UiImage, + CAMERA_UI, }; use bevy_ecs::bundle::Bundle; use bevy_render::{ @@ -31,6 +32,10 @@ pub struct NodeBundle { pub global_transform: GlobalTransform, /// Describes the visibility properties of the node pub visibility: Visibility, + /// Describes the radius of corners for the node + pub corner_radius: CornerRadius, + /// Describes the visual properties of the node's border + pub border: Border, } /// A UI node that is an image @@ -56,6 +61,10 @@ pub struct ImageBundle { pub global_transform: GlobalTransform, /// Describes the visibility properties of the node pub visibility: Visibility, + /// Describes the radius of corners for the node + pub corner_radius: CornerRadius, + /// Describes the visual properties of the node's border + pub border: Border, } /// A UI node that is text @@ -117,6 +126,10 @@ pub struct ButtonBundle { pub global_transform: GlobalTransform, /// Describes the visibility properties of the node pub visibility: Visibility, + /// 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 { @@ -132,6 +145,8 @@ impl Default for ButtonBundle { transform: Default::default(), global_transform: Default::default(), visibility: 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 cb2269e332d32..919ac8ad21698 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -12,7 +12,7 @@ use bevy_app::prelude::*; use bevy_asset::{load_internal_asset, AssetEvent, Assets, Handle, HandleUntyped}; use bevy_core::FloatOrd; use bevy_ecs::prelude::*; -use bevy_math::{const_vec3, Mat4, Vec2, Vec3, Vec4Swizzles}; +use bevy_math::{const_vec3, Mat4, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles}; use bevy_reflect::TypeUuid; use bevy_render::{ camera::ActiveCameras, @@ -20,7 +20,7 @@ use bevy_render::{ render_asset::RenderAssets, render_graph::{RenderGraph, SlotInfo, SlotType}, render_phase::{sort_phase_system, AddRenderCommand, DrawFunctions, RenderPhase}, - render_resource::*, + render_resource::{std140::AsStd140, *}, renderer::{RenderDevice, RenderQueue}, texture::Image, view::{ViewUniforms, Visibility}, @@ -34,7 +34,7 @@ use bevy_window::Windows; use bytemuck::{Pod, Zeroable}; -use crate::{CalculatedClip, Node, UiColor, UiImage}; +use crate::{Border, CalculatedClip, CornerRadius, Node, UiColor, UiImage}; pub mod node { pub const UI_PASS_DRIVER: &str = "ui_pass_driver"; @@ -126,6 +126,9 @@ pub struct ExtractedUiNode { pub image: Handle, pub atlas_size: Option, pub clip: Option, + pub border_color: Option, + pub border_width: Option, + pub corner_radius: Option<[f32; 4]>, } #[derive(Default)] @@ -143,11 +146,15 @@ pub fn extract_uinodes( &UiImage, &Visibility, Option<&CalculatedClip>, + Option<&CornerRadius>, + Option<&Border>, )>, ) { let mut extracted_uinodes = render_world.resource_mut::(); extracted_uinodes.uinodes.clear(); - for (uinode, transform, color, image, visibility, clip) in uinode_query.iter() { + for (uinode, transform, color, image, visibility, clip, corner_radius, border) in + uinode_query.iter() + { if !visibility.is_visible { continue; } @@ -166,6 +173,9 @@ pub fn extract_uinodes( image, atlas_size: None, clip: clip.map(|clip| clip.clip), + 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()), }); } } @@ -228,6 +238,9 @@ pub fn extract_text_uinodes( image: texture, atlas_size, clip: clip.map(|clip| clip.clip), + border_color: None, + border_width: None, + corner_radius: None, }); } } @@ -239,12 +252,34 @@ 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, AsStd140, Debug)] +pub struct UiUniform { + entries: [UiUniformEntry; MAX_UI_UNIFORM_ENTRIES], +} + +#[repr(C)] +#[derive(Copy, Clone, AsStd140, 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 AsStd140 results in a 16-bytes alignment. + pub corner_radius: Vec4, } pub struct UiMeta { vertices: BufferVec, view_bind_group: Option, + ui_uniforms: DynamicUniformVec, + ui_uniform_bind_group: Option, } impl Default for UiMeta { @@ -252,6 +287,8 @@ impl Default for UiMeta { Self { vertices: BufferVec::new(BufferUsages::VERTEX), view_bind_group: None, + ui_uniforms: Default::default(), + ui_uniform_bind_group: None, } } } @@ -265,10 +302,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, } @@ -280,6 +318,7 @@ pub fn prepare_uinodes( mut extracted_uinodes: ResMut, ) { ui_meta.vertices.clear(); + ui_meta.ui_uniforms.clear(); // sort by increasing z for correct transparency extracted_uinodes @@ -290,14 +329,27 @@ 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_bundle((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(); @@ -371,35 +423,54 @@ pub fn prepare_uinodes( ] .map(|pos| pos / atlas_extent); - let color = extracted_uinode.color.as_linear_rgba_f32(); - // encode color as a single u32 to save space - let color = (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; + 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(), - color, + uniform_index: current_uniform_index, }); } + 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_bundle((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(Default)] @@ -474,4 +545,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 dee26dfbc729a..6be1c35020e9e 100644 --- a/crates/bevy_ui/src/render/pipeline.rs +++ b/crates/bevy_ui/src/render/pipeline.rs @@ -6,9 +6,12 @@ use bevy_render::{ view::ViewUniform, }; +use crate::UiUniform; + pub struct UiPipeline { pub view_layout: BindGroupLayout, pub image_layout: BindGroupLayout, + pub ui_uniform_layout: BindGroupLayout, } impl FromWorld for UiPipeline { @@ -52,9 +55,26 @@ 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(UiUniform::std140_size_static() as u64), + }, + + count: None, + }], + label: Some("ui_uniform_layout"), + }); + UiPipeline { view_layout, image_layout, + ui_uniform_layout, } } } @@ -64,7 +84,6 @@ pub struct UiPipelineKey {} impl SpecializedPipeline for UiPipeline { type Key = UiPipelineKey; - /// FIXME: there are no specialization for now, should this be removed? fn specialize(&self, _key: Self::Key) -> RenderPipelineDescriptor { let vertex_layout = VertexBufferLayout::from_vertex_formats( VertexStepMode::Vertex, @@ -73,7 +92,7 @@ impl SpecializedPipeline for UiPipeline { VertexFormat::Float32x3, // uv VertexFormat::Float32x2, - // color + // ui_uniform entry index VertexFormat::Uint32, ], ); @@ -96,7 +115,11 @@ impl SpecializedPipeline for UiPipeline { write_mask: ColorWrites::ALL, }], }), - layout: Some(vec![self.view_layout.clone(), self.image_layout.clone()]), + layout: Some(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 005963f04f5d8..80446277e912b 100644 --- a/crates/bevy_ui/src/render/render_pass.rs +++ b/crates/bevy_ui/src/render/render_pass.rs @@ -137,6 +137,7 @@ pub type DrawUi = ( SetItemPipeline, SetUiViewBindGroup<0>, SetUiTextureBindGroup<1>, + SetUiUniformBindGroup<2>, DrawUiNode, ); @@ -176,6 +177,26 @@ impl EntityRenderCommand 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 EntityRenderCommand for DrawUiNode { type Param = (SRes, SQuery>); diff --git a/crates/bevy_ui/src/render/ui.wgsl b/crates/bevy_ui/src/render/ui.wgsl index 5c6c02664cec1..955f347a1a9c9 100644 --- a/crates/bevy_ui/src/render/ui.wgsl +++ b/crates/bevy_ui/src/render/ui.wgsl @@ -5,33 +5,86 @@ struct View { [[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(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; }; +fn unpack_color_from_u32(color: u32) -> vec4 { + return vec4((vec4(color) >> vec4(0u, 8u, 16u, 24u)) & vec4(255u)) / 255.0; +} + [[stage(vertex)]] fn vertex( [[location(0)]] vertex_position: vec3, [[location(1)]] vertex_uv: vec2, - [[location(2)]] vertex_color: u32, + [[location(2)]] 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 = vec4((vec4(vertex_color) >> vec4(0u, 8u, 16u, 24u)) & vec4(255u)) / 255.0; + out.color = unpack_color_from_u32(node.color); + 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); + return out; -} +} [[group(1), binding(0)]] 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; +} + [[stage(fragment)]] fn fragment(in: VertexOutput) -> [[location(0)]] vec4 { - var color = textureSample(sprite_texture, sprite_sampler, in.uv); + 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.0, 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; -} \ No newline at end of file +} diff --git a/crates/bevy_ui/src/ui_node.rs b/crates/bevy_ui/src/ui_node.rs index f29e087c825bc..7f1415f4c9c3b 100644 --- a/crates/bevy_ui/src/ui_node.rs +++ b/crates/bevy_ui/src/ui_node.rs @@ -391,3 +391,51 @@ pub struct CalculatedClip { /// The rect of the clip pub clip: bevy_sprite::Rect, } + +/// 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, +} diff --git a/examples/ui/button.rs b/examples/ui/button.rs index 0a0191a834fc1..42732db1d46d7 100644 --- a/examples/ui/button.rs +++ b/examples/ui/button.rs @@ -56,6 +56,11 @@ fn setup(mut commands: Commands, asset_server: Res) { ..default() }, color: NORMAL_BUTTON.into(), + corner_radius: CornerRadius::all(25.0), + border: Border { + color: Color::rgb(0.05, 0.05, 0.05), + width: 1.0, + }, ..default() }) .with_children(|parent| { diff --git a/examples/ui/ui.rs b/examples/ui/ui.rs index 74e1817829ecc..0402580ef7baf 100644 --- a/examples/ui/ui.rs +++ b/examples/ui/ui.rs @@ -34,42 +34,34 @@ fn setup(mut commands: Commands, asset_server: Res) { style: Style { size: Size::new(Val::Px(200.0), Val::Percent(100.0)), border: Rect::all(Val::Px(2.0)), + align_items: AlignItems::FlexEnd, ..default() }, - color: Color::rgb(0.65, 0.65, 0.65).into(), + 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_bundle(NodeBundle { - style: Style { - size: Size::new(Val::Percent(100.0), Val::Percent(100.0)), - align_items: AlignItems::FlexEnd, - ..default() - }, - color: Color::rgb(0.15, 0.15, 0.15).into(), + // text + parent.spawn_bundle(TextBundle { + style: Style { + margin: Rect::all(Val::Px(5.0)), ..default() - }) - .with_children(|parent| { - // text - parent.spawn_bundle(TextBundle { - style: Style { - margin: Rect::all(Val::Px(5.0)), - ..default() - }, - text: Text::with_section( - "Text Example", - TextStyle { - font: asset_server.load("fonts/FiraSans-Bold.ttf"), - font_size: 30.0, - color: Color::WHITE, - }, - Default::default(), - ), - ..default() - }); - }); + }, + text: Text::with_section( + "Text Example", + TextStyle { + font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font_size: 30.0, + color: Color::WHITE, + }, + Default::default(), + ), + ..default() + }); }); // right vertical fill parent