Implement occlusion culling for the deferred rendering pipeline. (#17934)

Deferred rendering currently doesn't support occlusion culling. This PR
implements it in a straightforward way, mirroring what we already do for
the non-deferred pipeline.

On the rend3 sci-fi base test scene, this resulted in roughly a 2×
speedup when applied on top of my other patches. For that scene, it was
useful to add another option, `--add-light`, which forces the addition
of a shadow-casting light, to the scene viewer, which I included in this
patch.
This commit is contained in:
Patrick Walton 2025-02-20 04:54:27 -08:00 committed by GitHub
parent f15437e4dc
commit 8de6b16e9d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 223 additions and 145 deletions

View File

@ -19,7 +19,8 @@ pub mod graph {
EarlyPrepass, EarlyPrepass,
EarlyDownsampleDepth, EarlyDownsampleDepth,
LatePrepass, LatePrepass,
DeferredPrepass, EarlyDeferredPrepass,
LateDeferredPrepass,
CopyDeferredLightingId, CopyDeferredLightingId,
EndPrepasses, EndPrepasses,
StartMainPass, StartMainPass,
@ -112,7 +113,8 @@ use tracing::warn;
use crate::{ use crate::{
core_3d::main_transmissive_pass_3d_node::MainTransmissivePass3dNode, core_3d::main_transmissive_pass_3d_node::MainTransmissivePass3dNode,
deferred::{ deferred::{
copy_lighting_id::CopyDeferredLightingIdNode, node::DeferredGBufferPrepassNode, copy_lighting_id::CopyDeferredLightingIdNode,
node::{EarlyDeferredGBufferPrepassNode, LateDeferredGBufferPrepassNode},
AlphaMask3dDeferred, Opaque3dDeferred, DEFERRED_LIGHTING_PASS_ID_FORMAT, AlphaMask3dDeferred, Opaque3dDeferred, DEFERRED_LIGHTING_PASS_ID_FORMAT,
DEFERRED_PREPASS_FORMAT, DEFERRED_PREPASS_FORMAT,
}, },
@ -179,9 +181,13 @@ impl Plugin for Core3dPlugin {
.add_render_sub_graph(Core3d) .add_render_sub_graph(Core3d)
.add_render_graph_node::<ViewNodeRunner<EarlyPrepassNode>>(Core3d, Node3d::EarlyPrepass) .add_render_graph_node::<ViewNodeRunner<EarlyPrepassNode>>(Core3d, Node3d::EarlyPrepass)
.add_render_graph_node::<ViewNodeRunner<LatePrepassNode>>(Core3d, Node3d::LatePrepass) .add_render_graph_node::<ViewNodeRunner<LatePrepassNode>>(Core3d, Node3d::LatePrepass)
.add_render_graph_node::<ViewNodeRunner<DeferredGBufferPrepassNode>>( .add_render_graph_node::<ViewNodeRunner<EarlyDeferredGBufferPrepassNode>>(
Core3d, Core3d,
Node3d::DeferredPrepass, Node3d::EarlyDeferredPrepass,
)
.add_render_graph_node::<ViewNodeRunner<LateDeferredGBufferPrepassNode>>(
Core3d,
Node3d::LateDeferredPrepass,
) )
.add_render_graph_node::<ViewNodeRunner<CopyDeferredLightingIdNode>>( .add_render_graph_node::<ViewNodeRunner<CopyDeferredLightingIdNode>>(
Core3d, Core3d,
@ -210,8 +216,9 @@ impl Plugin for Core3dPlugin {
Core3d, Core3d,
( (
Node3d::EarlyPrepass, Node3d::EarlyPrepass,
Node3d::EarlyDeferredPrepass,
Node3d::LatePrepass, Node3d::LatePrepass,
Node3d::DeferredPrepass, Node3d::LateDeferredPrepass,
Node3d::CopyDeferredLightingId, Node3d::CopyDeferredLightingId,
Node3d::EndPrepasses, Node3d::EndPrepasses,
Node3d::StartMainPass, Node3d::StartMainPass,
@ -943,7 +950,6 @@ fn configure_occlusion_culling_view_targets(
With<OcclusionCulling>, With<OcclusionCulling>,
Without<NoIndirectDrawing>, Without<NoIndirectDrawing>,
With<DepthPrepass>, With<DepthPrepass>,
Without<DeferredPrepass>,
), ),
>, >,
) { ) {

View File

@ -1,7 +1,8 @@
use bevy_ecs::{prelude::*, query::QueryItem}; use bevy_ecs::{prelude::*, query::QueryItem};
use bevy_render::experimental::occlusion_culling::OcclusionCulling;
use bevy_render::render_graph::ViewNode; use bevy_render::render_graph::ViewNode;
use bevy_render::view::ExtractedView; use bevy_render::view::{ExtractedView, NoIndirectDrawing};
use bevy_render::{ use bevy_render::{
camera::ExtractedCamera, camera::ExtractedCamera,
render_graph::{NodeRunError, RenderGraphContext}, render_graph::{NodeRunError, RenderGraphContext},
@ -18,30 +19,100 @@ use crate::prepass::ViewPrepassTextures;
use super::{AlphaMask3dDeferred, Opaque3dDeferred}; use super::{AlphaMask3dDeferred, Opaque3dDeferred};
/// Render node used by the prepass. /// The phase of the deferred prepass that draws meshes that were visible last
/// frame.
/// ///
/// By default, inserted before the main pass in the render graph. /// If occlusion culling isn't in use, this prepass simply draws all meshes.
///
/// Like all prepass nodes, this is inserted before the main pass in the render
/// graph.
#[derive(Default)] #[derive(Default)]
pub struct DeferredGBufferPrepassNode; pub struct EarlyDeferredGBufferPrepassNode;
impl ViewNode for DeferredGBufferPrepassNode { impl ViewNode for EarlyDeferredGBufferPrepassNode {
type ViewQuery = <LateDeferredGBufferPrepassNode as ViewNode>::ViewQuery;
fn run<'w>(
&self,
graph: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
view_query: QueryItem<'w, Self::ViewQuery>,
world: &'w World,
) -> Result<(), NodeRunError> {
run_deferred_prepass(
graph,
render_context,
view_query,
false,
world,
"early deferred prepass",
)
}
}
/// The phase of the prepass that runs after occlusion culling against the
/// meshes that were visible last frame.
///
/// If occlusion culling isn't in use, this is a no-op.
///
/// Like all prepass nodes, this is inserted before the main pass in the render
/// graph.
#[derive(Default)]
pub struct LateDeferredGBufferPrepassNode;
impl ViewNode for LateDeferredGBufferPrepassNode {
type ViewQuery = ( type ViewQuery = (
&'static ExtractedCamera, &'static ExtractedCamera,
&'static ExtractedView, &'static ExtractedView,
&'static ViewDepthTexture, &'static ViewDepthTexture,
&'static ViewPrepassTextures, &'static ViewPrepassTextures,
Has<OcclusionCulling>,
Has<NoIndirectDrawing>,
); );
fn run<'w>( fn run<'w>(
&self, &self,
graph: &mut RenderGraphContext, graph: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>, render_context: &mut RenderContext<'w>,
(camera, extracted_view, view_depth_texture, view_prepass_textures): QueryItem< view_query: QueryItem<'w, Self::ViewQuery>,
'w,
Self::ViewQuery,
>,
world: &'w World, world: &'w World,
) -> Result<(), NodeRunError> { ) -> Result<(), NodeRunError> {
let (_, _, _, _, occlusion_culling, no_indirect_drawing) = view_query;
if !occlusion_culling || no_indirect_drawing {
return Ok(());
}
run_deferred_prepass(
graph,
render_context,
view_query,
true,
world,
"late deferred prepass",
)
}
}
/// Runs the deferred prepass that draws all meshes to the depth buffer and
/// G-buffers.
///
/// If occlusion culling isn't in use, and a prepass is enabled, then there's
/// only one prepass. If occlusion culling is in use, then any prepass is split
/// into two: an *early* prepass and a *late* prepass. The early prepass draws
/// what was visible last frame, and the last prepass performs occlusion culling
/// against a conservative hierarchical Z buffer before drawing unoccluded
/// meshes.
fn run_deferred_prepass<'w>(
graph: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
(camera, extracted_view, view_depth_texture, view_prepass_textures, _, _): QueryItem<
'w,
<LateDeferredGBufferPrepassNode as ViewNode>::ViewQuery,
>,
is_late: bool,
world: &'w World,
label: &'static str,
) -> Result<(), NodeRunError> {
let (Some(opaque_deferred_phases), Some(alpha_mask_deferred_phases)) = ( let (Some(opaque_deferred_phases), Some(alpha_mask_deferred_phases)) = (
world.get_resource::<ViewBinnedRenderPhases<Opaque3dDeferred>>(), world.get_resource::<ViewBinnedRenderPhases<Opaque3dDeferred>>(),
world.get_resource::<ViewBinnedRenderPhases<AlphaMask3dDeferred>>(), world.get_resource::<ViewBinnedRenderPhases<AlphaMask3dDeferred>>(),
@ -76,18 +147,23 @@ impl ViewNode for DeferredGBufferPrepassNode {
// Appears to be unsupported: https://registry.khronos.org/webgl/specs/latest/2.0/#3.7.9 // Appears to be unsupported: https://registry.khronos.org/webgl/specs/latest/2.0/#3.7.9
// For webgl2 we fallback to manually clearing // For webgl2 we fallback to manually clearing
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
if !is_late {
if let Some(deferred_texture) = &view_prepass_textures.deferred { if let Some(deferred_texture) = &view_prepass_textures.deferred {
render_context.command_encoder().clear_texture( render_context.command_encoder().clear_texture(
&deferred_texture.texture.texture, &deferred_texture.texture.texture,
&bevy_render::render_resource::ImageSubresourceRange::default(), &bevy_render::render_resource::ImageSubresourceRange::default(),
); );
} }
}
color_attachments.push( color_attachments.push(
view_prepass_textures view_prepass_textures
.deferred .deferred
.as_ref() .as_ref()
.map(|deferred_texture| { .map(|deferred_texture| {
if is_late {
deferred_texture.get_attachment()
} else {
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))] #[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
{ {
bevy_render::render_resource::RenderPassColorAttachment { bevy_render::render_resource::RenderPassColorAttachment {
@ -105,6 +181,7 @@ impl ViewNode for DeferredGBufferPrepassNode {
feature = "webgpu" feature = "webgpu"
))] ))]
deferred_texture.get_attachment() deferred_texture.get_attachment()
}
}), }),
); );
@ -128,14 +205,13 @@ impl ViewNode for DeferredGBufferPrepassNode {
let _deferred_span = info_span!("deferred_prepass").entered(); let _deferred_span = info_span!("deferred_prepass").entered();
// Command encoder setup // Command encoder setup
let mut command_encoder = let mut command_encoder = render_device.create_command_encoder(&CommandEncoderDescriptor {
render_device.create_command_encoder(&CommandEncoderDescriptor {
label: Some("deferred_prepass_command_encoder"), label: Some("deferred_prepass_command_encoder"),
}); });
// Render pass setup // Render pass setup
let render_pass = command_encoder.begin_render_pass(&RenderPassDescriptor { let render_pass = command_encoder.begin_render_pass(&RenderPassDescriptor {
label: Some("deferred_prepass"), label: Some(label),
color_attachments: &color_attachments, color_attachments: &color_attachments,
depth_stencil_attachment, depth_stencil_attachment,
timestamp_writes: None, timestamp_writes: None,
@ -153,8 +229,7 @@ impl ViewNode for DeferredGBufferPrepassNode {
{ {
#[cfg(feature = "trace")] #[cfg(feature = "trace")]
let _opaque_prepass_span = info_span!("opaque_deferred_prepass").entered(); let _opaque_prepass_span = info_span!("opaque_deferred_prepass").entered();
if let Err(err) = opaque_deferred_phase.render(&mut render_pass, world, view_entity) if let Err(err) = opaque_deferred_phase.render(&mut render_pass, world, view_entity) {
{
error!("Error encountered while rendering the opaque deferred phase {err:?}"); error!("Error encountered while rendering the opaque deferred phase {err:?}");
} }
} }
@ -163,12 +238,9 @@ impl ViewNode for DeferredGBufferPrepassNode {
if !alpha_mask_deferred_phase.is_empty() { if !alpha_mask_deferred_phase.is_empty() {
#[cfg(feature = "trace")] #[cfg(feature = "trace")]
let _alpha_mask_deferred_span = info_span!("alpha_mask_deferred_prepass").entered(); let _alpha_mask_deferred_span = info_span!("alpha_mask_deferred_prepass").entered();
if let Err(err) = if let Err(err) = alpha_mask_deferred_phase.render(&mut render_pass, world, view_entity)
alpha_mask_deferred_phase.render(&mut render_pass, world, view_entity)
{ {
error!( error!("Error encountered while rendering the alpha mask deferred phase {err:?}");
"Error encountered while rendering the alpha mask deferred phase {err:?}"
);
} }
} }
@ -187,5 +259,4 @@ impl ViewNode for DeferredGBufferPrepassNode {
}); });
Ok(()) Ok(())
}
} }

View File

@ -46,7 +46,7 @@ use crate::{
graph::{Core3d, Node3d}, graph::{Core3d, Node3d},
prepare_core_3d_depth_textures, prepare_core_3d_depth_textures,
}, },
prepass::{DeferredPrepass, DepthPrepass}, prepass::DepthPrepass,
}; };
/// Identifies the `downsample_depth.wgsl` shader. /// Identifies the `downsample_depth.wgsl` shader.
@ -93,9 +93,10 @@ impl Plugin for MipGenerationPlugin {
Core3d, Core3d,
( (
Node3d::EarlyPrepass, Node3d::EarlyPrepass,
Node3d::EarlyDeferredPrepass,
Node3d::EarlyDownsampleDepth, Node3d::EarlyDownsampleDepth,
Node3d::LatePrepass, Node3d::LatePrepass,
Node3d::DeferredPrepass, Node3d::LateDeferredPrepass,
), ),
) )
.add_render_graph_edges( .add_render_graph_edges(
@ -651,7 +652,6 @@ fn prepare_view_depth_pyramids(
With<OcclusionCulling>, With<OcclusionCulling>,
Without<NoIndirectDrawing>, Without<NoIndirectDrawing>,
With<DepthPrepass>, With<DepthPrepass>,
Without<DeferredPrepass>,
), ),
>, >,
) { ) {

View File

@ -66,6 +66,7 @@ impl ViewNode for LatePrepassNode {
Option<&'static PreviousViewUniformOffset>, Option<&'static PreviousViewUniformOffset>,
Has<OcclusionCulling>, Has<OcclusionCulling>,
Has<NoIndirectDrawing>, Has<NoIndirectDrawing>,
Has<DeferredPrepass>,
); );
fn run<'w>( fn run<'w>(
@ -77,7 +78,7 @@ impl ViewNode for LatePrepassNode {
) -> Result<(), NodeRunError> { ) -> Result<(), NodeRunError> {
// We only need a late prepass if we have occlusion culling and indirect // We only need a late prepass if we have occlusion culling and indirect
// drawing. // drawing.
let (_, _, _, _, _, _, _, _, _, occlusion_culling, no_indirect_drawing) = query; let (_, _, _, _, _, _, _, _, _, occlusion_culling, no_indirect_drawing, _) = query;
if !occlusion_culling || no_indirect_drawing { if !occlusion_culling || no_indirect_drawing {
return Ok(()); return Ok(());
} }
@ -110,10 +111,18 @@ fn run_prepass<'w>(
view_prev_uniform_offset, view_prev_uniform_offset,
_, _,
_, _,
has_deferred,
): QueryItem<'w, <LatePrepassNode as ViewNode>::ViewQuery>, ): QueryItem<'w, <LatePrepassNode as ViewNode>::ViewQuery>,
world: &'w World, world: &'w World,
label: &'static str, label: &'static str,
) -> Result<(), NodeRunError> { ) -> Result<(), NodeRunError> {
// If we're using deferred rendering, there will be a deferred prepass
// instead of this one. Just bail out so we don't have to bother looking at
// the empty bins.
if has_deferred {
return Ok(());
}
let (Some(opaque_prepass_phases), Some(alpha_mask_prepass_phases)) = ( let (Some(opaque_prepass_phases), Some(alpha_mask_prepass_phases)) = (
world.get_resource::<ViewBinnedRenderPhases<Opaque3dPrepass>>(), world.get_resource::<ViewBinnedRenderPhases<Opaque3dPrepass>>(),
world.get_resource::<ViewBinnedRenderPhases<AlphaMask3dPrepass>>(), world.get_resource::<ViewBinnedRenderPhases<AlphaMask3dPrepass>>(),

View File

@ -258,8 +258,6 @@ impl Plugin for MeshletPlugin {
NodeMeshlet::Prepass, NodeMeshlet::Prepass,
// //
NodeMeshlet::DeferredPrepass, NodeMeshlet::DeferredPrepass,
Node3d::DeferredPrepass,
Node3d::CopyDeferredLightingId,
Node3d::EndPrepasses, Node3d::EndPrepasses,
// //
Node3d::StartMainPass, Node3d::StartMainPass,

View File

@ -13,10 +13,7 @@ use bevy_asset::{load_internal_asset, weak_handle, Handle};
use bevy_core_pipeline::{ use bevy_core_pipeline::{
core_3d::graph::{Core3d, Node3d}, core_3d::graph::{Core3d, Node3d},
experimental::mip_generation::ViewDepthPyramid, experimental::mip_generation::ViewDepthPyramid,
prepass::{ prepass::{DepthPrepass, PreviousViewData, PreviousViewUniformOffset, PreviousViewUniforms},
DeferredPrepass, DepthPrepass, PreviousViewData, PreviousViewUniformOffset,
PreviousViewUniforms,
},
}; };
use bevy_derive::{Deref, DerefMut}; use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{ use bevy_ecs::{
@ -140,7 +137,6 @@ pub struct LateGpuPreprocessNode {
Without<NoIndirectDrawing>, Without<NoIndirectDrawing>,
With<OcclusionCulling>, With<OcclusionCulling>,
With<DepthPrepass>, With<DepthPrepass>,
Without<DeferredPrepass>,
), ),
>, >,
} }
@ -159,7 +155,6 @@ pub struct EarlyPrepassBuildIndirectParametersNode {
Without<SkipGpuPreprocess>, Without<SkipGpuPreprocess>,
Without<NoIndirectDrawing>, Without<NoIndirectDrawing>,
With<DepthPrepass>, With<DepthPrepass>,
Without<DeferredPrepass>,
), ),
>, >,
} }
@ -180,7 +175,6 @@ pub struct LatePrepassBuildIndirectParametersNode {
Without<NoIndirectDrawing>, Without<NoIndirectDrawing>,
With<DepthPrepass>, With<DepthPrepass>,
With<OcclusionCulling>, With<OcclusionCulling>,
Without<DeferredPrepass>,
), ),
>, >,
} }
@ -527,21 +521,18 @@ impl Plugin for GpuMeshPreprocessPlugin {
NodePbr::EarlyGpuPreprocess, NodePbr::EarlyGpuPreprocess,
NodePbr::EarlyPrepassBuildIndirectParameters, NodePbr::EarlyPrepassBuildIndirectParameters,
Node3d::EarlyPrepass, Node3d::EarlyPrepass,
Node3d::EarlyDeferredPrepass,
Node3d::EarlyDownsampleDepth, Node3d::EarlyDownsampleDepth,
NodePbr::LateGpuPreprocess, NodePbr::LateGpuPreprocess,
NodePbr::LatePrepassBuildIndirectParameters, NodePbr::LatePrepassBuildIndirectParameters,
Node3d::LatePrepass, Node3d::LatePrepass,
Node3d::LateDeferredPrepass,
NodePbr::MainBuildIndirectParameters, NodePbr::MainBuildIndirectParameters,
// Shadows don't currently support occlusion culling, so we // Shadows don't currently support occlusion culling, so we
// treat shadows as effectively the main phase for our // treat shadows as effectively the main phase for our
// purposes. // purposes.
NodePbr::ShadowPass, NodePbr::ShadowPass,
), ),
)
.add_render_graph_edge(
Core3d,
NodePbr::MainBuildIndirectParameters,
Node3d::DeferredPrepass,
); );
} }
} }

View File

@ -49,6 +49,9 @@ struct Args {
/// enable deferred shading /// enable deferred shading
#[argh(switch)] #[argh(switch)]
deferred: Option<bool>, deferred: Option<bool>,
/// spawn a light even if the scene already has one
#[argh(switch)]
add_light: Option<bool>,
} }
fn main() { fn main() {
@ -204,7 +207,7 @@ fn setup_scene_after_load(
} }
// Spawn a default light if the scene does not have one // Spawn a default light if the scene does not have one
if !scene_handle.has_light { if !scene_handle.has_light || args.add_light == Some(true) {
info!("Spawning a directional light"); info!("Spawning a directional light");
commands.spawn(( commands.spawn((
DirectionalLight::default(), DirectionalLight::default(),