From f8bafc77275a2abeb27dd6b1f2006ce3415f3a94 Mon Sep 17 00:00:00 2001 From: Nicola Papale Date: Fri, 8 Jul 2022 18:42:38 +0200 Subject: [PATCH] Add RenderLayer support to UI cameras This enables having different UI per camera. With #5225, this enables having different interactive UIs per window. Although, to properly complete this, the focus system will need to account for RenderLayer of UI nodes. --- crates/bevy_ui/src/entity.rs | 14 +++++- crates/bevy_ui/src/render/mod.rs | 39 +++++++++++++---- crates/bevy_ui/src/render/render_pass.rs | 32 +++++++------- examples/window/multiple_windows.rs | 54 +++++++++++++++++++----- 4 files changed, 102 insertions(+), 37 deletions(-) diff --git a/crates/bevy_ui/src/entity.rs b/crates/bevy_ui/src/entity.rs index 3ffbddca992eb2..165342b0269151 100644 --- a/crates/bevy_ui/src/entity.rs +++ b/crates/bevy_ui/src/entity.rs @@ -9,7 +9,7 @@ use bevy_math::Vec2; use bevy_render::{ camera::{DepthCalculation, OrthographicProjection, WindowOrigin}, prelude::ComputedVisibility, - view::Visibility, + view::{RenderLayers, Visibility}, }; use bevy_text::Text; use bevy_transform::prelude::{GlobalTransform, Transform}; @@ -35,6 +35,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 @@ -62,6 +64,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 @@ -85,6 +89,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 { @@ -99,6 +105,7 @@ impl Default for TextBundle { global_transform: Default::default(), visibility: Default::default(), computed_visibility: Default::default(), + render_layers: Default::default(), } } } @@ -128,6 +135,8 @@ 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, } /// Configuration for cameras related to UI. @@ -145,6 +154,8 @@ pub struct UiCameraConfig { pub show_ui: bool, /// The position of the UI camera in UI space. pub position: Vec2, + /// The ui camera layers this camera can see. + pub ui_render_layers: RenderLayers, /// The projection data for the UI camera. /// /// The code relies on this not being set, @@ -172,6 +183,7 @@ impl Default for UiCameraConfig { Self { show_ui: true, position: Vec2::ZERO, + ui_render_layers: Default::default(), projection: OrthographicProjection { far: UI_CAMERA_FAR, window_origin: WindowOrigin::BottomLeft, diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index 8cefec24fb199e..d4e610071f09b8 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -20,7 +20,7 @@ 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}; @@ -166,6 +166,7 @@ pub struct ExtractedUiNode { pub image: Handle, pub atlas_size: Option, pub clip: Option, + pub render_layers: RenderLayers, } #[derive(Default)] @@ -183,12 +184,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 +209,7 @@ pub fn extract_uinodes( image, atlas_size: None, clip: clip.map(|clip| clip.clip), + render_layers: *render_layers, }); } } @@ -225,6 +228,7 @@ const UI_CAMERA_TRANSFORM_OFFSET: f32 = -0.1; #[derive(Component, Debug)] pub struct UiCamera { pub entity: Entity, + layers: RenderLayers, } pub fn extract_default_ui_camera_view( @@ -261,7 +265,10 @@ pub fn extract_default_ui_camera_view( }) .id(); commands.get_or_spawn(camera_entity).insert_bundle(( - UiCamera { entity: ui_camera }, + UiCamera { + entity: ui_camera, + layers: ui_config.ui_render_layers, + }, RenderPhase::::default(), )); } @@ -280,12 +287,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; } @@ -308,7 +317,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.), @@ -321,6 +330,7 @@ pub fn extract_text_uinodes( image: texture, atlas_size, clip: clip.map(|clip| clip.clip), + render_layers: *render_layers, }); } } @@ -358,8 +368,10 @@ const QUAD_VERTEX_POSITIONS: [Vec3; 4] = [ const QUAD_INDICES: [usize; 6] = [0, 2, 3, 0, 1, 2]; +// Ui nodes are batches per image and per layer #[derive(Component)] pub struct UiBatch { + pub layers: RenderLayers, pub range: Range, pub image: Handle, pub z: f32, @@ -382,20 +394,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); @@ -473,7 +491,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; } @@ -481,6 +498,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, @@ -507,7 +525,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 @@ -531,8 +549,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 2b3208d9305a52..a60148e3135969 100644 --- a/crates/bevy_ui/src/render/render_pass.rs +++ b/crates/bevy_ui/src/render/render_pass.rs @@ -17,9 +17,9 @@ use bevy_render::{ use bevy_utils::FloatOrd; pub struct UiPassNode { - ui_view_query: + view_query: QueryState<(&'static RenderPhase, &'static ViewTarget), With>, - default_camera_view_query: QueryState<&'static UiCamera>, + ui_camera_query: QueryState<&'static UiCamera>, } impl UiPassNode { @@ -27,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(), } } } @@ -39,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( @@ -49,10 +49,10 @@ 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) = - if let Ok(result) = self.ui_view_query.get_manual(world, input_view_entity) { + if let Ok(result) = self.view_query.get_manual(world, camera_view) { result } else { return Ok(()); @@ -61,14 +61,12 @@ impl Node for UiPassNode { if transparent_phase.items.is_empty() { return Ok(()); } - let view_entity = if let Ok(default_view) = self - .default_camera_view_query - .get_manual(world, input_view_entity) - { - default_view.entity - } else { - input_view_entity - }; + let ui_view_entity = + if let Ok(ui_view) = self.ui_camera_query.get_manual(world, camera_view) { + ui_view.entity + } else { + return Ok(()); + }; let pass_descriptor = RenderPassDescriptor { label: Some("ui_pass"), @@ -93,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() }); }