//! Renders four cameras to the same window to accomplish "split screen". use std::f32::consts::PI; use bevy::{ camera::Viewport, feathers::{ controls::FeathersButton, dark_theme::create_dark_theme, display::caption, theme::UiTheme, FeathersPlugins, }, light::CascadeShadowConfigBuilder, prelude::*, ui_widgets::Activate, window::WindowResized, }; fn main() { App::new() .add_plugins((DefaultPlugins, FeathersPlugins)) .insert_resource(UiTheme(create_dark_theme())) .add_systems(Startup, setup) .add_systems(Update, set_camera_viewports) .add_observer(on_activate_rotate_camera) .run(); } /// set up a simple 3D scene fn setup( mut commands: Commands, asset_server: Res, mut meshes: ResMut>, mut materials: ResMut>, ) { // plane commands.spawn(( Mesh3d(meshes.add(Plane3d::default().mesh().size(100.0, 100.0))), MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))), )); commands.spawn(WorldAssetRoot( asset_server.load(GltfAssetLabel::Scene(0).from_asset("models/animated/Fox.glb")), )); // Light commands.spawn(( Transform::from_rotation(Quat::from_euler(EulerRot::ZYX, 0.0, 1.0, -PI / 4.)), DirectionalLight { shadow_maps_enabled: true, ..default() }, CascadeShadowConfigBuilder { num_cascades: if cfg!(all( feature = "webgl2", target_arch = "wasm32", not(feature = "webgpu") )) { // Limited to 1 cascade in WebGL 1 } else { 2 }, first_cascade_far_bound: 200.0, maximum_distance: 280.0, ..default() } .build(), )); // Cameras and their dedicated UI for (index, (camera_name, camera_pos)) in [ ("Player 1", Vec3::new(0.0, 200.0, -150.0)), ("Player 2", Vec3::new(150.0, 150., 50.0)), ("Player 3", Vec3::new(100.0, 150., -150.0)), ("Player 4", Vec3::new(-100.0, 80., 150.0)), ] .iter() .enumerate() { let camera = commands .spawn(( Camera3d::default(), Transform::from_translation(*camera_pos).looking_at(Vec3::ZERO, Vec3::Y), Camera { // Renders cameras with different priorities to prevent ambiguities order: index as isize, ..default() }, CameraPosition { pos: UVec2::new((index % 2) as u32, (index / 2) as u32), }, )) .id(); // Set up UI let buttons_entity = commands.spawn_scene(buttons_panel()).id(); commands .spawn(( UiTargetCamera(camera), Node { width: percent(100), height: percent(100), ..default() }, children![( Text::new(*camera_name), Node { margin: px(12).all(), ..default() }, ),], )) .add_child(buttons_entity); } fn buttons_panel() -> impl Scene { bsn! { Node { position_type: PositionType::Absolute, width: percent(100), height: percent(100), display: Display::Flex, flex_direction: FlexDirection::Row, justify_content: JustifyContent::SpaceBetween, align_items: AlignItems::Center, padding: UiRect::all(px(20)), } Children [ @rotate_button("<", Direction::Left) -- @rotate_button(">", Direction::Right) ] } } fn rotate_button(text_caption: &'static str, direction: Direction) -> impl Scene { bsn! { RotateCamera(direction) @FeathersButton { @caption: bsn! { @caption(text_caption) }, } Node { width: px(40), } } } } #[derive(Component)] struct CameraPosition { pos: UVec2, } #[derive(Component, Clone, Default, PartialEq)] struct RotateCamera(Direction); #[derive(Clone, Default, PartialEq)] enum Direction { #[default] Left, Right, } fn set_camera_viewports( windows: Query<&Window>, mut window_resized_reader: MessageReader, mut query: Query<(&CameraPosition, &mut Camera)>, ) { // We need to dynamically resize the camera's viewports whenever the window size changes // so then each camera always takes up half the screen. // A resize_event is sent when the window is first created, allowing us to reuse this system for initial setup. for window_resized in window_resized_reader.read() { let window = windows.get(window_resized.window).unwrap(); let size = window.physical_size() / 2; for (camera_position, mut camera) in &mut query { camera.viewport = Some(Viewport { physical_position: camera_position.pos * size, physical_size: size, ..default() }); } } } fn on_activate_rotate_camera( event: On, button_query: Query<(&ComputedUiTargetCamera, &RotateCamera), With>, mut camera_query: Query<&mut Transform, With>, ) { let Ok((computed_target, RotateCamera(direction))) = button_query.get(event.entity) else { return; }; // Since TargetCamera propagates to the children, we can use it to find // which side of the screen the button is on. if let Some(mut camera_transform) = computed_target .get() .and_then(|camera| camera_query.get_mut(camera).ok()) { let angle = match direction { Direction::Left => -0.1, Direction::Right => 0.1, }; camera_transform.rotate_around(Vec3::ZERO, Quat::from_axis_angle(Vec3::Y, angle)); } }