From c09da4eca31d45688029cd0ebd1e994dffcd69a3 Mon Sep 17 00:00:00 2001 From: Nicola Papale Date: Thu, 7 Jul 2022 09:24:25 +0200 Subject: [PATCH] Add component to control UI camera position Extend `CameraUiConfig` to include info about the UI camera position in the "ui world". This allows fancy effects like moving UI, which was possible before the migration to camera-driven rendering. This reverts the regression caused by #4765 preventing users from moving the UI camera. --- crates/bevy_ui/src/entity.rs | 48 ++++++++++++----- crates/bevy_ui/src/lib.rs | 16 ++++-- crates/bevy_ui/src/render/mod.rs | 42 ++++----------- crates/bevy_ui/src/render/render_pass.rs | 20 ++----- crates/bevy_ui/src/update.rs | 67 +++++++++++++++++++++++- examples/ui/ui.rs | 31 ++++++++++- 6 files changed, 158 insertions(+), 66 deletions(-) diff --git a/crates/bevy_ui/src/entity.rs b/crates/bevy_ui/src/entity.rs index 6bd97a524e134..ead1a8a0a4656 100644 --- a/crates/bevy_ui/src/entity.rs +++ b/crates/bevy_ui/src/entity.rs @@ -4,12 +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_render::{camera::Camera, extract_component::ExtractComponent, view::Visibility}; +use bevy_ecs::{bundle::Bundle, prelude::Component}; +use bevy_math::Vec2; +use bevy_reflect::Reflect; +use bevy_render::{camera::OrthographicProjection, view::Visibility}; use bevy_text::Text; use bevy_transform::prelude::{GlobalTransform, Transform}; @@ -136,26 +134,50 @@ impl Default for ButtonBundle { } } } + /// Configuration for cameras related to UI. /// /// When a [`Camera`] doesn't have the [`CameraUiConfig`] component, /// it will display the UI by default. /// /// [`Camera`]: bevy_render::camera::Camera -#[derive(Component, Clone, Default)] +#[derive(Component, Reflect, Clone, Debug)] pub struct CameraUiConfig { /// Whether to output UI to this camera view. /// /// When a `Camera` doesn't have the [`CameraUiConfig`] component, /// it will display the UI by default. pub show_ui: bool, + /// Scale of the UI. + pub scale: f32, + /// Position of the camera compared to the UI. + pub position: Vec2, +} +impl Default for CameraUiConfig { + fn default() -> Self { + Self { + show_ui: false, + scale: 1.0, + position: Vec2::ZERO, + } + } } -impl ExtractComponent for CameraUiConfig { - type Query = &'static Self; - type Filter = With; - - fn extract_component(item: QueryItem) -> Self { - item.clone() +/// Data related to the UI camera attached to this camera. +#[derive(Component, Clone, Debug)] +pub struct UiCameraRenderInfo { + pub(crate) projection: OrthographicProjection, + pub(crate) position: Vec2, + // Used to only update the data when the + pub(crate) old_logical_size: Vec2, +} +impl UiCameraRenderInfo { + /// The orthographic projection used by the UI camera. + pub fn projection(&self) -> &OrthographicProjection { + &self.projection + } + /// The position of the UI camera in UI space. + pub fn position(&self) -> &Vec2 { + &self.position } } diff --git a/crates/bevy_ui/src/lib.rs b/crates/bevy_ui/src/lib.rs index 19778cb4e665b..632e1f3522c17 100644 --- a/crates/bevy_ui/src/lib.rs +++ b/crates/bevy_ui/src/lib.rs @@ -12,7 +12,7 @@ pub mod entity; pub mod update; pub mod widget; -use bevy_render::extract_component::ExtractComponentPlugin; +use bevy_core_pipeline::{core_2d::Camera2d, prelude::Camera3d}; pub use flex::*; pub use focus::*; pub use geometry::*; @@ -27,11 +27,11 @@ pub mod prelude { use crate::Size; use bevy_app::prelude::*; -use bevy_ecs::schedule::{ParallelSystemDescriptorCoercion, SystemLabel}; +use bevy_ecs::schedule::{ParallelSystemDescriptorCoercion, SystemLabel, SystemSet}; use bevy_input::InputSystem; use bevy_transform::TransformSystem; use bevy_window::ModifiesWindows; -use update::{ui_z_system, update_clipping_system}; +use update::{ui_z_system, update_clipping_system, update_ui_camera_data}; use crate::prelude::CameraUiConfig; @@ -50,8 +50,8 @@ 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::() .register_type::() @@ -105,6 +105,12 @@ impl Plugin for UiPlugin { .after(UiSystem::Flex) .before(TransformSystem::TransformPropagate), ) + .add_system_set_to_stage( + CoreStage::PostUpdate, + SystemSet::new() + .with_system(update_ui_camera_data::) + .with_system(update_ui_camera_data::), + ) .add_system_to_stage( CoreStage::PostUpdate, update_clipping_system.after(TransformSystem::TransformPropagate), diff --git a/crates/bevy_ui/src/render/mod.rs b/crates/bevy_ui/src/render/mod.rs index c039cfffc7237..bd234c8eca9e6 100644 --- a/crates/bevy_ui/src/render/mod.rs +++ b/crates/bevy_ui/src/render/mod.rs @@ -5,14 +5,14 @@ use bevy_core_pipeline::{core_2d::Camera2d, core_3d::Camera3d}; pub use pipeline::*; pub use render_pass::*; -use crate::{prelude::CameraUiConfig, CalculatedClip, Node, UiColor, UiImage}; +use crate::{prelude::UiCameraRenderInfo, 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}, @@ -70,14 +70,8 @@ pub fn build_ui_render(app: &mut App) { .init_resource::() .init_resource::>() .add_render_command::() - .add_system_to_stage( - RenderStage::Extract, - extract_default_ui_camera_view::, - ) - .add_system_to_stage( - RenderStage::Extract, - extract_default_ui_camera_view::, - ) + .add_system_to_stage(RenderStage::Extract, extract_ui_camera_view::) + .add_system_to_stage(RenderStage::Extract, extract_ui_camera_view::) .add_system_to_stage( RenderStage::Extract, extract_uinodes.label(RenderUiSystem::ExtractNode), @@ -215,7 +209,7 @@ 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. @@ -224,36 +218,22 @@ const UI_CAMERA_TRANSFORM_OFFSET: f32 = -0.1; #[derive(Component)] pub struct DefaultCameraView(pub Entity); -pub fn extract_default_ui_camera_view( +pub fn extract_ui_camera_view( mut commands: Commands, render_world: Res, - query: Query<(Entity, &Camera, Option<&CameraUiConfig>), With>, + query: Query<(Entity, &Camera, &UiCameraRenderInfo), With>, ) { for (entity, camera, camera_ui) in query.iter() { - // ignore cameras with disabled ui - if matches!(camera_ui, Some(&CameraUiConfig { show_ui: false, .. })) { - 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); + if let Some(physical_size) = camera.physical_viewport_size() { // This roundabout approach is required because spawn().id() won't work in this context let default_camera_view = render_world.entities().reserve_entity(); commands .get_or_spawn(default_camera_view) .insert(ExtractedView { - projection: projection.get_projection_matrix(), + projection: camera_ui.projection().get_projection_matrix(), transform: GlobalTransform::from_xyz( - 0.0, - 0.0, + camera_ui.position().x, + camera_ui.position().y, UI_CAMERA_FAR + UI_CAMERA_TRANSFORM_OFFSET, ), width: physical_size.x, diff --git a/crates/bevy_ui/src/render/render_pass.rs b/crates/bevy_ui/src/render/render_pass.rs index e4e7566163108..968b9ffd400e8 100644 --- a/crates/bevy_ui/src/render/render_pass.rs +++ b/crates/bevy_ui/src/render/render_pass.rs @@ -1,5 +1,5 @@ use super::{UiBatch, UiImageBindGroups, UiMeta}; -use crate::{prelude::CameraUiConfig, DefaultCameraView}; +use crate::DefaultCameraView; use bevy_ecs::{ prelude::*, system::{lifetimeless::*, SystemParamItem}, @@ -16,14 +16,8 @@ use bevy_render::{ use bevy_utils::FloatOrd; pub struct UiPassNode { - ui_view_query: QueryState< - ( - &'static RenderPhase, - &'static ViewTarget, - Option<&'static CameraUiConfig>, - ), - With, - >, + ui_view_query: + QueryState<(&'static RenderPhase, &'static ViewTarget), With>, default_camera_view_query: QueryState<&'static DefaultCameraView>, } @@ -56,7 +50,7 @@ impl Node for UiPassNode { ) -> Result<(), NodeRunError> { let input_view_entity = graph.get_input_entity(Self::IN_VIEW)?; - let (transparent_phase, target, camera_ui) = + let (transparent_phase, target) = if let Ok(result) = self.ui_view_query.get_manual(world, input_view_entity) { result } else { @@ -65,10 +59,6 @@ impl Node for UiPassNode { if transparent_phase.items.is_empty() { return Ok(()); } - // Don't render UI for cameras where it is explicitly disabled - if matches!(camera_ui, Some(&CameraUiConfig { show_ui: false })) { - return Ok(()); - } // use the "default" view entity if it is defined let view_entity = if let Ok(default_view) = self @@ -77,7 +67,7 @@ impl Node for UiPassNode { { default_view.0 } else { - input_view_entity + return Ok(()); }; let pass_descriptor = RenderPassDescriptor { label: Some("ui_pass"), diff --git a/crates/bevy_ui/src/update.rs b/crates/bevy_ui/src/update.rs index 8af7d72caca62..d6559e7d3bbd0 100644 --- a/crates/bevy_ui/src/update.rs +++ b/crates/bevy_ui/src/update.rs @@ -1,15 +1,22 @@ //! This module contains systems that update the UI when something changes -use crate::{CalculatedClip, Overflow, Style}; +use crate::{ + prelude::{CameraUiConfig, UiCameraRenderInfo}, + CalculatedClip, Overflow, Style, UI_CAMERA_FAR, +}; use super::Node; use bevy_ecs::{ entity::Entity, + prelude::{ChangeTrackers, Changed, Component, Or}, query::{With, Without}, system::{Commands, Query}, }; use bevy_hierarchy::{Children, Parent}; use bevy_math::Vec2; +use bevy_render::camera::{ + Camera, CameraProjection, DepthCalculation, OrthographicProjection, WindowOrigin, +}; use bevy_sprite::Rect; use bevy_transform::components::{GlobalTransform, Transform}; @@ -131,6 +138,64 @@ fn update_clipping( } } +pub fn update_ui_camera_data( + mut commands: Commands, + mut query: Query< + ( + Entity, + &Camera, + Option<&CameraUiConfig>, + Option<&mut UiCameraRenderInfo>, + Option>, + ), + (With, Or<(Changed, Changed)>), + >, +) { + for (entity, camera, config, render_info, config_changed) in query.iter_mut() { + if matches!(config, Some(&CameraUiConfig { show_ui: false, .. })) { + commands.entity(entity).remove::(); + continue; + } + let logical_size = if let Some(logical_size) = camera.logical_viewport_size() { + logical_size + } else { + commands.entity(entity).remove::(); + continue; + }; + // skip work if there is no changes. + if let (Some(projection), Some(config_changed)) = (&render_info, config_changed) { + if projection.old_logical_size == logical_size && !config_changed.is_changed() { + continue; + } + } + + let (view_pos, scale) = if let Some(config) = config { + (config.position, config.scale) + } else { + (Vec2::new(0.0, 0.0), 1.0) + }; + let mut new_projection = OrthographicProjection { + far: UI_CAMERA_FAR, + scale, + window_origin: WindowOrigin::BottomLeft, + depth_calculation: DepthCalculation::ZDifference, + ..Default::default() + }; + new_projection.update(logical_size.x, logical_size.y); + if let Some(mut info) = render_info { + info.projection = new_projection; + info.position = view_pos; + info.old_logical_size = logical_size; + } else { + commands.entity(entity).insert(UiCameraRenderInfo { + projection: new_projection, + position: view_pos, + old_logical_size: logical_size, + }); + } + } +} + #[cfg(test)] mod tests { use bevy_ecs::{ diff --git a/examples/ui/ui.rs b/examples/ui/ui.rs index 5746b9e7306dc..44b8c1b3ba2ea 100644 --- a/examples/ui/ui.rs +++ b/examples/ui/ui.rs @@ -13,12 +13,18 @@ fn main() { .insert_resource(WinitSettings::desktop_app()) .add_startup_system(setup) .add_system(mouse_scroll) + .add_system(change_ui_camera) .run(); } fn setup(mut commands: Commands, asset_server: Res) { // Camera - commands.spawn_bundle(Camera2dBundle::default()); + commands + .spawn_bundle(Camera2dBundle::default()) + .insert(CameraUiConfig { + show_ui: true, + ..default() + }); // root node commands @@ -309,6 +315,29 @@ struct ScrollingList { position: f32, } +fn change_ui_camera( + mouse: Res>, + keyboard: Res>, + mut ui_config: Query<&mut CameraUiConfig>, +) { + for mut config in ui_config.iter_mut() { + if mouse.just_pressed(MouseButton::Left) { + config.show_ui = !config.show_ui; + } + if keyboard.pressed(KeyCode::A) { + config.position.x -= 1.0; + } + if keyboard.pressed(KeyCode::D) { + config.position.x += 1.0; + } + if keyboard.pressed(KeyCode::W) { + config.scale *= 0.99; + } + if keyboard.pressed(KeyCode::S) { + config.scale *= 1.01; + } + } +} fn mouse_scroll( mut mouse_wheel_events: EventReader, mut query_list: Query<(&mut ScrollingList, &mut Style, &Children, &Node)>,