3D Rendering / Camera sub view

Back to examples View in GitHub

Support Warning

WebGPU is currently only supported on Chrome starting with version 113, and only on desktop. If they don't work on your configuration, you can check the WebGL2 examples here.

camera_sub_view.rs:
//! Demonstrates different sub view effects.
//!
//! A sub view is essentially a smaller section of a larger viewport. Some use
//! cases include:
//! - Split one image across multiple cameras, for use in a multimonitor setups
//! - Magnify a section of the image, by rendering a small sub view in another
//!   camera
//! - Rapidly change the sub view offset to get a screen shake effect
use bevy::{
    prelude::*,
    render::camera::{ScalingMode, SubCameraView, Viewport},
};

fn main() {
    App::new()
        .add_plugins(DefaultPlugins)
        .add_systems(Startup, setup)
        .add_systems(Update, (move_camera_view, resize_viewports))
        .run();
}

#[derive(Debug, Component)]
struct MovingCameraMarker;

/// Set up a simple 3D scene
fn setup(
    mut commands: Commands,
    mut meshes: ResMut<Assets<Mesh>>,
    mut materials: ResMut<Assets<StandardMaterial>>,
) {
    let transform = Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y);

    // Plane
    commands.spawn((
        Mesh3d(meshes.add(Plane3d::default().mesh().size(5.0, 5.0))),
        MeshMaterial3d(materials.add(Color::srgb(0.3, 0.5, 0.3))),
    ));

    // Cube
    commands.spawn((
        Mesh3d(meshes.add(Cuboid::default())),
        MeshMaterial3d(materials.add(Color::srgb(0.8, 0.7, 0.6))),
        Transform::from_xyz(0.0, 0.5, 0.0),
    ));

    // Light
    commands.spawn((
        PointLight {
            shadows_enabled: true,
            ..default()
        },
        Transform::from_xyz(4.0, 8.0, 4.0),
    ));

    // Main perspective camera:
    //
    // The main perspective image to use as a comparison for the sub views.
    commands.spawn((
        Camera3d::default(),
        Camera::default(),
        ExampleViewports::PerspectiveMain,
        transform,
    ));

    // Perspective camera right half:
    //
    // For this camera, the projection is perspective, and `size` is half the
    // width of the `full_size`, while the x value of `offset` is set to half
    // the value of the full width, causing the right half of the image to be
    // shown. Since the viewport has an aspect ratio of 1x1 and the sub view has
    // an aspect ratio of 1x2, the image appears stretched along the horizontal
    // axis.
    commands.spawn((
        Camera3d::default(),
        Camera {
            sub_camera_view: Some(SubCameraView {
                // The values of `full_size` and `size` do not have to be the
                // exact values of your physical viewport. The important part is
                // the ratio between them.
                full_size: UVec2::new(10, 10),
                // The `offset` is also relative to the values in `full_size`
                // and `size`
                offset: Vec2::new(5.0, 0.0),
                size: UVec2::new(5, 10),
            }),
            order: 1,
            ..default()
        },
        ExampleViewports::PerspectiveStretched,
        transform,
    ));

    // Perspective camera moving:
    //
    // For this camera, the projection is perspective, and the offset is updated
    // continuously in 150 units per second in `move_camera_view`. Since the
    // `full_size` is 500x500, the image should appear to be moving across the
    // full image once every 3.3 seconds. `size` is a fifth of the size of
    // `full_size`, so the image will appear zoomed in.
    commands.spawn((
        Camera3d::default(),
        Camera {
            sub_camera_view: Some(SubCameraView {
                full_size: UVec2::new(500, 500),
                offset: Vec2::ZERO,
                size: UVec2::new(100, 100),
            }),
            order: 2,
            ..default()
        },
        transform,
        ExampleViewports::PerspectiveMoving,
        MovingCameraMarker,
    ));

    // Perspective camera different aspect ratio:
    //
    // For this camera, the projection is perspective, and the aspect ratio of
    // the sub view (2x1) is different to the aspect ratio of the full view
    // (2x2). The aspect ratio of the sub view matches the aspect ratio of
    // the viewport and should show an unstretched image of the top half of the
    // full perspective image.
    commands.spawn((
        Camera3d::default(),
        Camera {
            sub_camera_view: Some(SubCameraView {
                full_size: UVec2::new(800, 800),
                offset: Vec2::ZERO,
                size: UVec2::new(800, 400),
            }),
            order: 3,
            ..default()
        },
        ExampleViewports::PerspectiveControl,
        transform,
    ));

    // Main orthographic camera:
    //
    // The main orthographic image to use as a comparison for the sub views.
    commands.spawn((
        Camera3d::default(),
        Projection::from(OrthographicProjection {
            scaling_mode: ScalingMode::FixedVertical {
                viewport_height: 6.0,
            },
            ..OrthographicProjection::default_3d()
        }),
        Camera {
            order: 4,
            ..default()
        },
        ExampleViewports::OrthographicMain,
        transform,
    ));

    // Orthographic camera left half:
    //
    // For this camera, the projection is orthographic, and `size` is half the
    // width of the `full_size`, causing the left half of the image to be shown.
    // Since the viewport has an aspect ratio of 1x1 and the sub view has an
    // aspect ratio of 1x2, the image appears stretched along the horizontal axis.
    commands.spawn((
        Camera3d::default(),
        Projection::from(OrthographicProjection {
            scaling_mode: ScalingMode::FixedVertical {
                viewport_height: 6.0,
            },
            ..OrthographicProjection::default_3d()
        }),
        Camera {
            sub_camera_view: Some(SubCameraView {
                full_size: UVec2::new(2, 2),
                offset: Vec2::ZERO,
                size: UVec2::new(1, 2),
            }),
            order: 5,
            ..default()
        },
        ExampleViewports::OrthographicStretched,
        transform,
    ));

    // Orthographic camera moving:
    //
    // For this camera, the projection is orthographic, and the offset is
    // updated continuously in 150 units per second in `move_camera_view`. Since
    // the `full_size` is 500x500, the image should appear to be moving across
    // the full image once every 3.3 seconds. `size` is a fifth of the size of
    // `full_size`, so the image will appear zoomed in.
    commands.spawn((
        Camera3d::default(),
        Projection::from(OrthographicProjection {
            scaling_mode: ScalingMode::FixedVertical {
                viewport_height: 6.0,
            },
            ..OrthographicProjection::default_3d()
        }),
        Camera {
            sub_camera_view: Some(SubCameraView {
                full_size: UVec2::new(500, 500),
                offset: Vec2::ZERO,
                size: UVec2::new(100, 100),
            }),
            order: 6,
            ..default()
        },
        transform,
        ExampleViewports::OrthographicMoving,
        MovingCameraMarker,
    ));

    // Orthographic camera different aspect ratio:
    //
    // For this camera, the projection is orthographic, and the aspect ratio of
    // the sub view (2x1) is different to the aspect ratio of the full view
    // (2x2). The aspect ratio of the sub view matches the aspect ratio of
    // the viewport and should show an unstretched image of the top half of the
    // full orthographic image.
    commands.spawn((
        Camera3d::default(),
        Projection::from(OrthographicProjection {
            scaling_mode: ScalingMode::FixedVertical {
                viewport_height: 6.0,
            },
            ..OrthographicProjection::default_3d()
        }),
        Camera {
            sub_camera_view: Some(SubCameraView {
                full_size: UVec2::new(200, 200),
                offset: Vec2::ZERO,
                size: UVec2::new(200, 100),
            }),
            order: 7,
            ..default()
        },
        ExampleViewports::OrthographicControl,
        transform,
    ));
}

fn move_camera_view(
    mut movable_camera_query: Query<&mut Camera, With<MovingCameraMarker>>,
    time: Res<Time>,
) {
    for mut camera in movable_camera_query.iter_mut() {
        if let Some(sub_view) = &mut camera.sub_camera_view {
            sub_view.offset.x = (time.elapsed_secs() * 150.) % 450.0 - 50.0;
            sub_view.offset.y = sub_view.offset.x;
        }
    }
}

// To ensure viewports remain the same at any window size
fn resize_viewports(
    windows: Query<&Window, With<bevy::window::PrimaryWindow>>,
    mut viewports: Query<(&mut Camera, &ExampleViewports)>,
) {
    let Ok(window) = windows.get_single() else {
        return;
    };

    let window_size = window.physical_size();

    let small_height = window_size.y / 5;
    let small_width = window_size.x / 8;

    let large_height = small_height * 4;
    let large_width = small_width * 4;

    let large_size = UVec2::new(large_width, large_height);

    // Enforce the aspect ratio of the small viewports to ensure the images
    // appear unstretched
    let small_dim = small_height.min(small_width);
    let small_size = UVec2::new(small_dim, small_dim);

    let small_wide_size = UVec2::new(small_dim * 2, small_dim);

    for (mut camera, example_viewport) in viewports.iter_mut() {
        if camera.viewport.is_none() {
            camera.viewport = Some(Viewport::default());
        };

        let Some(viewport) = &mut camera.viewport else {
            continue;
        };

        let (size, position) = match example_viewport {
            ExampleViewports::PerspectiveMain => (large_size, UVec2::new(0, small_height)),
            ExampleViewports::PerspectiveStretched => (small_size, UVec2::ZERO),
            ExampleViewports::PerspectiveMoving => (small_size, UVec2::new(small_width, 0)),
            ExampleViewports::PerspectiveControl => {
                (small_wide_size, UVec2::new(small_width * 2, 0))
            }
            ExampleViewports::OrthographicMain => {
                (large_size, UVec2::new(large_width, small_height))
            }
            ExampleViewports::OrthographicStretched => (small_size, UVec2::new(small_width * 4, 0)),
            ExampleViewports::OrthographicMoving => (small_size, UVec2::new(small_width * 5, 0)),
            ExampleViewports::OrthographicControl => {
                (small_wide_size, UVec2::new(small_width * 6, 0))
            }
        };

        viewport.physical_size = size;
        viewport.physical_position = position;
    }
}

#[derive(Component)]
enum ExampleViewports {
    PerspectiveMain,
    PerspectiveStretched,
    PerspectiveMoving,
    PerspectiveControl,
    OrthographicMain,
    OrthographicStretched,
    OrthographicMoving,
    OrthographicControl,
}