diff --git a/Cargo.toml b/Cargo.toml index 9602ef33fbb58..6e30a32d8a31b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3952,13 +3952,13 @@ category = "3D Rendering" wasm = false [[example]] -name = "camera_sub_view" -path = "examples/3d/camera_sub_view.rs" +name = "camera_crop" +path = "examples/3d/camera_crop.rs" doc-scrape-examples = true -[package.metadata.example.camera_sub_view] +[package.metadata.example.camera_crop] name = "Camera sub view" -description = "Demonstrates using different sub view effects on a camera" +description = "Demonstrates using different camera crop effects" category = "3D Rendering" wasm = true diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index 2828486fd4d68..8eb5851386719 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -6,7 +6,7 @@ use super::{ClearColorConfig, Projection}; use crate::{ batching::gpu_preprocessing::{GpuPreprocessingMode, GpuPreprocessingSupport}, camera::{CameraProjection, ManualTextureViewHandle, ManualTextureViews}, - primitives::Frustum, + primitives::{Frustum, SubRect}, render_asset::RenderAssets, render_graph::{InternedRenderSubGraph, RenderSubGraph}, render_resource::TextureView, @@ -112,55 +112,6 @@ impl Viewport { } } -/// Settings to define a camera sub view. -/// -/// When [`Camera::sub_camera_view`] is `Some`, only the sub-section of the -/// image defined by `size` and `offset` (relative to the `full_size` of the -/// whole image) is projected to the cameras viewport. -/// -/// Take the example of the following multi-monitor setup: -/// ```css -/// ┌───┬───┐ -/// │ A │ B │ -/// ├───┼───┤ -/// │ C │ D │ -/// └───┴───┘ -/// ``` -/// If each monitor is 1920x1080, the whole image will have a resolution of -/// 3840x2160. For each monitor we can use a single camera with a viewport of -/// the same size as the monitor it corresponds to. To ensure that the image is -/// cohesive, we can use a different sub view on each camera: -/// - Camera A: `full_size` = 3840x2160, `size` = 1920x1080, `offset` = 0,0 -/// - Camera B: `full_size` = 3840x2160, `size` = 1920x1080, `offset` = 1920,0 -/// - Camera C: `full_size` = 3840x2160, `size` = 1920x1080, `offset` = 0,1080 -/// - Camera D: `full_size` = 3840x2160, `size` = 1920x1080, `offset` = -/// 1920,1080 -/// -/// However since only the ratio between the values is important, they could all -/// be divided by 120 and still produce the same image. Camera D would for -/// example have the following values: -/// `full_size` = 32x18, `size` = 16x9, `offset` = 16,9 -#[derive(Debug, Clone, Copy, Reflect, PartialEq)] -#[reflect(Clone, PartialEq, Default)] -pub struct SubCameraView { - /// Size of the entire camera view - pub full_size: UVec2, - /// Offset of the sub camera - pub offset: Vec2, - /// Size of the sub camera - pub size: UVec2, -} - -impl Default for SubCameraView { - fn default() -> Self { - Self { - full_size: UVec2::new(1, 1), - offset: Vec2::new(0., 0.), - size: UVec2::new(1, 1), - } - } -} - /// Information about the current [`RenderTarget`]. #[derive(Default, Debug, Clone)] pub struct RenderTargetInfo { @@ -180,7 +131,7 @@ pub struct ComputedCameraValues { target_info: Option, // size of the `Viewport` old_viewport_size: Option, - old_sub_camera_view: Option, + old_crop: Option, } /// How much energy a `Camera3d` absorbs from incoming light. @@ -367,8 +318,9 @@ pub struct Camera { pub msaa_writeback: bool, /// The clear color operation to perform on the render target. pub clear_color: ClearColorConfig, - /// If set, this camera will be a sub camera of a large view, defined by a [`SubCameraView`]. - pub sub_camera_view: Option, + /// If set, this camera will still render to its entire viewport, but its projection will + /// adjust to only render the specified [`SubRect`] of the total view. + pub crop: Option, } fn warn_on_no_render_graph(world: DeferredWorld, HookContext { entity, caller, .. }: HookContext) { @@ -388,7 +340,7 @@ impl Default for Camera { output_mode: Default::default(), msaa_writeback: true, clear_color: Default::default(), - sub_camera_view: None, + crop: None, } } } @@ -988,7 +940,7 @@ pub fn camera_system( || camera.is_added() || camera_projection.is_changed() || camera.computed.old_viewport_size != viewport_size - || camera.computed.old_sub_camera_view != camera.sub_camera_view + || camera.computed.old_crop != camera.crop { let new_computed_target_info = normalized_target.get_render_target_info( windows, @@ -1034,7 +986,7 @@ pub fn camera_system( if let Some(size) = camera.logical_viewport_size() { if size.x != 0.0 && size.y != 0.0 { camera_projection.update(size.x, size.y); - camera.computed.clip_from_view = match &camera.sub_camera_view { + camera.computed.clip_from_view = match &camera.crop { Some(sub_view) => { camera_projection.get_clip_from_view_for_sub(sub_view) } @@ -1049,8 +1001,8 @@ pub fn camera_system( camera.computed.old_viewport_size = viewport_size; } - if camera.computed.old_sub_camera_view != camera.sub_camera_view { - camera.computed.old_sub_camera_view = camera.sub_camera_view; + if camera.computed.old_crop != camera.crop { + camera.computed.old_crop = camera.crop; } } } diff --git a/crates/bevy_render/src/camera/projection.rs b/crates/bevy_render/src/camera/projection.rs index a7796a1d1ad07..ae5e9ef2f70d9 100644 --- a/crates/bevy_render/src/camera/projection.rs +++ b/crates/bevy_render/src/camera/projection.rs @@ -1,6 +1,9 @@ use core::fmt::Debug; -use crate::{primitives::Frustum, view::VisibilitySystems}; +use crate::{ + primitives::{Frustum, SubRect}, + view::VisibilitySystems, +}; use bevy_app::{App, Plugin, PostStartup, PostUpdate}; use bevy_asset::AssetEventSystems; use bevy_derive::{Deref, DerefMut}; @@ -69,8 +72,8 @@ pub trait CameraProjection { /// Generate the projection matrix. fn get_clip_from_view(&self) -> Mat4; - /// Generate the projection matrix for a [`SubCameraView`](super::SubCameraView). - fn get_clip_from_view_for_sub(&self, sub_view: &super::SubCameraView) -> Mat4; + /// Generate the projection matrix for a cropped camera view. See [`Camera::crop`](super::Camera::crop). + fn get_clip_from_view_for_sub(&self, sub_rect: &SubRect) -> Mat4; /// When the area this camera renders to changes dimensions, this method will be automatically /// called. Use this to update any projection properties that depend on the aspect ratio or @@ -261,7 +264,7 @@ impl CameraProjection for Projection { } } - fn get_clip_from_view_for_sub(&self, sub_view: &super::SubCameraView) -> Mat4 { + fn get_clip_from_view_for_sub(&self, sub_view: &SubRect) -> Mat4 { match self { Projection::Perspective(projection) => projection.get_clip_from_view_for_sub(sub_view), Projection::Orthographic(projection) => projection.get_clip_from_view_for_sub(sub_view), @@ -337,14 +340,14 @@ impl CameraProjection for PerspectiveProjection { Mat4::perspective_infinite_reverse_rh(self.fov, self.aspect_ratio, self.near) } - fn get_clip_from_view_for_sub(&self, sub_view: &super::SubCameraView) -> Mat4 { - let full_width = sub_view.full_size.x as f32; - let full_height = sub_view.full_size.y as f32; - let sub_width = sub_view.size.x as f32; - let sub_height = sub_view.size.y as f32; - let offset_x = sub_view.offset.x; + fn get_clip_from_view_for_sub(&self, sub_rect: &SubRect) -> Mat4 { + let full_width = sub_rect.full_size.x as f32; + let full_height = sub_rect.full_size.y as f32; + let sub_width = sub_rect.size.x as f32; + let sub_height = sub_rect.size.y as f32; + let offset_x = sub_rect.offset.x; // Y-axis increases from top to bottom - let offset_y = full_height - (sub_view.offset.y + sub_height); + let offset_y = full_height - (sub_rect.offset.y + sub_height); let full_aspect = full_width / full_height; @@ -565,13 +568,13 @@ impl CameraProjection for OrthographicProjection { ) } - fn get_clip_from_view_for_sub(&self, sub_view: &super::SubCameraView) -> Mat4 { - let full_width = sub_view.full_size.x as f32; - let full_height = sub_view.full_size.y as f32; - let offset_x = sub_view.offset.x; - let offset_y = sub_view.offset.y; - let sub_width = sub_view.size.x as f32; - let sub_height = sub_view.size.y as f32; + fn get_clip_from_view_for_sub(&self, sub_rect: &SubRect) -> Mat4 { + let full_width = sub_rect.full_size.x as f32; + let full_height = sub_rect.full_size.y as f32; + let offset_x = sub_rect.offset.x; + let offset_y = sub_rect.offset.y; + let sub_width = sub_rect.size.x as f32; + let sub_height = sub_rect.size.y as f32; let full_aspect = full_width / full_height; diff --git a/crates/bevy_render/src/primitives/mod.rs b/crates/bevy_render/src/primitives/mod.rs index ca664fc338c77..f1d4d4752031c 100644 --- a/crates/bevy_render/src/primitives/mod.rs +++ b/crates/bevy_render/src/primitives/mod.rs @@ -1,7 +1,9 @@ use core::borrow::Borrow; use bevy_ecs::{component::Component, entity::EntityHashMap, reflect::ReflectComponent}; -use bevy_math::{Affine3A, Mat3A, Mat4, Vec3, Vec3A, Vec4, Vec4Swizzles}; +use bevy_math::{ + Affine3A, CompassOctant, Mat3A, Mat4, URect, UVec2, Vec2, Vec3, Vec3A, Vec4, Vec4Swizzles, +}; use bevy_reflect::prelude::*; /// An axis-aligned bounding box, defined by: @@ -138,6 +140,221 @@ impl Sphere { } } +/// A proportionally-sized "sub-rectangle". +/// +/// When [`Camera::crop`](crate::camera::Camera::crop) is `Some`, only the sub-section of the +/// image defined by `size` and `offset` (relative to the `full_size` of the +/// whole image) is projected to the cameras viewport. +/// +/// Take the example of the following multi-monitor setup: +/// ```css +/// ┌───┬───┐ +/// │ A │ B │ +/// ├───┼───┤ +/// │ C │ D │ +/// └───┴───┘ +/// ``` +/// If each monitor is 1920x1080, the whole image will have a resolution of +/// 3840x2160. For each monitor we can use a single camera with a viewport of +/// the same size as the monitor it corresponds to. To ensure that the image is +/// cohesive, we can set a different crop rectangle on each camera: +/// - Camera A: `full_size` = 3840x2160, `size` = 1920x1080, `offset` = 0,0 +/// - Camera B: `full_size` = 3840x2160, `size` = 1920x1080, `offset` = 1920,0 +/// - Camera C: `full_size` = 3840x2160, `size` = 1920x1080, `offset` = 0,1080 +/// - Camera D: `full_size` = 3840x2160, `size` = 1920x1080, `offset` = +/// 1920,1080 +/// +/// However since only the ratio between the values is important, they could all +/// be divided by 120 and still produce the same image--[`SubRect::reduced`] +/// does this. Camera D would for example have the following values: +/// `full_size` = 32x18, `size` = 16x9, `offset` = 16,9 +#[derive(Copy, Clone, PartialEq, Debug, Reflect)] +pub struct SubRect { + /// Size of the whole rectantle + pub full_size: UVec2, + /// Size of the sub-rectangle. + pub size: UVec2, + /// Offset of the sub-rectangle from the top-left. + pub offset: Vec2, +} + +impl Default for SubRect { + #[inline] + fn default() -> Self { + Self { + full_size: UVec2::ONE, + size: UVec2::ONE, + offset: Vec2::ZERO, + } + } +} + +impl SubRect { + /// Returns a `SubRect` representing either a quadrant or half of the full view, + /// depending on the value of `oct`. For each of the cardinal directions, this + /// will return the corresponding half, while for each of the intercardinal (NE, NW, SE, SW) + /// directions, this will return the corresponding quadrant. + pub fn octant(oct: CompassOctant) -> Self { + let size = match oct { + CompassOctant::NorthEast + | CompassOctant::NorthWest + | CompassOctant::SouthEast + | CompassOctant::SouthWest => UVec2::splat(1), + CompassOctant::North | CompassOctant::South => UVec2::new(2, 1), + CompassOctant::East | CompassOctant::West => UVec2::new(1, 2), + }; + + let offset = match oct { + CompassOctant::NorthWest | CompassOctant::North | CompassOctant::West => { + Vec2::splat(0.) + } + CompassOctant::NorthEast | CompassOctant::East => Vec2::new(1., 0.), + CompassOctant::SouthWest | CompassOctant::South => Vec2::new(0., 1.), + CompassOctant::SouthEast => Vec2::splat(1.), + }; + + Self { + full_size: UVec2::splat(2), + size, + offset, + } + } + + /// Returns this [`SubRect`] with a new value for `full_size` + #[inline] + pub fn with_full_size(mut self, full_size: UVec2) -> Self { + self.full_size = full_size; + self + } + + /// Returns this [`SubRect`] with a new value for `size` + #[inline] + pub fn with_size(mut self, size: UVec2) -> Self { + self.size = size; + self + } + + /// Returns this [`SubRect`] with a new value for `offset` + #[inline] + pub fn with_offset(mut self, offset: Vec2) -> Self { + self.offset = offset; + self + } + + /// Returns this [`SubRect`] but with all extraneous factors removed + pub fn reduced(mut self) -> Self { + let size_gcd = UVec2 { + x: ops::gcd(self.full_size.x, self.size.x), + y: ops::gcd(self.full_size.y, self.size.y), + }; + + self.full_size /= size_gcd; + self.offset /= size_gcd.as_vec2(); + self.size /= size_gcd; + self + } + + /// Returns this [`SubRect`] scaled to a new full size. + /// + /// Returns Ok if the conversion is lossless (i.e. doesn't cause a change + /// to the relative `size`), and returns Err otherwise with the closest + /// possible approximation + pub fn scaled_to(self, full_size: UVec2) -> Result { + let rough = self.scaled_roughly_to(full_size); + + let num = full_size; + let denom = self.full_size; + if ((self.size * num) % denom).cmpeq(UVec2::ZERO).all() { + Ok(rough) + } else { + Err(rough) + } + } + + /// Returns this [`SubRect`] scaled to a new full size. + /// + /// Unlike [`Self::scaled_to`], this method does not check if the + /// result is lossless, so there might be a change to the relative + /// `size` + pub fn scaled_roughly_to(self, full_size: UVec2) -> Self { + let num = full_size; + let denom = self.full_size; + + Self { + full_size, + size: self.size * num / denom, + offset: self.offset * num.as_vec2() / denom.as_vec2(), + } + } + + /// Returns this [`SubRect`] centered in the full rectangle + #[inline] + pub fn centered(self) -> Self { + self.with_offset((self.full_size - self.size).as_vec2() / 2.) + } + + // Returns the inverse of this [`SubRect`]. + #[inline] + pub fn inverted(self) -> Self { + Self { + full_size: self.size, + offset: -self.offset, + size: self.full_size, + } + } + + #[inline] + pub fn as_urect(self) -> URect { + let offset = self.offset.as_uvec2(); + URect { + min: offset, + max: self.size + offset, + } + } + + #[inline] + pub fn from_urect(full_size: UVec2, rect: URect) -> Self { + Self { + full_size, + offset: rect.min.as_vec2(), + size: rect.max - rect.min, + } + } +} + +mod ops { + // implementations copied from `num` crate, though since they're standard algorithms (and + // fairly small snippets) do we still need to credit? + + /// Calculates the Greatest Common Divisor (GCD) of the number and `other` + #[inline] + pub fn gcd(mut a: u32, mut b: u32) -> u32 { + // Use Stein's algorithm + if a == 0 || b == 0 { + return 0; + } + + // find common factors of 2 + let shift = (a | b).trailing_zeros(); + + // divide n and m by 2 until odd + a >>= a.trailing_zeros(); + b >>= b.trailing_zeros(); + + while a != b { + if a > b { + a -= b; + a >>= a.trailing_zeros(); + } else { + b -= a; + b >>= b.trailing_zeros(); + } + } + + a << shift + } +} + /// A region of 3D space, specifically an open set whose border is a bisecting 2D plane. /// /// This bisecting plane partitions 3D space into two infinite regions, @@ -623,4 +840,129 @@ mod tests { ); assert!(!frustum.contains_aabb(&aabb, &model)); } + + #[test] + fn sub_rect_centered() { + let top_left = SubRect::octant(CompassOctant::NorthWest); + let right = SubRect::octant(CompassOctant::East); + + assert_eq!( + top_left.centered(), + SubRect { + offset: Vec2::splat(0.5), + ..top_left + } + ); + + assert_eq!( + right.centered(), + SubRect { + offset: Vec2::new(0.5, 0.0), + ..right + } + ); + } + + #[test] + fn sub_rect_reduced() { + let reducible_same_factor = SubRect { + full_size: UVec2::new(200, 160), + size: UVec2::new(50, 40), + offset: Vec2::ZERO, + }; + + let reducible_diff_factor = SubRect { + full_size: UVec2::new(80, 160), + size: UVec2::new(30, 40), + offset: Vec2::ZERO, + }; + + let irreducible = SubRect { + full_size: UVec2::new(17, 5), + size: UVec2::new(4, 3), + offset: Vec2::ZERO, + }; + + assert_eq!( + reducible_same_factor.reduced(), + SubRect { + full_size: UVec2::splat(4), + size: UVec2::splat(1), + offset: Vec2::ZERO + } + ); + + assert_eq!( + reducible_diff_factor.reduced(), + SubRect { + full_size: UVec2::new(8, 4), + size: UVec2::new(3, 1), + offset: Vec2::ZERO + } + ); + + assert_eq!(irreducible.reduced(), irreducible); + } + + #[test] + fn sub_rect_scaled_to() { + let top_left = SubRect::octant(CompassOctant::NorthWest); + + assert_eq!( + top_left.scaled_to(UVec2::splat(200)), + Ok(SubRect { + full_size: UVec2::splat(200), + size: UVec2::splat(100), + offset: Vec2::ZERO, + }), + ); + + assert_eq!( + top_left.scaled_to(UVec2::new(1920, 1080)), + Ok(SubRect { + full_size: UVec2::new(1920, 1080), + size: UVec2::new(960, 540), + offset: Vec2::ZERO, + }), + ); + + // Don't need to guarantee exact error values, as long as they're approximately correct + assert!(top_left.scaled_to(UVec2::new(100, 99)).is_err()); + assert!(top_left.scaled_to(UVec2::new(11, 11)).is_err()); + } + + #[test] + fn sub_rect_inverse() { + let rects = [ + SubRect::default(), + SubRect::octant(CompassOctant::SouthEast), + SubRect::octant(CompassOctant::SouthEast) + .scaled_to(UVec2::new(184, 240)) + .unwrap(), + SubRect { + full_size: UVec2::new(1740, 1800), + size: UVec2::splat(200), + offset: Vec2::splat(100.), + }, + SubRect { + full_size: UVec2::new(203, 160), + size: UVec2::new(1, 28), + offset: Vec2::new(170., 100.), + }, + SubRect { + full_size: UVec2::new(10, 8202742), + size: UVec2::new(10, 10000), + offset: Vec2::splat(0.), + }, + SubRect { + full_size: UVec2::splat(180), + size: UVec2::splat(179), + offset: Vec2::splat(1.), + }, + ]; + + for r in rects { + assert_eq!(r.inverted().inverted(), r); + } + } } diff --git a/examples/3d/camera_sub_view.rs b/examples/3d/camera_crop.rs similarity index 87% rename from examples/3d/camera_sub_view.rs rename to examples/3d/camera_crop.rs index 0fea1633d0455..3bb0d3e5adb23 100644 --- a/examples/3d/camera_sub_view.rs +++ b/examples/3d/camera_crop.rs @@ -1,14 +1,17 @@ -//! Demonstrates different sub view effects. +//! Demonstrates different camera crop effects. //! -//! A sub view is essentially a smaller section of a larger viewport. Some use -//! cases include: +//! When camera cropping is enabled, a (usually smaller) area of the scene will +//! be projected to render to the whole 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 +//! - Magnify a section of the image, by rendering a small cropped view in another //! camera -//! - Rapidly change the sub view offset to get a screen shake effect +//! - Rapidly change the cropping offset to get a screen shake effect use bevy::{ prelude::*, - render::camera::{ScalingMode, SubCameraView, Viewport}, + render::{ + camera::{ScalingMode, Viewport}, + primitives::SubRect, + }, }; fn main() { @@ -54,7 +57,7 @@ fn setup( // Main perspective camera: // - // The main perspective image to use as a comparison for the sub views. + // The main perspective image to use as a comparison for the cropped views. commands.spawn(( Camera3d::default(), Camera::default(), @@ -67,13 +70,13 @@ fn setup( // 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. + // shown. Since the viewport has an aspect ratio of 1x1 and the cropped + // 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 { + crop: Some(SubRect { // 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. @@ -100,7 +103,7 @@ fn setup( commands.spawn(( Camera3d::default(), Camera { - sub_camera_view: Some(SubCameraView { + crop: Some(SubRect { full_size: UVec2::new(500, 500), offset: Vec2::ZERO, size: UVec2::new(100, 100), @@ -116,14 +119,14 @@ fn setup( // 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 cropped view (2x1) is different to the aspect ratio of the full view + // (2x2). The aspect ratio of the cropped views 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 { + crop: Some(SubRect { full_size: UVec2::new(800, 800), offset: Vec2::ZERO, size: UVec2::new(800, 400), @@ -137,7 +140,7 @@ fn setup( // Main orthographic camera: // - // The main orthographic image to use as a comparison for the sub views. + // The main orthographic image to use as a comparison for the cropped views. commands.spawn(( Camera3d::default(), Projection::from(OrthographicProjection { @@ -158,7 +161,7 @@ fn setup( // // 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 + // Since the viewport has an aspect ratio of 1x1 and the cropped view has an // aspect ratio of 1x2, the image appears stretched along the horizontal axis. commands.spawn(( Camera3d::default(), @@ -169,7 +172,7 @@ fn setup( ..OrthographicProjection::default_3d() }), Camera { - sub_camera_view: Some(SubCameraView { + crop: Some(SubRect { full_size: UVec2::new(2, 2), offset: Vec2::ZERO, size: UVec2::new(1, 2), @@ -197,7 +200,7 @@ fn setup( ..OrthographicProjection::default_3d() }), Camera { - sub_camera_view: Some(SubCameraView { + crop: Some(SubRect { full_size: UVec2::new(500, 500), offset: Vec2::ZERO, size: UVec2::new(100, 100), @@ -213,8 +216,8 @@ fn setup( // 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 cropped view (2x1) is different to the aspect ratio of the full view + // (2x2). The aspect ratio of the cropped 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(( @@ -226,7 +229,7 @@ fn setup( ..OrthographicProjection::default_3d() }), Camera { - sub_camera_view: Some(SubCameraView { + crop: Some(SubRect { full_size: UVec2::new(200, 200), offset: Vec2::ZERO, size: UVec2::new(200, 100), @@ -244,9 +247,9 @@ fn move_camera_view( time: Res