docs/bevy/examples/3d/camera_sub_view.rs 10.6 K · 317 lines · raw · history

1 //! Demonstrates different sub view effects.
2 //!
3 //! A sub view is essentially a smaller section of a larger viewport. Some use
4 //! cases include:
5 //! - Split one image across multiple cameras, for use in a multimonitor setups
6 //! - Magnify a section of the image, by rendering a small sub view in another
7 //! camera
8 //! - Rapidly change the sub view offset to get a screen shake effect
9 use bevy::{
10 camera::{ScalingMode, SubCameraView, Viewport},
11 prelude::*,
12 };
13
14 fn main() {
15 App::new()
16 .add_plugins(DefaultPlugins)
17 .add_systems(Startup, setup)
18 .add_systems(Update, (move_camera_view, resize_viewports))
19 .run();
20 }
21
22 #[derive(Debug, Component)]
23 struct MovingCameraMarker;
24
25 /// Set up a simple 3D scene
26 fn setup(
27 mut commands: Commands,
28 mut meshes: ResMut<Assets<Mesh>>,
29 mut materials: ResMut<Assets<StandardMaterial>>,
30 ) {
31 let transform = Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y);
32
33 // Plane
34 commands.spawn((
35 Mesh3d(meshes.add(Plane3d::default().mesh().size(5.0, 5.0))),
36 MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))),
37 ));
38
39 // Cube
40 commands.spawn((
41 Mesh3d(meshes.add(Cuboid::default())),
42 MeshMaterial3d(materials.add(Color::srgb(0.8, 0.7, 0.6))),
43 Transform::from_xyz(0.0, 0.5, 0.0),
44 ));
45
46 // Light
47 commands.spawn((
48 PointLight {
49 shadow_maps_enabled: true,
50 ..default()
51 },
52 Transform::from_xyz(4.0, 8.0, 4.0),
53 ));
54
55 // Main perspective camera:
56 //
57 // The main perspective image to use as a comparison for the sub views.
58 commands.spawn((
59 Camera3d::default(),
60 Camera::default(),
61 ExampleViewports::PerspectiveMain,
62 transform,
63 ));
64
65 // Perspective camera right half:
66 //
67 // For this camera, the projection is perspective, and `size` is half the
68 // width of the `full_size`, while the x value of `offset` is set to half
69 // the value of the full width, causing the right half of the image to be
70 // shown. Since the viewport has an aspect ratio of 1x1 and the sub view has
71 // an aspect ratio of 1x2, the image appears stretched along the horizontal
72 // axis.
73 commands.spawn((
74 Camera3d::default(),
75 Camera {
76 sub_camera_view: Some(SubCameraView {
77 // The values of `full_size` and `size` do not have to be the
78 // exact values of your physical viewport. The important part is
79 // the ratio between them.
80 full_size: UVec2::new(10, 10),
81 // The `offset` is also relative to the values in `full_size`
82 // and `size`
83 offset: Vec2::new(5.0, 0.0),
84 size: UVec2::new(5, 10),
85 }),
86 order: 1,
87 ..default()
88 },
89 ExampleViewports::PerspectiveStretched,
90 transform,
91 ));
92
93 // Perspective camera moving:
94 //
95 // For this camera, the projection is perspective, and the offset is updated
96 // continuously in 150 units per second in `move_camera_view`. Since the
97 // `full_size` is 500x500, the image should appear to be moving across the
98 // full image once every 3.3 seconds. `size` is a fifth of the size of
99 // `full_size`, so the image will appear zoomed in.
100 commands.spawn((
101 Camera3d::default(),
102 Camera {
103 sub_camera_view: Some(SubCameraView {
104 full_size: UVec2::new(500, 500),
105 offset: Vec2::ZERO,
106 size: UVec2::new(100, 100),
107 }),
108 order: 2,
109 ..default()
110 },
111 transform,
112 ExampleViewports::PerspectiveMoving,
113 MovingCameraMarker,
114 ));
115
116 // Perspective camera different aspect ratio:
117 //
118 // For this camera, the projection is perspective, and the aspect ratio of
119 // the sub view (2x1) is different to the aspect ratio of the full view
120 // (2x2). The aspect ratio of the sub view matches the aspect ratio of
121 // the viewport and should show an unstretched image of the top half of the
122 // full perspective image.
123 commands.spawn((
124 Camera3d::default(),
125 Camera {
126 sub_camera_view: Some(SubCameraView {
127 full_size: UVec2::new(800, 800),
128 offset: Vec2::ZERO,
129 size: UVec2::new(800, 400),
130 }),
131 order: 3,
132 ..default()
133 },
134 ExampleViewports::PerspectiveControl,
135 transform,
136 ));
137
138 // Main orthographic camera:
139 //
140 // The main orthographic image to use as a comparison for the sub views.
141 commands.spawn((
142 Camera3d::default(),
143 Projection::from(OrthographicProjection {
144 scaling_mode: ScalingMode::FixedVertical {
145 viewport_height: 6.0,
146 },
147 ..OrthographicProjection::default_3d()
148 }),
149 Camera {
150 order: 4,
151 ..default()
152 },
153 ExampleViewports::OrthographicMain,
154 transform,
155 ));
156
157 // Orthographic camera left half:
158 //
159 // For this camera, the projection is orthographic, and `size` is half the
160 // width of the `full_size`, causing the left half of the image to be shown.
161 // Since the viewport has an aspect ratio of 1x1 and the sub view has an
162 // aspect ratio of 1x2, the image appears stretched along the horizontal axis.
163 commands.spawn((
164 Camera3d::default(),
165 Projection::from(OrthographicProjection {
166 scaling_mode: ScalingMode::FixedVertical {
167 viewport_height: 6.0,
168 },
169 ..OrthographicProjection::default_3d()
170 }),
171 Camera {
172 sub_camera_view: Some(SubCameraView {
173 full_size: UVec2::new(2, 2),
174 offset: Vec2::ZERO,
175 size: UVec2::new(1, 2),
176 }),
177 order: 5,
178 ..default()
179 },
180 ExampleViewports::OrthographicStretched,
181 transform,
182 ));
183
184 // Orthographic camera moving:
185 //
186 // For this camera, the projection is orthographic, and the offset is
187 // updated continuously in 150 units per second in `move_camera_view`. Since
188 // the `full_size` is 500x500, the image should appear to be moving across
189 // the full image once every 3.3 seconds. `size` is a fifth of the size of
190 // `full_size`, so the image will appear zoomed in.
191 commands.spawn((
192 Camera3d::default(),
193 Projection::from(OrthographicProjection {
194 scaling_mode: ScalingMode::FixedVertical {
195 viewport_height: 6.0,
196 },
197 ..OrthographicProjection::default_3d()
198 }),
199 Camera {
200 sub_camera_view: Some(SubCameraView {
201 full_size: UVec2::new(500, 500),
202 offset: Vec2::ZERO,
203 size: UVec2::new(100, 100),
204 }),
205 order: 6,
206 ..default()
207 },
208 transform,
209 ExampleViewports::OrthographicMoving,
210 MovingCameraMarker,
211 ));
212
213 // Orthographic camera different aspect ratio:
214 //
215 // For this camera, the projection is orthographic, and the aspect ratio of
216 // the sub view (2x1) is different to the aspect ratio of the full view
217 // (2x2). The aspect ratio of the sub view matches the aspect ratio of
218 // the viewport and should show an unstretched image of the top half of the
219 // full orthographic image.
220 commands.spawn((
221 Camera3d::default(),
222 Projection::from(OrthographicProjection {
223 scaling_mode: ScalingMode::FixedVertical {
224 viewport_height: 6.0,
225 },
226 ..OrthographicProjection::default_3d()
227 }),
228 Camera {
229 sub_camera_view: Some(SubCameraView {
230 full_size: UVec2::new(200, 200),
231 offset: Vec2::ZERO,
232 size: UVec2::new(200, 100),
233 }),
234 order: 7,
235 ..default()
236 },
237 ExampleViewports::OrthographicControl,
238 transform,
239 ));
240 }
241
242 fn move_camera_view(
243 mut movable_camera_query: Query<&mut Camera, With<MovingCameraMarker>>,
244 time: Res<Time>,
245 ) {
246 for mut camera in movable_camera_query.iter_mut() {
247 if let Some(sub_view) = &mut camera.sub_camera_view {
248 sub_view.offset.x = (time.elapsed_secs() * 150.) % 450.0 - 50.0;
249 sub_view.offset.y = sub_view.offset.x;
250 }
251 }
252 }
253
254 // To ensure viewports remain the same at any window size
255 fn resize_viewports(
256 window: Single<&Window, With<bevy::window::PrimaryWindow>>,
257 mut viewports: Query<(&mut Camera, &ExampleViewports)>,
258 ) {
259 let window_size = window.physical_size();
260
261 let small_height = window_size.y / 5;
262 let small_width = window_size.x / 8;
263
264 let large_height = small_height * 4;
265 let large_width = small_width * 4;
266
267 let large_size = UVec2::new(large_width, large_height);
268
269 // Enforce the aspect ratio of the small viewports to ensure the images
270 // appear unstretched
271 let small_dim = small_height.min(small_width);
272 let small_size = UVec2::new(small_dim, small_dim);
273
274 let small_wide_size = UVec2::new(small_dim * 2, small_dim);
275
276 for (mut camera, example_viewport) in viewports.iter_mut() {
277 if camera.viewport.is_none() {
278 camera.viewport = Some(Viewport::default());
279 };
280
281 let Some(viewport) = &mut camera.viewport else {
282 continue;
283 };
284
285 let (size, position) = match example_viewport {
286 ExampleViewports::PerspectiveMain => (large_size, UVec2::new(0, small_height)),
287 ExampleViewports::PerspectiveStretched => (small_size, UVec2::ZERO),
288 ExampleViewports::PerspectiveMoving => (small_size, UVec2::new(small_width, 0)),
289 ExampleViewports::PerspectiveControl => {
290 (small_wide_size, UVec2::new(small_width * 2, 0))
291 }
292 ExampleViewports::OrthographicMain => {
293 (large_size, UVec2::new(large_width, small_height))
294 }
295 ExampleViewports::OrthographicStretched => (small_size, UVec2::new(small_width * 4, 0)),
296 ExampleViewports::OrthographicMoving => (small_size, UVec2::new(small_width * 5, 0)),
297 ExampleViewports::OrthographicControl => {
298 (small_wide_size, UVec2::new(small_width * 6, 0))
299 }
300 };
301
302 viewport.physical_size = size;
303 viewport.physical_position = position;
304 }
305 }
306
307 #[derive(Component)]
308 enum ExampleViewports {
309 PerspectiveMain,
310 PerspectiveStretched,
311 PerspectiveMoving,
312 PerspectiveControl,
313 OrthographicMain,
314 OrthographicStretched,
315 OrthographicMoving,
316 OrthographicControl,
317 }