Skip to content

Commit

Permalink
Camera Driven Viewports (#4898)
Browse files Browse the repository at this point in the history
# Objective

Users should be able to render cameras to specific areas of a render target, which enables scenarios like split screen, minimaps, etc.

Builds on the new Camera Driven Rendering added here: #4745 
Fixes: #202
Alternative to #1389 and #3626 (which are incompatible with the new Camera Driven Rendering)

## Solution

![image](https://user-images.githubusercontent.com/2694663/171560044-f0694f67-0cd9-4598-83e2-a9658c4fed57.png)


Cameras can now configure an optional "viewport", which defines a rectangle within their render target to draw to. If a `Viewport` is defined, the camera's `CameraProjection`, `View`, and visibility calculations will use the viewport configuration instead of the full render target. 

```rust
// This camera will render to the first half of the primary window (on the left side).
commands.spawn_bundle(Camera3dBundle {
    camera: Camera {
        viewport: Some(Viewport {
            physical_position: UVec2::new(0, 0),
            physical_size: UVec2::new(window.physical_width() / 2, window.physical_height()),
            depth: 0.0..1.0,
        }),
        ..default()
    },
    ..default()
});
```

To account for this, the `Camera` component has received a few adjustments:

* `Camera` now has some new getter functions:
  * `logical_viewport_size`, `physical_viewport_size`, `logical_target_size`, `physical_target_size`, `projection_matrix`
*  All computed camera values are now private and live on the `ComputedCameraValues` field (logical/physical width/height, the projection matrix). They are now exposed on `Camera` via getters/setters  This wasn't _needed_ for viewports, but it was long overdue.

---

## Changelog

### Added

* `Camera` components now have a `viewport` field, which can be set to draw to a portion of a render target instead of the full target.
* `Camera` component has some new functions: `logical_viewport_size`, `physical_viewport_size`, `logical_target_size`, `physical_target_size`, and `projection_matrix`
* Added a new split_screen example illustrating how to render two cameras to the same scene

## Migration Guide

`Camera::projection_matrix` is no longer a public field. Use the new `Camera::projection_matrix()` method instead:

```rust

// Bevy 0.7
let projection = camera.projection_matrix;

// Bevy 0.8
let projection = camera.projection_matrix();
```
  • Loading branch information
cart committed Jun 4, 2022
1 parent 8e08e26 commit f37ff34
Show file tree
Hide file tree
Showing 11 changed files with 355 additions and 97 deletions.
12 changes: 8 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,10 @@ path = "examples/2d/texture_atlas.rs"
name = "3d_scene"
path = "examples/3d/3d_scene.rs"

[[example]]
name = "3d_shapes"
path = "examples/3d/shapes.rs"

[[example]]
name = "lighting"
path = "examples/3d/lighting.rs"
Expand Down Expand Up @@ -208,10 +212,6 @@ path = "examples/3d/render_to_texture.rs"
name = "shadow_biases"
path = "examples/3d/shadow_biases.rs"

[[example]]
name = "3d_shapes"
path = "examples/3d/shapes.rs"

[[example]]
name = "shadow_caster_receiver"
path = "examples/3d/shadow_caster_receiver.rs"
Expand All @@ -220,6 +220,10 @@ path = "examples/3d/shadow_caster_receiver.rs"
name = "spherical_area_lights"
path = "examples/3d/spherical_area_lights.rs"

[[example]]
name = "split_screen"
path = "examples/3d/split_screen.rs"

[[example]]
name = "texture"
path = "examples/3d/texture.rs"
Expand Down
7 changes: 6 additions & 1 deletion crates/bevy_core_pipeline/src/core_2d/main_pass_2d_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use crate::{
};
use bevy_ecs::prelude::*;
use bevy_render::{
camera::ExtractedCamera,
render_graph::{Node, NodeRunError, RenderGraphContext, SlotInfo, SlotType},
render_phase::{DrawFunctions, RenderPhase, TrackedRenderPass},
render_resource::{LoadOp, Operations, RenderPassDescriptor},
Expand All @@ -14,6 +15,7 @@ use bevy_render::{
pub struct MainPass2dNode {
query: QueryState<
(
&'static ExtractedCamera,
&'static RenderPhase<Transparent2d>,
&'static ViewTarget,
&'static Camera2d,
Expand Down Expand Up @@ -48,7 +50,7 @@ impl Node for MainPass2dNode {
world: &World,
) -> Result<(), NodeRunError> {
let view_entity = graph.get_input_entity(Self::IN_VIEW)?;
let (transparent_phase, target, camera_2d) =
let (camera, transparent_phase, target, camera_2d) =
if let Ok(result) = self.query.get_manual(world, view_entity) {
result
} else {
Expand Down Expand Up @@ -79,6 +81,9 @@ 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);
}
for item in &transparent_phase.items {
let draw_function = draw_functions.get_mut(item.draw_function).unwrap();
draw_function.draw(world, &mut tracked_pass, view_entity, item);
Expand Down
13 changes: 12 additions & 1 deletion crates/bevy_core_pipeline/src/core_3d/main_pass_3d_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use crate::{
};
use bevy_ecs::prelude::*;
use bevy_render::{
camera::ExtractedCamera,
render_graph::{Node, NodeRunError, RenderGraphContext, SlotInfo, SlotType},
render_phase::{DrawFunctions, RenderPhase, TrackedRenderPass},
render_resource::{LoadOp, Operations, RenderPassDepthStencilAttachment, RenderPassDescriptor},
Expand All @@ -16,6 +17,7 @@ use bevy_utils::tracing::info_span;
pub struct MainPass3dNode {
query: QueryState<
(
&'static ExtractedCamera,
&'static RenderPhase<Opaque3d>,
&'static RenderPhase<AlphaMask3d>,
&'static RenderPhase<Transparent3d>,
Expand Down Expand Up @@ -53,7 +55,7 @@ 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, camera_3d, target, depth) =
let (camera, opaque_phase, alpha_mask_phase, transparent_phase, camera_3d, target, depth) =
match self.query.get_manual(world, view_entity) {
Ok(query) => query,
Err(_) => {
Expand Down Expand Up @@ -100,6 +102,9 @@ impl Node for MainPass3dNode {
.begin_render_pass(&pass_descriptor);
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);
}
for item in &opaque_phase.items {
let draw_function = draw_functions.get_mut(item.draw_function).unwrap();
draw_function.draw(world, &mut tracked_pass, view_entity, item);
Expand Down Expand Up @@ -136,6 +141,9 @@ impl Node for MainPass3dNode {
.begin_render_pass(&pass_descriptor);
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);
}
for item in &alpha_mask_phase.items {
let draw_function = draw_functions.get_mut(item.draw_function).unwrap();
draw_function.draw(world, &mut tracked_pass, view_entity, item);
Expand Down Expand Up @@ -177,6 +185,9 @@ impl Node for MainPass3dNode {
.begin_render_pass(&pass_descriptor);
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);
}
for item in &transparent_phase.items {
let draw_function = draw_functions.get_mut(item.draw_function).unwrap();
draw_function.draw(world, &mut tracked_pass, view_entity, item);
Expand Down
67 changes: 32 additions & 35 deletions crates/bevy_core_pipeline/src/core_3d/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ use bevy_render::{
},
renderer::RenderDevice,
texture::TextureCache,
view::{ExtractedView, ViewDepthTexture},
view::ViewDepthTexture,
RenderApp, RenderStage,
};
use bevy_utils::{FloatOrd, HashMap};
Expand All @@ -53,7 +53,7 @@ impl Plugin for Core3dPlugin {
.init_resource::<DrawFunctions<AlphaMask3d>>()
.init_resource::<DrawFunctions<Transparent3d>>()
.add_system_to_stage(RenderStage::Extract, extract_core_3d_camera_phases)
.add_system_to_stage(RenderStage::Prepare, prepare_core_3d_views_system)
.add_system_to_stage(RenderStage::Prepare, prepare_core_3d_depth_textures)
.add_system_to_stage(RenderStage::PhaseSort, sort_phase_system::<Opaque3d>)
.add_system_to_stage(RenderStage::PhaseSort, sort_phase_system::<AlphaMask3d>)
.add_system_to_stage(RenderStage::PhaseSort, sort_phase_system::<Transparent3d>);
Expand Down Expand Up @@ -199,13 +199,13 @@ pub fn extract_core_3d_camera_phases(
}
}

pub fn prepare_core_3d_views_system(
pub fn prepare_core_3d_depth_textures(
mut commands: Commands,
mut texture_cache: ResMut<TextureCache>,
msaa: Res<Msaa>,
render_device: Res<RenderDevice>,
views_3d: Query<
(Entity, &ExtractedView, Option<&ExtractedCamera>),
(Entity, &ExtractedCamera),
(
With<RenderPhase<Opaque3d>>,
With<RenderPhase<AlphaMask3d>>,
Expand All @@ -214,37 +214,34 @@ pub fn prepare_core_3d_views_system(
>,
) {
let mut textures = HashMap::default();
for (entity, view, camera) in views_3d.iter() {
let mut get_cached_texture = || {
texture_cache.get(
&render_device,
TextureDescriptor {
label: Some("view_depth_texture"),
size: Extent3d {
depth_or_array_layers: 1,
width: view.width as u32,
height: view.height as u32,
},
mip_level_count: 1,
sample_count: msaa.samples,
dimension: TextureDimension::D2,
format: TextureFormat::Depth32Float, /* PERF: vulkan docs recommend using 24
* bit depth for better performance */
usage: TextureUsages::RENDER_ATTACHMENT,
},
)
};
let cached_texture = if let Some(camera) = camera {
textures
for (entity, camera) in views_3d.iter() {
if let Some(physical_target_size) = camera.physical_target_size {
let cached_texture = textures
.entry(camera.target.clone())
.or_insert_with(get_cached_texture)
.clone()
} else {
get_cached_texture()
};
commands.entity(entity).insert(ViewDepthTexture {
texture: cached_texture.texture,
view: cached_texture.default_view,
});
.or_insert_with(|| {
texture_cache.get(
&render_device,
TextureDescriptor {
label: Some("view_depth_texture"),
size: Extent3d {
depth_or_array_layers: 1,
width: physical_target_size.x,
height: physical_target_size.y,
},
mip_level_count: 1,
sample_count: msaa.samples,
dimension: TextureDimension::D2,
format: TextureFormat::Depth32Float, /* PERF: vulkan docs recommend using 24
* bit depth for better performance */
usage: TextureUsages::RENDER_ATTACHMENT,
},
)
})
.clone();
commands.entity(entity).insert(ViewDepthTexture {
texture: cached_texture.texture,
view: cached_texture.default_view,
});
}
}
}
14 changes: 7 additions & 7 deletions crates/bevy_pbr/src/light.rs
Original file line number Diff line number Diff line change
Expand Up @@ -736,7 +736,7 @@ pub(crate) fn assign_lights_to_clusters(
continue;
}

let screen_size = if let Some(screen_size) = camera.physical_target_size {
let screen_size = if let Some(screen_size) = camera.physical_viewport_size() {
screen_size
} else {
clusters.clear();
Expand All @@ -747,7 +747,7 @@ pub(crate) fn assign_lights_to_clusters(

let view_transform = camera_transform.compute_matrix();
let inverse_view_transform = view_transform.inverse();
let is_orthographic = camera.projection_matrix.w_axis.w == 1.0;
let is_orthographic = camera.projection_matrix().w_axis.w == 1.0;

let far_z = match config.far_z_mode() {
ClusterFarZMode::MaxLightRange => {
Expand All @@ -772,7 +772,7 @@ pub(crate) fn assign_lights_to_clusters(
// 3,2 = r * far and 2,2 = r where r = 1.0 / (far - near)
// rearranging r = 1.0 / (far - near), r * (far - near) = 1.0, r * far - 1.0 = r * near, near = (r * far - 1.0) / r
// = (3,2 - 1.0) / 2,2
(camera.projection_matrix.w_axis.z - 1.0) / camera.projection_matrix.z_axis.z
(camera.projection_matrix().w_axis.z - 1.0) / camera.projection_matrix().z_axis.z
}
(false, 1) => config.first_slice_depth().max(far_z),
_ => config.first_slice_depth(),
Expand Down Expand Up @@ -804,7 +804,7 @@ pub(crate) fn assign_lights_to_clusters(
// it can overestimate more significantly when light ranges are only partially in view
let (light_aabb_min, light_aabb_max) = cluster_space_light_aabb(
inverse_view_transform,
camera.projection_matrix,
camera.projection_matrix(),
&light_sphere,
);

Expand Down Expand Up @@ -871,7 +871,7 @@ pub(crate) fn assign_lights_to_clusters(
clusters.dimensions.x * clusters.dimensions.y * clusters.dimensions.z <= 4096
);

let inverse_projection = camera.projection_matrix.inverse();
let inverse_projection = camera.projection_matrix().inverse();

for lights in &mut clusters.lights {
lights.entities.clear();
Expand Down Expand Up @@ -958,7 +958,7 @@ pub(crate) fn assign_lights_to_clusters(
let (light_aabb_xy_ndc_z_view_min, light_aabb_xy_ndc_z_view_max) =
cluster_space_light_aabb(
inverse_view_transform,
camera.projection_matrix,
camera.projection_matrix(),
&light_sphere,
);

Expand Down Expand Up @@ -991,7 +991,7 @@ pub(crate) fn assign_lights_to_clusters(
radius: light_sphere.radius,
};
let light_center_clip =
camera.projection_matrix * view_light_sphere.center.extend(1.0);
camera.projection_matrix() * view_light_sphere.center.extend(1.0);
let light_center_ndc = light_center_clip.xyz() / light_center_clip.w;
let cluster_coordinates = ndc_position_to_cluster(
clusters.dimensions,
Expand Down
Loading

0 comments on commit f37ff34

Please sign in to comment.