diff --git a/Cargo.toml b/Cargo.toml index b14b51b22ef91..9d7f3d7180ba7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -216,6 +216,10 @@ path = "examples/3d/texture.rs" name = "render_to_texture" path = "examples/3d/render_to_texture.rs" +[[example]] +name = "two_passes" +path = "examples/3d/two_passes.rs" + [[example]] name = "update_gltf_scene" path = "examples/3d/update_gltf_scene.rs" diff --git a/crates/bevy_core_pipeline/src/lib.rs b/crates/bevy_core_pipeline/src/lib.rs index a46bba2789f06..f8185f10f7f62 100644 --- a/crates/bevy_core_pipeline/src/lib.rs +++ b/crates/bevy_core_pipeline/src/lib.rs @@ -23,7 +23,7 @@ use bevy_app::{App, Plugin}; use bevy_core::FloatOrd; use bevy_ecs::prelude::*; use bevy_render::{ - camera::{ActiveCameras, CameraPlugin, RenderTarget}, + camera::{ActiveCameras, CameraPlugin, ExtractedCamera, RenderTarget}, color::Color, render_graph::{EmptyNode, RenderGraph, SlotInfo, SlotType}, render_phase::{ @@ -393,7 +393,7 @@ pub fn prepare_core_views_system( msaa: Res, render_device: Res, views_3d: Query< - (Entity, &ExtractedView), + (Entity, &ExtractedView, Option<&ExtractedCamera>), ( With>, With>, @@ -401,27 +401,41 @@ pub fn prepare_core_views_system( ), >, ) { - for (entity, view) in views_3d.iter() { - let 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, + let mut textures = HashMap::default(); + for (entity, view, camera) in views_3d.iter() { + let mut render_on_top = false; + 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, }, - 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 { + render_on_top = camera.render_on_top; + 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, + render_on_top, }); } } diff --git a/crates/bevy_core_pipeline/src/main_pass_3d.rs b/crates/bevy_core_pipeline/src/main_pass_3d.rs index 945e963769f36..58b7c3c19cb2a 100644 --- a/crates/bevy_core_pipeline/src/main_pass_3d.rs +++ b/crates/bevy_core_pipeline/src/main_pass_3d.rs @@ -68,7 +68,11 @@ impl Node for MainPass3dNode { view: &depth.view, // NOTE: The opaque main pass loads the depth buffer and possibly overwrites it depth_ops: Some(Operations { - load: LoadOp::Load, + load: if depth.render_on_top { + LoadOp::Clear(0.0) + } else { + LoadOp::Load + }, store: true, }), stencil_ops: None, @@ -139,7 +143,7 @@ impl Node for MainPass3dNode { // transparent ones. depth_ops: Some(Operations { load: LoadOp::Load, - store: false, + store: true, }), stencil_ops: None, }), diff --git a/crates/bevy_render/src/camera/camera.rs b/crates/bevy_render/src/camera/camera.rs index 07e748fe8211f..c51d348ef3870 100644 --- a/crates/bevy_render/src/camera/camera.rs +++ b/crates/bevy_render/src/camera/camera.rs @@ -27,6 +27,7 @@ pub struct Camera { pub name: Option, #[reflect(ignore)] pub target: RenderTarget, + pub render_on_top: bool, #[reflect(ignore)] pub depth_calculation: DepthCalculation, pub near: f32, diff --git a/crates/bevy_render/src/camera/mod.rs b/crates/bevy_render/src/camera/mod.rs index 35e3451a4320e..37a16146f7eab 100644 --- a/crates/bevy_render/src/camera/mod.rs +++ b/crates/bevy_render/src/camera/mod.rs @@ -74,6 +74,7 @@ pub struct ExtractedCamera { pub target: RenderTarget, pub name: Option, pub physical_size: Option, + pub render_on_top: bool, } fn extract_cameras( @@ -96,6 +97,7 @@ fn extract_cameras( target: camera.target.clone(), name: camera.name.clone(), physical_size: camera.target.get_physical_size(&windows, &images), + render_on_top: camera.render_on_top, }, ExtractedView { projection: camera.projection_matrix, diff --git a/crates/bevy_render/src/texture/texture_cache.rs b/crates/bevy_render/src/texture/texture_cache.rs index 43cac472c154f..d92878774aee7 100644 --- a/crates/bevy_render/src/texture/texture_cache.rs +++ b/crates/bevy_render/src/texture/texture_cache.rs @@ -18,6 +18,7 @@ struct CachedTextureMeta { /// A cached GPU [`Texture`] with corresponding [`TextureView`]. /// This is useful for textures that are created repeatedly (each frame) in the rendering process /// to reduce the amount of GPU memory allocations. +#[derive(Clone)] pub struct CachedTexture { pub texture: Texture, pub default_view: TextureView, diff --git a/crates/bevy_render/src/view/mod.rs b/crates/bevy_render/src/view/mod.rs index 95233083c6f6c..9887a92a6d08e 100644 --- a/crates/bevy_render/src/view/mod.rs +++ b/crates/bevy_render/src/view/mod.rs @@ -21,6 +21,7 @@ use bevy_app::{App, Plugin}; use bevy_ecs::prelude::*; use bevy_math::{Mat4, Vec3}; use bevy_transform::components::GlobalTransform; +use bevy_utils::HashMap; pub struct ViewPlugin; @@ -136,6 +137,7 @@ impl ViewTarget { pub struct ViewDepthTexture { pub texture: Texture, pub view: TextureView, + pub render_on_top: bool, } fn prepare_view_uniforms( @@ -183,6 +185,7 @@ fn prepare_view_targets( mut texture_cache: ResMut, cameras: Query<&ExtractedCamera>, ) { + let mut sampled_textures = HashMap::default(); for entity in camera_names.entities.values().copied() { let camera = if let Ok(camera) = cameras.get(entity) { camera @@ -192,22 +195,26 @@ fn prepare_view_targets( if let Some(size) = camera.physical_size { if let Some(texture_view) = camera.target.get_texture_view(&windows, &images) { let sampled_target = if msaa.samples > 1 { - let sampled_texture = texture_cache.get( - &render_device, - TextureDescriptor { - label: Some("sampled_color_attachment_texture"), - size: Extent3d { - width: size.x, - height: size.y, - depth_or_array_layers: 1, - }, - mip_level_count: 1, - sample_count: msaa.samples, - dimension: TextureDimension::D2, - format: TextureFormat::bevy_default(), - usage: TextureUsages::RENDER_ATTACHMENT, - }, - ); + let sampled_texture = sampled_textures + .entry(camera.target.clone()) + .or_insert_with(|| { + texture_cache.get( + &render_device, + TextureDescriptor { + label: Some("sampled_color_attachment_texture"), + size: Extent3d { + width: size.x, + height: size.y, + depth_or_array_layers: 1, + }, + mip_level_count: 1, + sample_count: msaa.samples, + dimension: TextureDimension::D2, + format: TextureFormat::bevy_default(), + usage: TextureUsages::RENDER_ATTACHMENT, + }, + ) + }); Some(sampled_texture.default_view.clone()) } else { None diff --git a/examples/3d/two_passes.rs b/examples/3d/two_passes.rs new file mode 100644 index 0000000000000..a328d7179efe9 --- /dev/null +++ b/examples/3d/two_passes.rs @@ -0,0 +1,203 @@ +use bevy::{ + core_pipeline::{draw_3d_graph, node, AlphaMask3d, Opaque3d, Transparent3d}, + prelude::*, + render::{ + camera::{ActiveCameras, Camera, ExtractedCameraNames, RenderTarget}, + render_graph::{NodeRunError, RenderGraph, RenderGraphContext, SlotValue}, + render_phase::RenderPhase, + renderer::RenderContext, + view::RenderLayers, + RenderApp, RenderStage, + }, + window::WindowId, +}; + +// The name of the final node of the first pass. +pub const FIRST_PASS_DRIVER: &str = "first_pass_driver"; + +// The name of the camera that determines the view rendered in the first pass. +pub const FIRST_PASS_CAMERA: &str = "first_pass_camera"; + +fn main() { + let mut app = App::new(); + app.insert_resource(Msaa { samples: 4 }) + .add_plugins(DefaultPlugins) + .add_startup_system(setup) + .add_system(cube_rotator_system) + .add_system(rotator_system) + .add_system(cycle_msaa); + + let render_app = app.sub_app_mut(RenderApp); + + // This will add 3D render phases for the new camera. + render_app.add_system_to_stage(RenderStage::Extract, extract_first_pass_camera_phases); + + let mut graph = render_app.world.get_resource_mut::().unwrap(); + + // Add a node for the first pass. + graph.add_node(FIRST_PASS_DRIVER, FirstPassCameraDriver); + + // The first pass's dependencies include those of the main pass. + graph + .add_node_edge(node::MAIN_PASS_DEPENDENCIES, FIRST_PASS_DRIVER) + .unwrap(); + + // Insert the first pass node: CLEAR_PASS_DRIVER -> FIRST_PASS_DRIVER -> MAIN_PASS_DRIVER + graph + .add_node_edge(node::CLEAR_PASS_DRIVER, FIRST_PASS_DRIVER) + .unwrap(); + graph + .add_node_edge(FIRST_PASS_DRIVER, node::MAIN_PASS_DRIVER) + .unwrap(); + app.run(); +} + +// Add 3D render phases for FIRST_PASS_CAMERA. +fn extract_first_pass_camera_phases(mut commands: Commands, active_cameras: Res) { + if let Some(camera) = active_cameras.get(FIRST_PASS_CAMERA) { + if let Some(entity) = camera.entity { + commands.get_or_spawn(entity).insert_bundle(( + RenderPhase::::default(), + RenderPhase::::default(), + RenderPhase::::default(), + )); + } + } +} + +// A node for the first pass camera that runs draw_3d_graph with this camera. +struct FirstPassCameraDriver; +impl bevy::render::render_graph::Node for FirstPassCameraDriver { + 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(FIRST_PASS_CAMERA) { + graph.run_sub_graph(draw_3d_graph::NAME, vec![SlotValue::Entity(*camera_3d)])?; + } + Ok(()) + } +} + +// Marks the first pass cube. +#[derive(Component)] +struct FirstPassCube; + +// Marks the main pass cube. +#[derive(Component)] +struct MainPassCube; + +fn setup( + mut commands: Commands, + mut meshes: ResMut>, + mut materials: ResMut>, + mut active_cameras: ResMut, +) { + let cube_handle = meshes.add(Mesh::from(shape::Cube { size: 4.0 })); + let cube_material_handle = materials.add(StandardMaterial { + base_color: Color::GREEN, + reflectance: 0.02, + unlit: false, + ..Default::default() + }); + + let split = 2.0; + + // This specifies the layer used for the first pass, which will be attached to the first pass camera and cube. + let first_pass_layer = RenderLayers::layer(1); + + // The first pass cube. + commands + .spawn_bundle(PbrBundle { + mesh: cube_handle, + material: cube_material_handle, + transform: Transform::from_translation(Vec3::new(-split, 0.0, 1.0)), + ..Default::default() + }) + .insert(FirstPassCube) + .insert(first_pass_layer); + + // Light + // NOTE: Currently lights are shared between passes - see https://github.com/bevyengine/bevy/issues/3462 + commands.spawn_bundle(PointLightBundle { + transform: Transform::from_translation(Vec3::new(0.0, 0.0, 10.0)), + ..Default::default() + }); + + // First pass camera + active_cameras.add(FIRST_PASS_CAMERA); + commands + .spawn_bundle(PerspectiveCameraBundle { + camera: Camera { + name: Some(FIRST_PASS_CAMERA.to_string()), + target: RenderTarget::Window(WindowId::primary()), + ..Default::default() + }, + transform: Transform::from_translation(Vec3::new(0.0, 0.0, 15.0)) + .looking_at(Vec3::default(), Vec3::Y), + ..Default::default() + }) + .insert(first_pass_layer); + + let cube_size = 4.0; + let cube_handle = meshes.add(Mesh::from(shape::Box::new(cube_size, cube_size, cube_size))); + + let material_handle = materials.add(StandardMaterial { + base_color: Color::RED, + reflectance: 0.02, + unlit: false, + ..Default::default() + }); + + // Main pass cube. + commands + .spawn_bundle(PbrBundle { + mesh: cube_handle, + material: material_handle, + transform: Transform { + translation: Vec3::new(split, 0.0, -4.5), + rotation: Quat::from_rotation_x(-std::f32::consts::PI / 5.0), + ..Default::default() + }, + ..Default::default() + }) + .insert(MainPassCube); + + // The main pass camera. + commands.spawn_bundle(PerspectiveCameraBundle { + transform: Transform::from_translation(Vec3::new(0.0, 0.0, 15.0)) + .looking_at(Vec3::default(), Vec3::Y), + ..Default::default() + }); +} + +/// Rotates the inner cube (first pass) +fn rotator_system(time: Res