Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Camera-driven UI #10559

Merged
merged 42 commits into from
Jan 16, 2024
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
9abf5b4
Update split_screen example; replace `window_roots` with `camera_roots`
bardt Nov 8, 2023
5d86151
Ui is painted to both left and right viewports
bardt Nov 10, 2023
310cb62
Independent panels rendering
bardt Nov 11, 2023
bcd8ee7
Propagate UiCamera to children
bardt Nov 11, 2023
19edd49
Default to any active primary window camera
bardt Nov 11, 2023
b4546f1
Layout based on camera scale factor
bardt Nov 11, 2023
3bd738e
Update camera target scale factor on window change
bardt Nov 11, 2023
c94cb9b
Propagate UI camera on children change
bardt Nov 12, 2023
5f1db01
Relative cursor position
bardt Nov 12, 2023
79a2ed9
Resize viewport on scale factor change
bardt Nov 13, 2023
b80b6c1
Restore initial ui_layout_system order
bardt Nov 14, 2023
1dfc40f
Merge branch 'main' into camera-driven-ui
bardt Nov 14, 2023
2a2d254
Post-merge formatting
bardt Nov 14, 2023
4ea0fb6
Interactive UI in split screen example
bardt Nov 15, 2023
7bc0a5e
Revers button example, update cursor position example
bardt Nov 15, 2023
3f6b606
Can layout without a camera
bardt Nov 16, 2023
d2889f1
Default to primary window itself, not a camera
bardt Nov 17, 2023
436e1d8
Improve code formatting
bardt Nov 17, 2023
94e0c3b
Code formatting in examples
bardt Nov 18, 2023
f5c2aff
Remove redundant explicit ordering
bardt Nov 18, 2023
febd33c
Example of rendering UI to a texture
bardt Nov 24, 2023
9631616
Merge branch 'main' into camera-driven-ui
bardt Nov 24, 2023
7676e63
Pre-calculate camera layout into
bardt Nov 28, 2023
441542c
Make layout system execution order closer to original
bardt Nov 28, 2023
0b45e1e
Optimize UiCamera popagation
bardt Nov 29, 2023
54e8e6c
Fix `relative_cursor_position` example
bardt Nov 29, 2023
468da45
Update examples/ui/render_ui_to_texture.rs
bardt Dec 3, 2023
402109b
Default to a single camera if only one exists
bardt Dec 3, 2023
1d36e12
Reduce number of iterations over nodes
bardt Dec 3, 2023
3116a58
Clippy fix
bardt Dec 3, 2023
aa3a714
Fix imports in example
bardt Dec 3, 2023
9b06ca2
Rename UiCamera to TargetCamera
bardt Dec 15, 2023
9e91865
Merge branch 'main' into camera-driven-ui
bardt Dec 15, 2023
ccc6b51
Fix compilation issues after merging main
bardt Dec 15, 2023
5b9826d
Merge branch 'main' into camera-driven-ui
bardt Jan 9, 2024
085fce0
Predefined query for default UI camera
Jan 14, 2024
cc64788
Merge remote-tracking branch 'upstream/main' into camera-driven-ui
Jan 14, 2024
0502405
HashMap for camera cursor positions
Jan 14, 2024
5856694
Fix disappeared text and borders
Jan 14, 2024
067429d
Remove UiCameraConfig component
Jan 14, 2024
6f30f35
Merge branch 'main' into camera-driven-ui
alice-i-cecile Jan 16, 2024
afe71da
Repair merge conflicts
Jan 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 50 additions & 6 deletions crates/bevy_render/src/camera/camera.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ use bevy_transform::components::GlobalTransform;
use bevy_utils::{HashMap, HashSet};
use bevy_window::{
NormalizedWindowRef, PrimaryWindow, Window, WindowCreated, WindowRef, WindowResized,
WindowScaleFactorChanged,
};
use std::{borrow::Cow, ops::Range};
use wgpu::{BlendState, LoadOp, TextureFormat};
Expand Down Expand Up @@ -74,7 +75,7 @@ pub struct RenderTargetInfo {
pub struct ComputedCameraValues {
projection_matrix: Mat4,
target_info: Option<RenderTargetInfo>,
// position and size of the `Viewport`
// size of the `Viewport`
old_viewport_size: Option<UVec2>,
}

Expand Down Expand Up @@ -219,6 +220,11 @@ impl Camera {
self.computed.target_info.as_ref().map(|t| t.physical_size)
}

#[inline]
pub fn target_scaling_factor(&self) -> Option<f64> {
self.computed.target_info.as_ref().map(|t| t.scale_factor)
}

/// The projection matrix computed using this camera's [`CameraProjection`].
#[inline]
pub fn projection_matrix(&self) -> Mat4 {
Expand Down Expand Up @@ -357,6 +363,13 @@ impl Camera {

(!world_space_coords.is_nan()).then_some(world_space_coords)
}

pub fn is_primary(&self) -> bool {
match self.target {
RenderTarget::Window(WindowRef::Primary) => self.is_active,
_ => false,
}
}
}

/// Control how this camera outputs once rendering is completed.
Expand Down Expand Up @@ -546,9 +559,9 @@ impl NormalizedRenderTarget {

/// System in charge of updating a [`Camera`] when its window or projection changes.
///
/// The system detects window creation and resize events to update the camera projection if
/// needed. It also queries any [`CameraProjection`] component associated with the same entity
/// as the [`Camera`] one, to automatically update the camera projection matrix.
/// The system detects window creation, resize, and scale factor change events to update the camera
/// projection if needed. It also queries any [`CameraProjection`] component associated with the same
/// entity as the [`Camera`] one, to automatically update the camera projection matrix.
///
/// The system function is generic over the camera projection type, and only instances of
/// [`OrthographicProjection`] and [`PerspectiveProjection`] are automatically added to
Expand All @@ -567,6 +580,7 @@ impl NormalizedRenderTarget {
pub fn camera_system<T: CameraProjection + Component>(
mut window_resized_events: EventReader<WindowResized>,
mut window_created_events: EventReader<WindowCreated>,
mut window_scale_factor_changed_events: EventReader<WindowScaleFactorChanged>,
mut image_asset_events: EventReader<AssetEvent<Image>>,
primary_window: Query<Entity, With<PrimaryWindow>>,
windows: Query<(Entity, &Window)>,
Expand All @@ -579,6 +593,11 @@ pub fn camera_system<T: CameraProjection + Component>(
let mut changed_window_ids = HashSet::new();
changed_window_ids.extend(window_created_events.read().map(|event| event.window));
changed_window_ids.extend(window_resized_events.read().map(|event| event.window));
let scale_factor_changed_window_ids: HashSet<_> = window_scale_factor_changed_events
.read()
.map(|event| event.window)
.collect();
changed_window_ids.extend(scale_factor_changed_window_ids.clone());

let changed_image_handles: HashSet<&AssetId<Image>> = image_asset_events
.read()
Expand All @@ -592,7 +611,7 @@ pub fn camera_system<T: CameraProjection + Component>(
.collect();

for (mut camera, mut camera_projection) in &mut cameras {
let viewport_size = camera
let mut viewport_size = camera
.viewport
.as_ref()
.map(|viewport| viewport.physical_size);
Expand All @@ -603,11 +622,36 @@ pub fn camera_system<T: CameraProjection + Component>(
|| camera_projection.is_changed()
|| camera.computed.old_viewport_size != viewport_size
{
camera.computed.target_info = normalized_target.get_render_target_info(
let new_computed_target_info = normalized_target.get_render_target_info(
&windows,
&images,
&manual_texture_views,
);
// Check for the scale factor changing, and resize the viewport if needed.
// This can happen when the window is moved between monitors with different DPIs.
// Without this, the viewport will take a smaller portion of the window moved to
// a higher DPI monitor.
if normalized_target.is_changed(&scale_factor_changed_window_ids, &HashSet::new()) {
if let (Some(new_scale_factor), Some(old_scale_factor)) = (
new_computed_target_info
.as_ref()
.map(|info| info.scale_factor),
camera
.computed
.target_info
.as_ref()
.map(|info| info.scale_factor),
) {
let resize_factor = new_scale_factor / old_scale_factor;
if let Some(ref mut viewport) = camera.viewport {
let resize = |vec: UVec2| (vec.as_dvec2() * resize_factor).as_uvec2();
viewport.physical_position = resize(viewport.physical_position);
viewport.physical_size = resize(viewport.physical_size);
viewport_size = Some(viewport.physical_size);
}
}
}
camera.computed.target_info = new_computed_target_info;
if let Some(size) = camera.logical_viewport_size() {
camera_projection.update(size.x, size.y);
camera.computed.projection_matrix = camera_projection.get_projection_matrix();
Expand Down
50 changes: 41 additions & 9 deletions crates/bevy_ui/src/focus.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use crate::{camera_config::UiCameraConfig, CalculatedClip, Node, UiScale, UiStack};
use crate::{camera_config::UiCameraConfig, CalculatedClip, Node, UiCamera, UiScale, UiStack};
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{
change_detection::DetectChangesMut,
Expand Down Expand Up @@ -126,6 +126,7 @@ pub struct NodeQuery {
focus_policy: Option<&'static FocusPolicy>,
calculated_clip: Option<&'static CalculatedClip>,
view_visibility: Option<&'static ViewVisibility>,
ui_camera: Option<&'static UiCamera>,
}

/// The system that sets Interaction for all UI elements based on the mouse cursor activity
Expand All @@ -134,7 +135,7 @@ pub struct NodeQuery {
#[allow(clippy::too_many_arguments)]
pub fn ui_focus_system(
mut state: Local<State>,
camera: Query<(&Camera, Option<&UiCameraConfig>)>,
camera_query: Query<(Entity, &Camera, Option<&UiCameraConfig>)>,
windows: Query<&Window>,
mouse_button_input: Res<Input<MouseButton>>,
touches_input: Res<Touches>,
Expand Down Expand Up @@ -170,28 +171,43 @@ pub fn ui_focus_system(
let is_ui_disabled =
|camera_ui| matches!(camera_ui, Some(&UiCameraConfig { show_ui: false, .. }));

let cursor_position = camera
let primary_camera = camera_query.iter().find_map(|(entity, camera, _)| {
if camera.is_primary() {
Some(entity)
} else {
None
}
});

let camera_cursor_positions: Vec<(Entity, Vec2)> = camera_query
.iter()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this should probably be a hashmap

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

separately here, it would be nice to provide a way for users to provide cursor coords for non-window UIs so that we can have all the interaction functionality for world-space UIs as well. it could definitely be a follow up, but perhaps something as simple as an Option<ManualCursorPosition> on the camera entity would be easy and sufficient?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To be honest, I am running out of motivation on this pull request. I was also thinking about a way to provide coordinates manually, but this requires a little more thinking and testing that I have in me currently. I would love to focus on the changes which are necessary for this to be a mergable valuable piece of work, and leave all the rest of improvements for later (and probably for someone else to work on).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm willing to do a follow-up PR after this is merged.

.filter(|(_, camera_ui)| !is_ui_disabled(*camera_ui))
.filter_map(|(camera, _)| {
.filter(|(_, _, camera_ui)| !is_ui_disabled(*camera_ui))
.filter_map(|(entity, camera, _)| {
if let Some(NormalizedRenderTarget::Window(window_ref)) =
camera.target.normalize(primary_window)
{
Some(window_ref)
Some((entity, camera, window_ref))
} else {
None
}
})
.find_map(|window_ref| {
.filter_map(|(entity, camera, window_ref)| {
let viewport_position = camera
.logical_viewport_rect()
.map(|rect| rect.min)
.unwrap_or_default();

windows
.get(window_ref.entity())
.ok()
.and_then(|window| window.cursor_position())
.or_else(|| touches_input.first_pressed_position())
.map(|cursor_position| (entity, cursor_position - viewport_position))
})
.or_else(|| touches_input.first_pressed_position())
// The cursor position returned by `Window` only takes into account the window scale factor and not `UiScale`.
// To convert the cursor position to logical UI viewport coordinates we have to divide it by `UiScale`.
.map(|cursor_position| cursor_position / ui_scale.0 as f32);
.map(|(entity, cursor_position)| (entity, cursor_position / ui_scale.0 as f32))
.collect();

// prepare an iterator that contains all the nodes that have the cursor in their rect,
// from the top node to the bottom one. this will also reset the interaction to `None`
Expand All @@ -216,6 +232,22 @@ pub fn ui_focus_system(
}
}

let Some(ui_camera_entity) =
node.ui_camera.map(UiCamera::entity).or(primary_camera)
else {
return None;
};
let cursor_position =
camera_cursor_positions
.iter()
.find_map(|(camera_entity, position)| {
if *camera_entity == ui_camera_entity {
Some(*position)
} else {
None
}
});

let position = node.global_transform.translation();
let ui_position = position.truncate();
let extents = node.node.size() / 2.0;
Expand Down
4 changes: 2 additions & 2 deletions crates/bevy_ui/src/layout/debug.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ pub fn print_ui_layout_tree(ui_surface: &UiSurface) {
.iter()
.map(|(entity, node)| (*node, *entity))
.collect();
for (&entity, roots) in &ui_surface.window_roots {
for (&entity, roots) in &ui_surface.camera_roots {
let mut out = String::new();
for root in roots {
print_node(
Expand All @@ -25,7 +25,7 @@ pub fn print_ui_layout_tree(ui_surface: &UiSurface) {
&mut out,
);
}
bevy_log::info!("Layout tree for window entity: {entity:?}\n{out}");
bevy_log::info!("Layout tree for camera entity: {entity:?}\n{out}");
}
}

Expand Down
Loading
Loading