diff --git a/crates/bevy_core_pipeline/src/core_2d/main_pass_2d_node.rs b/crates/bevy_core_pipeline/src/core_2d/main_pass_2d_node.rs index 626b022d46ede..a1636b8d02973 100644 --- a/crates/bevy_core_pipeline/src/core_2d/main_pass_2d_node.rs +++ b/crates/bevy_core_pipeline/src/core_2d/main_pass_2d_node.rs @@ -86,7 +86,7 @@ impl Node for MainPass2dNode { let mut draw_functions = draw_functions.write(); let mut tracked_pass = TrackedRenderPass::new(render_pass); if let Some(viewport) = camera.viewport.as_ref() { - tracked_pass.set_camera_viewport(viewport); + tracked_pass.set_camera_viewport(viewport, camera.physical_target_size); } for item in &transparent_phase.items { let draw_function = draw_functions.get_mut(item.draw_function).unwrap(); diff --git a/crates/bevy_core_pipeline/src/core_3d/main_pass_3d_node.rs b/crates/bevy_core_pipeline/src/core_3d/main_pass_3d_node.rs index ec9aa7a6ff884..dbe0e7975696c 100644 --- a/crates/bevy_core_pipeline/src/core_3d/main_pass_3d_node.rs +++ b/crates/bevy_core_pipeline/src/core_3d/main_pass_3d_node.rs @@ -103,7 +103,7 @@ impl Node for MainPass3dNode { let mut draw_functions = draw_functions.write(); let mut tracked_pass = TrackedRenderPass::new(render_pass); if let Some(viewport) = camera.viewport.as_ref() { - tracked_pass.set_camera_viewport(viewport); + tracked_pass.set_camera_viewport(viewport, camera.physical_target_size); } for item in &opaque_phase.items { let draw_function = draw_functions.get_mut(item.draw_function).unwrap(); @@ -142,7 +142,7 @@ impl Node for MainPass3dNode { let mut draw_functions = draw_functions.write(); let mut tracked_pass = TrackedRenderPass::new(render_pass); if let Some(viewport) = camera.viewport.as_ref() { - tracked_pass.set_camera_viewport(viewport); + tracked_pass.set_camera_viewport(viewport, camera.physical_target_size); } for item in &alpha_mask_phase.items { let draw_function = draw_functions.get_mut(item.draw_function).unwrap(); @@ -186,7 +186,7 @@ impl Node for MainPass3dNode { let mut draw_functions = draw_functions.write(); let mut tracked_pass = TrackedRenderPass::new(render_pass); if let Some(viewport) = camera.viewport.as_ref() { - tracked_pass.set_camera_viewport(viewport); + tracked_pass.set_camera_viewport(viewport, camera.physical_target_size); } for item in &transparent_phase.items { let draw_function = draw_functions.get_mut(item.draw_function).unwrap(); diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index e1e91c28486b8..985837678523a 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -36,14 +36,85 @@ use wgpu::Extent3d; pub struct Viewport { /// The physical position to render this viewport to within the [`RenderTarget`] of this [`Camera`]. /// (0,0) corresponds to the top-left corner - pub physical_position: UVec2, + pub physical_position: ViewportPosition, /// The physical size of the viewport rectangle to render to within the [`RenderTarget`] of this [`Camera`]. /// The origin of the rectangle is in the top-left corner. - pub physical_size: UVec2, + pub physical_size: ViewportSize, /// The minimum and maximum depth to render (on a scale from 0.0 to 1.0). pub depth: Range, } +/// An enum that represents viewport's top-left corner position either as absolute value (in pixels) +/// or as a percentage of camera's render target's size. +/// (0,0) corresponds to the top-left corner +#[derive(Reflect, Debug, Clone, Serialize, Deserialize)] +pub enum ViewportPosition { + Absolute(UVec2), + Percentage(Vec2), +} + +impl Default for ViewportPosition { + fn default() -> Self { + Self::Absolute(Default::default()) + } +} + +impl ViewportPosition { + /// Converts this size into a absolute size in pixels. + #[inline] + pub fn as_absolute(&self, of: UVec2) -> UVec2 { + match self { + ViewportPosition::Absolute(v) => *v, + ViewportPosition::Percentage(v) => (of.as_vec2() * *v).as_uvec2(), + } + } + + /// Converts this size into a absolute size in pixels if possible. + /// + /// Returns `None` if self is `Percentage` and argument is `None` + #[inline] + pub fn try_as_absolute(&self, of_opt: Option) -> Option { + match self { + ViewportPosition::Absolute(v) => Some(*v), + ViewportPosition::Percentage(_) => of_opt.map(|of| self.as_absolute(of)), + } + } +} +/// An enum that represents viewport's size either as absolute value (in pixels) or as a percentage of camera's render target's size. +#[derive(Reflect, Debug, Clone, Serialize, Deserialize)] +pub enum ViewportSize { + Absolute(UVec2), + Percentage(Vec2), +} + +impl Default for ViewportSize { + fn default() -> Self { + Self::Absolute(Default::default()) + } +} + +impl ViewportSize { + /// Converts this size into a absolute size in pixels. + #[inline] + pub fn as_absolute(&self, of: UVec2) -> UVec2 { + match self { + ViewportSize::Absolute(v) => *v, + ViewportSize::Percentage(v) => (of.as_vec2() * *v).as_uvec2(), + } + } + + /// Converts this size into a absolute size in pixels if possible. + /// + /// Returns `None` if self is `Percentage` and argument is `None` + #[inline] + pub fn try_as_absolute(&self, of_opt: Option) -> Option { + match self { + ViewportSize::Absolute(v) => Some(*v), + ViewportSize::Percentage(_) => of_opt.map(|of| self.as_absolute(of)), + } + } +} + impl Default for Viewport { fn default() -> Self { Self { @@ -111,16 +182,26 @@ impl Camera { Some((physical_size.as_dvec2() / scale).as_vec2()) } + /// Returns absolute size of this `Camera`'s viewport. + /// + /// I.e. multiplies the percentage size of viewport with `physical_target_size` or uses + /// viewport's absolute size + /// + /// Returns `None` if viewport size is a `Percentage` and [`Camera::physical_target_size`] returns + /// `None` + #[inline] + pub fn viewport_absolute_size(&self) -> Option { + self.viewport + .as_ref() + .and_then(|v| v.physical_size.try_as_absolute(self.physical_target_size())) + } + /// The rendered physical bounds (minimum, maximum) of the camera. If the `viewport` field is /// set to [`Some`], this will be the rect of that custom viewport. Otherwise it will default to /// the full physical rect of the current [`RenderTarget`]. #[inline] pub fn physical_viewport_rect(&self) -> Option<(UVec2, UVec2)> { - let min = self - .viewport - .as_ref() - .map(|v| v.physical_position) - .unwrap_or(UVec2::ZERO); + let min = self.viewport_absolute_size().unwrap_or(UVec2::ZERO); let max = min + self.physical_viewport_size()?; Some((min, max)) } @@ -141,9 +222,8 @@ impl Camera { /// [`RenderTarget`], prefer [`Camera::logical_target_size`]. #[inline] pub fn logical_viewport_size(&self) -> Option { - self.viewport - .as_ref() - .and_then(|v| self.to_logical(v.physical_size)) + self.viewport_absolute_size() + .and_then(|s| self.to_logical(s)) .or_else(|| self.logical_target_size()) } @@ -153,9 +233,7 @@ impl Camera { /// For logic that requires the full physical size of the [`RenderTarget`], prefer [`Camera::physical_target_size`]. #[inline] pub fn physical_viewport_size(&self) -> Option { - self.viewport - .as_ref() - .map(|v| v.physical_size) + self.viewport_absolute_size() .or_else(|| self.physical_target_size()) } diff --git a/crates/bevy_render/src/render_phase/draw_state.rs b/crates/bevy_render/src/render_phase/draw_state.rs index a709c078f921d..2f6cdc5d90d54 100644 --- a/crates/bevy_render/src/render_phase/draw_state.rs +++ b/crates/bevy_render/src/render_phase/draw_state.rs @@ -340,12 +340,28 @@ impl<'a> TrackedRenderPass<'a> { /// Set the rendering viewport to the given [`Camera`](crate::camera::Viewport) [`Viewport`]. /// /// Subsequent draw calls will be projected into that viewport. - pub fn set_camera_viewport(&mut self, viewport: &Viewport) { + pub fn set_camera_viewport( + &mut self, + viewport: &Viewport, + physical_target_size: Option, + ) { + let physical_size = viewport + .physical_size + .try_as_absolute(physical_target_size) + .expect("Couldn't get camera.physical_target_size (needed for relative viewport size)"); + + let physical_position = viewport + .physical_position + .try_as_absolute(physical_target_size) + .expect( + "Couldn't get camera.physical_target_size (needed for relative viewport position)", + ); + self.set_viewport( - viewport.physical_position.x as f32, - viewport.physical_position.y as f32, - viewport.physical_size.x as f32, - viewport.physical_size.y as f32, + physical_position.x as f32, + physical_position.y as f32, + physical_size.x as f32, + physical_size.y as f32, viewport.depth.start, viewport.depth.end, ); diff --git a/examples/3d/split_screen.rs b/examples/3d/split_screen.rs index 70cc464ea2593..ab9f7a3b37e7d 100644 --- a/examples/3d/split_screen.rs +++ b/examples/3d/split_screen.rs @@ -3,7 +3,7 @@ use bevy::{ core_pipeline::clear_color::ClearColorConfig, prelude::*, - render::camera::Viewport, + render::camera::{Viewport, ViewportPosition, ViewportSize}, window::{WindowId, WindowResized}, }; @@ -96,15 +96,20 @@ fn set_camera_viewports( let window = windows.primary(); let mut left_camera = left_camera.single_mut(); left_camera.viewport = Some(Viewport { - physical_position: UVec2::new(0, 0), - physical_size: UVec2::new(window.physical_width() / 2, window.physical_height()), + // You can use absolute pixel values... + physical_position: ViewportPosition::Absolute(UVec2::ZERO), + physical_size: ViewportSize::Absolute(UVec2::new( + window.physical_width() / 2, + window.physical_height(), + )), ..default() }); let mut right_camera = right_camera.single_mut(); right_camera.viewport = Some(Viewport { - physical_position: UVec2::new(window.physical_width() / 2, 0), - physical_size: UVec2::new(window.physical_width() / 2, window.physical_height()), + // ... or specify an adaptive percentage of the render target. + physical_position: ViewportPosition::Percentage(Vec2::new(0.5, 0.0)), + physical_size: ViewportSize::Percentage(Vec2::new(0.5, 1.0)), ..default() }); }