diff --git a/Cargo.toml b/Cargo.toml index 453e18a148..d982bf05fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2412,6 +2412,17 @@ description = "Demonstrates resizing and responding to resizing a window" category = "Window" wasm = true +[[example]] +name = "ui_material" +path = "examples/ui/ui_material.rs" +doc-scrape-examples = true + +[package.metadata.example.ui_material] +name = "UI Material" +description = "Demonstrates creating and using custom Ui materials" +category = "UI (User Interface)" +wasm = true + [profile.wasm-release] inherits = "release" opt-level = "z" diff --git a/assets/shaders/circle_shader.wgsl b/assets/shaders/circle_shader.wgsl new file mode 100644 index 0000000000..94f7445e08 --- /dev/null +++ b/assets/shaders/circle_shader.wgsl @@ -0,0 +1,21 @@ +// This shader draws a circle with a given input color +#import bevy_ui::ui_vertex_output::UiVertexOutput + +struct CustomUiMaterial { + @location(0) color: vec4 +} + +@group(1) @binding(0) +var input: CustomUiMaterial; + +@fragment +fn fragment(in: UiVertexOutput) -> @location(0) vec4 { + // the UVs are now adjusted around the middle of the rect. + let uv = in.uv * 2.0 - 1.0; + + // circle alpha, the higher the power the harsher the falloff. + let alpha = 1.0 - pow(sqrt(dot(uv, uv)), 100.0); + + return vec4(input.color.rgb, alpha); +} + diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index ddfe5f430f..e72eb5ea77 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -8,6 +8,7 @@ pub mod camera_config; pub mod measurement; pub mod node_bundles; +pub mod ui_material; pub mod update; pub mod widget; @@ -29,6 +30,7 @@ pub use geometry::*; pub use layout::*; pub use measurement::*; pub use render::*; +pub use ui_material::*; pub use ui_node::*; use widget::UiImageSize; @@ -36,8 +38,8 @@ use widget::UiImageSize; pub mod prelude { #[doc(hidden)] pub use crate::{ - camera_config::*, geometry::*, node_bundles::*, ui_node::*, widget::Button, widget::Label, - Interaction, UiScale, + camera_config::*, geometry::*, node_bundles::*, ui_material::*, ui_node::*, widget::Button, + widget::Label, Interaction, UiMaterialPlugin, UiScale, }; } diff --git a/crates/bevy_ui/src/node_bundles.rs b/crates/bevy_ui/src/node_bundles.rs index ac527d2172..c2401446bc 100644 --- a/crates/bevy_ui/src/node_bundles.rs +++ b/crates/bevy_ui/src/node_bundles.rs @@ -5,7 +5,7 @@ use crate::widget::TextFlags; use crate::{ widget::{Button, UiImageSize}, BackgroundColor, BorderColor, ContentSize, FocusPolicy, Interaction, Node, Style, UiImage, - UiTextureAtlasImage, ZIndex, + UiMaterial, UiTextureAtlasImage, ZIndex, }; use bevy_asset::Handle; use bevy_ecs::bundle::Bundle; @@ -342,3 +342,52 @@ impl Default for ButtonBundle { } } } + +/// A UI node that is rendered using a [`UiMaterial`] +#[derive(Bundle, Clone, Debug)] +pub struct MaterialNodeBundle { + /// Describes the logical size of the node + pub node: Node, + /// Styles which control the layout (size and position) of the node and it's children + /// In some cases these styles also affect how the node drawn/painted. + pub style: Style, + /// The [`UiMaterial`] used to render the node. + pub material: Handle, + /// Whether this node should block interaction with lower nodes + pub focus_policy: FocusPolicy, + /// The transform of the node + /// + /// This field is automatically managed by the UI layout system. + /// To alter the position of the `NodeBundle`, use the properties of the [`Style`] component. + pub transform: Transform, + /// The global transform of the node + /// + /// This field is automatically managed by the UI layout system. + /// To alter the position of the `NodeBundle`, use the properties of the [`Style`] component. + pub global_transform: GlobalTransform, + /// Describes the visibility properties of the node + pub visibility: Visibility, + /// Inherited visibility of an entity. + pub inherited_visibility: InheritedVisibility, + /// 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 + pub z_index: ZIndex, +} + +impl Default for MaterialNodeBundle { + fn default() -> Self { + Self { + node: Default::default(), + style: Default::default(), + material: Default::default(), + focus_policy: Default::default(), + transform: Default::default(), + global_transform: Default::default(), + visibility: Default::default(), + inherited_visibility: Default::default(), + view_visibility: 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 a0a332bb61..f63fd1a0dc 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -1,5 +1,6 @@ mod pipeline; mod render_pass; +mod ui_material_pipeline; use bevy_core_pipeline::{core_2d::Camera2d, core_3d::Camera3d}; use bevy_hierarchy::Parent; @@ -9,6 +10,7 @@ use bevy_render::{render_resource::BindGroupEntries, ExtractSchedule, Render}; use bevy_window::{PrimaryWindow, Window}; pub use pipeline::*; pub use render_pass::*; +pub use ui_material_pipeline::*; use crate::Outline; use crate::{ @@ -253,7 +255,7 @@ pub fn extract_atlas_uinodes( } } -fn resolve_border_thickness(value: Val, parent_width: f32, viewport_size: Vec2) -> f32 { +pub(crate) fn resolve_border_thickness(value: Val, parent_width: f32, viewport_size: Vec2) -> f32 { match value { Val::Auto => 0., Val::Px(px) => px.max(0.), @@ -695,14 +697,14 @@ impl Default for UiMeta { } } -const QUAD_VERTEX_POSITIONS: [Vec3; 4] = [ +pub(crate) const QUAD_VERTEX_POSITIONS: [Vec3; 4] = [ Vec3::new(-0.5, -0.5, 0.0), Vec3::new(0.5, -0.5, 0.0), Vec3::new(0.5, 0.5, 0.0), Vec3::new(-0.5, 0.5, 0.0), ]; -const QUAD_INDICES: [usize; 6] = [0, 2, 3, 0, 1, 2]; +pub(crate) const QUAD_INDICES: [usize; 6] = [0, 2, 3, 0, 1, 2]; #[derive(Component)] pub struct UiBatch { diff --git a/crates/bevy_ui/src/render/ui_material.wgsl b/crates/bevy_ui/src/render/ui_material.wgsl new file mode 100644 index 0000000000..db9628559d --- /dev/null +++ b/crates/bevy_ui/src/render/ui_material.wgsl @@ -0,0 +1,23 @@ +#import bevy_render::view::View +#import bevy_ui::ui_vertex_output::UiVertexOutput + +@group(0) @binding(0) +var view: View; + +@vertex +fn vertex( + @location(0) vertex_position: vec3, + @location(1) vertex_uv: vec2, + @location(2) border_widths: vec4, +) -> UiVertexOutput { + var out: UiVertexOutput; + out.uv = vertex_uv; + out.position = view.view_proj * vec4(vertex_position, 1.0); + out.border_widths = border_widths; + return out; +} + +@fragment +fn fragment(in: UiVertexOutput) -> @location(0) vec4 { + return vec4(1.0); +} diff --git a/crates/bevy_ui/src/render/ui_material_pipeline.rs b/crates/bevy_ui/src/render/ui_material_pipeline.rs new file mode 100644 index 0000000000..f952f726e1 --- /dev/null +++ b/crates/bevy_ui/src/render/ui_material_pipeline.rs @@ -0,0 +1,756 @@ +use std::{hash::Hash, marker::PhantomData, ops::Range}; + +use bevy_app::{App, Plugin}; +use bevy_asset::*; +use bevy_derive::{Deref, DerefMut}; +use bevy_ecs::{ + prelude::{Component, Entity, EventReader}, + query::{ROQueryItem, With}, + schedule::IntoSystemConfigs, + storage::SparseSet, + system::lifetimeless::{Read, SRes}, + system::*, + world::{FromWorld, World}, +}; +use bevy_math::{Mat4, Rect, Vec2, Vec4Swizzles}; +use bevy_render::{ + extract_component::ExtractComponentPlugin, + render_asset::RenderAssets, + render_phase::*, + render_resource::*, + renderer::{RenderDevice, RenderQueue}, + texture::{BevyDefault, FallbackImage, Image}, + view::*, + Extract, ExtractSchedule, Render, RenderApp, RenderSet, +}; +use bevy_transform::prelude::GlobalTransform; +use bevy_utils::{FloatOrd, HashMap, HashSet}; +use bevy_window::{PrimaryWindow, Window}; +use bytemuck::{Pod, Zeroable}; + +use crate::*; + +pub const UI_MATERIAL_SHADER_HANDLE: Handle = Handle::weak_from_u128(10074188772096983955); + +const UI_VERTEX_OUTPUT_SHADER_HANDLE: Handle = Handle::weak_from_u128(10123618247720234751); + +/// Adds the necessary ECS resources and render logic to enable rendering entities using the given +/// [`UiMaterial`] asset type (which includes [`UiMaterial`] types). +pub struct UiMaterialPlugin(PhantomData); + +impl Default for UiMaterialPlugin { + fn default() -> Self { + Self(Default::default()) + } +} + +impl Plugin for UiMaterialPlugin +where + M::Data: PartialEq + Eq + Hash + Clone, +{ + fn build(&self, app: &mut bevy_app::App) { + load_internal_asset!( + app, + UI_VERTEX_OUTPUT_SHADER_HANDLE, + "ui_vertex_output.wgsl", + Shader::from_wgsl + ); + load_internal_asset!( + app, + UI_MATERIAL_SHADER_HANDLE, + "ui_material.wgsl", + Shader::from_wgsl + ); + app.init_asset::() + .add_plugins(ExtractComponentPlugin::>::extract_visible()); + + if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { + render_app + .add_render_command::>() + .init_resource::>() + .init_resource::>() + .init_resource::>() + .init_resource::() + .init_resource::>>() + .add_systems( + ExtractSchedule, + ( + extract_ui_materials::, + extract_ui_material_nodes::.in_set(RenderUiSystem::ExtractNode), + ), + ) + .add_systems( + Render, + ( + prepare_ui_materials::.in_set(RenderSet::PrepareAssets), + queue_ui_material_nodes::.in_set(RenderSet::Queue), + prepare_uimaterial_nodes::.in_set(RenderSet::PrepareBindGroups), + ), + ); + } + } + + fn finish(&self, app: &mut App) { + if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { + render_app.init_resource::>(); + } + } +} + +#[derive(Resource)] +pub struct UiMaterialMeta { + vertices: BufferVec, + view_bind_group: Option, +} + +impl Default for UiMaterialMeta { + fn default() -> Self { + Self { + vertices: BufferVec::new(BufferUsages::VERTEX), + view_bind_group: Default::default(), + } + } +} + +#[repr(C)] +#[derive(Copy, Clone, Pod, Zeroable)] +pub struct UiMaterialVertex { + pub position: [f32; 3], + pub uv: [f32; 2], + pub border_widths: [f32; 4], +} + +// in this [`UiMaterialPipeline`] there is (currently) no batching going on. +// Therefore the [`UiMaterialBatch`] is more akin to a draw call. +#[derive(Component)] +pub struct UiMaterialBatch { + /// The range of vertices inside the [`UiMaterialMeta`] + pub range: Range, + pub material: AssetId, +} + +/// Render pipeline data for a given [`UiMaterial`] +#[derive(Resource)] +pub struct UiMaterialPipeline { + pub ui_layout: BindGroupLayout, + pub view_layout: BindGroupLayout, + pub vertex_shader: Option>, + pub fragment_shader: Option>, + marker: PhantomData, +} + +impl SpecializedRenderPipeline for UiMaterialPipeline +where + M::Data: PartialEq + Eq + Hash + Clone, +{ + type Key = UiMaterialKey; + + fn specialize(&self, key: Self::Key) -> RenderPipelineDescriptor { + let vertex_layout = VertexBufferLayout::from_vertex_formats( + VertexStepMode::Vertex, + vec![ + // position + VertexFormat::Float32x3, + // uv + VertexFormat::Float32x2, + // border_widths + VertexFormat::Float32x4, + ], + ); + let shader_defs = Vec::new(); + + let mut descriptor = RenderPipelineDescriptor { + vertex: VertexState { + shader: UI_MATERIAL_SHADER_HANDLE, + entry_point: "vertex".into(), + shader_defs: shader_defs.clone(), + buffers: vec![vertex_layout], + }, + fragment: Some(FragmentState { + shader: UI_MATERIAL_SHADER_HANDLE, + shader_defs, + entry_point: "fragment".into(), + targets: vec![Some(ColorTargetState { + format: if key.hdr { + ViewTarget::TEXTURE_FORMAT_HDR + } else { + TextureFormat::bevy_default() + }, + blend: Some(BlendState::ALPHA_BLENDING), + write_mask: ColorWrites::ALL, + })], + }), + layout: vec![], + push_constant_ranges: Vec::new(), + primitive: PrimitiveState { + front_face: FrontFace::Ccw, + cull_mode: None, + unclipped_depth: false, + polygon_mode: PolygonMode::Fill, + conservative: false, + topology: PrimitiveTopology::TriangleList, + strip_index_format: None, + }, + depth_stencil: None, + multisample: MultisampleState { + count: 1, + mask: !0, + alpha_to_coverage_enabled: false, + }, + label: Some("ui_material_pipeline".into()), + }; + if let Some(vertex_shader) = &self.vertex_shader { + descriptor.vertex.shader = vertex_shader.clone(); + } + + if let Some(fragment_shader) = &self.fragment_shader { + descriptor.fragment.as_mut().unwrap().shader = fragment_shader.clone(); + } + + descriptor.layout = vec![self.view_layout.clone(), self.ui_layout.clone()]; + + M::specialize(&mut descriptor, key); + + descriptor + } +} + +impl FromWorld for UiMaterialPipeline { + fn from_world(world: &mut World) -> Self { + let asset_server = world.resource::(); + let render_device = world.resource::(); + let ui_layout = M::bind_group_layout(render_device); + + let view_layout = render_device.create_bind_group_layout(&BindGroupLayoutDescriptor { + entries: &[BindGroupLayoutEntry { + binding: 0, + visibility: ShaderStages::VERTEX | ShaderStages::FRAGMENT, + ty: BindingType::Buffer { + ty: BufferBindingType::Uniform, + has_dynamic_offset: true, + min_binding_size: Some(ViewUniform::min_size()), + }, + count: None, + }], + label: Some("ui_view_layout"), + }); + UiMaterialPipeline { + ui_layout, + view_layout, + vertex_shader: match M::vertex_shader() { + ShaderRef::Default => None, + ShaderRef::Handle(handle) => Some(handle), + ShaderRef::Path(path) => Some(asset_server.load(path)), + }, + fragment_shader: match M::fragment_shader() { + ShaderRef::Default => None, + ShaderRef::Handle(handle) => Some(handle), + ShaderRef::Path(path) => Some(asset_server.load(path)), + }, + marker: PhantomData, + } + } +} + +pub type DrawUiMaterial = ( + SetItemPipeline, + SetMatUiViewBindGroup, + SetUiMaterialBindGroup, + DrawUiMaterialNode, +); + +pub struct SetMatUiViewBindGroup(PhantomData); +impl RenderCommand

for SetMatUiViewBindGroup { + type Param = SRes; + type ViewWorldQuery = Read; + type ItemWorldQuery = (); + + fn render<'w>( + _item: &P, + view_uniform: &'w ViewUniformOffset, + _entity: (), + ui_meta: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + pass.set_bind_group( + I, + ui_meta.into_inner().view_bind_group.as_ref().unwrap(), + &[view_uniform.offset], + ); + RenderCommandResult::Success + } +} + +pub struct SetUiMaterialBindGroup(PhantomData); +impl RenderCommand

+ for SetUiMaterialBindGroup +{ + type Param = SRes>; + type ViewWorldQuery = (); + type ItemWorldQuery = Read>; + + fn render<'w>( + _item: &P, + _view: (), + material_handle: ROQueryItem<'_, Self::ItemWorldQuery>, + materials: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + let material = materials + .into_inner() + .get(&material_handle.material) + .unwrap(); + pass.set_bind_group(I, &material.bind_group, &[]); + RenderCommandResult::Success + } +} + +pub struct DrawUiMaterialNode(PhantomData); +impl RenderCommand

for DrawUiMaterialNode { + type Param = SRes; + type ViewWorldQuery = (); + type ItemWorldQuery = Read>; + + #[inline] + fn render<'w>( + _item: &P, + _view: (), + batch: &'w UiMaterialBatch, + ui_meta: SystemParamItem<'w, '_, Self::Param>, + pass: &mut TrackedRenderPass<'w>, + ) -> RenderCommandResult { + pass.set_vertex_buffer(0, ui_meta.into_inner().vertices.buffer().unwrap().slice(..)); + pass.draw(batch.range.clone(), 0..1); + RenderCommandResult::Success + } +} + +pub struct ExtractedUiMaterialNode { + pub stack_index: usize, + pub transform: Mat4, + pub rect: Rect, + pub border: [f32; 4], + pub material: AssetId, + pub clip: Option, +} + +#[derive(Resource)] +pub struct ExtractedUiMaterialNodes { + pub uinodes: SparseSet>, +} + +impl Default for ExtractedUiMaterialNodes { + fn default() -> Self { + Self { + uinodes: Default::default(), + } + } +} + +pub fn extract_ui_material_nodes( + mut extracted_uinodes: ResMut>, + materials: Extract>>, + ui_stack: Extract>, + uinode_query: Extract< + Query<( + Entity, + &Node, + &Style, + &GlobalTransform, + &Handle, + &ViewVisibility, + Option<&CalculatedClip>, + )>, + >, + windows: Extract>>, + ui_scale: Extract>, +) { + let ui_logical_viewport_size = windows + .get_single() + .map(|window| Vec2::new(window.resolution.width(), window.resolution.height())) + .unwrap_or(Vec2::ZERO) + // The logical window resolution returned by `Window` only takes into account the window scale factor and not `UiScale`, + // so we have to divide by `UiScale` to get the size of the UI viewport. + / ui_scale.0 as f32; + for (stack_index, entity) in ui_stack.uinodes.iter().enumerate() { + if let Ok((entity, uinode, style, transform, handle, view_visibility, clip)) = + uinode_query.get(*entity) + { + // skip invisible nodes + if !view_visibility.get() { + continue; + } + + // Skip loading materials + if !materials.contains(handle) { + continue; + } + + // Both vertical and horizontal percentage border values are calculated based on the width of the parent node + // + let parent_width = uinode.size().x; + let left = + resolve_border_thickness(style.border.left, parent_width, ui_logical_viewport_size) + / uinode.size().x; + let right = resolve_border_thickness( + style.border.right, + parent_width, + ui_logical_viewport_size, + ) / uinode.size().y; + let top = + resolve_border_thickness(style.border.top, parent_width, ui_logical_viewport_size) + / uinode.size().y; + let bottom = resolve_border_thickness( + style.border.bottom, + parent_width, + ui_logical_viewport_size, + ) / uinode.size().y; + + extracted_uinodes.uinodes.insert( + entity, + ExtractedUiMaterialNode { + stack_index, + transform: transform.compute_matrix(), + material: handle.id(), + rect: Rect { + min: Vec2::ZERO, + max: uinode.calculated_size, + }, + border: [left, right, top, bottom], + clip: clip.map(|clip| clip.clip), + }, + ); + }; + } +} + +#[allow(clippy::too_many_arguments)] +pub fn prepare_uimaterial_nodes( + mut commands: Commands, + render_device: Res, + render_queue: Res, + mut ui_meta: ResMut, + mut extracted_uinodes: ResMut>, + view_uniforms: Res, + ui_material_pipeline: Res>, + mut phases: Query<&mut RenderPhase>, + mut previous_len: Local, +) { + if let Some(view_binding) = view_uniforms.uniforms.binding() { + let mut batches: Vec<(Entity, UiMaterialBatch)> = Vec::with_capacity(*previous_len); + + ui_meta.vertices.clear(); + ui_meta.view_bind_group = Some(render_device.create_bind_group( + "ui_material_view_bind_group", + &ui_material_pipeline.view_layout, + &BindGroupEntries::single(view_binding), + )); + let mut index = 0; + + for mut ui_phase in &mut phases { + let mut batch_item_index = 0; + let mut batch_shader_handle = AssetId::invalid(); + + for item_index in 0..ui_phase.items.len() { + let item = &mut ui_phase.items[item_index]; + if let Some(extracted_uinode) = extracted_uinodes.uinodes.get(item.entity) { + let mut existing_batch = batches + .last_mut() + .filter(|_| batch_shader_handle == extracted_uinode.material); + + if existing_batch.is_none() { + batch_item_index = item_index; + batch_shader_handle = extracted_uinode.material; + + let new_batch = UiMaterialBatch { + range: index..index, + material: extracted_uinode.material, + }; + + batches.push((item.entity, new_batch)); + + existing_batch = batches.last_mut(); + } + + let uinode_rect = extracted_uinode.rect; + + let rect_size = uinode_rect.size().extend(1.0); + + let positions = QUAD_VERTEX_POSITIONS.map(|pos| { + (extracted_uinode.transform * (pos * rect_size).extend(1.0)).xyz() + }); + + let positions_diff = if let Some(clip) = extracted_uinode.clip { + [ + Vec2::new( + f32::max(clip.min.x - positions[0].x, 0.), + f32::max(clip.min.y - positions[0].y, 0.), + ), + Vec2::new( + f32::min(clip.max.x - positions[1].x, 0.), + f32::max(clip.min.y - positions[1].y, 0.), + ), + Vec2::new( + f32::min(clip.max.x - positions[2].x, 0.), + f32::min(clip.max.y - positions[2].y, 0.), + ), + Vec2::new( + f32::max(clip.min.x - positions[3].x, 0.), + f32::min(clip.max.y - positions[3].y, 0.), + ), + ] + } else { + [Vec2::ZERO; 4] + }; + + let positions_clipped = [ + positions[0] + positions_diff[0].extend(0.), + positions[1] + positions_diff[1].extend(0.), + positions[2] + positions_diff[2].extend(0.), + positions[3] + positions_diff[3].extend(0.), + ]; + + let transformed_rect_size = + extracted_uinode.transform.transform_vector3(rect_size); + + // Don't try to cull nodes that have a rotation + // In a rotation around the Z-axis, this value is 0.0 for an angle of 0.0 or π + // In those two cases, the culling check can proceed normally as corners will be on + // horizontal / vertical lines + // For all other angles, bypass the culling check + // This does not properly handles all rotations on all axis + if extracted_uinode.transform.x_axis[1] == 0.0 { + // Cull nodes that are completely clipped + if positions_diff[0].x - positions_diff[1].x >= transformed_rect_size.x + || positions_diff[1].y - positions_diff[2].y >= transformed_rect_size.y + { + continue; + } + } + let uvs = [ + Vec2::new( + uinode_rect.min.x + positions_diff[0].x, + uinode_rect.min.y + positions_diff[0].y, + ), + Vec2::new( + uinode_rect.max.x + positions_diff[1].x, + uinode_rect.min.y + positions_diff[1].y, + ), + Vec2::new( + uinode_rect.max.x + positions_diff[2].x, + uinode_rect.max.y + positions_diff[2].y, + ), + Vec2::new( + uinode_rect.min.x + positions_diff[3].x, + uinode_rect.max.y + positions_diff[3].y, + ), + ] + .map(|pos| pos / uinode_rect.max); + + for i in QUAD_INDICES { + ui_meta.vertices.push(UiMaterialVertex { + position: positions_clipped[i].into(), + uv: uvs[i].into(), + border_widths: extracted_uinode.border, + }); + } + + index += QUAD_INDICES.len() as u32; + existing_batch.unwrap().1.range.end = index; + ui_phase.items[batch_item_index].batch_range_mut().end += 1; + } else { + batch_shader_handle = AssetId::invalid(); + } + } + } + ui_meta.vertices.write_buffer(&render_device, &render_queue); + *previous_len = batches.len(); + commands.insert_or_spawn_batch(batches); + } + extracted_uinodes.uinodes.clear(); +} + +#[derive(Resource, Deref, DerefMut)] +pub struct RenderUiMaterials(HashMap, PreparedUiMaterial>); + +impl Default for RenderUiMaterials { + fn default() -> Self { + Self(Default::default()) + } +} + +pub struct PreparedUiMaterial { + pub bindings: Vec<(u32, OwnedBindingResource)>, + pub bind_group: BindGroup, + pub key: T::Data, +} + +#[derive(Resource)] +pub struct ExtractedUiMaterials { + extracted: Vec<(AssetId, M)>, + removed: Vec>, +} + +impl Default for ExtractedUiMaterials { + fn default() -> Self { + Self { + extracted: Default::default(), + removed: Default::default(), + } + } +} + +pub fn extract_ui_materials( + mut commands: Commands, + mut events: Extract>>, + assets: Extract>>, +) { + let mut changed_assets = HashSet::default(); + let mut removed = Vec::new(); + for event in events.read() { + match event { + AssetEvent::Added { id } | AssetEvent::Modified { id } => { + changed_assets.insert(*id); + } + AssetEvent::Removed { id } => { + changed_assets.remove(id); + removed.push(*id); + } + AssetEvent::LoadedWithDependencies { .. } => { + // not implemented + } + } + } + + let mut extracted_assets = Vec::new(); + for id in changed_assets.drain() { + if let Some(asset) = assets.get(id) { + extracted_assets.push((id, asset.clone())); + } + } + + commands.insert_resource(ExtractedUiMaterials { + extracted: extracted_assets, + removed, + }); +} + +pub struct PrepareNextFrameMaterials { + assets: Vec<(AssetId, M)>, +} + +impl Default for PrepareNextFrameMaterials { + fn default() -> Self { + Self { + assets: Default::default(), + } + } +} + +pub fn prepare_ui_materials( + mut prepare_next_frame: Local>, + mut extracted_assets: ResMut>, + mut render_materials: ResMut>, + render_device: Res, + images: Res>, + fallback_image: Res, + pipeline: Res>, +) { + let queued_assets = std::mem::take(&mut prepare_next_frame.assets); + for (id, material) in queued_assets { + match prepare_ui_material( + &material, + &render_device, + &images, + &fallback_image, + &pipeline, + ) { + Ok(prepared_asset) => { + render_materials.insert(id, prepared_asset); + } + Err(AsBindGroupError::RetryNextUpdate) => { + prepare_next_frame.assets.push((id, material)); + } + } + } + + for removed in std::mem::take(&mut extracted_assets.removed) { + render_materials.remove(&removed); + } + + for (handle, material) in std::mem::take(&mut extracted_assets.extracted) { + match prepare_ui_material( + &material, + &render_device, + &images, + &fallback_image, + &pipeline, + ) { + Ok(prepared_asset) => { + render_materials.insert(handle, prepared_asset); + } + Err(AsBindGroupError::RetryNextUpdate) => { + prepare_next_frame.assets.push((handle, material)); + } + } + } +} + +fn prepare_ui_material( + material: &M, + render_device: &RenderDevice, + images: &RenderAssets, + fallback_image: &Res, + pipeline: &UiMaterialPipeline, +) -> Result, AsBindGroupError> { + let prepared = + material.as_bind_group(&pipeline.ui_layout, render_device, images, fallback_image)?; + Ok(PreparedUiMaterial { + bindings: prepared.bindings, + bind_group: prepared.bind_group, + key: prepared.data, + }) +} + +#[allow(clippy::too_many_arguments)] +pub fn queue_ui_material_nodes( + extracted_uinodes: Res>, + draw_functions: Res>, + ui_material_pipeline: Res>, + mut pipelines: ResMut>>, + pipeline_cache: Res, + render_materials: Res>, + mut views: Query<(&ExtractedView, &mut RenderPhase)>, +) where + M::Data: PartialEq + Eq + Hash + Clone, +{ + let draw_function = draw_functions.read().id::>(); + + for (entity, extracted_uinode) in extracted_uinodes.uinodes.iter() { + let material = render_materials.get(&extracted_uinode.material).unwrap(); + for (view, mut transparent_phase) in &mut views { + let pipeline = pipelines.specialize( + &pipeline_cache, + &ui_material_pipeline, + UiMaterialKey { + hdr: view.hdr, + bind_group_data: material.key.clone(), + }, + ); + transparent_phase + .items + .reserve(extracted_uinodes.uinodes.len()); + transparent_phase.add(TransparentUi { + draw_function, + pipeline, + entity: *entity, + sort_key: ( + FloatOrd(extracted_uinode.stack_index as f32), + entity.index(), + ), + batch_range: 0..0, + dynamic_offset: None, + }); + } + } +} diff --git a/crates/bevy_ui/src/render/ui_vertex_output.wgsl b/crates/bevy_ui/src/render/ui_vertex_output.wgsl new file mode 100644 index 0000000000..de41c52819 --- /dev/null +++ b/crates/bevy_ui/src/render/ui_vertex_output.wgsl @@ -0,0 +1,9 @@ +#define_import_path bevy_ui::ui_vertex_output + +// The Vertex output of the default vertex shader for the Ui Material pipeline. +struct UiVertexOutput { + @location(0) uv: vec2, + // The size of the borders in UV space. Order is Left, Right, Top, Bottom. + @location(1) border_widths: vec4, + @builtin(position) position: vec4, +}; diff --git a/crates/bevy_ui/src/ui_material.rs b/crates/bevy_ui/src/ui_material.rs new file mode 100644 index 0000000000..680d4aa410 --- /dev/null +++ b/crates/bevy_ui/src/ui_material.rs @@ -0,0 +1,145 @@ +use std::hash::Hash; + +use bevy_asset::Asset; +use bevy_render::render_resource::{AsBindGroup, RenderPipelineDescriptor, ShaderRef}; + +/// Materials are used alongside [`UiMaterialPlugin`](crate::UiMaterialPipeline) and [`MaterialNodeBundle`](crate::prelude::MaterialNodeBundle) +/// to spawn entities that are rendered with a specific [`UiMaterial`] type. They serve as an easy to use high level +/// way to render `Node` entities with custom shader logic. +/// +/// `UiMaterials` must implement [`AsBindGroup`] to define how data will be transferred to the GPU and bound in shaders. +/// [`AsBindGroup`] can be derived, which makes generating bindings straightforward. See the [`AsBindGroup`] docs for details. +/// +/// Materials must also implement [`Asset`] so they can be treated as such. +/// +/// If you are only using the fragment shader, make sure your shader imports the `UiVertexOutput` +/// from `bevy_ui::ui_vertex_output` and uses it as the input of your fragment shader like the +/// example below does. +/// +/// # Example +/// +/// Here is a simple [`UiMaterial`] implementation. The [`AsBindGroup`] derive has many features. To see what else is available, +/// check out the [`AsBindGroup`] documentation. +/// ``` +/// # use bevy_ui::prelude::*; +/// # use bevy_ecs::prelude::*; +/// # use bevy_reflect::TypePath; +/// # use bevy_render::{render_resource::{AsBindGroup, ShaderRef}, texture::Image, color::Color}; +/// # use bevy_asset::{Handle, AssetServer, Assets, Asset}; +/// +/// #[derive(AsBindGroup, Asset, TypePath, Debug, Clone)] +/// pub struct CustomMaterial { +/// // Uniform bindings must implement `ShaderType`, which will be used to convert the value to +/// // its shader-compatible equivalent. Most core math types already implement `ShaderType`. +/// #[uniform(0)] +/// color: Color, +/// // Images can be bound as textures in shaders. If the Image's sampler is also needed, just +/// // add the sampler attribute with a different binding index. +/// #[texture(1)] +/// #[sampler(2)] +/// color_texture: Handle, +/// } +/// +/// // All functions on `UiMaterial` have default impls. You only need to implement the +/// // functions that are relevant for your material. +/// impl UiMaterial for CustomMaterial { +/// fn fragment_shader() -> ShaderRef { +/// "shaders/custom_material.wgsl".into() +/// } +/// } +/// +/// // Spawn an entity using `CustomMaterial`. +/// fn setup(mut commands: Commands, mut materials: ResMut>, asset_server: Res) { +/// commands.spawn(MaterialNodeBundle { +/// style: Style { +/// width: Val::Percent(100.0), +/// ..Default::default() +/// }, +/// material: materials.add(CustomMaterial { +/// color: Color::RED, +/// color_texture: asset_server.load("some_image.png"), +/// }), +/// ..Default::default() +/// }); +/// } +/// ``` +/// In WGSL shaders, the material's binding would look like this: +/// +/// If you only use the fragment shader make sure to import `UiVertexOutput` from +/// `bevy_ui::ui_vertex_output` in your wgsl shader. +/// Also note that bind group 0 is always bound to the [`View Uniform`](bevy_render::view::ViewUniform). +/// +/// ```wgsl +/// #import bevy_ui::ui_vertex_output UiVertexOutput +/// +/// struct CustomMaterial { +/// color: vec4, +/// } +/// +/// @group(1) @binding(0) +/// var material: CustomMaterial; +/// @group(1) @binding(1) +/// var color_texture: texture_2d; +/// @group(1) @binding(2) +/// var color_sampler: sampler; +/// +/// @fragment +/// fn fragment(in: UiVertexOutput) -> @location(0) vec4 { +/// +/// } +/// ``` +pub trait UiMaterial: AsBindGroup + Asset + Clone + Sized { + /// Returns this materials vertex shader. If [`ShaderRef::Default`] is returned, the default UI + /// vertex shader will be used. + fn vertex_shader() -> ShaderRef { + ShaderRef::Default + } + + /// Returns this materials fragment shader. If [`ShaderRef::Default`] is returned, the default + /// UI fragment shader will be used. + fn fragment_shader() -> ShaderRef { + ShaderRef::Default + } + + #[allow(unused_variables)] + #[inline] + fn specialize(descriptor: &mut RenderPipelineDescriptor, key: UiMaterialKey) {} +} + +pub struct UiMaterialKey { + pub hdr: bool, + pub bind_group_data: M::Data, +} + +impl Eq for UiMaterialKey where M::Data: PartialEq {} + +impl PartialEq for UiMaterialKey +where + M::Data: PartialEq, +{ + fn eq(&self, other: &Self) -> bool { + self.hdr == other.hdr && self.bind_group_data == other.bind_group_data + } +} + +impl Clone for UiMaterialKey +where + M::Data: Clone, +{ + fn clone(&self) -> Self { + Self { + hdr: self.hdr, + bind_group_data: self.bind_group_data.clone(), + } + } +} + +impl Hash for UiMaterialKey +where + M::Data: Hash, +{ + fn hash(&self, state: &mut H) { + self.hdr.hash(state); + self.bind_group_data.hash(state); + } +} diff --git a/examples/README.md b/examples/README.md index ed5a095aeb..db73c982f0 100644 --- a/examples/README.md +++ b/examples/README.md @@ -369,6 +369,7 @@ Example | Description [Text Wrap Debug](../examples/ui/text_wrap_debug.rs) | Demonstrates text wrapping [Transparency UI](../examples/ui/transparency_ui.rs) | Demonstrates transparency for UI [UI](../examples/ui/ui.rs) | Illustrates various features of Bevy UI +[UI Material](../examples/ui/ui_material.rs) | Demonstrates creating and using custom Ui materials [UI Scaling](../examples/ui/ui_scaling.rs) | Illustrates how to scale the UI [UI Texture Atlas](../examples/ui/ui_texture_atlas.rs) | Illustrates how to use TextureAtlases in UI [UI Z-Index](../examples/ui/z_index.rs) | Demonstrates how to control the relative depth (z-position) of UI elements diff --git a/examples/ui/ui_material.rs b/examples/ui/ui_material.rs new file mode 100644 index 0000000000..82a2509f83 --- /dev/null +++ b/examples/ui/ui_material.rs @@ -0,0 +1,65 @@ +//! Demonstrates the use of [`UiMaterials`](UiMaterial) and how to change material values + +use bevy::prelude::*; +use bevy::reflect::TypePath; +use bevy::render::render_resource::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_plugins(UiMaterialPlugin::::default()) + .add_systems(Startup, setup) + .add_systems(Update, update) + .run(); +} + +fn update(time: Res