Skip to content

Commit c09da4e

Browse files
committed
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.
1 parent bef8285 commit c09da4e

File tree

6 files changed

+158
-66
lines changed

6 files changed

+158
-66
lines changed

crates/bevy_ui/src/entity.rs

Lines changed: 35 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,10 @@ use crate::{
44
widget::{Button, ImageMode},
55
CalculatedSize, FocusPolicy, Interaction, Node, Style, UiColor, UiImage,
66
};
7-
use bevy_ecs::{
8-
bundle::Bundle,
9-
prelude::{Component, With},
10-
query::QueryItem,
11-
};
12-
use bevy_render::{camera::Camera, extract_component::ExtractComponent, view::Visibility};
7+
use bevy_ecs::{bundle::Bundle, prelude::Component};
8+
use bevy_math::Vec2;
9+
use bevy_reflect::Reflect;
10+
use bevy_render::{camera::OrthographicProjection, view::Visibility};
1311
use bevy_text::Text;
1412
use bevy_transform::prelude::{GlobalTransform, Transform};
1513

@@ -136,26 +134,50 @@ impl Default for ButtonBundle {
136134
}
137135
}
138136
}
137+
139138
/// Configuration for cameras related to UI.
140139
///
141140
/// When a [`Camera`] doesn't have the [`CameraUiConfig`] component,
142141
/// it will display the UI by default.
143142
///
144143
/// [`Camera`]: bevy_render::camera::Camera
145-
#[derive(Component, Clone, Default)]
144+
#[derive(Component, Reflect, Clone, Debug)]
146145
pub struct CameraUiConfig {
147146
/// Whether to output UI to this camera view.
148147
///
149148
/// When a `Camera` doesn't have the [`CameraUiConfig`] component,
150149
/// it will display the UI by default.
151150
pub show_ui: bool,
151+
/// Scale of the UI.
152+
pub scale: f32,
153+
/// Position of the camera compared to the UI.
154+
pub position: Vec2,
155+
}
156+
impl Default for CameraUiConfig {
157+
fn default() -> Self {
158+
Self {
159+
show_ui: false,
160+
scale: 1.0,
161+
position: Vec2::ZERO,
162+
}
163+
}
152164
}
153165

154-
impl ExtractComponent for CameraUiConfig {
155-
type Query = &'static Self;
156-
type Filter = With<Camera>;
157-
158-
fn extract_component(item: QueryItem<Self::Query>) -> Self {
159-
item.clone()
166+
/// Data related to the UI camera attached to this camera.
167+
#[derive(Component, Clone, Debug)]
168+
pub struct UiCameraRenderInfo {
169+
pub(crate) projection: OrthographicProjection,
170+
pub(crate) position: Vec2,
171+
// Used to only update the data when the
172+
pub(crate) old_logical_size: Vec2,
173+
}
174+
impl UiCameraRenderInfo {
175+
/// The orthographic projection used by the UI camera.
176+
pub fn projection(&self) -> &OrthographicProjection {
177+
&self.projection
178+
}
179+
/// The position of the UI camera in UI space.
180+
pub fn position(&self) -> &Vec2 {
181+
&self.position
160182
}
161183
}

crates/bevy_ui/src/lib.rs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ pub mod entity;
1212
pub mod update;
1313
pub mod widget;
1414

15-
use bevy_render::extract_component::ExtractComponentPlugin;
15+
use bevy_core_pipeline::{core_2d::Camera2d, prelude::Camera3d};
1616
pub use flex::*;
1717
pub use focus::*;
1818
pub use geometry::*;
@@ -27,11 +27,11 @@ pub mod prelude {
2727

2828
use crate::Size;
2929
use bevy_app::prelude::*;
30-
use bevy_ecs::schedule::{ParallelSystemDescriptorCoercion, SystemLabel};
30+
use bevy_ecs::schedule::{ParallelSystemDescriptorCoercion, SystemLabel, SystemSet};
3131
use bevy_input::InputSystem;
3232
use bevy_transform::TransformSystem;
3333
use bevy_window::ModifiesWindows;
34-
use update::{ui_z_system, update_clipping_system};
34+
use update::{ui_z_system, update_clipping_system, update_ui_camera_data};
3535

3636
use crate::prelude::CameraUiConfig;
3737

@@ -50,8 +50,8 @@ pub enum UiSystem {
5050

5151
impl Plugin for UiPlugin {
5252
fn build(&self, app: &mut App) {
53-
app.add_plugin(ExtractComponentPlugin::<CameraUiConfig>::default())
54-
.init_resource::<FlexSurface>()
53+
app.init_resource::<FlexSurface>()
54+
.register_type::<CameraUiConfig>()
5555
.register_type::<AlignContent>()
5656
.register_type::<AlignItems>()
5757
.register_type::<AlignSelf>()
@@ -105,6 +105,12 @@ impl Plugin for UiPlugin {
105105
.after(UiSystem::Flex)
106106
.before(TransformSystem::TransformPropagate),
107107
)
108+
.add_system_set_to_stage(
109+
CoreStage::PostUpdate,
110+
SystemSet::new()
111+
.with_system(update_ui_camera_data::<Camera2d>)
112+
.with_system(update_ui_camera_data::<Camera3d>),
113+
)
108114
.add_system_to_stage(
109115
CoreStage::PostUpdate,
110116
update_clipping_system.after(TransformSystem::TransformPropagate),

crates/bevy_ui/src/render/mod.rs

Lines changed: 11 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,14 @@ use bevy_core_pipeline::{core_2d::Camera2d, core_3d::Camera3d};
55
pub use pipeline::*;
66
pub use render_pass::*;
77

8-
use crate::{prelude::CameraUiConfig, CalculatedClip, Node, UiColor, UiImage};
8+
use crate::{prelude::UiCameraRenderInfo, CalculatedClip, Node, UiColor, UiImage};
99
use bevy_app::prelude::*;
1010
use bevy_asset::{load_internal_asset, AssetEvent, Assets, Handle, HandleUntyped};
1111
use bevy_ecs::prelude::*;
1212
use bevy_math::{Mat4, Vec2, Vec3, Vec4Swizzles};
1313
use bevy_reflect::TypeUuid;
1414
use bevy_render::{
15-
camera::{Camera, CameraProjection, DepthCalculation, OrthographicProjection, WindowOrigin},
15+
camera::{Camera, CameraProjection},
1616
color::Color,
1717
render_asset::RenderAssets,
1818
render_graph::{RenderGraph, RunGraphOnViewNode, SlotInfo, SlotType},
@@ -70,14 +70,8 @@ pub fn build_ui_render(app: &mut App) {
7070
.init_resource::<ExtractedUiNodes>()
7171
.init_resource::<DrawFunctions<TransparentUi>>()
7272
.add_render_command::<TransparentUi, DrawUi>()
73-
.add_system_to_stage(
74-
RenderStage::Extract,
75-
extract_default_ui_camera_view::<Camera2d>,
76-
)
77-
.add_system_to_stage(
78-
RenderStage::Extract,
79-
extract_default_ui_camera_view::<Camera3d>,
80-
)
73+
.add_system_to_stage(RenderStage::Extract, extract_ui_camera_view::<Camera2d>)
74+
.add_system_to_stage(RenderStage::Extract, extract_ui_camera_view::<Camera3d>)
8175
.add_system_to_stage(
8276
RenderStage::Extract,
8377
extract_uinodes.label(RenderUiSystem::ExtractNode),
@@ -215,7 +209,7 @@ pub fn extract_uinodes(
215209
/// as ui elements are "stacked on top of each other", they are within the camera's view
216210
/// and have room to grow.
217211
// TODO: Consider computing this value at runtime based on the maximum z-value.
218-
const UI_CAMERA_FAR: f32 = 1000.0;
212+
pub(crate) const UI_CAMERA_FAR: f32 = 1000.0;
219213

220214
// This value is subtracted from the far distance for the camera's z-position to ensure nodes at z == 0.0 are rendered
221215
// TODO: Evaluate if we still need this.
@@ -224,36 +218,22 @@ const UI_CAMERA_TRANSFORM_OFFSET: f32 = -0.1;
224218
#[derive(Component)]
225219
pub struct DefaultCameraView(pub Entity);
226220

227-
pub fn extract_default_ui_camera_view<T: Component>(
221+
pub fn extract_ui_camera_view<T: Component>(
228222
mut commands: Commands,
229223
render_world: Res<RenderWorld>,
230-
query: Query<(Entity, &Camera, Option<&CameraUiConfig>), With<T>>,
224+
query: Query<(Entity, &Camera, &UiCameraRenderInfo), With<T>>,
231225
) {
232226
for (entity, camera, camera_ui) in query.iter() {
233-
// ignore cameras with disabled ui
234-
if matches!(camera_ui, Some(&CameraUiConfig { show_ui: false, .. })) {
235-
continue;
236-
}
237-
if let (Some(logical_size), Some(physical_size)) = (
238-
camera.logical_viewport_size(),
239-
camera.physical_viewport_size(),
240-
) {
241-
let mut projection = OrthographicProjection {
242-
far: UI_CAMERA_FAR,
243-
window_origin: WindowOrigin::BottomLeft,
244-
depth_calculation: DepthCalculation::ZDifference,
245-
..Default::default()
246-
};
247-
projection.update(logical_size.x, logical_size.y);
227+
if let Some(physical_size) = camera.physical_viewport_size() {
248228
// This roundabout approach is required because spawn().id() won't work in this context
249229
let default_camera_view = render_world.entities().reserve_entity();
250230
commands
251231
.get_or_spawn(default_camera_view)
252232
.insert(ExtractedView {
253-
projection: projection.get_projection_matrix(),
233+
projection: camera_ui.projection().get_projection_matrix(),
254234
transform: GlobalTransform::from_xyz(
255-
0.0,
256-
0.0,
235+
camera_ui.position().x,
236+
camera_ui.position().y,
257237
UI_CAMERA_FAR + UI_CAMERA_TRANSFORM_OFFSET,
258238
),
259239
width: physical_size.x,

crates/bevy_ui/src/render/render_pass.rs

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use super::{UiBatch, UiImageBindGroups, UiMeta};
2-
use crate::{prelude::CameraUiConfig, DefaultCameraView};
2+
use crate::DefaultCameraView;
33
use bevy_ecs::{
44
prelude::*,
55
system::{lifetimeless::*, SystemParamItem},
@@ -16,14 +16,8 @@ use bevy_render::{
1616
use bevy_utils::FloatOrd;
1717

1818
pub struct UiPassNode {
19-
ui_view_query: QueryState<
20-
(
21-
&'static RenderPhase<TransparentUi>,
22-
&'static ViewTarget,
23-
Option<&'static CameraUiConfig>,
24-
),
25-
With<ExtractedView>,
26-
>,
19+
ui_view_query:
20+
QueryState<(&'static RenderPhase<TransparentUi>, &'static ViewTarget), With<ExtractedView>>,
2721
default_camera_view_query: QueryState<&'static DefaultCameraView>,
2822
}
2923

@@ -56,7 +50,7 @@ impl Node for UiPassNode {
5650
) -> Result<(), NodeRunError> {
5751
let input_view_entity = graph.get_input_entity(Self::IN_VIEW)?;
5852

59-
let (transparent_phase, target, camera_ui) =
53+
let (transparent_phase, target) =
6054
if let Ok(result) = self.ui_view_query.get_manual(world, input_view_entity) {
6155
result
6256
} else {
@@ -65,10 +59,6 @@ impl Node for UiPassNode {
6559
if transparent_phase.items.is_empty() {
6660
return Ok(());
6761
}
68-
// Don't render UI for cameras where it is explicitly disabled
69-
if matches!(camera_ui, Some(&CameraUiConfig { show_ui: false })) {
70-
return Ok(());
71-
}
7262

7363
// use the "default" view entity if it is defined
7464
let view_entity = if let Ok(default_view) = self
@@ -77,7 +67,7 @@ impl Node for UiPassNode {
7767
{
7868
default_view.0
7969
} else {
80-
input_view_entity
70+
return Ok(());
8171
};
8272
let pass_descriptor = RenderPassDescriptor {
8373
label: Some("ui_pass"),

crates/bevy_ui/src/update.rs

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,22 @@
11
//! This module contains systems that update the UI when something changes
22
3-
use crate::{CalculatedClip, Overflow, Style};
3+
use crate::{
4+
prelude::{CameraUiConfig, UiCameraRenderInfo},
5+
CalculatedClip, Overflow, Style, UI_CAMERA_FAR,
6+
};
47

58
use super::Node;
69
use bevy_ecs::{
710
entity::Entity,
11+
prelude::{ChangeTrackers, Changed, Component, Or},
812
query::{With, Without},
913
system::{Commands, Query},
1014
};
1115
use bevy_hierarchy::{Children, Parent};
1216
use bevy_math::Vec2;
17+
use bevy_render::camera::{
18+
Camera, CameraProjection, DepthCalculation, OrthographicProjection, WindowOrigin,
19+
};
1320
use bevy_sprite::Rect;
1421
use bevy_transform::components::{GlobalTransform, Transform};
1522

@@ -131,6 +138,64 @@ fn update_clipping(
131138
}
132139
}
133140

141+
pub fn update_ui_camera_data<T: Component>(
142+
mut commands: Commands,
143+
mut query: Query<
144+
(
145+
Entity,
146+
&Camera,
147+
Option<&CameraUiConfig>,
148+
Option<&mut UiCameraRenderInfo>,
149+
Option<ChangeTrackers<CameraUiConfig>>,
150+
),
151+
(With<T>, Or<(Changed<Camera>, Changed<CameraUiConfig>)>),
152+
>,
153+
) {
154+
for (entity, camera, config, render_info, config_changed) in query.iter_mut() {
155+
if matches!(config, Some(&CameraUiConfig { show_ui: false, .. })) {
156+
commands.entity(entity).remove::<UiCameraRenderInfo>();
157+
continue;
158+
}
159+
let logical_size = if let Some(logical_size) = camera.logical_viewport_size() {
160+
logical_size
161+
} else {
162+
commands.entity(entity).remove::<UiCameraRenderInfo>();
163+
continue;
164+
};
165+
// skip work if there is no changes.
166+
if let (Some(projection), Some(config_changed)) = (&render_info, config_changed) {
167+
if projection.old_logical_size == logical_size && !config_changed.is_changed() {
168+
continue;
169+
}
170+
}
171+
172+
let (view_pos, scale) = if let Some(config) = config {
173+
(config.position, config.scale)
174+
} else {
175+
(Vec2::new(0.0, 0.0), 1.0)
176+
};
177+
let mut new_projection = OrthographicProjection {
178+
far: UI_CAMERA_FAR,
179+
scale,
180+
window_origin: WindowOrigin::BottomLeft,
181+
depth_calculation: DepthCalculation::ZDifference,
182+
..Default::default()
183+
};
184+
new_projection.update(logical_size.x, logical_size.y);
185+
if let Some(mut info) = render_info {
186+
info.projection = new_projection;
187+
info.position = view_pos;
188+
info.old_logical_size = logical_size;
189+
} else {
190+
commands.entity(entity).insert(UiCameraRenderInfo {
191+
projection: new_projection,
192+
position: view_pos,
193+
old_logical_size: logical_size,
194+
});
195+
}
196+
}
197+
}
198+
134199
#[cfg(test)]
135200
mod tests {
136201
use bevy_ecs::{

examples/ui/ui.rs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,18 @@ fn main() {
1313
.insert_resource(WinitSettings::desktop_app())
1414
.add_startup_system(setup)
1515
.add_system(mouse_scroll)
16+
.add_system(change_ui_camera)
1617
.run();
1718
}
1819

1920
fn setup(mut commands: Commands, asset_server: Res<AssetServer>) {
2021
// Camera
21-
commands.spawn_bundle(Camera2dBundle::default());
22+
commands
23+
.spawn_bundle(Camera2dBundle::default())
24+
.insert(CameraUiConfig {
25+
show_ui: true,
26+
..default()
27+
});
2228

2329
// root node
2430
commands
@@ -309,6 +315,29 @@ struct ScrollingList {
309315
position: f32,
310316
}
311317

318+
fn change_ui_camera(
319+
mouse: Res<Input<MouseButton>>,
320+
keyboard: Res<Input<KeyCode>>,
321+
mut ui_config: Query<&mut CameraUiConfig>,
322+
) {
323+
for mut config in ui_config.iter_mut() {
324+
if mouse.just_pressed(MouseButton::Left) {
325+
config.show_ui = !config.show_ui;
326+
}
327+
if keyboard.pressed(KeyCode::A) {
328+
config.position.x -= 1.0;
329+
}
330+
if keyboard.pressed(KeyCode::D) {
331+
config.position.x += 1.0;
332+
}
333+
if keyboard.pressed(KeyCode::W) {
334+
config.scale *= 0.99;
335+
}
336+
if keyboard.pressed(KeyCode::S) {
337+
config.scale *= 1.01;
338+
}
339+
}
340+
}
312341
fn mouse_scroll(
313342
mut mouse_wheel_events: EventReader<MouseWheel>,
314343
mut query_list: Query<(&mut ScrollingList, &mut Style, &Children, &Node)>,

0 commit comments

Comments
 (0)