diff --git a/crates/bevy_ui/src/entity.rs b/crates/bevy_ui/src/entity.rs index 5345ee3dd67264..43cc8f41425ce8 100644 --- a/crates/bevy_ui/src/entity.rs +++ b/crates/bevy_ui/src/entity.rs @@ -4,14 +4,10 @@ use crate::{ widget::{Button, ImageMode}, CalculatedSize, FocusPolicy, Interaction, Node, Style, UiColor, UiImage, }; -use bevy_ecs::{ - bundle::Bundle, - prelude::{Component, With}, - query::QueryItem, -}; +use bevy_ecs::{bundle::Bundle, prelude::Component}; use bevy_render::{ - camera::Camera, extract_component::ExtractComponent, prelude::ComputedVisibility, - view::Visibility, + prelude::ComputedVisibility, + view::{RenderLayers, Visibility}, }; use bevy_text::Text; use bevy_transform::prelude::{GlobalTransform, Transform}; @@ -37,6 +33,8 @@ pub struct NodeBundle { pub visibility: Visibility, /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering pub computed_visibility: ComputedVisibility, + /// The UI camera layers this node is visible in. + pub render_layers: RenderLayers, } /// A UI node that is an image @@ -64,6 +62,8 @@ pub struct ImageBundle { pub visibility: Visibility, /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering pub computed_visibility: ComputedVisibility, + /// The ui camera layers this image is visible in. + pub render_layers: RenderLayers, } /// A UI node that is text @@ -87,6 +87,8 @@ pub struct TextBundle { pub visibility: Visibility, /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering pub computed_visibility: ComputedVisibility, + /// The ui camera layers this text is visible in. + pub render_layers: RenderLayers, } impl Default for TextBundle { @@ -101,12 +103,13 @@ impl Default for TextBundle { global_transform: Default::default(), visibility: Default::default(), computed_visibility: Default::default(), + render_layers: Default::default(), } } } /// A UI node that is a button -#[derive(Bundle, Clone, Debug)] +#[derive(Bundle, Clone, Debug, Default)] pub struct ButtonBundle { /// Describes the size of the node pub node: Node, @@ -130,25 +133,10 @@ pub struct ButtonBundle { pub visibility: Visibility, /// Algorithmically-computed indication of whether an entity is visible and should be extracted for rendering pub computed_visibility: ComputedVisibility, + /// The ui camera layers this button is visible in. + pub render_layers: RenderLayers, } -impl Default for ButtonBundle { - fn default() -> Self { - ButtonBundle { - button: Button, - interaction: Default::default(), - focus_policy: Default::default(), - node: Default::default(), - style: Default::default(), - color: Default::default(), - image: Default::default(), - transform: Default::default(), - global_transform: Default::default(), - visibility: Default::default(), - computed_visibility: Default::default(), - } - } -} /// Configuration for cameras related to UI. /// /// When a [`Camera`] doesn't have the [`UiCameraConfig`] component, @@ -162,19 +150,15 @@ pub struct UiCameraConfig { /// When a `Camera` doesn't have the [`UiCameraConfig`] component, /// it will display the UI by default. pub show_ui: bool, + /// The ui camera layers this camera can see. + pub ui_render_layers: RenderLayers, } impl Default for UiCameraConfig { fn default() -> Self { - Self { show_ui: true } - } -} - -impl ExtractComponent for UiCameraConfig { - type Query = &'static Self; - type Filter = With; - - fn extract_component(item: QueryItem) -> Self { - item.clone() + Self { + show_ui: true, + ui_render_layers: Default::default(), + } } } diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index 54a0e75189f3bf..d72c1b83495d16 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -12,7 +12,6 @@ pub mod entity; pub mod update; pub mod widget; -use bevy_render::extract_component::ExtractComponentPlugin; pub use flex::*; pub use focus::*; pub use geometry::*; @@ -32,8 +31,6 @@ use bevy_transform::TransformSystem; use bevy_window::ModifiesWindows; use update::{ui_z_system, update_clipping_system}; -use crate::prelude::UiCameraConfig; - /// The basic plugin for Bevy UI #[derive(Default)] pub struct UiPlugin; @@ -49,8 +46,7 @@ pub enum UiSystem { impl Plugin for UiPlugin { fn build(&self, app: &mut App) { - app.add_plugin(ExtractComponentPlugin::::default()) - .init_resource::() + app.init_resource::() .register_type::() .register_type::() .register_type::() diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index e26d91ae23ac4e..4b911c937e2b86 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -20,14 +20,13 @@ use bevy_render::{ render_resource::*, renderer::{RenderDevice, RenderQueue}, texture::Image, - view::{ComputedVisibility, ExtractedView, ViewUniforms}, + view::{ComputedVisibility, ExtractedView, RenderLayers, ViewUniforms}, Extract, RenderApp, RenderStage, }; use bevy_sprite::{Rect, SpriteAssetEvents, TextureAtlas}; use bevy_text::{DefaultTextPipeline, Text}; use bevy_transform::components::GlobalTransform; -use bevy_utils::FloatOrd; -use bevy_utils::HashMap; +use bevy_utils::{FloatOrd, HashMap}; use bevy_window::{WindowId, Windows}; use bytemuck::{Pod, Zeroable}; use std::ops::Range; @@ -166,6 +165,7 @@ pub struct ExtractedUiNode { pub image: Handle, pub atlas_size: Option, pub clip: Option, + pub render_layers: RenderLayers, } #[derive(Default)] @@ -183,12 +183,13 @@ pub fn extract_uinodes( &UiColor, &UiImage, &ComputedVisibility, + &RenderLayers, Option<&CalculatedClip>, )>, >, ) { extracted_uinodes.uinodes.clear(); - for (uinode, transform, color, image, visibility, clip) in uinode_query.iter() { + for (uinode, transform, color, image, visibility, render_layers, clip) in uinode_query.iter() { if !visibility.is_visible() { continue; } @@ -207,6 +208,7 @@ pub fn extract_uinodes( image, atlas_size: None, clip: clip.map(|clip| clip.clip), + render_layers: *render_layers, }); } } @@ -222,30 +224,36 @@ const UI_CAMERA_FAR: f32 = 1000.0; // TODO: Evaluate if we still need this. const UI_CAMERA_TRANSFORM_OFFSET: f32 = -0.1; -#[derive(Component)] -pub struct DefaultCameraView(pub Entity); +#[derive(Component, Debug)] +pub struct UiCamera { + pub entity: Entity, + layers: RenderLayers, +} pub fn extract_default_ui_camera_view( mut commands: Commands, query: Extract), With>>, ) { - for (entity, camera, camera_ui) in query.iter() { + for (camera_entity, camera, opt_ui_config) in query.iter() { + let ui_config = opt_ui_config.cloned().unwrap_or_default(); // ignore cameras with disabled ui - if matches!(camera_ui, Some(&UiCameraConfig { show_ui: false, .. })) { + if !ui_config.show_ui { continue; } - if let (Some(logical_size), Some(physical_size)) = ( - camera.logical_viewport_size(), - camera.physical_viewport_size(), - ) { - let mut projection = OrthographicProjection { - far: UI_CAMERA_FAR, - window_origin: WindowOrigin::BottomLeft, - depth_calculation: DepthCalculation::ZDifference, - ..Default::default() - }; - projection.update(logical_size.x, logical_size.y); - let default_camera_view = commands + let logical_size = if let Some(logical_size) = camera.logical_viewport_size() { + logical_size + } else { + continue; + }; + let mut projection = OrthographicProjection { + far: UI_CAMERA_FAR, + window_origin: WindowOrigin::BottomLeft, + depth_calculation: DepthCalculation::ZDifference, + ..Default::default() + }; + projection.update(logical_size.x, logical_size.y); + if let Some(physical_size) = camera.physical_viewport_size() { + let ui_camera = commands .spawn() .insert(ExtractedView { projection: projection.get_projection_matrix(), @@ -258,8 +266,11 @@ pub fn extract_default_ui_camera_view( height: physical_size.y, }) .id(); - commands.get_or_spawn(entity).insert_bundle(( - DefaultCameraView(default_camera_view), + commands.get_or_spawn(camera_entity).insert_bundle(( + UiCamera { + entity: ui_camera, + layers: ui_config.ui_render_layers, + }, RenderPhase::::default(), )); } @@ -278,12 +289,14 @@ pub fn extract_text_uinodes( &GlobalTransform, &Text, &ComputedVisibility, + &RenderLayers, Option<&CalculatedClip>, )>, >, ) { let scale_factor = windows.scale_factor(WindowId::primary()) as f32; - for (entity, uinode, global_transform, text, visibility, clip) in uinode_query.iter() { + + for (entity, uinode, transform, text, visibility, render_layers, clip) in uinode_query.iter() { if !visibility.is_visible() { continue; } @@ -306,7 +319,7 @@ pub fn extract_text_uinodes( let atlas_size = Some(atlas.size); // NOTE: Should match `bevy_text::text2d::extract_text2d_sprite` - let extracted_transform = global_transform.compute_matrix() + let extracted_transform = transform.compute_matrix() * Mat4::from_scale(Vec3::splat(scale_factor.recip())) * Mat4::from_translation( alignment_offset * scale_factor + text_glyph.position.extend(0.), @@ -319,6 +332,7 @@ pub fn extract_text_uinodes( image: texture, atlas_size, clip: clip.map(|clip| clip.clip), + render_layers: *render_layers, }); } } @@ -356,8 +370,10 @@ const QUAD_VERTEX_POSITIONS: [Vec3; 4] = [ const QUAD_INDICES: [usize; 6] = [0, 2, 3, 0, 1, 2]; +/// UI nodes are batched per image and per layer #[derive(Component)] pub struct UiBatch { + pub layers: RenderLayers, pub range: Range, pub image: Handle, pub z: f32, @@ -380,20 +396,26 @@ pub fn prepare_uinodes( let mut start = 0; let mut end = 0; let mut current_batch_handle = Default::default(); + let mut current_batch_layers = Default::default(); let mut last_z = 0.0; for extracted_uinode in &extracted_uinodes.uinodes { - if current_batch_handle != extracted_uinode.image { + let same_layers = current_batch_layers == extracted_uinode.render_layers; + let same_handle = current_batch_handle == extracted_uinode.image; + if !same_handle || !same_layers { if start != end { commands.spawn_bundle((UiBatch { + layers: current_batch_layers, range: start..end, image: current_batch_handle, z: last_z, },)); start = end; } + current_batch_layers = extracted_uinode.render_layers; current_batch_handle = extracted_uinode.image.clone_weak(); } + // TODO: the following code is hard to grasp, a refactor would be welcome :) let uinode_rect = extracted_uinode.rect; let rect_size = uinode_rect.size().extend(1.0); @@ -471,7 +493,6 @@ pub fn prepare_uinodes( color: extracted_uinode.color.as_linear_rgba_f32(), }); } - last_z = extracted_uinode.transform.w_axis[2]; end += QUAD_INDICES.len() as u32; } @@ -479,6 +500,7 @@ pub fn prepare_uinodes( // if start != end, there is one last batch to process if start != end { commands.spawn_bundle((UiBatch { + layers: current_batch_layers, range: start..end, image: current_batch_handle, z: last_z, @@ -505,7 +527,7 @@ pub fn queue_uinodes( mut image_bind_groups: ResMut, gpu_images: Res>, ui_batches: Query<(Entity, &UiBatch)>, - mut views: Query<&mut RenderPhase>, + mut views: Query<(&mut RenderPhase, &UiCamera)>, events: Res, ) { // If an image has changed, the GpuImage has (probably) changed @@ -529,8 +551,11 @@ pub fn queue_uinodes( })); let draw_ui_function = draw_functions.read().get_id::().unwrap(); let pipeline = pipelines.specialize(&mut pipeline_cache, &ui_pipeline, UiPipelineKey {}); - for mut transparent_phase in &mut views { + for (mut transparent_phase, cam_data) in &mut views { for (entity, batch) in &ui_batches { + if !batch.layers.intersects(&cam_data.layers) { + continue; + } image_bind_groups .values .entry(batch.image.clone_weak()) diff --git a/crates/bevy_ui/src/render/render_pass.rs b/crates/bevy_ui/src/render/render_pass.rs index 66445c92a17c3b..a60148e3135969 100644 --- a/crates/bevy_ui/src/render/render_pass.rs +++ b/crates/bevy_ui/src/render/render_pass.rs @@ -1,5 +1,6 @@ +use crate::UiCamera; + use super::{UiBatch, UiImageBindGroups, UiMeta}; -use crate::{prelude::UiCameraConfig, DefaultCameraView}; use bevy_ecs::{ prelude::*, system::{lifetimeless::*, SystemParamItem}, @@ -16,15 +17,9 @@ use bevy_render::{ use bevy_utils::FloatOrd; pub struct UiPassNode { - ui_view_query: QueryState< - ( - &'static RenderPhase, - &'static ViewTarget, - Option<&'static UiCameraConfig>, - ), - With, - >, - default_camera_view_query: QueryState<&'static DefaultCameraView>, + view_query: + QueryState<(&'static RenderPhase, &'static ViewTarget), With>, + ui_camera_query: QueryState<&'static UiCamera>, } impl UiPassNode { @@ -32,8 +27,8 @@ impl UiPassNode { pub fn new(world: &mut World) -> Self { Self { - ui_view_query: world.query_filtered(), - default_camera_view_query: world.query(), + view_query: world.query_filtered(), + ui_camera_query: world.query(), } } } @@ -44,8 +39,8 @@ impl Node for UiPassNode { } fn update(&mut self, world: &mut World) { - self.ui_view_query.update_archetypes(world); - self.default_camera_view_query.update_archetypes(world); + self.view_query.update_archetypes(world); + self.ui_camera_query.update_archetypes(world); } fn run( @@ -54,31 +49,25 @@ impl Node for UiPassNode { render_context: &mut RenderContext, world: &World, ) -> Result<(), NodeRunError> { - let input_view_entity = graph.get_input_entity(Self::IN_VIEW)?; + let camera_view = graph.get_input_entity(Self::IN_VIEW)?; - let (transparent_phase, target, camera_ui) = - if let Ok(result) = self.ui_view_query.get_manual(world, input_view_entity) { + let (transparent_phase, target) = + if let Ok(result) = self.view_query.get_manual(world, camera_view) { result } else { return Ok(()); }; + if transparent_phase.items.is_empty() { return Ok(()); } - // Don't render UI for cameras where it is explicitly disabled - if matches!(camera_ui, Some(&UiCameraConfig { show_ui: false })) { - return Ok(()); - } + let ui_view_entity = + if let Ok(ui_view) = self.ui_camera_query.get_manual(world, camera_view) { + ui_view.entity + } else { + return Ok(()); + }; - // use the "default" view entity if it is defined - let view_entity = if let Ok(default_view) = self - .default_camera_view_query - .get_manual(world, input_view_entity) - { - default_view.0 - } else { - input_view_entity - }; let pass_descriptor = RenderPassDescriptor { label: Some("ui_pass"), color_attachments: &[Some(RenderPassColorAttachment { @@ -102,7 +91,7 @@ impl Node for UiPassNode { let mut tracked_pass = TrackedRenderPass::new(render_pass); for item in &transparent_phase.items { let draw_function = draw_functions.get_mut(item.draw_function).unwrap(); - draw_function.draw(world, &mut tracked_pass, view_entity, item); + draw_function.draw(world, &mut tracked_pass, ui_view_entity, item); } Ok(()) } diff --git a/examples/window/multiple_windows.rs b/examples/window/multiple_windows.rs index 82e8a5e99b928b..3d88f8ade21f9d 100644 --- a/examples/window/multiple_windows.rs +++ b/examples/window/multiple_windows.rs @@ -2,7 +2,7 @@ use bevy::{ prelude::*, - render::camera::RenderTarget, + render::{camera::RenderTarget, view::RenderLayers}, window::{CreateWindow, PresentMode, WindowId}, }; @@ -30,10 +30,15 @@ fn setup( ..default() }); // main camera - commands.spawn_bundle(Camera3dBundle { - transform: Transform::from_xyz(0.0, 0.0, 6.0).looking_at(Vec3::ZERO, Vec3::Y), - ..default() - }); + commands + .spawn_bundle(Camera3dBundle { + transform: Transform::from_xyz(0.0, 0.0, 6.0).looking_at(Vec3::ZERO, Vec3::Y), + ..default() + }) + .insert(UiCameraConfig { + ui_render_layers: RenderLayers::layer(1), + ..default() + }); let window_id = WindowId::new(); @@ -50,12 +55,41 @@ fn setup( }); // second window camera - commands.spawn_bundle(Camera3dBundle { - transform: Transform::from_xyz(6.0, 0.0, 0.0).looking_at(Vec3::ZERO, Vec3::Y), - camera: Camera { - target: RenderTarget::Window(window_id), + commands + .spawn_bundle(Camera3dBundle { + transform: Transform::from_xyz(6.0, 0.0, 0.0).looking_at(Vec3::ZERO, Vec3::Y), + camera: Camera { + target: RenderTarget::Window(window_id), + ..default() + }, ..default() - }, + }) + .insert(UiCameraConfig { + ui_render_layers: RenderLayers::layer(2), + ..default() + }); + let text_style = TextStyle { + font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font_size: 100.0, + color: Color::WHITE, + }; + let align = TextAlignment { + horizontal: HorizontalAlign::Center, + ..default() + }; + commands.spawn_bundle(TextBundle { + text: Text::with_section("Face", text_style.clone(), align), + render_layers: RenderLayers::layer(1), + ..default() + }); + commands.spawn_bundle(TextBundle { + text: Text::with_section("Profile", text_style.clone(), align), + render_layers: RenderLayers::layer(2), + ..default() + }); + commands.spawn_bundle(TextBundle { + text: Text::with_section("view", text_style, align), + render_layers: RenderLayers::all(), ..default() }); }