diff --git a/Cargo.toml b/Cargo.toml index ef6cb40a7da7b..bcba436c9dfe0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -165,6 +165,10 @@ path = "examples/2d/texture_atlas.rs" name = "3d_scene" path = "examples/3d/3d_scene.rs" +[[example]] +name = "split_screen_viewport" +path = "examples/3d/split_screen_viewport.rs" + [[example]] name = "lighting" path = "examples/3d/lighting.rs" diff --git a/crates/bevy_core_pipeline/Cargo.toml b/crates/bevy_core_pipeline/Cargo.toml index 1ebec225db6d1..83a53c6d07a01 100644 --- a/crates/bevy_core_pipeline/Cargo.toml +++ b/crates/bevy_core_pipeline/Cargo.toml @@ -19,4 +19,5 @@ bevy_asset = { path = "../bevy_asset", version = "0.6.0" } bevy_core = { path = "../bevy_core", version = "0.6.0" } bevy_ecs = { path = "../bevy_ecs", version = "0.6.0" } bevy_render = { path = "../bevy_render", version = "0.6.0" } - +bevy_math = { path = "../bevy_math", version = "0.6.0" } +wgpu = "0.12" diff --git a/crates/bevy_core_pipeline/src/main_pass_2d.rs b/crates/bevy_core_pipeline/src/main_pass_2d.rs index b3ad74a05927f..c5c12d30f6a56 100644 --- a/crates/bevy_core_pipeline/src/main_pass_2d.rs +++ b/crates/bevy_core_pipeline/src/main_pass_2d.rs @@ -1,5 +1,6 @@ use crate::Transparent2d; use bevy_ecs::prelude::*; +use bevy_math::Vec2; use bevy_render::{ render_graph::{Node, NodeRunError, RenderGraphContext, SlotInfo, SlotType}, render_phase::{DrawFunctions, RenderPhase, TrackedRenderPass}, @@ -9,8 +10,11 @@ use bevy_render::{ }; pub struct MainPass2dNode { - query: - QueryState<(&'static RenderPhase, &'static ViewTarget), With>, + query: QueryState<( + &'static RenderPhase, + &'static ViewTarget, + &'static ExtractedView, + )>, } impl MainPass2dNode { @@ -39,7 +43,7 @@ impl Node for MainPass2dNode { world: &World, ) -> Result<(), NodeRunError> { let view_entity = graph.get_input_entity(Self::IN_VIEW)?; - let (transparent_phase, target) = self + let (transparent_phase, target, view) = self .query .get_manual(world, view_entity) .expect("view entity should exist"); @@ -57,10 +61,24 @@ impl Node for MainPass2dNode { .get_resource::>() .unwrap(); - let render_pass = render_context + let mut render_pass = render_context .command_encoder .begin_render_pass(&pass_descriptor); + if let Some(viewport) = &view.viewport { + let target_size = Vec2::new(view.width as f32, view.height as f32); + let pos = viewport.scaled_pos(target_size); + let size = viewport.scaled_size(target_size); + render_pass.set_viewport( + pos.x, + pos.y, + size.x, + size.y, + viewport.min_depth, + viewport.max_depth, + ); + } + let mut draw_functions = draw_functions.write(); let mut tracked_pass = TrackedRenderPass::new(render_pass); for item in &transparent_phase.items { diff --git a/crates/bevy_core_pipeline/src/main_pass_3d.rs b/crates/bevy_core_pipeline/src/main_pass_3d.rs index ebbb178ac8747..d783453751e6f 100644 --- a/crates/bevy_core_pipeline/src/main_pass_3d.rs +++ b/crates/bevy_core_pipeline/src/main_pass_3d.rs @@ -1,5 +1,6 @@ use crate::{AlphaMask3d, Opaque3d, Transparent3d}; use bevy_ecs::prelude::*; +use bevy_math::Vec2; use bevy_render::{ render_graph::{Node, NodeRunError, RenderGraphContext, SlotInfo, SlotType}, render_phase::{DrawFunctions, RenderPhase, TrackedRenderPass}, @@ -9,16 +10,14 @@ use bevy_render::{ }; pub struct MainPass3dNode { - query: QueryState< - ( - &'static RenderPhase, - &'static RenderPhase, - &'static RenderPhase, - &'static ViewTarget, - &'static ViewDepthTexture, - ), - With, - >, + query: QueryState<( + &'static RenderPhase, + &'static RenderPhase, + &'static RenderPhase, + &'static ViewTarget, + &'static ViewDepthTexture, + &'static ExtractedView, + )>, } impl MainPass3dNode { @@ -47,12 +46,28 @@ impl Node for MainPass3dNode { world: &World, ) -> Result<(), NodeRunError> { let view_entity = graph.get_input_entity(Self::IN_VIEW)?; - let (opaque_phase, alpha_mask_phase, transparent_phase, target, depth) = + let (opaque_phase, alpha_mask_phase, transparent_phase, target, depth, view) = match self.query.get_manual(world, view_entity) { Ok(query) => query, Err(_) => return Ok(()), // No window }; + let set_viewport = |render_pass: &mut wgpu::RenderPass| { + if let Some(viewport) = &view.viewport { + let target_size = Vec2::new(view.width as f32, view.height as f32); + let pos = viewport.scaled_pos(target_size); + let size = viewport.scaled_size(target_size); + render_pass.set_viewport( + pos.x, + pos.y, + size.x, + size.y, + viewport.min_depth, + viewport.max_depth, + ); + } + }; + { // Run the opaque pass, sorted front-to-back // NOTE: Scoped to drop the mutable borrow of render_context @@ -77,9 +92,10 @@ impl Node for MainPass3dNode { let draw_functions = world.get_resource::>().unwrap(); - let render_pass = render_context + let mut render_pass = render_context .command_encoder .begin_render_pass(&pass_descriptor); + set_viewport(&mut render_pass); let mut draw_functions = draw_functions.write(); let mut tracked_pass = TrackedRenderPass::new(render_pass); for item in &opaque_phase.items { @@ -111,9 +127,10 @@ impl Node for MainPass3dNode { let draw_functions = world.get_resource::>().unwrap(); - let render_pass = render_context + let mut render_pass = render_context .command_encoder .begin_render_pass(&pass_descriptor); + set_viewport(&mut render_pass); let mut draw_functions = draw_functions.write(); let mut tracked_pass = TrackedRenderPass::new(render_pass); for item in &alpha_mask_phase.items { @@ -149,9 +166,10 @@ impl Node for MainPass3dNode { .get_resource::>() .unwrap(); - let render_pass = render_context + let mut render_pass = render_context .command_encoder .begin_render_pass(&pass_descriptor); + set_viewport(&mut render_pass); let mut draw_functions = draw_functions.write(); let mut tracked_pass = TrackedRenderPass::new(render_pass); for item in &transparent_phase.items { diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index cc606057f2944..2a0a5e2b1d9c8 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -782,6 +782,7 @@ pub fn prepare_lights( projection: cube_face_projection, near: POINT_LIGHT_NEAR_Z, far: light.range, + viewport: None, }, RenderPhase::::default(), LightEntity::Point { @@ -867,6 +868,7 @@ pub fn prepare_lights( projection, near: light.near, far: light.far, + viewport: None, }, RenderPhase::::default(), LightEntity::Directional { light_entity }, diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index 4f73ebaf90230..133ac0f485ea9 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -3,8 +3,7 @@ use bevy_ecs::{ component::Component, entity::Entity, event::EventReader, - prelude::{DetectChanges, QueryState}, - query::Added, + prelude::{Changed, DetectChanges, QueryState}, reflect::ReflectComponent, system::{QuerySet, Res}, }; @@ -25,6 +24,60 @@ pub struct Camera { pub depth_calculation: DepthCalculation, pub near: f32, pub far: f32, + pub viewport: Option, +} + +#[derive(Debug, Clone, Reflect, Serialize, Deserialize)] +pub struct Viewport { + pub x: f32, + pub y: f32, + pub w: f32, + pub h: f32, + /// In range `0..=1` + pub min_depth: f32, + /// In range `0..=1` + pub max_depth: f32, + + /// Whether `x`, `y`, `w` and `h` should be in range `0..=1` or in pixels + pub scaling_mode: ViewportScalingMode, +} +impl Default for Viewport { + fn default() -> Self { + Self { + x: 0.0, + y: 0.0, + w: 1.0, + h: 1.0, + min_depth: 0.0, + max_depth: 1.0, + scaling_mode: ViewportScalingMode::Normalized, + } + } +} + +impl Viewport { + pub fn scaled_pos(&self, target_size: Vec2) -> Vec2 { + let pos = Vec2::new(self.x, self.y); + match self.scaling_mode { + ViewportScalingMode::Normalized => pos * target_size, + ViewportScalingMode::Pixels => pos, + } + } + pub fn scaled_size(&self, target_size: Vec2) -> Vec2 { + let size = Vec2::new(self.w, self.h); + match self.scaling_mode { + ViewportScalingMode::Normalized => size * target_size, + ViewportScalingMode::Pixels => size, + } + } +} + +#[derive(Debug, Clone, Reflect, Serialize, Deserialize)] +pub enum ViewportScalingMode { + /// `x`, `y`, `w` and `h` are in `0..=1` + Normalized, + /// Pixel units + Pixels, } #[derive(Debug, Clone, Copy, Reflect, Serialize, Deserialize)] @@ -77,7 +130,7 @@ pub fn camera_system( windows: Res, mut queries: QuerySet<( QueryState<(Entity, &mut Camera, &mut T)>, - QueryState>, + QueryState>, )>, ) { let mut changed_window_ids = Vec::new(); @@ -111,7 +164,13 @@ pub fn camera_system( || added_cameras.contains(&entity) || camera_projection.is_changed() { - camera_projection.update(window.width(), window.height()); + let target_size = Vec2::new(window.width(), window.height()); + let size = camera + .viewport + .as_ref() + .map(|viewport| viewport.scaled_size(target_size)) + .unwrap_or(target_size); + camera_projection.update(size.x, size.y); camera.projection_matrix = camera_projection.get_projection_matrix(); camera.depth_calculation = camera_projection.depth_calculation(); } diff --git a/crates/bevy_render/src/camera/mod.rs b/crates/bevy_render/src/camera/mod.rs index 5be7298689707..b258087f15cde 100644 --- a/crates/bevy_render/src/camera/mod.rs +++ b/crates/bevy_render/src/camera/mod.rs @@ -47,11 +47,13 @@ impl Plugin for CameraPlugin { .add_system_to_stage(CoreStage::PostUpdate, crate::camera::active_cameras_system) .add_system_to_stage( CoreStage::PostUpdate, - crate::camera::camera_system::, + crate::camera::camera_system:: + .label(UpdateCameraProjectionSystem), ) .add_system_to_stage( CoreStage::PostUpdate, - crate::camera::camera_system::, + crate::camera::camera_system:: + .label(UpdateCameraProjectionSystem), ); if let Ok(render_app) = app.get_sub_app_mut(RenderApp) { render_app @@ -61,6 +63,9 @@ impl Plugin for CameraPlugin { } } +#[derive(SystemLabel, Debug, PartialEq, Eq, Clone, Hash)] +pub struct UpdateCameraProjectionSystem; + #[derive(Default)] pub struct ExtractedCameraNames { pub entities: HashMap, @@ -98,6 +103,7 @@ fn extract_cameras( height: window.physical_height().max(1), near: camera.near, far: camera.far, + viewport: camera.viewport.clone(), }, visible_entities.clone(), )); diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index abee8bd1aeee0..d9896fca965db 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -9,7 +9,7 @@ use wgpu::{ pub use window::*; use crate::{ - camera::{ExtractedCamera, ExtractedCameraNames}, + camera::{ExtractedCamera, ExtractedCameraNames, Viewport}, render_resource::{std140::AsStd140, DynamicUniformVec, Texture, TextureView}, renderer::{RenderDevice, RenderQueue}, texture::{BevyDefault, TextureCache}, @@ -81,6 +81,7 @@ pub struct ExtractedView { pub height: u32, pub near: f32, pub far: f32, + pub viewport: Option, } #[derive(Clone, AsStd140)] @@ -148,6 +149,7 @@ fn prepare_view_uniforms( let projection = camera.projection; let view = camera.transform.compute_matrix(); let inverse_view = view.inverse(); + let view_uniforms = ViewUniformOffset { offset: view_uniforms.uniforms.push(ViewUniform { view_proj: projection * inverse_view, diff --git a/crates/bevy_ui/src/render/render_pass.rs b/crates/bevy_ui/src/render/render_pass.rs index 831b6dee9f9ae..45fe8a2a18c0a 100644 --- a/crates/bevy_ui/src/render/render_pass.rs +++ b/crates/bevy_ui/src/render/render_pass.rs @@ -3,6 +3,7 @@ use bevy_ecs::{ prelude::*, system::{lifetimeless::*, SystemParamItem}, }; +use bevy_math::Vec2; use bevy_render::{ camera::ExtractedCameraNames, render_graph::*, @@ -35,8 +36,11 @@ impl bevy_render::render_graph::Node for UiPassDriverNode { } pub struct UiPassNode { - query: - QueryState<(&'static RenderPhase, &'static ViewTarget), With>, + query: QueryState<( + &'static RenderPhase, + &'static ViewTarget, + &'static ExtractedView, + )>, } impl UiPassNode { @@ -65,7 +69,7 @@ impl bevy_render::render_graph::Node for UiPassNode { world: &World, ) -> Result<(), NodeRunError> { let view_entity = graph.get_input_entity(Self::IN_VIEW)?; - let (transparent_phase, target) = self + let (transparent_phase, target, view) = self .query .get_manual(world, view_entity) .expect("view entity should exist"); @@ -86,10 +90,24 @@ impl bevy_render::render_graph::Node for UiPassNode { .get_resource::>() .unwrap(); - let render_pass = render_context + let mut render_pass = render_context .command_encoder .begin_render_pass(&pass_descriptor); + if let Some(viewport) = &view.viewport { + let target_size = Vec2::new(view.width as f32, view.height as f32); + let pos = viewport.scaled_pos(target_size); + let size = viewport.scaled_size(target_size); + render_pass.set_viewport( + pos.x, + pos.y, + size.x, + size.y, + viewport.min_depth, + viewport.max_depth, + ); + } + let mut draw_functions = draw_functions.write(); let mut tracked_pass = TrackedRenderPass::new(render_pass); for item in &transparent_phase.items { diff --git a/examples/3d/split_screen_viewport.rs b/examples/3d/split_screen_viewport.rs new file mode 100644 index 0000000000000..6fcd14f147d96 --- /dev/null +++ b/examples/3d/split_screen_viewport.rs @@ -0,0 +1,138 @@ +use bevy::{ + core_pipeline::{draw_3d_graph, AlphaMask3d, Opaque3d, Transparent3d}, + prelude::*, + render::{ + camera::{ActiveCameras, CameraPlugin, ExtractedCameraNames, Viewport}, + render_graph::{self, NodeRunError, RenderGraph, RenderGraphContext, SlotValue}, + render_phase::RenderPhase, + renderer::RenderContext, + RenderApp, RenderStage, + }, +}; + +fn main() { + App::new() + // MSAA doesn't work due to https://github.com/bevyengine/bevy/issues/3499 + .insert_resource(Msaa { samples: 1 }) + .add_plugins(DefaultPlugins) + .add_plugin(SecondaryCamPlugin) + .add_startup_system(setup) + .run(); +} + +/// set up a simple 3D scene +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, + mut active_cameras: ResMut, +) { + // plane + commands.spawn_bundle(PbrBundle { + mesh: meshes.add(Mesh::from(shape::Plane { size: 5.0 })), + material: materials.add(Color::rgb(0.3, 0.5, 0.3).into()), + ..Default::default() + }); + // cube + commands.spawn_bundle(PbrBundle { + mesh: meshes.add(Mesh::from(shape::Cube { size: 1.0 })), + material: materials.add(Color::rgb(0.8, 0.7, 0.6).into()), + transform: Transform::from_xyz(0.0, 0.5, 0.0), + ..Default::default() + }); + // light + commands.spawn_bundle(PointLightBundle { + point_light: PointLight { + intensity: 1500.0, + shadows_enabled: true, + ..Default::default() + }, + transform: Transform::from_xyz(4.0, 8.0, 4.0), + ..Default::default() + }); + + // top camera + commands.spawn_bundle(PerspectiveCameraBundle { + transform: Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y), + camera: Camera { + name: Some(CameraPlugin::CAMERA_3D.to_string()), + viewport: Some(Viewport { + x: 0.0, + y: 0.0, + w: 1.0, + h: 0.5, + ..Default::default() + }), + ..Camera::default() + }, + ..Default::default() + }); + // bottom camera + commands.spawn_bundle(PerspectiveCameraBundle { + transform: Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y), + camera: Camera { + name: Some(SECONDARY_CAMERA_NAME.to_string()), + viewport: Some(Viewport { + x: 0.0, + y: 0.5, + w: 1.0, + h: 0.5, + ..Default::default() + }), + ..Camera::default() + }, + ..Default::default() + }); + active_cameras.add(SECONDARY_CAMERA_NAME); +} + +const SECONDARY_CAMERA_NAME: &str = "Secondary"; +const SECONDARY_PASS_DRIVER: &str = "secondary_pass_driver"; + +struct SecondaryCamPlugin; + +impl Plugin for SecondaryCamPlugin { + fn build(&self, app: &mut App) { + let render_app = app.sub_app_mut(RenderApp); + render_app.add_system_to_stage(RenderStage::Extract, extract_secondary_camera_phases); + let mut graph = render_app.world.get_resource_mut::().unwrap(); + graph.add_node(SECONDARY_PASS_DRIVER, SecondaryCameraDriver); + graph + .add_node_edge( + bevy::core_pipeline::node::MAIN_PASS_DEPENDENCIES, + SECONDARY_PASS_DRIVER, + ) + .unwrap(); + graph + .add_node_edge(SECONDARY_PASS_DRIVER, bevy::ui::node::UI_PASS_DRIVER) + .unwrap(); + } +} + +fn extract_secondary_camera_phases(mut commands: Commands, active_cameras: Res) { + if let Some(secondary) = active_cameras.get(SECONDARY_CAMERA_NAME) { + if let Some(entity) = secondary.entity { + commands.get_or_spawn(entity).insert_bundle(( + RenderPhase::::default(), + RenderPhase::::default(), + RenderPhase::::default(), + )); + } + } +} + +struct SecondaryCameraDriver; +impl render_graph::Node for SecondaryCameraDriver { + fn run( + &self, + graph: &mut RenderGraphContext, + _render_context: &mut RenderContext, + world: &World, + ) -> Result<(), NodeRunError> { + let extracted_cameras = world.get_resource::().unwrap(); + if let Some(camera_3d) = extracted_cameras.entities.get(SECONDARY_CAMERA_NAME) { + graph.run_sub_graph(draw_3d_graph::NAME, vec![SlotValue::Entity(*camera_3d)])?; + } + Ok(()) + } +} diff --git a/examples/README.md b/examples/README.md index aeaff8c7845f2..81fb9b7a35da0 100644 --- a/examples/README.md +++ b/examples/README.md @@ -100,6 +100,7 @@ Example | File | Description Example | File | Description --- | --- | --- `3d_scene` | [`3d/3d_scene.rs`](./3d/3d_scene.rs) | Simple 3D scene with basic shapes and lighting +`split_screen_viewport` | [`3d/split_screen_viewport.rs`](./3d/split_screen_viewport.rs) | 3D scene drawn twice on top of each other `lighting` | [`3d/lighting.rs`](./3d/lighting.rs) | Illustrates various lighting options in a simple scene `load_gltf` | [`3d/load_gltf.rs`](./3d/load_gltf.rs) | Loads and renders a gltf file as a scene `many_cubes` | [`3d/many_cubes.rs`](./3d/many_cubes.rs) | Simple benchmark to test per-entity draw overhead