Skip to main content

camera_sub_view/
camera_sub_view.rs

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
9use bevy::{
10    camera::{ScalingMode, SubCameraView, Viewport},
11    prelude::*,
12};
13
14fn 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)]
23struct MovingCameraMarker;
24
25/// Set up a simple 3D scene
26fn 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
242fn 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
255fn 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)]
308enum ExampleViewports {
309    PerspectiveMain,
310    PerspectiveStretched,
311    PerspectiveMoving,
312    PerspectiveControl,
313    OrthographicMain,
314    OrthographicStretched,
315    OrthographicMoving,
316    OrthographicControl,
317}