diff --git a/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs b/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs index 366ac2c22a..20474c8e1d 100644 --- a/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs +++ b/crates/bevy_core_pipeline/src/core_3d/camera_3d.rs @@ -15,7 +15,7 @@ use bevy_transform::prelude::{GlobalTransform, Transform}; use serde::{Deserialize, Serialize}; /// Configuration for the "main 3d render graph". -#[derive(Component, Reflect, Clone, Default, ExtractComponent)] +#[derive(Component, Reflect, Clone, ExtractComponent)] #[extract_component_filter(With)] #[reflect(Component)] pub struct Camera3d { @@ -23,6 +23,22 @@ pub struct Camera3d { pub clear_color: ClearColorConfig, /// The depth clear operation to perform for the main 3d pass. pub depth_load_op: Camera3dDepthLoadOp, + /// How many individual steps should be performed in the transmissive 3d pass + /// + /// Roughly corresponds to how many “layers of transparency” are rendered for + /// transmissive objects. Each step requires making one additional texture copy, + /// so it's recommended to keep this number to a resonably low value. + pub transmissive_steps: usize, +} + +impl Default for Camera3d { + fn default() -> Self { + Camera3d { + clear_color: ClearColorConfig::default(), + depth_load_op: Camera3dDepthLoadOp::default(), + transmissive_steps: 1, + } + } } /// The depth clear operation to perform for the main 3d pass. diff --git a/crates/bevy_core_pipeline/src/core_3d/main_transmissive_pass_3d_node.rs b/crates/bevy_core_pipeline/src/core_3d/main_transmissive_pass_3d_node.rs index 0f8c0e32df..dd086bcc81 100644 --- a/crates/bevy_core_pipeline/src/core_3d/main_transmissive_pass_3d_node.rs +++ b/crates/bevy_core_pipeline/src/core_3d/main_transmissive_pass_3d_node.rs @@ -1,4 +1,4 @@ -use super::ViewTransmissionTexture; +use super::{Camera3d, ViewTransmissionTexture}; use crate::core_3d::Transmissive3d; use bevy_ecs::prelude::*; use bevy_render::{ @@ -13,15 +13,17 @@ use bevy_render::{ }; #[cfg(feature = "trace")] use bevy_utils::tracing::info_span; +use std::ops::Range; /// A [`Node`] that runs the [`Transmissive3d`] [`RenderPhase`]. pub struct MainTransmissivePass3dNode { query: QueryState< ( &'static ExtractedCamera, + &'static Camera3d, &'static RenderPhase, &'static ViewTarget, - &'static ViewTransmissionTexture, + Option<&'static ViewTransmissionTexture>, &'static ViewDepthTexture, ), With, @@ -50,6 +52,7 @@ impl Node for MainTransmissivePass3dNode { let view_entity = graph.view_entity(); let Ok(( camera, + camera_3d, transmissive_phase, target, transmission, @@ -60,47 +63,94 @@ impl Node for MainTransmissivePass3dNode { }; let physical_target_size = camera.physical_target_size.unwrap(); - render_context.command_encoder().copy_texture_to_texture( - target.main_texture().as_image_copy(), - transmission.texture.as_image_copy(), - Extent3d { - width: physical_target_size.x, - height: physical_target_size.y, - depth_or_array_layers: 1, - }, - ); - if !transmissive_phase.items.is_empty() { - // Run the transmissive pass, sorted back-to-front - // NOTE: Scoped to drop the mutable borrow of render_context - #[cfg(feature = "trace")] - let _main_transmissive_pass_3d_span = info_span!("main_transmissive_pass_3d").entered(); - - let mut render_pass = render_context.begin_tracked_render_pass(RenderPassDescriptor { - label: Some("main_transmissive_pass_3d"), - // NOTE: The transmissive pass loads the color buffer as well as overwriting it where appropriate. - color_attachments: &[Some(target.get_color_attachment(Operations { + let render_pass_descriptor = RenderPassDescriptor { + label: Some("main_transmissive_pass_3d"), + // NOTE: The transmissive pass loads the color buffer as well as overwriting it where appropriate. + color_attachments: &[Some(target.get_color_attachment(Operations { + load: LoadOp::Load, + store: true, + }))], + depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { + view: &depth.view, + // NOTE: The transmissive main pass loads the depth buffer and possibly overwrites it + depth_ops: Some(Operations { load: LoadOp::Load, store: true, - }))], - depth_stencil_attachment: Some(RenderPassDepthStencilAttachment { - view: &depth.view, - // NOTE: The transmissive main pass loads the depth buffer and possibly overwrites it - depth_ops: Some(Operations { - load: LoadOp::Load, - store: true, - }), - stencil_ops: None, }), - }); + stencil_ops: None, + }), + }; - if let Some(viewport) = camera.viewport.as_ref() { - render_pass.set_camera_viewport(viewport); + // Run the transmissive pass, sorted back-to-front + // NOTE: Scoped to drop the mutable borrow of render_context + #[cfg(feature = "trace")] + let _main_transmissive_pass_3d_span = info_span!("main_transmissive_pass_3d").entered(); + + if !transmissive_phase.items.is_empty() { + let transmissive_steps = camera_3d.transmissive_steps; + if transmissive_steps > 0 { + let transmission = + transmission.expect("`ViewTransmissionTexture` should exist at this point"); + for range in split_range(0..transmissive_phase.items.len(), transmissive_steps) { + render_context.command_encoder().copy_texture_to_texture( + target.main_texture().as_image_copy(), + transmission.texture.as_image_copy(), + Extent3d { + width: physical_target_size.x, + height: physical_target_size.y, + depth_or_array_layers: 1, + }, + ); + + let mut render_pass = + render_context.begin_tracked_render_pass(render_pass_descriptor.clone()); + + if let Some(viewport) = camera.viewport.as_ref() { + render_pass.set_camera_viewport(viewport); + } + + transmissive_phase.render_range(&mut render_pass, world, view_entity, range); + } + } else { + let mut render_pass = + render_context.begin_tracked_render_pass(render_pass_descriptor); + + if let Some(viewport) = camera.viewport.as_ref() { + render_pass.set_camera_viewport(viewport); + } + + transmissive_phase.render(&mut render_pass, world, view_entity); } - - transmissive_phase.render(&mut render_pass, world, view_entity); } Ok(()) } } + +/// Splits a [`Range`] into at most `max_num_splits` sub-ranges without overlaps +/// +/// Properly takes into account remainders of inexact divisions (by adding extra +/// elements to the initial sub-ranges as needed) +fn split_range(range: Range, max_num_splits: usize) -> impl Iterator> { + let len = range.end - range.start; + assert!(len > 0, "to be split, a range must not be empty"); + assert!(max_num_splits > 0, "max_num_splits must be at least 1"); + let num_splits = max_num_splits.min(len); + let step = len / num_splits; + let mut rem = len % num_splits; + let mut start = range.start; + + (0..num_splits).map(move |_| { + let extra = if rem > 0 { + rem -= 1; + 1 + } else { + 0 + }; + let end = (start + step + extra).min(range.end); + let result = start..end; + start = end; + result + }) +} diff --git a/crates/bevy_core_pipeline/src/core_3d/mod.rs b/crates/bevy_core_pipeline/src/core_3d/mod.rs index 7630a892e5..7cb42885da 100644 --- a/crates/bevy_core_pipeline/src/core_3d/mod.rs +++ b/crates/bevy_core_pipeline/src/core_3d/mod.rs @@ -374,7 +374,7 @@ pub fn prepare_core_3d_transmission_textures( mut texture_cache: ResMut, render_device: Res, views_3d: Query< - (Entity, &ExtractedCamera, &ExtractedView), + (Entity, &ExtractedCamera, &Camera3d, &ExtractedView), ( With>, With>, @@ -384,11 +384,16 @@ pub fn prepare_core_3d_transmission_textures( >, ) { let mut textures = HashMap::default(); - for (entity, camera, view) in &views_3d { + for (entity, camera, camera_3d, view) in &views_3d { let Some(physical_target_size) = camera.physical_target_size else { continue; }; + // Don't prepare a transmission texture if the number of steps is set to 0 + if camera_3d.transmissive_steps == 0 { + continue; + } + let cached_texture = textures .entry(camera.target.clone()) .or_insert_with(|| { diff --git a/crates/bevy_pbr/src/render/mesh.rs b/crates/bevy_pbr/src/render/mesh.rs index aabbc83583..898d0b27f2 100644 --- a/crates/bevy_pbr/src/render/mesh.rs +++ b/crates/bevy_pbr/src/render/mesh.rs @@ -34,7 +34,7 @@ use bevy_render::{ render_resource::*, renderer::{RenderDevice, RenderQueue}, texture::{ - BevyDefault, DefaultImageSampler, FallbackImageCubemap, FallbackImagesDepth, + BevyDefault, DefaultImageSampler, FallbackImage, FallbackImageCubemap, FallbackImagesDepth, FallbackImagesMsaa, GpuImage, Image, ImageSampler, TextureFormatPixelInfo, }, view::{ComputedVisibility, ViewTarget, ViewUniform, ViewUniformOffset, ViewUniforms}, @@ -997,11 +997,12 @@ pub fn queue_mesh_view_bind_groups( Option<&ViewPrepassTextures>, Option<&EnvironmentMapLight>, &Tonemapping, - &ViewTransmissionTexture, + Option<&ViewTransmissionTexture>, )>, images: Res>, mut fallback_images: FallbackImagesMsaa, mut fallback_depths: FallbackImagesDepth, + fallback_image: Res, fallback_cubemap: Res, msaa: Res, globals_buffer: Res, @@ -1113,11 +1114,17 @@ pub fn queue_mesh_view_bind_groups( entries.extend_from_slice(&[ BindGroupEntry { binding: 19, - resource: BindingResource::TextureView(&transmission.view), + resource: BindingResource::TextureView(transmission.map_or_else( + || &fallback_image.texture_view, + |transmission| &transmission.view, + )), }, BindGroupEntry { binding: 20, - resource: BindingResource::Sampler(&transmission.sampler), + resource: BindingResource::Sampler(&transmission.map_or_else( + || &fallback_image.sampler, + |transmission| &transmission.sampler, + )), }, ]); diff --git a/crates/bevy_render/src/render_phase/mod.rs b/crates/bevy_render/src/render_phase/mod.rs index 7c7ad41a5e..1ea9f16245 100644 --- a/crates/bevy_render/src/render_phase/mod.rs +++ b/crates/bevy_render/src/render_phase/mod.rs @@ -89,6 +89,29 @@ impl RenderPhase { draw_function.draw(world, render_pass, view, item); } } + + /// Renders a range of the [`PhaseItem`]s using their corresponding draw functions. + pub fn render_range<'w>( + &self, + render_pass: &mut TrackedRenderPass<'w>, + world: &'w World, + view: Entity, + range: Range, + ) { + let draw_functions = world.resource::>(); + let mut draw_functions = draw_functions.write(); + draw_functions.prepare(world); + + for item in self + .items + .iter() + .skip(range.start) + .take(range.end - range.start) + { + let draw_function = draw_functions.get_mut(item.draw_function()).unwrap(); + draw_function.draw(world, render_pass, view, item); + } + } } impl RenderPhase {