diff --git a/Cargo.toml b/Cargo.toml index ba1947661a078..bcda40129cd3b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1427,6 +1427,16 @@ description = "Illustrates various features of Bevy UI" category = "UI (User Interface)" wasm = true +[[example]] +name = "ui_camera_movement" +path = "examples/ui/ui_camera_movement.rs" + +[package.metadata.example.ui_camera_movement] +name = "UI Camera control" +description = "Illustrates how to move and zoom the UI camera" +category = "UI (User Interface)" +wasm = true + # Window [[example]] name = "clear_color" diff --git a/crates/bevy_ui/src/entity.rs b/crates/bevy_ui/src/entity.rs index 3aa587f564d0d..37353ed869a8e 100644 --- a/crates/bevy_ui/src/entity.rs +++ b/crates/bevy_ui/src/entity.rs @@ -4,14 +4,12 @@ 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_math::Vec2; use bevy_render::{ - camera::Camera, extract_component::ExtractComponent, prelude::ComputedVisibility, - view::Visibility, + camera::{OrthographicProjection, WindowOrigin}, + prelude::ComputedVisibility, + view::{RenderLayers, Visibility}, }; use bevy_text::{Text, TextAlignment, TextSection, TextStyle}; use bevy_transform::prelude::{GlobalTransform, Transform}; @@ -37,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 @@ -64,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 @@ -87,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 TextBundle { @@ -135,12 +139,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, @@ -164,51 +169,65 @@ 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, /// it will display the UI by default. /// +/// Note that the projection is available as the [`UiCameraProjection`] component, +/// and is updated in the [`update_ui_camera_projection`] system. +/// /// [`Camera`]: bevy_render::camera::Camera -#[derive(Component, Clone)] +/// [`update_ui_camera_projection`]: crate::update::update_ui_camera_projection +#[derive(Component, Debug, Clone)] pub struct UiCameraConfig { /// Whether to output UI to this camera view. /// /// 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, + /// The position of the UI camera in UI space. + pub position: Vec2, + /// The scale of this camera's UI. + pub scale: f32, + /// The window origin of the UI camera's perspective. + pub window_origin: WindowOrigin, } -impl Default for UiCameraConfig { - fn default() -> Self { - Self { show_ui: true } +/// The projection data for the UI camera. +/// +/// This is read-only, use [`UiCameraProjection::projection`] +/// to get the projection of the UI camera attached to this camera. +/// +/// This component is on a [`Camera`] entity with a set UI camera. +/// +/// Note that the projection is updated in the [`update_ui_camera_projection`] system. +/// +/// [`Camera`]: bevy_render::camera::Camera +/// [`update_ui_camera_projection`]: crate::update::update_ui_camera_projection +#[derive(Component, Debug)] +pub struct UiCameraProjection(pub(crate) OrthographicProjection); +impl UiCameraProjection { + /// The projection of the UI camera attached to this camera. + pub fn projection(&self) -> &OrthographicProjection { + &self.0 } } -impl ExtractComponent for UiCameraConfig { - type Query = &'static Self; - type Filter = With; - - fn extract_component(item: QueryItem) -> Self { - item.clone() +impl Default for UiCameraConfig { + fn default() -> Self { + Self { + show_ui: true, + ui_render_layers: Default::default(), + position: Vec2::ZERO, + scale: 1.0, + window_origin: WindowOrigin::BottomLeft, + } } } diff --git a/crates/bevy_ui/src/focus.rs b/crates/bevy_ui/src/focus.rs index b9ef20069e9a3..a25bf04e31a6a 100644 --- a/crates/bevy_ui/src/focus.rs +++ b/crates/bevy_ui/src/focus.rs @@ -1,4 +1,4 @@ -use crate::{entity::UiCameraConfig, CalculatedClip, Node}; +use crate::{entity::UiCameraConfig, prelude::UiCameraProjection, CalculatedClip, Node}; use bevy_ecs::{ entity::Entity, prelude::Component, @@ -12,7 +12,7 @@ use bevy_render::camera::{Camera, RenderTarget}; use bevy_render::view::ComputedVisibility; use bevy_transform::components::GlobalTransform; use bevy_utils::FloatOrd; -use bevy_window::Windows; +use bevy_window::{Window, Windows}; use serde::{Deserialize, Serialize}; use smallvec::SmallVec; @@ -67,7 +67,7 @@ pub struct State { /// Entities with a hidden [`ComputedVisibility`] are always treated as released. pub fn ui_focus_system( mut state: Local, - camera: Query<(&Camera, Option<&UiCameraConfig>)>, + camera: Query<(&Camera, Option<&UiCameraConfig>, &UiCameraProjection)>, windows: Res, mouse_button_input: Res>, touches_input: Res, @@ -108,20 +108,31 @@ pub fn ui_focus_system( let is_ui_disabled = |camera_ui| matches!(camera_ui, Some(&UiCameraConfig { show_ui: false, .. })); + let curosr_position = + |(window, cam_position, projection): (&Window, Vec2, &UiCameraProjection)| { + let position = match window.cursor_position() { + Some(pos) => pos, + None => touches_input.first_pressed_position()?, + }; + // Adapt the cursor position based on UI cam position + let projection = projection.projection(); + let proj_offset = Vec2::new(projection.left, projection.bottom); + Some((position + proj_offset) * projection.scale + cam_position) + }; let cursor_position = camera .iter() - .filter(|(_, camera_ui)| !is_ui_disabled(*camera_ui)) - .filter_map(|(camera, _)| { + .filter(|(_, camera_ui, _)| !is_ui_disabled(*camera_ui)) + .filter_map(|(camera, ui_config, proj)| { if let RenderTarget::Window(window_id) = camera.target { - Some(window_id) + let ui_cam_position = ui_config.map_or(Vec2::ZERO, |c| c.position); + Some((window_id, ui_cam_position, proj)) } else { None } }) - .filter_map(|window_id| windows.get(window_id)) - .filter(|window| window.is_focused()) - .find_map(|window| window.cursor_position()) - .or_else(|| touches_input.first_pressed_position()); + .filter_map(|(window_id, pos, proj)| windows.get(window_id).map(|w| (w, pos, proj))) + .filter(|(window, _, _)| window.is_focused()) + .find_map(curosr_position); let mut moused_over_z_sorted_nodes = node_query .iter_mut() diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index 54a0e75189f3b..29e5af98d1f12 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::*; @@ -28,11 +27,12 @@ pub mod prelude { use bevy_app::prelude::*; use bevy_ecs::schedule::{ParallelSystemDescriptorCoercion, SystemLabel}; use bevy_input::InputSystem; +use bevy_render::view::VisibilitySystems; use bevy_transform::TransformSystem; use bevy_window::ModifiesWindows; -use update::{ui_z_system, update_clipping_system}; - -use crate::prelude::UiCameraConfig; +use update::{ + ui_z_system, update_clipping_system, update_layer_visibility, update_ui_camera_projection, +}; /// The basic plugin for Bevy UI #[derive(Default)] @@ -45,12 +45,19 @@ pub enum UiSystem { Flex, /// After this label, input interactions with UI entities have been updated for this frame Focus, + /// Update the [`ComputedVisibility`] component of [`Node`] entities to reflect + /// their visibility in accordance to UI cameras. + /// + /// [`ComputedVisibility`]: bevy_render::view::ComputedVisibility + LayerVisibility, + /// Update UI camera projection to fit changes to the viewport logical size + /// or configurated UI scale. + UiCameraProjection, } 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::() @@ -91,6 +98,10 @@ impl Plugin for UiPlugin { CoreStage::PostUpdate, widget::image_node_system.before(UiSystem::Flex), ) + .add_system_to_stage( + CoreStage::PostUpdate, + update_ui_camera_projection.label(UiSystem::UiCameraProjection), + ) .add_system_to_stage( CoreStage::PostUpdate, flex_node_system @@ -98,6 +109,12 @@ impl Plugin for UiPlugin { .before(TransformSystem::TransformPropagate) .after(ModifiesWindows), ) + .add_system_to_stage( + CoreStage::PostUpdate, + update_layer_visibility + .label(UiSystem::LayerVisibility) + .after(VisibilitySystems::CheckVisibility), + ) .add_system_to_stage( CoreStage::PostUpdate, ui_z_system diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index c336bbe0eaac5..3922139863227 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -5,14 +5,17 @@ use bevy_core_pipeline::{core_2d::Camera2d, core_3d::Camera3d}; pub use pipeline::*; pub use render_pass::*; -use crate::{prelude::UiCameraConfig, CalculatedClip, Node, UiColor, UiImage}; +use crate::{ + entity::{UiCameraConfig, UiCameraProjection}, + CalculatedClip, Node, UiColor, UiImage, +}; use bevy_app::prelude::*; use bevy_asset::{load_internal_asset, AssetEvent, Assets, Handle, HandleUntyped}; use bevy_ecs::prelude::*; use bevy_math::{Mat4, Vec2, Vec3, Vec4Swizzles}; use bevy_reflect::TypeUuid; use bevy_render::{ - camera::{Camera, CameraProjection, DepthCalculation, OrthographicProjection, WindowOrigin}, + camera::{Camera, CameraProjection}, color::Color, render_asset::RenderAssets, render_graph::{RenderGraph, RunGraphOnViewNode, SlotInfo, SlotType}, @@ -20,14 +23,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; @@ -159,6 +161,7 @@ fn get_ui_graph(render_app: &mut App) -> RenderGraph { ui_graph } +#[derive(Debug)] pub struct ExtractedUiNode { pub transform: Mat4, pub color: Color, @@ -166,6 +169,7 @@ pub struct ExtractedUiNode { pub image: Handle, pub atlas_size: Option, pub clip: Option, + pub render_layers: RenderLayers, } #[derive(Default)] @@ -183,12 +187,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 +212,7 @@ pub fn extract_uinodes( image, atlas_size: None, clip: clip.map(|clip| clip.clip), + render_layers: *render_layers, }); } } @@ -216,50 +222,67 @@ pub fn extract_uinodes( /// as ui elements are "stacked on top of each other", they are within the camera's view /// and have room to grow. // TODO: Consider computing this value at runtime based on the maximum z-value. -const UI_CAMERA_FAR: f32 = 1000.0; +pub(crate) const UI_CAMERA_FAR: f32 = 1000.0; // This value is subtracted from the far distance for the camera's z-position to ensure nodes at z == 0.0 are rendered // TODO: Evaluate if we still need this. const UI_CAMERA_TRANSFORM_OFFSET: f32 = -0.1; -#[derive(Component)] -pub struct DefaultCameraView(pub Entity); +/// The UI camera used by this Camera's viewport. +/// +/// This component is inserted into the render world in the +/// [`extract_default_ui_camera_view`] system. +/// +/// The component is attached to the "actual" viewport's camera. +/// The UI camera's `ExtractedView` is attached to the entity in the +/// `entity` field. +#[derive(Component, Debug)] +pub struct UiCamera { + /// The entity for the UI camera. + pub entity: Entity, + /// UI nodes layer this camera shows. + layers: RenderLayers, +} pub fn extract_default_ui_camera_view( mut commands: Commands, - query: Extract), With>>, + query: Extract< + Query< + ( + Entity, + &Camera, + &UiCameraProjection, + Option<&UiCameraConfig>, + ), + With, + >, + >, ) { - for (entity, camera, camera_ui) in query.iter() { + for (camera_entity, camera, UiCameraProjection(ui_proj), 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 + if let Some(physical_size) = camera.physical_viewport_size() { + let ui_camera = commands .spawn() .insert(ExtractedView { - projection: projection.get_projection_matrix(), + projection: ui_proj.get_projection_matrix(), transform: GlobalTransform::from_xyz( - 0.0, - 0.0, + ui_config.position.x, + ui_config.position.y, UI_CAMERA_FAR + UI_CAMERA_TRANSFORM_OFFSET, ), width: physical_size.x, 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 +301,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 +331,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 +344,7 @@ pub fn extract_text_uinodes( image: texture, atlas_size, clip: clip.map(|clip| clip.clip), + render_layers: *render_layers, }); } } @@ -356,8 +382,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 +408,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); @@ -479,7 +513,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; } @@ -487,6 +520,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, @@ -513,7 +547,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 @@ -537,8 +571,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 66445c92a17c3..a60148e313596 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/crates/bevy_ui/src/update.rs b/crates/bevy_ui/src/update.rs index 05628555f1980..e7d1da7cd3ba5 100644 --- a/crates/bevy_ui/src/update.rs +++ b/crates/bevy_ui/src/update.rs @@ -1,15 +1,23 @@ //! This module contains systems that update the UI when something changes -use crate::{CalculatedClip, Overflow, Style}; +use crate::{ + entity::UiCameraConfig, prelude::UiCameraProjection, CalculatedClip, Overflow, Style, + UI_CAMERA_FAR, +}; use super::Node; use bevy_ecs::{ entity::Entity, + prelude::{Changed, Or}, query::{With, Without}, system::{Commands, Query}, }; use bevy_hierarchy::{Children, Parent}; use bevy_math::Vec2; +use bevy_render::{ + camera::{Camera, CameraProjection, DepthCalculation, OrthographicProjection}, + view::{ComputedVisibility, RenderLayers}, +}; use bevy_sprite::Rect; use bevy_transform::components::{GlobalTransform, Transform}; @@ -64,6 +72,30 @@ fn update_hierarchy( current_global_z } +/// Correct the `ComputedVisibility` set by [`check_visibility`] for UI nodes. +/// +/// Since [`check_visibility`] has no concept of UI cameras, we need a "post-pass" +/// where we re-enable UI nodes that are visible because they have a matching +/// UI camera in their layer. +/// +/// [`check_visibility`]: bevy_render::view::visibility::check_visibility +pub fn update_layer_visibility( + ui_cams: Query<&UiCameraConfig>, + mut nodes: Query<(&mut ComputedVisibility, &RenderLayers), With>, +) { + for config in &ui_cams { + // Skip configs with default render layers, since `check_visibility` assumes it + if config.ui_render_layers == RenderLayers::default() { + continue; + } + for (mut visibility, node_layers) in &mut nodes { + if config.ui_render_layers.intersects(node_layers) { + visibility.set_visible_in_view(); + } + } + } +} + /// Updates clipping for all nodes pub fn update_clipping_system( mut commands: Commands, @@ -82,6 +114,43 @@ pub fn update_clipping_system( } } +/// Update the UI camera projection based on changes to [`Camera`] and [`UiCameraConfig`]. +/// +/// It has the [`crate::UiSystem::UiCameraProjection`] label. +pub fn update_ui_camera_projection( + mut commands: Commands, + mut query: Query< + ( + Entity, + Option<&mut UiCameraProjection>, + &Camera, + Option<&UiCameraConfig>, + ), + Or<(Changed, Changed)>, + >, +) { + for (entity, opt_projection, camera, opt_config) in &mut query { + if let Some(logical_size) = camera.logical_viewport_size() { + let config = opt_config.cloned().unwrap_or_default(); + if let Some(mut proj) = opt_projection { + proj.0.scale = config.scale; + proj.0.window_origin = config.window_origin; + proj.0.update(logical_size.x, logical_size.y); + } else { + let mut proj = OrthographicProjection { + far: UI_CAMERA_FAR, + window_origin: config.window_origin, + depth_calculation: DepthCalculation::ZDifference, + scale: config.scale, + ..Default::default() + }; + proj.update(logical_size.x, logical_size.y); + commands.entity(entity).insert(UiCameraProjection(proj)); + } + } + } +} + fn update_clipping( commands: &mut Commands, children_query: &Query<&Children>, diff --git a/examples/README.md b/examples/README.md index 6fa1f9acda669..b59171d785226 100644 --- a/examples/README.md +++ b/examples/README.md @@ -311,6 +311,7 @@ Example | Description [Text Debug](../examples/ui/text_debug.rs) | An example for debugging text layout [Transparency UI](../examples/ui/transparency_ui.rs) | Demonstrates transparency for UI [UI](../examples/ui/ui.rs) | Illustrates various features of Bevy UI +[UI Camera control](../examples/ui/ui_camera_movement.rs) | Illustrates how to move and zoom the UI camera ## Window diff --git a/examples/ui/ui_camera_movement.rs b/examples/ui/ui_camera_movement.rs new file mode 100644 index 0000000000000..5238af11106a2 --- /dev/null +++ b/examples/ui/ui_camera_movement.rs @@ -0,0 +1,102 @@ +use bevy::{prelude::*, render::camera::WindowOrigin}; + +/// This example shows how to manipulate the UI camera projection and position. +/// Controls: +/// * Arrow keys: move UI camera around, +/// * Left/Right mouse buttons: zoom in/out +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_startup_system(setup) + .add_system(cam_system) + .add_system(button_system) + .run(); +} + +#[derive(Component)] +struct IdleColor(UiColor); + +// UI Camera data are modifyed through the `UiCameraConfig` component. +// Note that you must insert one to your screen camera. +fn cam_system( + mut cam_configs: Query<&mut UiCameraConfig>, + keyboard: Res>, + mouse: Res>, +) { + for mut cam_config in &mut cam_configs { + let mut offset = match () { + () if keyboard.pressed(KeyCode::Left) => -Vec2::X, + () if keyboard.pressed(KeyCode::Right) => Vec2::X, + () => Vec2::ZERO, + }; + offset += match () { + () if keyboard.pressed(KeyCode::Down) => -Vec2::Y, + () if keyboard.pressed(KeyCode::Up) => Vec2::Y, + () => Vec2::ZERO, + }; + // We only modify the transform when there is a change, so as + // to not trigger change detection. + if offset != Vec2::ZERO { + let scale = cam_config.scale; + cam_config.position += offset * scale * 30.0; + } + let scale_offset = match () { + () if mouse.pressed(MouseButton::Left) => 0.9, + () if mouse.pressed(MouseButton::Right) => 1.1, + () => 0.0, + }; + if scale_offset != 0.0 { + cam_config.scale *= scale_offset; + } + } +} + +fn button_system( + mut interaction_query: Query<(&Interaction, &mut UiColor, &IdleColor), Changed>, +) { + for (interaction, mut material, IdleColor(idle_color)) in &mut interaction_query { + if let Interaction::Hovered = interaction { + *material = Color::WHITE.into(); + } else { + *material = *idle_color; + } + } +} + +fn setup(mut commands: Commands) { + let button_row_count = 35; + let as_rainbow = |i: u32| Color::hsl((i as f32 / button_row_count as f32) * 360.0, 0.9, 0.5); + commands + .spawn_bundle(Camera2dBundle::default()) + // Insert a UiCameraConfig to customize the UI camera. + .insert(UiCameraConfig { + window_origin: WindowOrigin::Center, + ..default() + }); + for i in 0..button_row_count { + for j in 0..button_row_count { + let full = (i + j).max(1); + let color = as_rainbow((i * j) % full).into(); + spawn_button(&mut commands, color, button_row_count, i, j); + } + } +} +fn spawn_button(commands: &mut Commands, color: UiColor, max: u32, i: u32, j: u32) { + let size = 340.0 / max as f32; + commands + .spawn_bundle(ButtonBundle { + color, + style: Style { + size: Size::new(Val::Percent(size), Val::Percent(size)), + position_type: PositionType::Absolute, + position: UiRect { + bottom: Val::Percent((400.0 / max as f32) * i as f32), + left: Val::Percent((400.0 / max as f32) * j as f32), + ..default() + }, + ..default() + }, + ..default() + }) + .insert(IdleColor(color)); +} diff --git a/examples/window/multiple_windows.rs b/examples/window/multiple_windows.rs index 82e8a5e99b928..faee661653db4 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,17 @@ 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 { + // We set the UI cameras of each window to use a different layer, + // so that we can display different text per window. + ui_render_layers: RenderLayers::layer(1), + ..default() + }); let window_id = WindowId::new(); @@ -50,12 +57,34 @@ 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() - }, - ..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, + }; + commands.spawn_bundle(TextBundle { + render_layers: RenderLayers::layer(1), + ..TextBundle::from_section("Face", text_style.clone()) + }); + commands.spawn_bundle(TextBundle { + render_layers: RenderLayers::layer(2), + ..TextBundle::from_section("Profile", text_style.clone()) + }); + commands.spawn_bundle(TextBundle { + render_layers: RenderLayers::all(), + ..TextBundle::from_section("view", text_style) }); }