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  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(); ```
This commit is contained in:
parent
8e08e26c25
commit
5e2cfb2f19
12
Cargo.toml
12
Cargo.toml
@ -176,6 +176,10 @@ path = "examples/2d/texture_atlas.rs"
|
|||||||
name = "3d_scene"
|
name = "3d_scene"
|
||||||
path = "examples/3d/3d_scene.rs"
|
path = "examples/3d/3d_scene.rs"
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "3d_shapes"
|
||||||
|
path = "examples/3d/shapes.rs"
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "lighting"
|
name = "lighting"
|
||||||
path = "examples/3d/lighting.rs"
|
path = "examples/3d/lighting.rs"
|
||||||
@ -208,10 +212,6 @@ path = "examples/3d/render_to_texture.rs"
|
|||||||
name = "shadow_biases"
|
name = "shadow_biases"
|
||||||
path = "examples/3d/shadow_biases.rs"
|
path = "examples/3d/shadow_biases.rs"
|
||||||
|
|
||||||
[[example]]
|
|
||||||
name = "3d_shapes"
|
|
||||||
path = "examples/3d/shapes.rs"
|
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "shadow_caster_receiver"
|
name = "shadow_caster_receiver"
|
||||||
path = "examples/3d/shadow_caster_receiver.rs"
|
path = "examples/3d/shadow_caster_receiver.rs"
|
||||||
@ -220,6 +220,10 @@ path = "examples/3d/shadow_caster_receiver.rs"
|
|||||||
name = "spherical_area_lights"
|
name = "spherical_area_lights"
|
||||||
path = "examples/3d/spherical_area_lights.rs"
|
path = "examples/3d/spherical_area_lights.rs"
|
||||||
|
|
||||||
|
[[example]]
|
||||||
|
name = "split_screen"
|
||||||
|
path = "examples/3d/split_screen.rs"
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "texture"
|
name = "texture"
|
||||||
path = "examples/3d/texture.rs"
|
path = "examples/3d/texture.rs"
|
||||||
|
@ -4,6 +4,7 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use bevy_ecs::prelude::*;
|
use bevy_ecs::prelude::*;
|
||||||
use bevy_render::{
|
use bevy_render::{
|
||||||
|
camera::ExtractedCamera,
|
||||||
render_graph::{Node, NodeRunError, RenderGraphContext, SlotInfo, SlotType},
|
render_graph::{Node, NodeRunError, RenderGraphContext, SlotInfo, SlotType},
|
||||||
render_phase::{DrawFunctions, RenderPhase, TrackedRenderPass},
|
render_phase::{DrawFunctions, RenderPhase, TrackedRenderPass},
|
||||||
render_resource::{LoadOp, Operations, RenderPassDescriptor},
|
render_resource::{LoadOp, Operations, RenderPassDescriptor},
|
||||||
@ -14,6 +15,7 @@ use bevy_render::{
|
|||||||
pub struct MainPass2dNode {
|
pub struct MainPass2dNode {
|
||||||
query: QueryState<
|
query: QueryState<
|
||||||
(
|
(
|
||||||
|
&'static ExtractedCamera,
|
||||||
&'static RenderPhase<Transparent2d>,
|
&'static RenderPhase<Transparent2d>,
|
||||||
&'static ViewTarget,
|
&'static ViewTarget,
|
||||||
&'static Camera2d,
|
&'static Camera2d,
|
||||||
@ -48,7 +50,7 @@ impl Node for MainPass2dNode {
|
|||||||
world: &World,
|
world: &World,
|
||||||
) -> Result<(), NodeRunError> {
|
) -> Result<(), NodeRunError> {
|
||||||
let view_entity = graph.get_input_entity(Self::IN_VIEW)?;
|
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) {
|
if let Ok(result) = self.query.get_manual(world, view_entity) {
|
||||||
result
|
result
|
||||||
} else {
|
} else {
|
||||||
@ -79,6 +81,9 @@ impl Node for MainPass2dNode {
|
|||||||
|
|
||||||
let mut draw_functions = draw_functions.write();
|
let mut draw_functions = draw_functions.write();
|
||||||
let mut tracked_pass = TrackedRenderPass::new(render_pass);
|
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 {
|
for item in &transparent_phase.items {
|
||||||
let draw_function = draw_functions.get_mut(item.draw_function).unwrap();
|
let draw_function = draw_functions.get_mut(item.draw_function).unwrap();
|
||||||
draw_function.draw(world, &mut tracked_pass, view_entity, item);
|
draw_function.draw(world, &mut tracked_pass, view_entity, item);
|
||||||
|
@ -4,6 +4,7 @@ use crate::{
|
|||||||
};
|
};
|
||||||
use bevy_ecs::prelude::*;
|
use bevy_ecs::prelude::*;
|
||||||
use bevy_render::{
|
use bevy_render::{
|
||||||
|
camera::ExtractedCamera,
|
||||||
render_graph::{Node, NodeRunError, RenderGraphContext, SlotInfo, SlotType},
|
render_graph::{Node, NodeRunError, RenderGraphContext, SlotInfo, SlotType},
|
||||||
render_phase::{DrawFunctions, RenderPhase, TrackedRenderPass},
|
render_phase::{DrawFunctions, RenderPhase, TrackedRenderPass},
|
||||||
render_resource::{LoadOp, Operations, RenderPassDepthStencilAttachment, RenderPassDescriptor},
|
render_resource::{LoadOp, Operations, RenderPassDepthStencilAttachment, RenderPassDescriptor},
|
||||||
@ -16,6 +17,7 @@ use bevy_utils::tracing::info_span;
|
|||||||
pub struct MainPass3dNode {
|
pub struct MainPass3dNode {
|
||||||
query: QueryState<
|
query: QueryState<
|
||||||
(
|
(
|
||||||
|
&'static ExtractedCamera,
|
||||||
&'static RenderPhase<Opaque3d>,
|
&'static RenderPhase<Opaque3d>,
|
||||||
&'static RenderPhase<AlphaMask3d>,
|
&'static RenderPhase<AlphaMask3d>,
|
||||||
&'static RenderPhase<Transparent3d>,
|
&'static RenderPhase<Transparent3d>,
|
||||||
@ -53,7 +55,7 @@ impl Node for MainPass3dNode {
|
|||||||
world: &World,
|
world: &World,
|
||||||
) -> Result<(), NodeRunError> {
|
) -> Result<(), NodeRunError> {
|
||||||
let view_entity = graph.get_input_entity(Self::IN_VIEW)?;
|
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) {
|
match self.query.get_manual(world, view_entity) {
|
||||||
Ok(query) => query,
|
Ok(query) => query,
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
@ -100,6 +102,9 @@ impl Node for MainPass3dNode {
|
|||||||
.begin_render_pass(&pass_descriptor);
|
.begin_render_pass(&pass_descriptor);
|
||||||
let mut draw_functions = draw_functions.write();
|
let mut draw_functions = draw_functions.write();
|
||||||
let mut tracked_pass = TrackedRenderPass::new(render_pass);
|
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 {
|
for item in &opaque_phase.items {
|
||||||
let draw_function = draw_functions.get_mut(item.draw_function).unwrap();
|
let draw_function = draw_functions.get_mut(item.draw_function).unwrap();
|
||||||
draw_function.draw(world, &mut tracked_pass, view_entity, item);
|
draw_function.draw(world, &mut tracked_pass, view_entity, item);
|
||||||
@ -136,6 +141,9 @@ impl Node for MainPass3dNode {
|
|||||||
.begin_render_pass(&pass_descriptor);
|
.begin_render_pass(&pass_descriptor);
|
||||||
let mut draw_functions = draw_functions.write();
|
let mut draw_functions = draw_functions.write();
|
||||||
let mut tracked_pass = TrackedRenderPass::new(render_pass);
|
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 {
|
for item in &alpha_mask_phase.items {
|
||||||
let draw_function = draw_functions.get_mut(item.draw_function).unwrap();
|
let draw_function = draw_functions.get_mut(item.draw_function).unwrap();
|
||||||
draw_function.draw(world, &mut tracked_pass, view_entity, item);
|
draw_function.draw(world, &mut tracked_pass, view_entity, item);
|
||||||
@ -177,6 +185,9 @@ impl Node for MainPass3dNode {
|
|||||||
.begin_render_pass(&pass_descriptor);
|
.begin_render_pass(&pass_descriptor);
|
||||||
let mut draw_functions = draw_functions.write();
|
let mut draw_functions = draw_functions.write();
|
||||||
let mut tracked_pass = TrackedRenderPass::new(render_pass);
|
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 {
|
for item in &transparent_phase.items {
|
||||||
let draw_function = draw_functions.get_mut(item.draw_function).unwrap();
|
let draw_function = draw_functions.get_mut(item.draw_function).unwrap();
|
||||||
draw_function.draw(world, &mut tracked_pass, view_entity, item);
|
draw_function.draw(world, &mut tracked_pass, view_entity, item);
|
||||||
|
@ -31,7 +31,7 @@ use bevy_render::{
|
|||||||
},
|
},
|
||||||
renderer::RenderDevice,
|
renderer::RenderDevice,
|
||||||
texture::TextureCache,
|
texture::TextureCache,
|
||||||
view::{ExtractedView, ViewDepthTexture},
|
view::ViewDepthTexture,
|
||||||
RenderApp, RenderStage,
|
RenderApp, RenderStage,
|
||||||
};
|
};
|
||||||
use bevy_utils::{FloatOrd, HashMap};
|
use bevy_utils::{FloatOrd, HashMap};
|
||||||
@ -53,7 +53,7 @@ impl Plugin for Core3dPlugin {
|
|||||||
.init_resource::<DrawFunctions<AlphaMask3d>>()
|
.init_resource::<DrawFunctions<AlphaMask3d>>()
|
||||||
.init_resource::<DrawFunctions<Transparent3d>>()
|
.init_resource::<DrawFunctions<Transparent3d>>()
|
||||||
.add_system_to_stage(RenderStage::Extract, extract_core_3d_camera_phases)
|
.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::<Opaque3d>)
|
||||||
.add_system_to_stage(RenderStage::PhaseSort, sort_phase_system::<AlphaMask3d>)
|
.add_system_to_stage(RenderStage::PhaseSort, sort_phase_system::<AlphaMask3d>)
|
||||||
.add_system_to_stage(RenderStage::PhaseSort, sort_phase_system::<Transparent3d>);
|
.add_system_to_stage(RenderStage::PhaseSort, sort_phase_system::<Transparent3d>);
|
||||||
@ -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 commands: Commands,
|
||||||
mut texture_cache: ResMut<TextureCache>,
|
mut texture_cache: ResMut<TextureCache>,
|
||||||
msaa: Res<Msaa>,
|
msaa: Res<Msaa>,
|
||||||
render_device: Res<RenderDevice>,
|
render_device: Res<RenderDevice>,
|
||||||
views_3d: Query<
|
views_3d: Query<
|
||||||
(Entity, &ExtractedView, Option<&ExtractedCamera>),
|
(Entity, &ExtractedCamera),
|
||||||
(
|
(
|
||||||
With<RenderPhase<Opaque3d>>,
|
With<RenderPhase<Opaque3d>>,
|
||||||
With<RenderPhase<AlphaMask3d>>,
|
With<RenderPhase<AlphaMask3d>>,
|
||||||
@ -214,37 +214,34 @@ pub fn prepare_core_3d_views_system(
|
|||||||
>,
|
>,
|
||||||
) {
|
) {
|
||||||
let mut textures = HashMap::default();
|
let mut textures = HashMap::default();
|
||||||
for (entity, view, camera) in views_3d.iter() {
|
for (entity, camera) in views_3d.iter() {
|
||||||
let mut get_cached_texture = || {
|
if let Some(physical_target_size) = camera.physical_target_size {
|
||||||
texture_cache.get(
|
let cached_texture = textures
|
||||||
&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
|
|
||||||
.entry(camera.target.clone())
|
.entry(camera.target.clone())
|
||||||
.or_insert_with(get_cached_texture)
|
.or_insert_with(|| {
|
||||||
.clone()
|
texture_cache.get(
|
||||||
} else {
|
&render_device,
|
||||||
get_cached_texture()
|
TextureDescriptor {
|
||||||
};
|
label: Some("view_depth_texture"),
|
||||||
commands.entity(entity).insert(ViewDepthTexture {
|
size: Extent3d {
|
||||||
texture: cached_texture.texture,
|
depth_or_array_layers: 1,
|
||||||
view: cached_texture.default_view,
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -736,7 +736,7 @@ pub(crate) fn assign_lights_to_clusters(
|
|||||||
continue;
|
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
|
screen_size
|
||||||
} else {
|
} else {
|
||||||
clusters.clear();
|
clusters.clear();
|
||||||
@ -747,7 +747,7 @@ pub(crate) fn assign_lights_to_clusters(
|
|||||||
|
|
||||||
let view_transform = camera_transform.compute_matrix();
|
let view_transform = camera_transform.compute_matrix();
|
||||||
let inverse_view_transform = view_transform.inverse();
|
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() {
|
let far_z = match config.far_z_mode() {
|
||||||
ClusterFarZMode::MaxLightRange => {
|
ClusterFarZMode::MaxLightRange => {
|
||||||
@ -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)
|
// 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
|
// 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
|
// = (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),
|
(false, 1) => config.first_slice_depth().max(far_z),
|
||||||
_ => config.first_slice_depth(),
|
_ => config.first_slice_depth(),
|
||||||
@ -804,7 +804,7 @@ pub(crate) fn assign_lights_to_clusters(
|
|||||||
// it can overestimate more significantly when light ranges are only partially in view
|
// it can overestimate more significantly when light ranges are only partially in view
|
||||||
let (light_aabb_min, light_aabb_max) = cluster_space_light_aabb(
|
let (light_aabb_min, light_aabb_max) = cluster_space_light_aabb(
|
||||||
inverse_view_transform,
|
inverse_view_transform,
|
||||||
camera.projection_matrix,
|
camera.projection_matrix(),
|
||||||
&light_sphere,
|
&light_sphere,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -871,7 +871,7 @@ pub(crate) fn assign_lights_to_clusters(
|
|||||||
clusters.dimensions.x * clusters.dimensions.y * clusters.dimensions.z <= 4096
|
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 {
|
for lights in &mut clusters.lights {
|
||||||
lights.entities.clear();
|
lights.entities.clear();
|
||||||
@ -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) =
|
let (light_aabb_xy_ndc_z_view_min, light_aabb_xy_ndc_z_view_max) =
|
||||||
cluster_space_light_aabb(
|
cluster_space_light_aabb(
|
||||||
inverse_view_transform,
|
inverse_view_transform,
|
||||||
camera.projection_matrix,
|
camera.projection_matrix(),
|
||||||
&light_sphere,
|
&light_sphere,
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -991,7 +991,7 @@ pub(crate) fn assign_lights_to_clusters(
|
|||||||
radius: light_sphere.radius,
|
radius: light_sphere.radius,
|
||||||
};
|
};
|
||||||
let light_center_clip =
|
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 light_center_ndc = light_center_clip.xyz() / light_center_clip.w;
|
||||||
let cluster_coordinates = ndc_position_to_cluster(
|
let cluster_coordinates = ndc_position_to_cluster(
|
||||||
clusters.dimensions,
|
clusters.dimensions,
|
||||||
|
@ -22,21 +22,72 @@ use bevy_transform::components::GlobalTransform;
|
|||||||
use bevy_utils::HashSet;
|
use bevy_utils::HashSet;
|
||||||
use bevy_window::{WindowCreated, WindowId, WindowResized, Windows};
|
use bevy_window::{WindowCreated, WindowId, WindowResized, Windows};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::borrow::Cow;
|
use std::{borrow::Cow, ops::Range};
|
||||||
use wgpu::Extent3d;
|
use wgpu::Extent3d;
|
||||||
|
|
||||||
|
/// Render viewport configuration for the [`Camera`] component.
|
||||||
|
///
|
||||||
|
/// The viewport defines the area on the render target to which the camera renders its image.
|
||||||
|
/// You can overlay multiple cameras in a single window using viewports to create effects like
|
||||||
|
/// split screen, minimaps, and character viewers.
|
||||||
|
// TODO: remove reflect_value when possible
|
||||||
|
#[derive(Reflect, Debug, Clone, Serialize, Deserialize)]
|
||||||
|
#[reflect_value(Default, Serialize, Deserialize)]
|
||||||
|
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,
|
||||||
|
/// 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,
|
||||||
|
/// The minimum and maximum depth to render (on a scale from 0.0 to 1.0).
|
||||||
|
pub depth: Range<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Viewport {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
physical_position: Default::default(),
|
||||||
|
physical_size: Default::default(),
|
||||||
|
depth: 0.0..1.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Information about the current [`RenderTarget`].
|
||||||
|
#[derive(Default, Debug, Clone)]
|
||||||
|
pub struct RenderTargetInfo {
|
||||||
|
/// The physical size of this render target (ignores scale factor).
|
||||||
|
pub physical_size: UVec2,
|
||||||
|
/// The scale factor of this render target.
|
||||||
|
pub scale_factor: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Holds internally computed [`Camera`] values.
|
||||||
|
#[derive(Default, Debug, Clone)]
|
||||||
|
pub struct ComputedCameraValues {
|
||||||
|
projection_matrix: Mat4,
|
||||||
|
target_info: Option<RenderTargetInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Component, Debug, Reflect, Clone)]
|
#[derive(Component, Debug, Reflect, Clone)]
|
||||||
#[reflect(Component)]
|
#[reflect(Component)]
|
||||||
pub struct Camera {
|
pub struct Camera {
|
||||||
pub projection_matrix: Mat4,
|
/// If set, this camera will render to the given [`Viewport`] rectangle within the configured [`RenderTarget`].
|
||||||
pub logical_target_size: Option<Vec2>,
|
pub viewport: Option<Viewport>,
|
||||||
pub physical_target_size: Option<UVec2>,
|
/// Cameras with a lower priority will be rendered before cameras with a higher priority.
|
||||||
pub priority: isize,
|
pub priority: isize,
|
||||||
|
/// If this is set to true, this camera will be rendered to its specified [`RenderTarget`]. If false, this
|
||||||
|
/// camera will not be rendered.
|
||||||
pub is_active: bool,
|
pub is_active: bool,
|
||||||
|
/// The method used to calculate this camera's depth. This will be used for projections and visibility.
|
||||||
|
pub depth_calculation: DepthCalculation,
|
||||||
|
/// Computed values for this camera, such as the projection matrix and the render target size.
|
||||||
|
#[reflect(ignore)]
|
||||||
|
pub computed: ComputedCameraValues,
|
||||||
|
/// The "target" that this camera will render to.
|
||||||
#[reflect(ignore)]
|
#[reflect(ignore)]
|
||||||
pub target: RenderTarget,
|
pub target: RenderTarget,
|
||||||
#[reflect(ignore)]
|
|
||||||
pub depth_calculation: DepthCalculation,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Camera {
|
impl Default for Camera {
|
||||||
@ -44,9 +95,8 @@ impl Default for Camera {
|
|||||||
Self {
|
Self {
|
||||||
is_active: true,
|
is_active: true,
|
||||||
priority: 0,
|
priority: 0,
|
||||||
projection_matrix: Default::default(),
|
viewport: None,
|
||||||
logical_target_size: Default::default(),
|
computed: Default::default(),
|
||||||
physical_target_size: Default::default(),
|
|
||||||
target: Default::default(),
|
target: Default::default(),
|
||||||
depth_calculation: Default::default(),
|
depth_calculation: Default::default(),
|
||||||
}
|
}
|
||||||
@ -54,6 +104,63 @@ impl Default for Camera {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Camera {
|
impl Camera {
|
||||||
|
/// The logical size of this camera's viewport. If the `viewport` field is set to [`Some`], this
|
||||||
|
/// will be the size of that custom viewport. Otherwise it will default to the full logical size of
|
||||||
|
/// the current [`RenderTarget`].
|
||||||
|
/// For logic that requires the full logical size of the [`RenderTarget`], prefer [`Camera::logical_target_size`].
|
||||||
|
#[inline]
|
||||||
|
pub fn logical_viewport_size(&self) -> Option<Vec2> {
|
||||||
|
let target_info = self.computed.target_info.as_ref()?;
|
||||||
|
self.viewport
|
||||||
|
.as_ref()
|
||||||
|
.map(|v| {
|
||||||
|
Vec2::new(
|
||||||
|
(v.physical_size.x as f64 / target_info.scale_factor) as f32,
|
||||||
|
(v.physical_size.y as f64 / target_info.scale_factor) as f32,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.or_else(|| self.logical_target_size())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The physical size of this camera's viewport. If the `viewport` field is set to [`Some`], this
|
||||||
|
/// will be the size of that custom viewport. Otherwise it will default to the full physical size of
|
||||||
|
/// the current [`RenderTarget`].
|
||||||
|
/// For logic that requires the full physical size of the [`RenderTarget`], prefer [`Camera::physical_target_size`].
|
||||||
|
#[inline]
|
||||||
|
pub fn physical_viewport_size(&self) -> Option<UVec2> {
|
||||||
|
self.viewport
|
||||||
|
.as_ref()
|
||||||
|
.map(|v| v.physical_size)
|
||||||
|
.or_else(|| self.physical_target_size())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The full logical size of this camera's [`RenderTarget`], ignoring custom `viewport` configuration.
|
||||||
|
/// Note that if the `viewport` field is [`Some`], this will not represent the size of the rendered area.
|
||||||
|
/// For logic that requires the size of the actually rendered area, prefer [`Camera::logical_viewport_size`].
|
||||||
|
#[inline]
|
||||||
|
pub fn logical_target_size(&self) -> Option<Vec2> {
|
||||||
|
self.computed.target_info.as_ref().map(|t| {
|
||||||
|
Vec2::new(
|
||||||
|
(t.physical_size.x as f64 / t.scale_factor) as f32,
|
||||||
|
(t.physical_size.y as f64 / t.scale_factor) as f32,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The full physical size of this camera's [`RenderTarget`], ignoring custom `viewport` configuration.
|
||||||
|
/// Note that if the `viewport` field is [`Some`], this will not represent the size of the rendered area.
|
||||||
|
/// For logic that requires the size of the actually rendered area, prefer [`Camera::physical_viewport_size`].
|
||||||
|
#[inline]
|
||||||
|
pub fn physical_target_size(&self) -> Option<UVec2> {
|
||||||
|
self.computed.target_info.as_ref().map(|t| t.physical_size)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The projection matrix computed using this camera's [`CameraProjection`].
|
||||||
|
#[inline]
|
||||||
|
pub fn projection_matrix(&self) -> Mat4 {
|
||||||
|
self.computed.projection_matrix
|
||||||
|
}
|
||||||
|
|
||||||
/// Given a position in world space, use the camera to compute the viewport-space coordinates.
|
/// Given a position in world space, use the camera to compute the viewport-space coordinates.
|
||||||
///
|
///
|
||||||
/// To get the coordinates in Normalized Device Coordinates, you should use
|
/// To get the coordinates in Normalized Device Coordinates, you should use
|
||||||
@ -63,7 +170,7 @@ impl Camera {
|
|||||||
camera_transform: &GlobalTransform,
|
camera_transform: &GlobalTransform,
|
||||||
world_position: Vec3,
|
world_position: Vec3,
|
||||||
) -> Option<Vec2> {
|
) -> Option<Vec2> {
|
||||||
let target_size = self.logical_target_size?;
|
let target_size = self.logical_viewport_size()?;
|
||||||
let ndc_space_coords = self.world_to_ndc(camera_transform, world_position)?;
|
let ndc_space_coords = self.world_to_ndc(camera_transform, world_position)?;
|
||||||
// NDC z-values outside of 0 < z < 1 are outside the camera frustum and are thus not in viewport-space
|
// NDC z-values outside of 0 < z < 1 are outside the camera frustum and are thus not in viewport-space
|
||||||
if ndc_space_coords.z < 0.0 || ndc_space_coords.z > 1.0 {
|
if ndc_space_coords.z < 0.0 || ndc_space_coords.z > 1.0 {
|
||||||
@ -86,7 +193,7 @@ impl Camera {
|
|||||||
) -> Option<Vec3> {
|
) -> Option<Vec3> {
|
||||||
// Build a transform to convert from world to NDC using camera data
|
// Build a transform to convert from world to NDC using camera data
|
||||||
let world_to_ndc: Mat4 =
|
let world_to_ndc: Mat4 =
|
||||||
self.projection_matrix * camera_transform.compute_matrix().inverse();
|
self.computed.projection_matrix * camera_transform.compute_matrix().inverse();
|
||||||
let ndc_space_coords: Vec3 = world_to_ndc.project_point3(world_position);
|
let ndc_space_coords: Vec3 = world_to_ndc.project_point3(world_position);
|
||||||
|
|
||||||
if !ndc_space_coords.is_nan() {
|
if !ndc_space_coords.is_nan() {
|
||||||
@ -109,6 +216,8 @@ impl CameraRenderGraph {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The "target" that a [`Camera`] will render to. For example, this could be a [`Window`](bevy_window::Window)
|
||||||
|
/// swapchain or an [`Image`].
|
||||||
#[derive(Debug, Clone, Reflect, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
#[derive(Debug, Clone, Reflect, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
pub enum RenderTarget {
|
pub enum RenderTarget {
|
||||||
/// Window to which the camera's view is rendered.
|
/// Window to which the camera's view is rendered.
|
||||||
@ -138,28 +247,29 @@ impl RenderTarget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pub fn get_physical_size(&self, windows: &Windows, images: &Assets<Image>) -> Option<UVec2> {
|
|
||||||
match self {
|
pub fn get_render_target_info(
|
||||||
RenderTarget::Window(window_id) => windows
|
&self,
|
||||||
.get(*window_id)
|
windows: &Windows,
|
||||||
.map(|window| UVec2::new(window.physical_width(), window.physical_height())),
|
images: &Assets<Image>,
|
||||||
RenderTarget::Image(image_handle) => images.get(image_handle).map(|image| {
|
) -> Option<RenderTargetInfo> {
|
||||||
|
Some(match self {
|
||||||
|
RenderTarget::Window(window_id) => {
|
||||||
|
let window = windows.get(*window_id)?;
|
||||||
|
RenderTargetInfo {
|
||||||
|
physical_size: UVec2::new(window.physical_width(), window.physical_height()),
|
||||||
|
scale_factor: window.scale_factor(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
RenderTarget::Image(image_handle) => {
|
||||||
|
let image = images.get(image_handle)?;
|
||||||
let Extent3d { width, height, .. } = image.texture_descriptor.size;
|
let Extent3d { width, height, .. } = image.texture_descriptor.size;
|
||||||
UVec2::new(width, height)
|
RenderTargetInfo {
|
||||||
}),
|
physical_size: UVec2::new(width, height),
|
||||||
}
|
scale_factor: 1.0,
|
||||||
.filter(|size| size.x > 0 && size.y > 0)
|
}
|
||||||
}
|
}
|
||||||
pub fn get_logical_size(&self, windows: &Windows, images: &Assets<Image>) -> Option<Vec2> {
|
})
|
||||||
match self {
|
|
||||||
RenderTarget::Window(window_id) => windows
|
|
||||||
.get(*window_id)
|
|
||||||
.map(|window| Vec2::new(window.width(), window.height())),
|
|
||||||
RenderTarget::Image(image_handle) => images.get(image_handle).map(|image| {
|
|
||||||
let Extent3d { width, height, .. } = image.texture_descriptor.size;
|
|
||||||
Vec2::new(width as f32, height as f32)
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Check if this render target is contained in the given changed windows or images.
|
// Check if this render target is contained in the given changed windows or images.
|
||||||
fn is_changed(
|
fn is_changed(
|
||||||
@ -243,11 +353,10 @@ pub fn camera_system<T: CameraProjection + Component>(
|
|||||||
|| added_cameras.contains(&entity)
|
|| added_cameras.contains(&entity)
|
||||||
|| camera_projection.is_changed()
|
|| camera_projection.is_changed()
|
||||||
{
|
{
|
||||||
camera.logical_target_size = camera.target.get_logical_size(&windows, &images);
|
camera.computed.target_info = camera.target.get_render_target_info(&windows, &images);
|
||||||
camera.physical_target_size = camera.target.get_physical_size(&windows, &images);
|
if let Some(size) = camera.logical_viewport_size() {
|
||||||
if let Some(size) = camera.logical_target_size {
|
|
||||||
camera_projection.update(size.x, size.y);
|
camera_projection.update(size.x, size.y);
|
||||||
camera.projection_matrix = camera_projection.get_projection_matrix();
|
camera.computed.projection_matrix = camera_projection.get_projection_matrix();
|
||||||
camera.depth_calculation = camera_projection.depth_calculation();
|
camera.depth_calculation = camera_projection.depth_calculation();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -257,7 +366,9 @@ pub fn camera_system<T: CameraProjection + Component>(
|
|||||||
#[derive(Component, Debug)]
|
#[derive(Component, Debug)]
|
||||||
pub struct ExtractedCamera {
|
pub struct ExtractedCamera {
|
||||||
pub target: RenderTarget,
|
pub target: RenderTarget,
|
||||||
pub physical_size: Option<UVec2>,
|
pub physical_viewport_size: Option<UVec2>,
|
||||||
|
pub physical_target_size: Option<UVec2>,
|
||||||
|
pub viewport: Option<Viewport>,
|
||||||
pub render_graph: Cow<'static, str>,
|
pub render_graph: Cow<'static, str>,
|
||||||
pub priority: isize,
|
pub priority: isize,
|
||||||
}
|
}
|
||||||
@ -276,19 +387,27 @@ pub fn extract_cameras(
|
|||||||
if !camera.is_active {
|
if !camera.is_active {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let Some(size) = camera.physical_target_size {
|
if let (Some(viewport_size), Some(target_size)) = (
|
||||||
|
camera.physical_viewport_size(),
|
||||||
|
camera.physical_target_size(),
|
||||||
|
) {
|
||||||
|
if target_size.x == 0 || target_size.y == 0 {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
commands.get_or_spawn(entity).insert_bundle((
|
commands.get_or_spawn(entity).insert_bundle((
|
||||||
ExtractedCamera {
|
ExtractedCamera {
|
||||||
target: camera.target.clone(),
|
target: camera.target.clone(),
|
||||||
physical_size: Some(size),
|
viewport: camera.viewport.clone(),
|
||||||
|
physical_viewport_size: Some(viewport_size),
|
||||||
|
physical_target_size: Some(target_size),
|
||||||
render_graph: camera_render_graph.0.clone(),
|
render_graph: camera_render_graph.0.clone(),
|
||||||
priority: camera.priority,
|
priority: camera.priority,
|
||||||
},
|
},
|
||||||
ExtractedView {
|
ExtractedView {
|
||||||
projection: camera.projection_matrix,
|
projection: camera.projection_matrix(),
|
||||||
transform: *transform,
|
transform: *transform,
|
||||||
width: size.x,
|
width: viewport_size.x,
|
||||||
height: size.y,
|
height: viewport_size.y,
|
||||||
},
|
},
|
||||||
visible_entities.clone(),
|
visible_entities.clone(),
|
||||||
));
|
));
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
use crate::{
|
use crate::{
|
||||||
|
camera::Viewport,
|
||||||
prelude::Color,
|
prelude::Color,
|
||||||
render_resource::{
|
render_resource::{
|
||||||
BindGroup, BindGroupId, Buffer, BufferId, BufferSlice, RenderPipeline, RenderPipelineId,
|
BindGroup, BindGroupId, Buffer, BufferId, BufferSlice, RenderPipeline, RenderPipelineId,
|
||||||
@ -336,6 +337,20 @@ impl<'a> TrackedRenderPass<'a> {
|
|||||||
.set_viewport(x, y, width, height, min_depth, max_depth);
|
.set_viewport(x, y, width, height, min_depth, max_depth);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 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) {
|
||||||
|
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,
|
||||||
|
viewport.depth.start,
|
||||||
|
viewport.depth.end,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/// Insert a single debug marker.
|
/// Insert a single debug marker.
|
||||||
///
|
///
|
||||||
/// This is a GPU debugging feature. This has no effect on the rendering itself.
|
/// This is a GPU debugging feature. This has no effect on the rendering itself.
|
||||||
|
@ -172,7 +172,7 @@ fn prepare_view_targets(
|
|||||||
) {
|
) {
|
||||||
let mut sampled_textures = HashMap::default();
|
let mut sampled_textures = HashMap::default();
|
||||||
for (entity, camera) in cameras.iter() {
|
for (entity, camera) in cameras.iter() {
|
||||||
if let Some(size) = camera.physical_size {
|
if let Some(target_size) = camera.physical_target_size {
|
||||||
if let Some(texture_view) = camera.target.get_texture_view(&windows, &images) {
|
if let Some(texture_view) = camera.target.get_texture_view(&windows, &images) {
|
||||||
let sampled_target = if msaa.samples > 1 {
|
let sampled_target = if msaa.samples > 1 {
|
||||||
let sampled_texture = sampled_textures
|
let sampled_texture = sampled_textures
|
||||||
@ -183,8 +183,8 @@ fn prepare_view_targets(
|
|||||||
TextureDescriptor {
|
TextureDescriptor {
|
||||||
label: Some("sampled_color_attachment_texture"),
|
label: Some("sampled_color_attachment_texture"),
|
||||||
size: Extent3d {
|
size: Extent3d {
|
||||||
width: size.x,
|
width: target_size.x,
|
||||||
height: size.y,
|
height: target_size.y,
|
||||||
depth_or_array_layers: 1,
|
depth_or_array_layers: 1,
|
||||||
},
|
},
|
||||||
mip_level_count: 1,
|
mip_level_count: 1,
|
||||||
|
@ -237,9 +237,10 @@ pub fn extract_default_ui_camera_view<T: Component>(
|
|||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if let (Some(logical_size), Some(physical_size)) =
|
if let (Some(logical_size), Some(physical_size)) = (
|
||||||
(camera.logical_target_size, camera.physical_target_size)
|
camera.logical_viewport_size(),
|
||||||
{
|
camera.physical_viewport_size(),
|
||||||
|
) {
|
||||||
let mut projection = OrthographicProjection {
|
let mut projection = OrthographicProjection {
|
||||||
far: UI_CAMERA_FAR,
|
far: UI_CAMERA_FAR,
|
||||||
window_origin: WindowOrigin::BottomLeft,
|
window_origin: WindowOrigin::BottomLeft,
|
||||||
|
108
examples/3d/split_screen.rs
Normal file
108
examples/3d/split_screen.rs
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
//! Renders two cameras to the same window to accomplish "split screen".
|
||||||
|
|
||||||
|
use bevy::{
|
||||||
|
core_pipeline::clear_color::ClearColorConfig,
|
||||||
|
prelude::*,
|
||||||
|
render::camera::Viewport,
|
||||||
|
window::{WindowId, WindowResized},
|
||||||
|
};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
App::new()
|
||||||
|
.add_plugins(DefaultPlugins)
|
||||||
|
.add_startup_system(setup)
|
||||||
|
.add_system(set_camera_viewports)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// set up a simple 3D scene
|
||||||
|
fn setup(
|
||||||
|
mut commands: Commands,
|
||||||
|
asset_server: Res<AssetServer>,
|
||||||
|
mut meshes: ResMut<Assets<Mesh>>,
|
||||||
|
mut materials: ResMut<Assets<StandardMaterial>>,
|
||||||
|
) {
|
||||||
|
// plane
|
||||||
|
commands.spawn_bundle(PbrBundle {
|
||||||
|
mesh: meshes.add(Mesh::from(shape::Plane { size: 100.0 })),
|
||||||
|
material: materials.add(Color::rgb(0.3, 0.5, 0.3).into()),
|
||||||
|
..default()
|
||||||
|
});
|
||||||
|
|
||||||
|
commands.spawn_scene(asset_server.load("models/animated/Fox.glb#Scene0"));
|
||||||
|
|
||||||
|
// Light
|
||||||
|
commands.spawn_bundle(DirectionalLightBundle {
|
||||||
|
transform: Transform::from_rotation(Quat::from_euler(
|
||||||
|
EulerRot::ZYX,
|
||||||
|
0.0,
|
||||||
|
1.0,
|
||||||
|
-std::f32::consts::FRAC_PI_4,
|
||||||
|
)),
|
||||||
|
directional_light: DirectionalLight {
|
||||||
|
shadows_enabled: true,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
..default()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Left Camera
|
||||||
|
commands
|
||||||
|
.spawn_bundle(Camera3dBundle {
|
||||||
|
transform: Transform::from_xyz(0.0, 200.0, -100.0).looking_at(Vec3::ZERO, Vec3::Y),
|
||||||
|
..default()
|
||||||
|
})
|
||||||
|
.insert(LeftCamera);
|
||||||
|
|
||||||
|
// Right Camera
|
||||||
|
commands
|
||||||
|
.spawn_bundle(Camera3dBundle {
|
||||||
|
transform: Transform::from_xyz(100.0, 100., 150.0).looking_at(Vec3::ZERO, Vec3::Y),
|
||||||
|
camera: Camera {
|
||||||
|
// Renders the right camera after the left camera, which has a default priority of 0
|
||||||
|
priority: 1,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
camera_3d: Camera3d {
|
||||||
|
// dont clear on the second camera because the first camera already cleared the window
|
||||||
|
clear_color: ClearColorConfig::None,
|
||||||
|
},
|
||||||
|
..default()
|
||||||
|
})
|
||||||
|
.insert(RightCamera);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
struct LeftCamera;
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
struct RightCamera;
|
||||||
|
|
||||||
|
fn set_camera_viewports(
|
||||||
|
windows: Res<Windows>,
|
||||||
|
mut resize_events: EventReader<WindowResized>,
|
||||||
|
mut left_camera: Query<&mut Camera, (With<LeftCamera>, Without<RightCamera>)>,
|
||||||
|
mut right_camera: Query<&mut Camera, With<RightCamera>>,
|
||||||
|
) {
|
||||||
|
// We need to dynamically resize the camera's viewports whenever the window size changes
|
||||||
|
// so then each camera always takes up half the screen.
|
||||||
|
// A resize_event is sent when the window is first created, allowing us to reuse this system for initial setup.
|
||||||
|
for resize_event in resize_events.iter() {
|
||||||
|
if resize_event.id == WindowId::primary() {
|
||||||
|
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()),
|
||||||
|
..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()),
|
||||||
|
..default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -103,6 +103,7 @@ Example | File | Description
|
|||||||
Example | File | Description
|
Example | File | Description
|
||||||
--- | --- | ---
|
--- | --- | ---
|
||||||
`3d_scene` | [`3d/3d_scene.rs`](./3d/3d_scene.rs) | Simple 3D scene with basic shapes and lighting
|
`3d_scene` | [`3d/3d_scene.rs`](./3d/3d_scene.rs) | Simple 3D scene with basic shapes and lighting
|
||||||
|
`3d_shapes` | [`3d/shapes.rs`](./3d/shapes.rs) | A scene showcasing the built-in 3D shapes
|
||||||
`lighting` | [`3d/lighting.rs`](./3d/lighting.rs) | Illustrates various lighting options in a simple scene
|
`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
|
`load_gltf` | [`3d/load_gltf.rs`](./3d/load_gltf.rs) | Loads and renders a gltf file as a scene
|
||||||
`msaa` | [`3d/msaa.rs`](./3d/msaa.rs) | Configures MSAA (Multi-Sample Anti-Aliasing) for smoother edges
|
`msaa` | [`3d/msaa.rs`](./3d/msaa.rs) | Configures MSAA (Multi-Sample Anti-Aliasing) for smoother edges
|
||||||
@ -113,12 +114,12 @@ Example | File | Description
|
|||||||
`shadow_caster_receiver` | [`3d/shadow_caster_receiver.rs`](./3d/shadow_caster_receiver.rs) | Demonstrates how to prevent meshes from casting/receiving shadows in a 3d scene
|
`shadow_caster_receiver` | [`3d/shadow_caster_receiver.rs`](./3d/shadow_caster_receiver.rs) | Demonstrates how to prevent meshes from casting/receiving shadows in a 3d scene
|
||||||
`shadow_biases` | [`3d/shadow_biases.rs`](./3d/shadow_biases.rs) | Demonstrates how shadow biases affect shadows in a 3d scene
|
`shadow_biases` | [`3d/shadow_biases.rs`](./3d/shadow_biases.rs) | Demonstrates how shadow biases affect shadows in a 3d scene
|
||||||
`spherical_area_lights` | [`3d/spherical_area_lights.rs`](./3d/spherical_area_lights.rs) | Demonstrates how point light radius values affect light behavior.
|
`spherical_area_lights` | [`3d/spherical_area_lights.rs`](./3d/spherical_area_lights.rs) | Demonstrates how point light radius values affect light behavior.
|
||||||
|
`split_screen` | [`3d/split_screen.rs`](./3d/split_screen.rs) | Demonstrates how to render two cameras to the same window to accomplish "split screen".
|
||||||
`texture` | [`3d/texture.rs`](./3d/texture.rs) | Shows configuration of texture materials
|
`texture` | [`3d/texture.rs`](./3d/texture.rs) | Shows configuration of texture materials
|
||||||
`two_passes` | [`3d/two_passes.rs`](./3d/two_passes.rs) | Renders two 3d passes to the same window from different perspectives.
|
`two_passes` | [`3d/two_passes.rs`](./3d/two_passes.rs) | Renders two 3d passes to the same window from different perspectives.
|
||||||
`update_gltf_scene` | [`3d/update_gltf_scene.rs`](./3d/update_gltf_scene.rs) | Update a scene from a gltf file, either by spawning the scene as a child of another entity, or by accessing the entities of the scene
|
`update_gltf_scene` | [`3d/update_gltf_scene.rs`](./3d/update_gltf_scene.rs) | Update a scene from a gltf file, either by spawning the scene as a child of another entity, or by accessing the entities of the scene
|
||||||
`vertex_colors` | [`3d/vertex_colors.rs`](./3d/vertex_colors.rs) | Shows the use of vertex colors
|
`vertex_colors` | [`3d/vertex_colors.rs`](./3d/vertex_colors.rs) | Shows the use of vertex colors
|
||||||
`wireframe` | [`3d/wireframe.rs`](./3d/wireframe.rs) | Showcases wireframe rendering
|
`wireframe` | [`3d/wireframe.rs`](./3d/wireframe.rs) | Showcases wireframe rendering
|
||||||
`3d_shapes` | [`3d/shapes.rs`](./3d/shapes.rs) | A scene showcasing the built-in 3D shapes
|
|
||||||
|
|
||||||
## Animation
|
## Animation
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user