Skip to content

Commit 5f8fe74

Browse files
committed
Camera Driven Viewports (#4898)
# 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(); ```
1 parent 8e08e26 commit 5f8fe74

File tree

11 files changed

+358
-97
lines changed

11 files changed

+358
-97
lines changed

Cargo.toml

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,10 @@ path = "examples/2d/texture_atlas.rs"
176176
name = "3d_scene"
177177
path = "examples/3d/3d_scene.rs"
178178

179+
[[example]]
180+
name = "3d_shapes"
181+
path = "examples/3d/shapes.rs"
182+
179183
[[example]]
180184
name = "lighting"
181185
path = "examples/3d/lighting.rs"
@@ -208,10 +212,6 @@ path = "examples/3d/render_to_texture.rs"
208212
name = "shadow_biases"
209213
path = "examples/3d/shadow_biases.rs"
210214

211-
[[example]]
212-
name = "3d_shapes"
213-
path = "examples/3d/shapes.rs"
214-
215215
[[example]]
216216
name = "shadow_caster_receiver"
217217
path = "examples/3d/shadow_caster_receiver.rs"
@@ -220,6 +220,10 @@ path = "examples/3d/shadow_caster_receiver.rs"
220220
name = "spherical_area_lights"
221221
path = "examples/3d/spherical_area_lights.rs"
222222

223+
[[example]]
224+
name = "split_screen"
225+
path = "examples/3d/split_screen.rs"
226+
223227
[[example]]
224228
name = "texture"
225229
path = "examples/3d/texture.rs"

crates/bevy_core_pipeline/src/core_2d/main_pass_2d_node.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use crate::{
44
};
55
use bevy_ecs::prelude::*;
66
use bevy_render::{
7+
camera::ExtractedCamera,
78
render_graph::{Node, NodeRunError, RenderGraphContext, SlotInfo, SlotType},
89
render_phase::{DrawFunctions, RenderPhase, TrackedRenderPass},
910
render_resource::{LoadOp, Operations, RenderPassDescriptor},
@@ -14,6 +15,7 @@ use bevy_render::{
1415
pub struct MainPass2dNode {
1516
query: QueryState<
1617
(
18+
&'static ExtractedCamera,
1719
&'static RenderPhase<Transparent2d>,
1820
&'static ViewTarget,
1921
&'static Camera2d,
@@ -48,7 +50,7 @@ impl Node for MainPass2dNode {
4850
world: &World,
4951
) -> Result<(), NodeRunError> {
5052
let view_entity = graph.get_input_entity(Self::IN_VIEW)?;
51-
let (transparent_phase, target, camera_2d) =
53+
let (camera, transparent_phase, target, camera_2d) =
5254
if let Ok(result) = self.query.get_manual(world, view_entity) {
5355
result
5456
} else {
@@ -79,6 +81,9 @@ impl Node for MainPass2dNode {
7981

8082
let mut draw_functions = draw_functions.write();
8183
let mut tracked_pass = TrackedRenderPass::new(render_pass);
84+
if let Some(viewport) = camera.viewport.as_ref() {
85+
tracked_pass.set_camera_viewport(viewport);
86+
}
8287
for item in &transparent_phase.items {
8388
let draw_function = draw_functions.get_mut(item.draw_function).unwrap();
8489
draw_function.draw(world, &mut tracked_pass, view_entity, item);

crates/bevy_core_pipeline/src/core_3d/main_pass_3d_node.rs

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ use crate::{
44
};
55
use bevy_ecs::prelude::*;
66
use bevy_render::{
7+
camera::ExtractedCamera,
78
render_graph::{Node, NodeRunError, RenderGraphContext, SlotInfo, SlotType},
89
render_phase::{DrawFunctions, RenderPhase, TrackedRenderPass},
910
render_resource::{LoadOp, Operations, RenderPassDepthStencilAttachment, RenderPassDescriptor},
@@ -16,6 +17,7 @@ use bevy_utils::tracing::info_span;
1617
pub struct MainPass3dNode {
1718
query: QueryState<
1819
(
20+
&'static ExtractedCamera,
1921
&'static RenderPhase<Opaque3d>,
2022
&'static RenderPhase<AlphaMask3d>,
2123
&'static RenderPhase<Transparent3d>,
@@ -53,7 +55,7 @@ impl Node for MainPass3dNode {
5355
world: &World,
5456
) -> Result<(), NodeRunError> {
5557
let view_entity = graph.get_input_entity(Self::IN_VIEW)?;
56-
let (opaque_phase, alpha_mask_phase, transparent_phase, camera_3d, target, depth) =
58+
let (camera, opaque_phase, alpha_mask_phase, transparent_phase, camera_3d, target, depth) =
5759
match self.query.get_manual(world, view_entity) {
5860
Ok(query) => query,
5961
Err(_) => {
@@ -100,6 +102,9 @@ impl Node for MainPass3dNode {
100102
.begin_render_pass(&pass_descriptor);
101103
let mut draw_functions = draw_functions.write();
102104
let mut tracked_pass = TrackedRenderPass::new(render_pass);
105+
if let Some(viewport) = camera.viewport.as_ref() {
106+
tracked_pass.set_camera_viewport(viewport);
107+
}
103108
for item in &opaque_phase.items {
104109
let draw_function = draw_functions.get_mut(item.draw_function).unwrap();
105110
draw_function.draw(world, &mut tracked_pass, view_entity, item);
@@ -136,6 +141,9 @@ impl Node for MainPass3dNode {
136141
.begin_render_pass(&pass_descriptor);
137142
let mut draw_functions = draw_functions.write();
138143
let mut tracked_pass = TrackedRenderPass::new(render_pass);
144+
if let Some(viewport) = camera.viewport.as_ref() {
145+
tracked_pass.set_camera_viewport(viewport);
146+
}
139147
for item in &alpha_mask_phase.items {
140148
let draw_function = draw_functions.get_mut(item.draw_function).unwrap();
141149
draw_function.draw(world, &mut tracked_pass, view_entity, item);
@@ -177,6 +185,9 @@ impl Node for MainPass3dNode {
177185
.begin_render_pass(&pass_descriptor);
178186
let mut draw_functions = draw_functions.write();
179187
let mut tracked_pass = TrackedRenderPass::new(render_pass);
188+
if let Some(viewport) = camera.viewport.as_ref() {
189+
tracked_pass.set_camera_viewport(viewport);
190+
}
180191
for item in &transparent_phase.items {
181192
let draw_function = draw_functions.get_mut(item.draw_function).unwrap();
182193
draw_function.draw(world, &mut tracked_pass, view_entity, item);

crates/bevy_core_pipeline/src/core_3d/mod.rs

Lines changed: 32 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ use bevy_render::{
3131
},
3232
renderer::RenderDevice,
3333
texture::TextureCache,
34-
view::{ExtractedView, ViewDepthTexture},
34+
view::ViewDepthTexture,
3535
RenderApp, RenderStage,
3636
};
3737
use bevy_utils::{FloatOrd, HashMap};
@@ -53,7 +53,7 @@ impl Plugin for Core3dPlugin {
5353
.init_resource::<DrawFunctions<AlphaMask3d>>()
5454
.init_resource::<DrawFunctions<Transparent3d>>()
5555
.add_system_to_stage(RenderStage::Extract, extract_core_3d_camera_phases)
56-
.add_system_to_stage(RenderStage::Prepare, prepare_core_3d_views_system)
56+
.add_system_to_stage(RenderStage::Prepare, prepare_core_3d_depth_textures)
5757
.add_system_to_stage(RenderStage::PhaseSort, sort_phase_system::<Opaque3d>)
5858
.add_system_to_stage(RenderStage::PhaseSort, sort_phase_system::<AlphaMask3d>)
5959
.add_system_to_stage(RenderStage::PhaseSort, sort_phase_system::<Transparent3d>);
@@ -199,13 +199,13 @@ pub fn extract_core_3d_camera_phases(
199199
}
200200
}
201201

202-
pub fn prepare_core_3d_views_system(
202+
pub fn prepare_core_3d_depth_textures(
203203
mut commands: Commands,
204204
mut texture_cache: ResMut<TextureCache>,
205205
msaa: Res<Msaa>,
206206
render_device: Res<RenderDevice>,
207207
views_3d: Query<
208-
(Entity, &ExtractedView, Option<&ExtractedCamera>),
208+
(Entity, &ExtractedCamera),
209209
(
210210
With<RenderPhase<Opaque3d>>,
211211
With<RenderPhase<AlphaMask3d>>,
@@ -214,37 +214,34 @@ pub fn prepare_core_3d_views_system(
214214
>,
215215
) {
216216
let mut textures = HashMap::default();
217-
for (entity, view, camera) in views_3d.iter() {
218-
let mut get_cached_texture = || {
219-
texture_cache.get(
220-
&render_device,
221-
TextureDescriptor {
222-
label: Some("view_depth_texture"),
223-
size: Extent3d {
224-
depth_or_array_layers: 1,
225-
width: view.width as u32,
226-
height: view.height as u32,
227-
},
228-
mip_level_count: 1,
229-
sample_count: msaa.samples,
230-
dimension: TextureDimension::D2,
231-
format: TextureFormat::Depth32Float, /* PERF: vulkan docs recommend using 24
232-
* bit depth for better performance */
233-
usage: TextureUsages::RENDER_ATTACHMENT,
234-
},
235-
)
236-
};
237-
let cached_texture = if let Some(camera) = camera {
238-
textures
217+
for (entity, camera) in views_3d.iter() {
218+
if let Some(physical_target_size) = camera.physical_target_size {
219+
let cached_texture = textures
239220
.entry(camera.target.clone())
240-
.or_insert_with(get_cached_texture)
241-
.clone()
242-
} else {
243-
get_cached_texture()
244-
};
245-
commands.entity(entity).insert(ViewDepthTexture {
246-
texture: cached_texture.texture,
247-
view: cached_texture.default_view,
248-
});
221+
.or_insert_with(|| {
222+
texture_cache.get(
223+
&render_device,
224+
TextureDescriptor {
225+
label: Some("view_depth_texture"),
226+
size: Extent3d {
227+
depth_or_array_layers: 1,
228+
width: physical_target_size.x,
229+
height: physical_target_size.y,
230+
},
231+
mip_level_count: 1,
232+
sample_count: msaa.samples,
233+
dimension: TextureDimension::D2,
234+
format: TextureFormat::Depth32Float, /* PERF: vulkan docs recommend using 24
235+
* bit depth for better performance */
236+
usage: TextureUsages::RENDER_ATTACHMENT,
237+
},
238+
)
239+
})
240+
.clone();
241+
commands.entity(entity).insert(ViewDepthTexture {
242+
texture: cached_texture.texture,
243+
view: cached_texture.default_view,
244+
});
245+
}
249246
}
250247
}

crates/bevy_pbr/src/light.rs

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -736,7 +736,7 @@ pub(crate) fn assign_lights_to_clusters(
736736
continue;
737737
}
738738

739-
let screen_size = if let Some(screen_size) = camera.physical_target_size {
739+
let screen_size = if let Some(screen_size) = camera.physical_viewport_size() {
740740
screen_size
741741
} else {
742742
clusters.clear();
@@ -747,7 +747,7 @@ pub(crate) fn assign_lights_to_clusters(
747747

748748
let view_transform = camera_transform.compute_matrix();
749749
let inverse_view_transform = view_transform.inverse();
750-
let is_orthographic = camera.projection_matrix.w_axis.w == 1.0;
750+
let is_orthographic = camera.projection_matrix().w_axis.w == 1.0;
751751

752752
let far_z = match config.far_z_mode() {
753753
ClusterFarZMode::MaxLightRange => {
@@ -772,7 +772,7 @@ pub(crate) fn assign_lights_to_clusters(
772772
// 3,2 = r * far and 2,2 = r where r = 1.0 / (far - near)
773773
// rearranging r = 1.0 / (far - near), r * (far - near) = 1.0, r * far - 1.0 = r * near, near = (r * far - 1.0) / r
774774
// = (3,2 - 1.0) / 2,2
775-
(camera.projection_matrix.w_axis.z - 1.0) / camera.projection_matrix.z_axis.z
775+
(camera.projection_matrix().w_axis.z - 1.0) / camera.projection_matrix().z_axis.z
776776
}
777777
(false, 1) => config.first_slice_depth().max(far_z),
778778
_ => config.first_slice_depth(),
@@ -804,7 +804,7 @@ pub(crate) fn assign_lights_to_clusters(
804804
// it can overestimate more significantly when light ranges are only partially in view
805805
let (light_aabb_min, light_aabb_max) = cluster_space_light_aabb(
806806
inverse_view_transform,
807-
camera.projection_matrix,
807+
camera.projection_matrix(),
808808
&light_sphere,
809809
);
810810

@@ -871,7 +871,7 @@ pub(crate) fn assign_lights_to_clusters(
871871
clusters.dimensions.x * clusters.dimensions.y * clusters.dimensions.z <= 4096
872872
);
873873

874-
let inverse_projection = camera.projection_matrix.inverse();
874+
let inverse_projection = camera.projection_matrix().inverse();
875875

876876
for lights in &mut clusters.lights {
877877
lights.entities.clear();
@@ -958,7 +958,7 @@ pub(crate) fn assign_lights_to_clusters(
958958
let (light_aabb_xy_ndc_z_view_min, light_aabb_xy_ndc_z_view_max) =
959959
cluster_space_light_aabb(
960960
inverse_view_transform,
961-
camera.projection_matrix,
961+
camera.projection_matrix(),
962962
&light_sphere,
963963
);
964964

@@ -991,7 +991,7 @@ pub(crate) fn assign_lights_to_clusters(
991991
radius: light_sphere.radius,
992992
};
993993
let light_center_clip =
994-
camera.projection_matrix * view_light_sphere.center.extend(1.0);
994+
camera.projection_matrix() * view_light_sphere.center.extend(1.0);
995995
let light_center_ndc = light_center_clip.xyz() / light_center_clip.w;
996996
let cluster_coordinates = ndc_position_to_cluster(
997997
clusters.dimensions,

0 commit comments

Comments
 (0)