diff --git a/Cargo.toml b/Cargo.toml index c1e1f34230dfc..599b77095e75e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -404,6 +404,10 @@ path = "examples/window/multiple_windows.rs" name = "scale_factor_override" path = "examples/window/scale_factor_override.rs" +[[example]] +name = "viewports" +path = "examples/window/viewports.rs" + [[example]] name = "window_settings" path = "examples/window/window_settings.rs" diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index bebbb39c87675..2d1bd385ef6a2 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -1,10 +1,9 @@ use super::CameraProjection; -use bevy_app::prelude::EventReader; -use bevy_ecs::{Added, Component, Entity, Query, QuerySet, Res}; +use crate::surface::Viewport; +use bevy_ecs::{Changed, Component, Query}; use bevy_math::{Mat4, Vec2, Vec3}; use bevy_reflect::{Reflect, ReflectComponent, ReflectDeserialize}; use bevy_transform::components::GlobalTransform; -use bevy_window::{WindowCreated, WindowId, WindowResized, Windows}; use serde::{Deserialize, Serialize}; #[derive(Default, Debug, Reflect)] @@ -13,8 +12,6 @@ pub struct Camera { pub projection_matrix: Mat4, pub name: Option, #[reflect(ignore)] - pub window: WindowId, - #[reflect(ignore)] pub depth_calculation: DepthCalculation, } @@ -37,12 +34,10 @@ impl Camera { /// Given a position in world space, use the camera to compute the screen space coordinates. pub fn world_to_screen( &self, - windows: &Windows, + viewport: &Viewport, camera_transform: &GlobalTransform, world_position: Vec3, ) -> Option { - let window = windows.get(self.window)?; - let window_size = Vec2::new(window.width(), window.height()); // Build a transform to convert from world to NDC using camera data let world_to_ndc: Mat4 = self.projection_matrix * camera_transform.compute_matrix().inverse(); @@ -52,50 +47,19 @@ impl Camera { return None; } // Once in NDC space, we can discard the z element and rescale x/y to fit the screen - let screen_space_coords = (ndc_space_coords.truncate() + Vec2::one()) / 2.0 * window_size; + let screen_space_coords = + viewport.origin() + (ndc_space_coords.truncate() + Vec2::one()) / 2.0 * viewport.size(); Some(screen_space_coords) } } pub fn camera_system( - mut window_resized_events: EventReader, - mut window_created_events: EventReader, - windows: Res, - mut queries: QuerySet<( - Query<(Entity, &mut Camera, &mut T)>, - Query>, - )>, + mut query: Query<(&mut Camera, &mut T, &Viewport), Changed>, ) { - let mut changed_window_ids = Vec::new(); - // handle resize events. latest events are handled first because we only want to resize each window once - for event in window_resized_events.iter().rev() { - if changed_window_ids.contains(&event.id) { - continue; - } - - changed_window_ids.push(event.id); - } - - // handle resize events. latest events are handled first because we only want to resize each window once - for event in window_created_events.iter().rev() { - if changed_window_ids.contains(&event.id) { - continue; - } - - changed_window_ids.push(event.id); - } - - let mut added_cameras = vec![]; - for entity in &mut queries.q1().iter() { - added_cameras.push(entity); - } - for (entity, mut camera, mut camera_projection) in queries.q0_mut().iter_mut() { - if let Some(window) = windows.get(camera.window) { - if changed_window_ids.contains(&window.id()) || added_cameras.contains(&entity) { - camera_projection.update(window.width(), window.height()); - camera.projection_matrix = camera_projection.get_projection_matrix(); - camera.depth_calculation = camera_projection.depth_calculation(); - } - } + for (mut camera, mut camera_projection, viewport) in query.iter_mut() { + let size = viewport.size(); + camera_projection.update(size.x, size.y); + camera.projection_matrix = camera_projection.get_projection_matrix(); + camera.depth_calculation = camera_projection.depth_calculation(); } } diff --git a/crates/bevy_render/src/entity.rs b/crates/bevy_render/src/entity.rs index 28344fd4bdeb7..5c620d8bfa69a 100644 --- a/crates/bevy_render/src/entity.rs +++ b/crates/bevy_render/src/entity.rs @@ -6,6 +6,7 @@ use crate::{ pipeline::RenderPipelines, prelude::Visible, render_graph::base, + surface::Viewport, Draw, Mesh, }; use base::MainPass; @@ -32,6 +33,7 @@ pub struct MeshBundle { pub struct PerspectiveCameraBundle { pub camera: Camera, pub perspective_projection: PerspectiveProjection, + pub viewport: Viewport, pub visible_entities: VisibleEntities, pub transform: Transform, pub global_transform: GlobalTransform, @@ -49,6 +51,7 @@ impl PerspectiveCameraBundle { ..Default::default() }, perspective_projection: Default::default(), + viewport: Default::default(), visible_entities: Default::default(), transform: Default::default(), global_transform: Default::default(), @@ -64,6 +67,7 @@ impl Default for PerspectiveCameraBundle { ..Default::default() }, perspective_projection: Default::default(), + viewport: Default::default(), visible_entities: Default::default(), transform: Default::default(), global_transform: Default::default(), @@ -78,6 +82,7 @@ impl Default for PerspectiveCameraBundle { pub struct OrthographicCameraBundle { pub camera: Camera, pub orthographic_projection: OrthographicProjection, + pub viewport: Viewport, pub visible_entities: VisibleEntities, pub transform: Transform, pub global_transform: GlobalTransform, @@ -98,6 +103,7 @@ impl OrthographicCameraBundle { depth_calculation: DepthCalculation::ZDifference, ..Default::default() }, + viewport: Default::default(), visible_entities: Default::default(), transform: Transform::from_xyz(0.0, 0.0, far - 0.1), global_transform: Default::default(), @@ -115,6 +121,7 @@ impl OrthographicCameraBundle { depth_calculation: DepthCalculation::Distance, ..Default::default() }, + viewport: Default::default(), visible_entities: Default::default(), transform: Default::default(), global_transform: Default::default(), @@ -128,6 +135,7 @@ impl OrthographicCameraBundle { ..Default::default() }, orthographic_projection: Default::default(), + viewport: Default::default(), visible_entities: Default::default(), transform: Default::default(), global_transform: Default::default(), diff --git a/crates/bevy_render/src/lib.rs b/crates/bevy_render/src/lib.rs index 62e97060735f0..c0976060ca55f 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -9,10 +9,11 @@ pub mod pipeline; pub mod render_graph; pub mod renderer; pub mod shader; +pub mod surface; pub mod texture; pub mod wireframe; -use bevy_ecs::{IntoExclusiveSystem, IntoSystem, SystemStage}; +use bevy_ecs::{IntoChainSystem, IntoExclusiveSystem, IntoSystem, SystemStage}; use bevy_reflect::RegisterTypeBuilder; use draw::Visible; pub use once_cell; @@ -149,20 +150,13 @@ impl Plugin for RenderPlugin { .add_system_to_stage(CoreStage::PreUpdate, draw::clear_draw_system.system()) .add_system_to_stage( CoreStage::PostUpdate, - camera::active_cameras_system.system(), - ) - .add_system_to_stage( - CoreStage::PostUpdate, - camera::camera_system::.system(), - ) - .add_system_to_stage( - CoreStage::PostUpdate, - camera::camera_system::.system(), - ) - // registration order matters here. this must come after all camera_system:: systems - .add_system_to_stage( - CoreStage::PostUpdate, - camera::visible_entities_system.system(), + surface::viewport_system + .system() + .chain(camera::active_cameras_system.system()) + .chain(camera::camera_system::.system()) + .chain(camera::camera_system::.system()) + // registration order matters here. this must come after all camera_system:: systems + .chain(camera::visible_entities_system.system()), ) .add_system_to_stage( RenderStage::RenderResource, diff --git a/crates/bevy_render/src/render_graph/nodes/pass_node.rs b/crates/bevy_render/src/render_graph/nodes/pass_node.rs index 2db0058ac52b4..50b34d55c11c6 100644 --- a/crates/bevy_render/src/render_graph/nodes/pass_node.rs +++ b/crates/bevy_render/src/render_graph/nodes/pass_node.rs @@ -11,6 +11,7 @@ use crate::{ renderer::{ BindGroup, BindGroupId, BufferId, RenderContext, RenderResourceBindings, RenderResourceType, }, + surface::Viewport, }; use bevy_asset::{Assets, Handle}; use bevy_ecs::{ReadOnlyFetch, Resources, World, WorldQuery}; @@ -216,13 +217,23 @@ where continue; }; - // get an ordered list of entities visible to the camera - let visible_entities = if let Some(camera_entity) = active_cameras.get(&camera_info.name) { - world.get::(camera_entity).unwrap() + let camera_entity = if let Some(camera_entity) = active_cameras.get(&camera_info.name) { + camera_entity } else { continue; }; + // get camera viewport and apply it + let viewport = world.get::(camera_entity) + .expect("A camera requires a Viewport component."); + let origin = viewport.physical_origin(); + let size = viewport.physical_size(); + let (min_depth, max_depth) = viewport.depth_range().into_inner(); + render_pass.set_viewport(origin.x, origin.y, size.x, size.y, min_depth, max_depth); + + // get an ordered list of entities visible to the camera + let visible_entities = world.get::(camera_entity).unwrap(); + // attempt to draw each visible entity let mut draw_state = DrawState::default(); for visible_entity in visible_entities.iter() { diff --git a/crates/bevy_render/src/surface/mod.rs b/crates/bevy_render/src/surface/mod.rs new file mode 100644 index 0000000000000..c548462b97f6e --- /dev/null +++ b/crates/bevy_render/src/surface/mod.rs @@ -0,0 +1,48 @@ +mod viewport; + +pub use viewport::*; + +use crate::renderer::TextureId; +use bevy_window::WindowId; + +#[derive(Debug, Copy, Clone, Hash, Eq, PartialEq)] +pub enum SurfaceId { + Window(WindowId), + Texture(TextureId), +} + +impl SurfaceId { + pub fn get_window(&self) -> Option { + if let SurfaceId::Window(id) = self { + Some(*id) + } else { + None + } + } + + pub fn get_texture(&self) -> Option { + if let SurfaceId::Texture(id) = self { + Some(*id) + } else { + None + } + } +} + +impl Default for SurfaceId { + fn default() -> Self { + WindowId::primary().into() + } +} + +impl From for SurfaceId { + fn from(value: WindowId) -> Self { + SurfaceId::Window(value) + } +} + +impl From for SurfaceId { + fn from(value: TextureId) -> Self { + SurfaceId::Texture(value) + } +} diff --git a/crates/bevy_render/src/surface/viewport.rs b/crates/bevy_render/src/surface/viewport.rs new file mode 100644 index 0000000000000..4f70ea65f1fd3 --- /dev/null +++ b/crates/bevy_render/src/surface/viewport.rs @@ -0,0 +1,223 @@ +use super::SurfaceId; +use bevy_app::prelude::EventReader; +use bevy_ecs::{Changed, Query, QuerySet, Res}; +use bevy_math::{clamp, vec2, Rect, Vec2}; +use bevy_reflect::{Reflect, ReflectComponent}; +use bevy_utils::HashSet; +use bevy_window::{WindowId, WindowResized, WindowScaleFactorChanged, Windows}; +use std::ops::{Add, AddAssign, RangeInclusive, Sub, SubAssign}; + +#[derive(Debug, PartialEq, Clone, Reflect)] +#[reflect(Component)] +pub struct Viewport { + #[reflect(ignore)] + pub surface: SurfaceId, + pub sides: Rect, + pub scale_factor: f64, + // computed values + origin: Vec2, + size: Vec2, + min_depth: f32, + max_depth: f32, +} + +impl Viewport { + const MIN_SIZE: f32 = 1.0; + + pub fn new(descriptor: ViewportDescriptor) -> Self { + let (min_depth, max_depth) = descriptor.depth_range.into_inner(); + assert_depth_bounds(min_depth); + assert_depth_bounds(max_depth); + Self { + surface: descriptor.surface, + sides: descriptor.sides, + scale_factor: descriptor.scale_factor, + min_depth, + max_depth, + origin: vec2(0.0, 0.0), + size: vec2(Self::MIN_SIZE, Self::MIN_SIZE), + } + } + + pub fn origin(&self) -> Vec2 { + self.origin + } + + pub fn size(&self) -> Vec2 { + self.size + } + + pub fn physical_origin(&self) -> Vec2 { + (self.origin.as_f64() * self.scale_factor).as_f32() + } + + pub fn physical_size(&self) -> Vec2 { + (self.size.as_f64() * self.scale_factor).as_f32() + } + + pub fn depth_range(&self) -> RangeInclusive { + self.min_depth..=self.max_depth + } + + pub fn set_depth_range(&mut self, range: RangeInclusive) { + let (min_depth, max_depth) = range.into_inner(); + assert_depth_bounds(min_depth); + assert_depth_bounds(max_depth); + self.min_depth = min_depth; + self.max_depth = max_depth; + } + + pub fn update_rectangle(&mut self, surface_size: Vec2) { + let x = match self.sides.left { + SideLocation::Absolute(value) => value, + SideLocation::Relative(value) => value * surface_size.x, + }; + let y = match self.sides.top { + SideLocation::Absolute(value) => value, + SideLocation::Relative(value) => value * surface_size.y, + }; + let w = match self.sides.right { + SideLocation::Absolute(value) => value - x, + SideLocation::Relative(value) => value * surface_size.x - x, + }; + let h = match self.sides.bottom { + SideLocation::Absolute(value) => value - y, + SideLocation::Relative(value) => value * surface_size.y - y, + }; + self.origin.x = clamp(x, Self::MIN_SIZE, surface_size.x - Self::MIN_SIZE); + self.origin.y = clamp(y, Self::MIN_SIZE, surface_size.y - Self::MIN_SIZE); + self.size.x = clamp(w, Self::MIN_SIZE, surface_size.x - self.origin.x); + self.size.y = clamp(h, Self::MIN_SIZE, surface_size.y - self.origin.y); + } +} + +#[inline] +fn assert_depth_bounds(value: f32) { + // this panics earlier and with a nicer error message, compared to letting it go through to wgpu + assert!( + (0.0..=1.0).contains(&value), + "depth value out of range: {} not in (0.0..=1.0)", + value + ); +} + +impl Default for Viewport { + fn default() -> Self { + Viewport::new(ViewportDescriptor::default()) + } +} + +#[derive(Debug, Clone)] +pub struct ViewportDescriptor { + pub surface: SurfaceId, + pub sides: Rect, + pub scale_factor: f64, + pub depth_range: RangeInclusive, +} + +impl Default for ViewportDescriptor { + fn default() -> Self { + Self { + surface: WindowId::primary().into(), + sides: Rect { + left: SideLocation::Relative(0.0), + right: SideLocation::Relative(1.0), + top: SideLocation::Relative(0.0), + bottom: SideLocation::Relative(1.0), + }, + scale_factor: 1.0, + depth_range: 0.0..=1.0, + } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Reflect)] +#[reflect_value(PartialEq)] +pub enum SideLocation { + Relative(f32), + Absolute(f32), +} + +impl Default for SideLocation { + fn default() -> Self { + Self::Relative(0.0) + } +} + +impl Add for SideLocation { + type Output = SideLocation; + + fn add(self, rhs: f32) -> Self::Output { + match self { + SideLocation::Relative(value) => SideLocation::Relative(value + rhs), + SideLocation::Absolute(value) => SideLocation::Absolute(value + rhs), + } + } +} + +impl Sub for SideLocation { + type Output = SideLocation; + + fn sub(self, rhs: f32) -> Self::Output { + match self { + SideLocation::Relative(value) => SideLocation::Relative(value - rhs), + SideLocation::Absolute(value) => SideLocation::Absolute(value - rhs), + } + } +} + +impl AddAssign for SideLocation { + fn add_assign(&mut self, rhs: f32) { + match self { + SideLocation::Relative(value) => *value += rhs, + SideLocation::Absolute(value) => *value += rhs, + } + } +} + +impl SubAssign for SideLocation { + fn sub_assign(&mut self, rhs: f32) { + match self { + SideLocation::Relative(value) => *value -= rhs, + SideLocation::Absolute(value) => *value -= rhs, + } + } +} + +pub fn viewport_system( + mut window_resized_events: EventReader, + mut window_scale_change_events: EventReader, + windows: Res, + mut queries: QuerySet<(Query<&Viewport, Changed>, Query<&mut Viewport>)>, +) { + let mut changed_window_ids: HashSet = HashSet::default(); + for event in window_resized_events.iter() { + changed_window_ids.insert(event.id); + } + for event in window_scale_change_events.iter() { + changed_window_ids.insert(event.id); + } + for viewport in queries.q0().iter() { + if let Some(id) = viewport.surface.get_window() { + changed_window_ids.insert(id); + } + } + + // update the surfaces + for mut viewport in queries.q1_mut().iter_mut() { + match viewport.surface { + SurfaceId::Window(id) => { + if changed_window_ids.contains(&id) { + let window = windows + .get(id) + .expect("Viewport surface refers to non-existent window"); + viewport.update_rectangle(vec2(window.width(), window.height())); + viewport.scale_factor = window.scale_factor(); + } + } + SurfaceId::Texture(_id) => { + // TODO: not implemented yet + } + } + } +} diff --git a/crates/bevy_ui/src/entity.rs b/crates/bevy_ui/src/entity.rs index 7e31c3e2b699e..c8fd97af09798 100644 --- a/crates/bevy_ui/src/entity.rs +++ b/crates/bevy_ui/src/entity.rs @@ -12,6 +12,7 @@ use bevy_render::{ mesh::Mesh, pipeline::{RenderPipeline, RenderPipelines}, prelude::Visible, + surface::Viewport, }; use bevy_sprite::{ColorMaterial, QUAD_HANDLE}; use bevy_text::{CalculatedSize, Text}; @@ -167,6 +168,7 @@ impl Default for ButtonBundle { pub struct UiCameraBundle { pub camera: Camera, pub orthographic_projection: OrthographicProjection, + pub viewport: Viewport, pub visible_entities: VisibleEntities, pub transform: Transform, pub global_transform: GlobalTransform, @@ -189,6 +191,7 @@ impl Default for UiCameraBundle { ..Default::default() }, visible_entities: Default::default(), + viewport: Default::default(), transform: Transform::from_xyz(0.0, 0.0, far - 0.1), global_transform: Default::default(), } diff --git a/examples/README.md b/examples/README.md index c5a2aaffd627d..0c9ac85cfbde0 100644 --- a/examples/README.md +++ b/examples/README.md @@ -208,6 +208,7 @@ Example | File | Description `clear_color` | [`window/clear_color.rs`](./window/clear_color.rs) | Creates a solid color window `multiple_windows` | [`window/multiple_windows.rs`](./window/multiple_windows.rs) | Creates two windows and cameras viewing the same mesh `scale_factor_override` | [`window/scale_factor_override.rs`](./window/scale_factor_override.rs) | Illustrates how to customize the default window settings +`viewports` | [`window/viewports.rs`](./window/viewports.rs) | Demonstrates how to divide a window into viewports and manage them `window_settings` | [`window/window_settings.rs`](./window/window_settings.rs) | Demonstrates customizing default window settings # Platform-Specific Examples diff --git a/examples/window/multiple_windows.rs b/examples/window/multiple_windows.rs index 34c868f128734..c66c032df703f 100644 --- a/examples/window/multiple_windows.rs +++ b/examples/window/multiple_windows.rs @@ -7,6 +7,7 @@ use bevy::{ base::MainPass, CameraNode, PassNode, RenderGraph, WindowSwapChainNode, WindowTextureNode, }, + surface::{Viewport, ViewportDescriptor}, texture::{Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsage}, }, window::{CreateWindow, WindowDescriptor, WindowId}, @@ -198,9 +199,12 @@ fn setup_pipeline( .spawn(PerspectiveCameraBundle { camera: Camera { name: Some("Secondary".to_string()), - window: window_id, ..Default::default() }, + viewport: Viewport::new(ViewportDescriptor { + surface: window_id.into(), + ..Default::default() + }), transform: Transform::from_xyz(6.0, 0.0, 0.0) .looking_at(Vec3::default(), Vec3::unit_y()), ..Default::default() diff --git a/examples/window/viewports.rs b/examples/window/viewports.rs new file mode 100644 index 0000000000000..70e530cbaf991 --- /dev/null +++ b/examples/window/viewports.rs @@ -0,0 +1,224 @@ +use bevy::{ + math::{clamp, Rect}, + prelude::*, + render::{ + camera::{ActiveCameras, Camera}, + render_graph::{base, CameraNode, PassNode, RenderGraph}, + surface::{SideLocation, Viewport, ViewportDescriptor}, + }, +}; + +/// This example creates a second window and draws a mesh from two different cameras. +fn main() { + App::build() + .insert_resource(Msaa { samples: 4 }) + .init_resource::() + .add_plugins(DefaultPlugins) + .add_startup_system(setup.system()) + .add_system(viewport_layout_system.system()) + .run(); +} + +const FRONT_CAMERA: &str = "FrontView"; +const FRONT_CAMERA_NODE: &str = "front_view_camera"; +const SIDE_CAMERA: &str = "SideView"; +const SIDE_CAMERA_NODE: &str = "side_view_camera"; + +fn setup( + commands: &mut Commands, + mut active_cameras: ResMut, + mut render_graph: ResMut, + asset_server: Res, +) { + // add new camera nodes for the secondary viewports + render_graph.add_system_node(FRONT_CAMERA_NODE, CameraNode::new(FRONT_CAMERA)); + render_graph.add_system_node(SIDE_CAMERA_NODE, CameraNode::new(SIDE_CAMERA)); + active_cameras.add(FRONT_CAMERA); + active_cameras.add(SIDE_CAMERA); + + // add the cameras to the main pass + { + let main_pass: &mut PassNode<&base::MainPass> = + render_graph.get_node_mut(base::node::MAIN_PASS).unwrap(); + main_pass.add_camera(FRONT_CAMERA); + main_pass.add_camera(SIDE_CAMERA); + } + render_graph + .add_node_edge(FRONT_CAMERA_NODE, base::node::MAIN_PASS) + .unwrap(); + render_graph + .add_node_edge(SIDE_CAMERA_NODE, base::node::MAIN_PASS) + .unwrap(); + + // SETUP SCENE + + // add entities to the world + commands + //.spawn_scene(asset_server.load("models/monkey/Monkey.gltf#Scene0")) + .spawn_scene(asset_server.load("models/FlightHelmet/FlightHelmet.gltf#Scene0")) + // light + .spawn(LightBundle { + transform: Transform::from_xyz(4.0, 5.0, 4.0), + ..Default::default() + }) + // main camera + .spawn(PerspectiveCameraBundle { + // the following is an example of how to setup static viewports + // and isn't really necessary in this case, as it will be + // immediately overwritten by the viewport_layout_system + viewport: Viewport::new(ViewportDescriptor { + sides: Rect { + // occupy the left 50% of the available horizontal space + left: SideLocation::Relative(0.0), + right: SideLocation::Relative(0.5), + // occupy the left 100% of the available vertical space + top: SideLocation::Relative(0.0), + bottom: SideLocation::Relative(1.0), + }, + ..Default::default() + }), + transform: Transform::from_xyz(-1.0, 1.0, 1.0) + .looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::unit_y()), + ..Default::default() + }) + // top right camera + .spawn(PerspectiveCameraBundle { + camera: Camera { + name: Some(FRONT_CAMERA.to_string()), + ..Default::default() + }, + transform: Transform::from_xyz(0.0, 0.3, 1.3) + .looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::unit_y()), + ..Default::default() + }) + // bottom right camera + .spawn(PerspectiveCameraBundle { + camera: Camera { + name: Some(SIDE_CAMERA.to_string()), + ..Default::default() + }, + transform: Transform::from_xyz(-1.3, 0.3, 0.0) + .looking_at(Vec3::new(0.0, 0.3, 0.0), Vec3::unit_y()), + ..Default::default() + }); + + // ui + let instructions_text = + "Use the arrow keys to resize the viewports\nPress Enter to swap the rightmost viewports"; + commands + .spawn(UiCameraBundle { + // viewports occupy the entire surface by default, and can overlap each other + ..Default::default() + }) + .spawn(TextBundle { + style: Style { + align_self: AlignSelf::FlexEnd, + ..Default::default() + }, + text: Text::with_section( + instructions_text, + TextStyle { + font: asset_server.load("fonts/FiraSans-Bold.ttf"), + font_size: 30.0, + color: Color::WHITE, + }, + Default::default(), + ), + ..Default::default() + }); +} + +struct ViewportLayout { + divide_x: f32, + divide_y: f32, + invert: bool, +} + +impl ViewportLayout { + pub fn main_view(&self) -> Rect { + Rect { + left: SideLocation::Relative(0.0), + right: SideLocation::Relative(self.divide_x), + top: SideLocation::Relative(0.0), + bottom: SideLocation::Relative(1.0), + } + } + + pub fn front_view_view(&self) -> Rect { + Rect { + left: SideLocation::Relative(self.divide_x), + right: SideLocation::Relative(1.0), + top: SideLocation::Relative(0.0), + bottom: SideLocation::Relative(self.divide_y), + } + } + + pub fn side_view_view(&self) -> Rect { + Rect { + left: SideLocation::Relative(self.divide_x), + right: SideLocation::Relative(1.0), + top: SideLocation::Relative(self.divide_y), + bottom: SideLocation::Relative(1.0), + } + } +} + +impl Default for ViewportLayout { + fn default() -> Self { + Self { + divide_x: 0.5, + divide_y: 0.5, + invert: false, + } + } +} + +fn viewport_layout_system( + keyboard_input: Res>, + mut layout: ResMut, + mut query: Query<(&Camera, &mut Viewport)>, +) { + // update the layout state + if keyboard_input.just_pressed(KeyCode::Left) { + layout.divide_x -= 0.05; + } + if keyboard_input.just_pressed(KeyCode::Right) { + layout.divide_x += 0.05; + } + if keyboard_input.just_pressed(KeyCode::Up) { + layout.divide_y -= 0.05; + } + if keyboard_input.just_pressed(KeyCode::Down) { + layout.divide_y += 0.05; + } + if keyboard_input.just_pressed(KeyCode::Return) { + layout.invert = !layout.invert; + } + layout.divide_x = clamp(layout.divide_x, 0.0, 1.0); + layout.divide_y = clamp(layout.divide_y, 0.0, 1.0); + + // resize the viewports + for (camera, mut viewport) in query.iter_mut() { + match camera.name.as_deref() { + // default camera + Some("Camera3d") => { + viewport.sides = layout.main_view(); + } + Some(FRONT_CAMERA) => { + if layout.invert { + viewport.sides = layout.front_view_view(); + } else { + viewport.sides = layout.side_view_view(); + } + } + Some(SIDE_CAMERA) => { + if layout.invert { + viewport.sides = layout.side_view_view(); + } else { + viewport.sides = layout.front_view_view(); + } + } + _ => {} + } + } +}