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

View File

@ -1,7 +1,8 @@
use bevy_ecs::{prelude::*, query::QueryItem};
use bevy_render::experimental::occlusion_culling::OcclusionCulling;
use bevy_render::render_graph::ViewNode;
use bevy_render::view::ExtractedView;
use bevy_render::view::{ExtractedView, NoIndirectDrawing};
use bevy_render::{
camera::ExtractedCamera,
render_graph::{NodeRunError, RenderGraphContext},
@ -18,76 +19,151 @@ use crate::prepass::ViewPrepassTextures;
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)]
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 = (
&'static ExtractedCamera,
&'static ExtractedView,
&'static ViewDepthTexture,
&'static ViewPrepassTextures,
Has<OcclusionCulling>,
Has<NoIndirectDrawing>,
);
fn run<'w>(
&self,
graph: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
(camera, extracted_view, view_depth_texture, view_prepass_textures): QueryItem<
'w,
Self::ViewQuery,
>,
view_query: QueryItem<'w, Self::ViewQuery>,
world: &'w World,
) -> Result<(), NodeRunError> {
let (Some(opaque_deferred_phases), Some(alpha_mask_deferred_phases)) = (
world.get_resource::<ViewBinnedRenderPhases<Opaque3dDeferred>>(),
world.get_resource::<ViewBinnedRenderPhases<AlphaMask3dDeferred>>(),
) else {
let (_, _, _, _, occlusion_culling, no_indirect_drawing) = view_query;
if !occlusion_culling || no_indirect_drawing {
return Ok(());
};
}
let (Some(opaque_deferred_phase), Some(alpha_mask_deferred_phase)) = (
opaque_deferred_phases.get(&extracted_view.retained_view_entity),
alpha_mask_deferred_phases.get(&extracted_view.retained_view_entity),
) else {
return Ok(());
};
run_deferred_prepass(
graph,
render_context,
view_query,
true,
world,
"late deferred prepass",
)
}
}
let mut color_attachments = vec![];
color_attachments.push(
view_prepass_textures
.normal
.as_ref()
.map(|normals_texture| normals_texture.get_attachment()),
);
color_attachments.push(
view_prepass_textures
.motion_vectors
.as_ref()
.map(|motion_vectors_texture| motion_vectors_texture.get_attachment()),
);
/// 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)) = (
world.get_resource::<ViewBinnedRenderPhases<Opaque3dDeferred>>(),
world.get_resource::<ViewBinnedRenderPhases<AlphaMask3dDeferred>>(),
) else {
return Ok(());
};
// If we clear the deferred texture with LoadOp::Clear(Default::default()) we get these errors:
// Chrome: GL_INVALID_OPERATION: No defined conversion between clear value and attachment format.
// Firefox: WebGL warning: clearBufferu?[fi]v: This attachment is of type FLOAT, but this function is of type UINT.
// Appears to be unsupported: https://registry.khronos.org/webgl/specs/latest/2.0/#3.7.9
// For webgl2 we fallback to manually clearing
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
let (Some(opaque_deferred_phase), Some(alpha_mask_deferred_phase)) = (
opaque_deferred_phases.get(&extracted_view.retained_view_entity),
alpha_mask_deferred_phases.get(&extracted_view.retained_view_entity),
) else {
return Ok(());
};
let mut color_attachments = vec![];
color_attachments.push(
view_prepass_textures
.normal
.as_ref()
.map(|normals_texture| normals_texture.get_attachment()),
);
color_attachments.push(
view_prepass_textures
.motion_vectors
.as_ref()
.map(|motion_vectors_texture| motion_vectors_texture.get_attachment()),
);
// If we clear the deferred texture with LoadOp::Clear(Default::default()) we get these errors:
// Chrome: GL_INVALID_OPERATION: No defined conversion between clear value and attachment format.
// Firefox: WebGL warning: clearBufferu?[fi]v: This attachment is of type FLOAT, but this function is of type UINT.
// Appears to be unsupported: https://registry.khronos.org/webgl/specs/latest/2.0/#3.7.9
// For webgl2 we fallback to manually clearing
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
if !is_late {
if let Some(deferred_texture) = &view_prepass_textures.deferred {
render_context.command_encoder().clear_texture(
&deferred_texture.texture.texture,
&bevy_render::render_resource::ImageSubresourceRange::default(),
);
}
}
color_attachments.push(
view_prepass_textures
.deferred
.as_ref()
.map(|deferred_texture| {
color_attachments.push(
view_prepass_textures
.deferred
.as_ref()
.map(|deferred_texture| {
if is_late {
deferred_texture.get_attachment()
} else {
#[cfg(all(feature = "webgl", target_arch = "wasm32", not(feature = "webgpu")))]
{
bevy_render::render_resource::RenderPassColorAttachment {
@ -105,87 +181,82 @@ impl ViewNode for DeferredGBufferPrepassNode {
feature = "webgpu"
))]
deferred_texture.get_attachment()
}),
);
color_attachments.push(
view_prepass_textures
.deferred_lighting_pass_id
.as_ref()
.map(|deferred_lighting_pass_id| deferred_lighting_pass_id.get_attachment()),
);
// If all color attachments are none: clear the color attachment list so that no fragment shader is required
if color_attachments.iter().all(Option::is_none) {
color_attachments.clear();
}
let depth_stencil_attachment = Some(view_depth_texture.get_attachment(StoreOp::Store));
let view_entity = graph.view_entity();
render_context.add_command_buffer_generation_task(move |render_device| {
#[cfg(feature = "trace")]
let _deferred_span = info_span!("deferred_prepass").entered();
// Command encoder setup
let mut command_encoder =
render_device.create_command_encoder(&CommandEncoderDescriptor {
label: Some("deferred_prepass_command_encoder"),
});
// Render pass setup
let render_pass = command_encoder.begin_render_pass(&RenderPassDescriptor {
label: Some("deferred_prepass"),
color_attachments: &color_attachments,
depth_stencil_attachment,
timestamp_writes: None,
occlusion_query_set: None,
});
let mut render_pass = TrackedRenderPass::new(&render_device, render_pass);
if let Some(viewport) = camera.viewport.as_ref() {
render_pass.set_camera_viewport(viewport);
}
// Opaque draws
if !opaque_deferred_phase.multidrawable_meshes.is_empty()
|| !opaque_deferred_phase.batchable_meshes.is_empty()
|| !opaque_deferred_phase.unbatchable_meshes.is_empty()
{
#[cfg(feature = "trace")]
let _opaque_prepass_span = info_span!("opaque_deferred_prepass").entered();
if let Err(err) = opaque_deferred_phase.render(&mut render_pass, world, view_entity)
{
error!("Error encountered while rendering the opaque deferred phase {err:?}");
}
}
}),
);
// Alpha masked draws
if !alpha_mask_deferred_phase.is_empty() {
#[cfg(feature = "trace")]
let _alpha_mask_deferred_span = info_span!("alpha_mask_deferred_prepass").entered();
if let Err(err) =
alpha_mask_deferred_phase.render(&mut render_pass, world, view_entity)
{
error!(
"Error encountered while rendering the alpha mask deferred phase {err:?}"
);
}
}
color_attachments.push(
view_prepass_textures
.deferred_lighting_pass_id
.as_ref()
.map(|deferred_lighting_pass_id| deferred_lighting_pass_id.get_attachment()),
);
drop(render_pass);
// If all color attachments are none: clear the color attachment list so that no fragment shader is required
if color_attachments.iter().all(Option::is_none) {
color_attachments.clear();
}
// After rendering to the view depth texture, copy it to the prepass depth texture
if let Some(prepass_depth_texture) = &view_prepass_textures.depth {
command_encoder.copy_texture_to_texture(
view_depth_texture.texture.as_image_copy(),
prepass_depth_texture.texture.texture.as_image_copy(),
view_prepass_textures.size,
);
}
let depth_stencil_attachment = Some(view_depth_texture.get_attachment(StoreOp::Store));
command_encoder.finish()
let view_entity = graph.view_entity();
render_context.add_command_buffer_generation_task(move |render_device| {
#[cfg(feature = "trace")]
let _deferred_span = info_span!("deferred_prepass").entered();
// Command encoder setup
let mut command_encoder = render_device.create_command_encoder(&CommandEncoderDescriptor {
label: Some("deferred_prepass_command_encoder"),
});
Ok(())
}
// Render pass setup
let render_pass = command_encoder.begin_render_pass(&RenderPassDescriptor {
label: Some(label),
color_attachments: &color_attachments,
depth_stencil_attachment,
timestamp_writes: None,
occlusion_query_set: None,
});
let mut render_pass = TrackedRenderPass::new(&render_device, render_pass);
if let Some(viewport) = camera.viewport.as_ref() {
render_pass.set_camera_viewport(viewport);
}
// Opaque draws
if !opaque_deferred_phase.multidrawable_meshes.is_empty()
|| !opaque_deferred_phase.batchable_meshes.is_empty()
|| !opaque_deferred_phase.unbatchable_meshes.is_empty()
{
#[cfg(feature = "trace")]
let _opaque_prepass_span = info_span!("opaque_deferred_prepass").entered();
if let Err(err) = opaque_deferred_phase.render(&mut render_pass, world, view_entity) {
error!("Error encountered while rendering the opaque deferred phase {err:?}");
}
}
// Alpha masked draws
if !alpha_mask_deferred_phase.is_empty() {
#[cfg(feature = "trace")]
let _alpha_mask_deferred_span = info_span!("alpha_mask_deferred_prepass").entered();
if let Err(err) = alpha_mask_deferred_phase.render(&mut render_pass, world, view_entity)
{
error!("Error encountered while rendering the alpha mask deferred phase {err:?}");
}
}
drop(render_pass);
// After rendering to the view depth texture, copy it to the prepass depth texture
if let Some(prepass_depth_texture) = &view_prepass_textures.depth {
command_encoder.copy_texture_to_texture(
view_depth_texture.texture.as_image_copy(),
prepass_depth_texture.texture.texture.as_image_copy(),
view_prepass_textures.size,
);
}
command_encoder.finish()
});
Ok(())
}

View File

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

View File

@ -66,6 +66,7 @@ impl ViewNode for LatePrepassNode {
Option<&'static PreviousViewUniformOffset>,
Has<OcclusionCulling>,
Has<NoIndirectDrawing>,
Has<DeferredPrepass>,
);
fn run<'w>(
@ -77,7 +78,7 @@ impl ViewNode for LatePrepassNode {
) -> Result<(), NodeRunError> {
// We only need a late prepass if we have occlusion culling and indirect
// drawing.
let (_, _, _, _, _, _, _, _, _, occlusion_culling, no_indirect_drawing) = query;
let (_, _, _, _, _, _, _, _, _, occlusion_culling, no_indirect_drawing, _) = query;
if !occlusion_culling || no_indirect_drawing {
return Ok(());
}
@ -110,10 +111,18 @@ fn run_prepass<'w>(
view_prev_uniform_offset,
_,
_,
has_deferred,
): QueryItem<'w, <LatePrepassNode as ViewNode>::ViewQuery>,
world: &'w World,
label: &'static str,
) -> 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)) = (
world.get_resource::<ViewBinnedRenderPhases<Opaque3dPrepass>>(),
world.get_resource::<ViewBinnedRenderPhases<AlphaMask3dPrepass>>(),

View File

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

View File

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

View File

@ -49,6 +49,9 @@ struct Args {
/// enable deferred shading
#[argh(switch)]
deferred: Option<bool>,
/// spawn a light even if the scene already has one
#[argh(switch)]
add_light: Option<bool>,
}
fn main() {
@ -204,7 +207,7 @@ fn setup_scene_after_load(
}
// 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");
commands.spawn((
DirectionalLight::default(),