scaling_modes/
scaling_modes.rs

1//! Renders two cameras to the same window to accomplish *split screen*. The left camera
2//! uses perspective projection whereas the right camera uses orthographic projection. Spawns three
3//! windows each with different scaling modes regarding window resize events:
4//!
5//!  1. window with fixed vertical field of view.
6//!  2. window with fixed horizontal field of view.
7//!  3. window with fixed unit per pixels.
8
9#![allow(clippy::similar_names)]
10
11use bevy::{
12	prelude::*,
13	render::camera::{RenderTarget, Viewport},
14	window::{PrimaryWindow, WindowRef, WindowResized},
15};
16use bevy_trackball::prelude::{Fixed, *};
17
18fn main() {
19	App::new()
20		.add_plugins(DefaultPlugins)
21		.add_plugins(TrackballPlugin)
22		.add_systems(Startup, setup)
23		.add_systems(Update, set_camera_viewports)
24		.run();
25}
26
27/// set up a simple 3D scene
28fn setup(
29	mut windows: Query<&mut Window>,
30	mut commands: Commands,
31	mut meshes: ResMut<Assets<Mesh>>,
32	mut materials: ResMut<Assets<StandardMaterial>>,
33) {
34	// circular base
35	commands.spawn((
36		Mesh3d(meshes.add(Circle::new(4.0))),
37		MeshMaterial3d(materials.add(Color::WHITE)),
38		Transform::from_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)),
39	));
40	// cube
41	commands.spawn((
42		Mesh3d(meshes.add(Cuboid::new(1.0, 1.0, 1.0))),
43		MeshMaterial3d(materials.add(Color::srgb_u8(124, 144, 255))),
44		Transform::from_xyz(0.0, 0.5, 0.0),
45	));
46	// light
47	commands.spawn((
48		PointLight {
49			shadows_enabled: true,
50			..default()
51		},
52		Transform::from_xyz(4.0, 8.0, 4.0),
53	));
54
55	// Windows
56	let mut window1 = windows.single_mut().unwrap();
57	"Fixed Vertical Field of View (Perspective vs Orthographic)".clone_into(&mut window1.title);
58	let res = &window1.resolution;
59	let max = Vec2::new(res.width() * 0.5, res.height()).into();
60	// Left and right camera orientation.
61	let [target, eye, up] = [Vec3::Y * 0.5, Vec3::new(-2.5, 4.5, 9.0) * 1.2, Vec3::Y];
62	// Spawn a 2nd window.
63	let window2 = commands
64		.spawn(Window {
65			title: "Fixed Horizontal Field of View (Perspective vs Orthographic)".to_owned(),
66			..default()
67		})
68		.id();
69	// Spawn a 3rd window.
70	let window3 = commands
71		.spawn(Window {
72			title: "Fixed Unit Per Pixels (Perspective vs Orthographic)".to_owned(),
73			..default()
74		})
75		.id();
76
77	// Cameras
78	let mut order = 0;
79	let fov = Fixed::default();
80	for (fov, window) in [
81		(fov, WindowRef::Primary),
82		(fov.to_hor(&max), WindowRef::Entity(window2)),
83		(fov.to_upp(&max), WindowRef::Entity(window3)),
84	] {
85		let mut scope = Scope::default();
86		scope.set_fov(fov);
87		// Left trackball controller and camera 3D bundle.
88		let left = commands
89			.spawn((
90				TrackballController::default(),
91				Camera {
92					target: RenderTarget::Window(window),
93					// Renders the right camera after the left camera,
94					// which has a default priority of 0.
95					order,
96					..default()
97				},
98				Camera3d::default(),
99				LeftCamera,
100			))
101			.id();
102		order += 1;
103		// Right trackball controller and camera 3D bundle.
104		let right = commands
105			.spawn((
106				TrackballController::default(),
107				Camera {
108					target: RenderTarget::Window(window),
109					// Renders the right camera after the left camera,
110					// which has a default priority of 0.
111					order,
112					// Don't clear on the second camera
113					// because the first camera already cleared the window.
114					clear_color: ClearColorConfig::None,
115					..default()
116				},
117				Camera3d::default(),
118				RightCamera,
119			))
120			.id();
121		order += 1;
122		// Insert left trackball camera and make it sensitive to right trackball controller as well.
123		commands.entity(left).insert(
124			TrackballCamera::look_at(target, eye, up)
125				.with_scope(scope)
126				.add_controller(right, true),
127		);
128		// Set orthographic projection mode for right camera.
129		scope.set_ortho(true);
130		// Insert right trackball camera and make it sensitive to left trackball controller as well.
131		commands.entity(right).insert(
132			TrackballCamera::look_at(target, eye, up)
133				.with_scope(scope)
134				.add_controller(left, true),
135		);
136	}
137}
138
139#[derive(Component)]
140struct LeftCamera;
141
142#[derive(Component)]
143struct RightCamera;
144
145#[allow(clippy::needless_pass_by_value)]
146fn set_camera_viewports(
147	primary_windows: Query<(Entity, &Window), With<PrimaryWindow>>,
148	windows: Query<(Entity, &Window)>,
149	mut resize_events: EventReader<WindowResized>,
150	mut left_cameras: Query<&mut Camera, (With<LeftCamera>, Without<RightCamera>)>,
151	mut right_cameras: Query<&mut Camera, With<RightCamera>>,
152) {
153	// We need to dynamically resize the camera's viewports whenever the window size changes,
154	// so then each camera always takes up half the screen. A `resize_event` is sent when the window
155	// is first created, allowing us to reuse this system for initial setup.
156	for resize_event in resize_events.read() {
157		let (resize_id, resize_window) = windows.get(resize_event.window).unwrap();
158		let resolution = &resize_window.resolution;
159		for mut left_camera in &mut left_cameras {
160			if let RenderTarget::Window(window_ref) = left_camera.target {
161				let Some((target_id, _target_window)) = (match window_ref {
162					WindowRef::Primary => primary_windows.single().ok(),
163					WindowRef::Entity(id) => windows.get(id).ok(),
164				}) else {
165					continue;
166				};
167				if target_id == resize_id {
168					left_camera.viewport = Some(Viewport {
169						physical_position: UVec2::new(0, 0),
170						physical_size: UVec2::new(
171							resolution.physical_width() / 2,
172							resolution.physical_height(),
173						),
174						..default()
175					});
176				}
177			}
178		}
179		for mut right_camera in &mut right_cameras {
180			if let RenderTarget::Window(window_ref) = right_camera.target {
181				let Some((target_id, _target_window)) = (match window_ref {
182					WindowRef::Primary => primary_windows.single().ok(),
183					WindowRef::Entity(id) => windows.get(id).ok(),
184				}) else {
185					continue;
186				};
187				if target_id == resize_id {
188					right_camera.viewport = Some(Viewport {
189						physical_position: UVec2::new(resolution.physical_width() / 2, 0),
190						physical_size: UVec2::new(
191							resolution.physical_width() / 2,
192							resolution.physical_height(),
193						),
194						..default()
195					});
196				}
197			}
198		}
199	}
200}