Implement occlusion culling for directional light shadow maps. (#17951)

Two-phase occlusion culling can be helpful for shadow maps just as it
can for a prepass, in order to reduce vertex and alpha mask fragment
shading overhead. This patch implements occlusion culling for shadow
maps from directional lights, when the `OcclusionCulling` component is
present on the entities containing the lights. Shadow maps from point
lights are deferred to a follow-up patch. Much of this patch involves
expanding the hierarchical Z-buffer to cover shadow maps in addition to
standard view depth buffers.

The `scene_viewer` example has been updated to add `OcclusionCulling` to
the directional light that it creates.

This improved the performance of the rend3 sci-fi test scene when
enabling shadows.
This commit is contained in:
Patrick Walton 2025-02-20 21:56:15 -08:00 committed by GitHub
parent 28441337bb
commit 4880a231de
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 351 additions and 110 deletions

View File

@ -14,7 +14,7 @@ use bevy_ecs::{
component::Component,
entity::Entity,
prelude::{resource_exists, Without},
query::{QueryItem, With},
query::{Or, QueryState, With},
resource::Resource,
schedule::IntoSystemConfigs as _,
system::{lifetimeless::Read, Commands, Local, Query, Res, ResMut},
@ -22,8 +22,10 @@ use bevy_ecs::{
};
use bevy_math::{uvec2, UVec2, Vec4Swizzles as _};
use bevy_render::{
experimental::occlusion_culling::OcclusionCulling,
render_graph::{NodeRunError, RenderGraphApp, RenderGraphContext, ViewNode, ViewNodeRunner},
experimental::occlusion_culling::{
OcclusionCulling, OcclusionCullingSubview, OcclusionCullingSubviewEntities,
},
render_graph::{Node, NodeRunError, RenderGraphApp, RenderGraphContext},
render_resource::{
binding_types::{sampler, texture_2d, texture_2d_multisampled, texture_storage_2d},
BindGroup, BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries,
@ -41,12 +43,9 @@ use bevy_render::{
};
use bitflags::bitflags;
use crate::{
core_3d::{
graph::{Core3d, Node3d},
prepare_core_3d_depth_textures,
},
prepass::DepthPrepass,
use crate::core_3d::{
graph::{Core3d, Node3d},
prepare_core_3d_depth_textures,
};
/// Identifies the `downsample_depth.wgsl` shader.
@ -81,14 +80,8 @@ impl Plugin for MipGenerationPlugin {
render_app
.init_resource::<SpecializedComputePipelines<DownsampleDepthPipeline>>()
.add_render_graph_node::<ViewNodeRunner<DownsampleDepthNode>>(
Core3d,
Node3d::EarlyDownsampleDepth,
)
.add_render_graph_node::<ViewNodeRunner<DownsampleDepthNode>>(
Core3d,
Node3d::LateDownsampleDepth,
)
.add_render_graph_node::<DownsampleDepthNode>(Core3d, Node3d::EarlyDownsampleDepth)
.add_render_graph_node::<DownsampleDepthNode>(Core3d, Node3d::LateDownsampleDepth)
.add_render_graph_edges(
Core3d,
(
@ -137,7 +130,7 @@ impl Plugin for MipGenerationPlugin {
///
/// This runs the single-pass downsampling (SPD) shader with the *min* filter in
/// order to generate a series of mipmaps for the Z buffer. The resulting
/// hierarchical Z buffer can be used for occlusion culling.
/// hierarchical Z-buffer can be used for occlusion culling.
///
/// There are two instances of this node. The *early* downsample depth pass is
/// the first hierarchical Z-buffer stage, which runs after the early prepass
@ -148,79 +141,150 @@ impl Plugin for MipGenerationPlugin {
/// of the *next* frame will perform.
///
/// This node won't do anything if occlusion culling isn't on.
#[derive(Default)]
pub struct DownsampleDepthNode;
impl ViewNode for DownsampleDepthNode {
type ViewQuery = (
pub struct DownsampleDepthNode {
/// The query that we use to find views that need occlusion culling for
/// their Z-buffer.
main_view_query: QueryState<(
Read<ViewDepthPyramid>,
Read<ViewDownsampleDepthBindGroup>,
Read<ViewDepthTexture>,
);
Option<Read<OcclusionCullingSubviewEntities>>,
)>,
/// The query that we use to find shadow maps that need occlusion culling.
shadow_view_query: QueryState<(
Read<ViewDepthPyramid>,
Read<ViewDownsampleDepthBindGroup>,
Read<OcclusionCullingSubview>,
)>,
}
impl FromWorld for DownsampleDepthNode {
fn from_world(world: &mut World) -> Self {
Self {
main_view_query: QueryState::new(world),
shadow_view_query: QueryState::new(world),
}
}
}
impl Node for DownsampleDepthNode {
fn update(&mut self, world: &mut World) {
self.main_view_query.update_archetypes(world);
self.shadow_view_query.update_archetypes(world);
}
fn run<'w>(
&self,
render_graph_context: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
(view_depth_pyramid, view_downsample_depth_bind_group, view_depth_texture): QueryItem<
'w,
Self::ViewQuery,
>,
world: &'w World,
) -> Result<(), NodeRunError> {
// Produce a depth pyramid from the current depth buffer for a single
// view. The resulting depth pyramid can be used for occlusion testing.
let downsample_depth_pipelines = world.resource::<DownsampleDepthPipelines>();
let pipeline_cache = world.resource::<PipelineCache>();
// Despite the name "single-pass downsampling", we actually need two
// passes because of the lack of `coherent` buffers in WGPU/WGSL.
// Between each pass, there's an implicit synchronization barrier.
// Fetch the appropriate pipeline ID, depending on whether the depth
// buffer is multisampled or not.
let (Some(first_downsample_depth_pipeline_id), Some(second_downsample_depth_pipeline_id)) =
(if view_depth_texture.texture.sample_count() > 1 {
(
downsample_depth_pipelines.first_multisample.pipeline_id,
downsample_depth_pipelines.second_multisample.pipeline_id,
)
} else {
(
downsample_depth_pipelines.first.pipeline_id,
downsample_depth_pipelines.second.pipeline_id,
)
})
let Ok((
view_depth_pyramid,
view_downsample_depth_bind_group,
view_depth_texture,
maybe_view_light_entities,
)) = self
.main_view_query
.get_manual(world, render_graph_context.view_entity())
else {
return Ok(());
};
// Fetch the pipelines for the two passes.
let (Some(first_downsample_depth_pipeline), Some(second_downsample_depth_pipeline)) = (
pipeline_cache.get_compute_pipeline(first_downsample_depth_pipeline_id),
pipeline_cache.get_compute_pipeline(second_downsample_depth_pipeline_id),
) else {
return Ok(());
};
// Run the depth downsampling.
let view_size = uvec2(
view_depth_texture.texture.width(),
view_depth_texture.texture.height(),
);
view_depth_pyramid.downsample_depth(
&format!("{:?}", render_graph_context.label()),
// Downsample depth for the main Z-buffer.
downsample_depth(
render_graph_context,
render_context,
view_size,
world,
view_depth_pyramid,
view_downsample_depth_bind_group,
first_downsample_depth_pipeline,
second_downsample_depth_pipeline,
);
uvec2(
view_depth_texture.texture.width(),
view_depth_texture.texture.height(),
),
view_depth_texture.texture.sample_count(),
)?;
// Downsample depth for shadow maps that have occlusion culling enabled.
if let Some(view_light_entities) = maybe_view_light_entities {
for &view_light_entity in &view_light_entities.0 {
let Ok((view_depth_pyramid, view_downsample_depth_bind_group, occlusion_culling)) =
self.shadow_view_query.get_manual(world, view_light_entity)
else {
continue;
};
downsample_depth(
render_graph_context,
render_context,
world,
view_depth_pyramid,
view_downsample_depth_bind_group,
UVec2::splat(occlusion_culling.depth_texture_size),
1,
)?;
}
}
Ok(())
}
}
/// Produces a depth pyramid from the current depth buffer for a single view.
/// The resulting depth pyramid can be used for occlusion testing.
fn downsample_depth<'w>(
render_graph_context: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
world: &'w World,
view_depth_pyramid: &ViewDepthPyramid,
view_downsample_depth_bind_group: &ViewDownsampleDepthBindGroup,
view_size: UVec2,
sample_count: u32,
) -> Result<(), NodeRunError> {
let downsample_depth_pipelines = world.resource::<DownsampleDepthPipelines>();
let pipeline_cache = world.resource::<PipelineCache>();
// Despite the name "single-pass downsampling", we actually need two
// passes because of the lack of `coherent` buffers in WGPU/WGSL.
// Between each pass, there's an implicit synchronization barrier.
// Fetch the appropriate pipeline ID, depending on whether the depth
// buffer is multisampled or not.
let (Some(first_downsample_depth_pipeline_id), Some(second_downsample_depth_pipeline_id)) =
(if sample_count > 1 {
(
downsample_depth_pipelines.first_multisample.pipeline_id,
downsample_depth_pipelines.second_multisample.pipeline_id,
)
} else {
(
downsample_depth_pipelines.first.pipeline_id,
downsample_depth_pipelines.second.pipeline_id,
)
})
else {
return Ok(());
};
// Fetch the pipelines for the two passes.
let (Some(first_downsample_depth_pipeline), Some(second_downsample_depth_pipeline)) = (
pipeline_cache.get_compute_pipeline(first_downsample_depth_pipeline_id),
pipeline_cache.get_compute_pipeline(second_downsample_depth_pipeline_id),
) else {
return Ok(());
};
// Run the depth downsampling.
view_depth_pyramid.downsample_depth(
&format!("{:?}", render_graph_context.label()),
render_context,
view_size,
view_downsample_depth_bind_group,
first_downsample_depth_pipeline,
second_downsample_depth_pipeline,
);
Ok(())
}
/// A single depth downsample pipeline.
#[derive(Resource)]
pub struct DownsampleDepthPipeline {
@ -641,19 +705,12 @@ impl ViewDepthPyramid {
}
/// Creates depth pyramids for views that have occlusion culling enabled.
fn prepare_view_depth_pyramids(
pub fn prepare_view_depth_pyramids(
mut commands: Commands,
render_device: Res<RenderDevice>,
mut texture_cache: ResMut<TextureCache>,
depth_pyramid_dummy_texture: Res<DepthPyramidDummyTexture>,
views: Query<
(Entity, &ExtractedView),
(
With<OcclusionCulling>,
Without<NoIndirectDrawing>,
With<DepthPrepass>,
),
>,
views: Query<(Entity, &ExtractedView), (With<OcclusionCulling>, Without<NoIndirectDrawing>)>,
) {
for (view_entity, view) in &views {
commands.entity(view_entity).insert(ViewDepthPyramid::new(
@ -680,10 +737,21 @@ fn prepare_downsample_depth_view_bind_groups(
mut commands: Commands,
render_device: Res<RenderDevice>,
downsample_depth_pipelines: Res<DownsampleDepthPipelines>,
view_depth_textures: Query<(Entity, &ViewDepthPyramid, &ViewDepthTexture)>,
view_depth_textures: Query<
(
Entity,
&ViewDepthPyramid,
Option<&ViewDepthTexture>,
Option<&OcclusionCullingSubview>,
),
Or<(With<ViewDepthTexture>, With<OcclusionCullingSubview>)>,
>,
) {
for (view_entity, view_depth_pyramid, view_depth_texture) in &view_depth_textures {
let is_multisampled = view_depth_texture.texture.sample_count() > 1;
for (view_entity, view_depth_pyramid, view_depth_texture, shadow_occlusion_culling) in
&view_depth_textures
{
let is_multisampled = view_depth_texture
.is_some_and(|view_depth_texture| view_depth_texture.texture.sample_count() > 1);
commands
.entity(view_entity)
.insert(ViewDownsampleDepthBindGroup(
@ -701,7 +769,13 @@ fn prepare_downsample_depth_view_bind_groups(
} else {
&downsample_depth_pipelines.first.bind_group_layout
},
view_depth_texture.view(),
match (view_depth_texture, shadow_occlusion_culling) {
(Some(view_depth_texture), _) => view_depth_texture.view(),
(None, Some(shadow_occlusion_culling)) => {
&shadow_occlusion_culling.depth_texture_view
}
(None, None) => panic!("Should never happen"),
},
&downsample_depth_pipelines.sampler,
),
));

View File

@ -87,22 +87,36 @@ pub mod prelude {
pub mod graph {
use bevy_render::render_graph::RenderLabel;
/// Render graph nodes specific to 3D PBR rendering.
#[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)]
pub enum NodePbr {
/// Label for the shadow pass node.
ShadowPass,
/// Label for the shadow pass node that draws meshes that were visible
/// from the light last frame.
EarlyShadowPass,
/// Label for the shadow pass node that draws meshes that became visible
/// from the light this frame.
LateShadowPass,
/// Label for the screen space ambient occlusion render node.
ScreenSpaceAmbientOcclusion,
DeferredLightingPass,
/// Label for the volumetric lighting pass.
VolumetricFog,
/// Label for the compute shader instance data building pass.
/// Label for the shader that transforms and culls meshes that were
/// visible last frame.
EarlyGpuPreprocess,
/// Label for the shader that transforms and culls meshes that became
/// visible this frame.
LateGpuPreprocess,
/// Label for the screen space reflections pass.
ScreenSpaceReflections,
/// Label for the node that builds indirect draw parameters for meshes
/// that were visible last frame.
EarlyPrepassBuildIndirectParameters,
/// Label for the node that builds indirect draw parameters for meshes
/// that became visible this frame.
LatePrepassBuildIndirectParameters,
/// Label for the node that builds indirect draw parameters for the main
/// rendering pass, containing all meshes that are visible this frame.
MainBuildIndirectParameters,
ClearIndirectParametersMetadata,
}
@ -473,11 +487,17 @@ impl Plugin for PbrPlugin {
.add_observer(remove_light_view_entities);
render_app.world_mut().add_observer(extracted_light_removed);
let shadow_pass_node = ShadowPassNode::new(render_app.world_mut());
let early_shadow_pass_node = EarlyShadowPassNode::from_world(render_app.world_mut());
let late_shadow_pass_node = LateShadowPassNode::from_world(render_app.world_mut());
let mut graph = render_app.world_mut().resource_mut::<RenderGraph>();
let draw_3d_graph = graph.get_sub_graph_mut(Core3d).unwrap();
draw_3d_graph.add_node(NodePbr::ShadowPass, shadow_pass_node);
draw_3d_graph.add_node_edge(NodePbr::ShadowPass, Node3d::StartMainPass);
draw_3d_graph.add_node(NodePbr::EarlyShadowPass, early_shadow_pass_node);
draw_3d_graph.add_node(NodePbr::LateShadowPass, late_shadow_pass_node);
draw_3d_graph.add_node_edges((
NodePbr::EarlyShadowPass,
NodePbr::LateShadowPass,
Node3d::StartMainPass,
));
}
fn finish(&self, app: &mut App) {

View File

@ -253,7 +253,7 @@ impl Plugin for MeshletPlugin {
Core3d,
(
NodeMeshlet::VisibilityBufferRasterPass,
NodePbr::ShadowPass,
NodePbr::EarlyShadowPass,
//
NodeMeshlet::Prepass,
//

View File

@ -20,7 +20,7 @@ use bevy_ecs::{
component::Component,
entity::Entity,
prelude::resource_exists,
query::{Has, QueryState, With, Without},
query::{Has, Or, QueryState, With, Without},
resource::Resource,
schedule::IntoSystemConfigs as _,
system::{lifetimeless::Read, Commands, Query, Res, ResMut},
@ -61,7 +61,7 @@ use crate::{
graph::NodePbr, MeshCullingData, MeshCullingDataBuffer, MeshInputUniform, MeshUniform,
};
use super::ViewLightEntities;
use super::{ShadowView, ViewLightEntities};
/// The handle to the `mesh_preprocess.wgsl` compute shader.
pub const MESH_PREPROCESS_SHADER_HANDLE: Handle<Shader> =
@ -154,7 +154,7 @@ pub struct EarlyPrepassBuildIndirectParametersNode {
(
Without<SkipGpuPreprocess>,
Without<NoIndirectDrawing>,
With<DepthPrepass>,
Or<(With<DepthPrepass>, With<ShadowView>)>,
),
>,
}
@ -173,7 +173,7 @@ pub struct LatePrepassBuildIndirectParametersNode {
(
Without<SkipGpuPreprocess>,
Without<NoIndirectDrawing>,
With<DepthPrepass>,
Or<(With<DepthPrepass>, With<ShadowView>)>,
With<OcclusionCulling>,
),
>,
@ -528,11 +528,22 @@ impl Plugin for GpuMeshPreprocessPlugin {
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,
Node3d::StartMainPass,
),
).add_render_graph_edges(
Core3d,
(
NodePbr::EarlyPrepassBuildIndirectParameters,
NodePbr::EarlyShadowPass,
Node3d::EarlyDownsampleDepth,
)
).add_render_graph_edges(
Core3d,
(
NodePbr::LatePrepassBuildIndirectParameters,
NodePbr::LateShadowPass,
NodePbr::MainBuildIndirectParameters,
)
);
}
}

View File

@ -15,6 +15,9 @@ use bevy_ecs::{
use bevy_math::{ops, Mat4, UVec4, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles};
use bevy_platform_support::collections::{HashMap, HashSet};
use bevy_platform_support::hash::FixedHasher;
use bevy_render::experimental::occlusion_culling::{
OcclusionCulling, OcclusionCullingSubview, OcclusionCullingSubviewEntities,
};
use bevy_render::sync_world::MainEntityHashMap;
use bevy_render::{
batching::gpu_preprocessing::{GpuPreprocessingMode, GpuPreprocessingSupport},
@ -82,6 +85,8 @@ pub struct ExtractedDirectionalLight {
pub frusta: EntityHashMap<Vec<Frustum>>,
pub render_layers: RenderLayers,
pub soft_shadow_size: Option<f32>,
/// True if this light is using two-phase occlusion culling.
pub occlusion_culling: bool,
}
// NOTE: These must match the bit flags in bevy_pbr/src/render/mesh_view_types.wgsl!
@ -254,6 +259,7 @@ pub fn extract_lights(
&ViewVisibility,
Option<&RenderLayers>,
Option<&VolumetricLight>,
Has<OcclusionCulling>,
),
Without<SpotLight>,
>,
@ -418,6 +424,7 @@ pub fn extract_lights(
view_visibility,
maybe_layers,
volumetric_light,
occlusion_culling,
) in &directional_lights
{
if !view_visibility.get() {
@ -483,6 +490,7 @@ pub fn extract_lights(
cascades: extracted_cascades,
frusta: extracted_frusta,
render_layers: maybe_layers.unwrap_or_default().clone(),
occlusion_culling,
},
RenderCascadesVisibleEntities {
entities: cascade_visible_entities,
@ -1148,7 +1156,9 @@ pub fn prepare_lights(
.filter_map(|sorted_camera| views.get(sorted_camera.entity).ok())
{
live_views.insert(entity);
let mut view_lights = Vec::new();
let mut view_occlusion_culling_lights = Vec::new();
let gpu_preprocessing_mode = gpu_preprocessing_support.min(if !no_indirect_drawing {
GpuPreprocessingMode::Culling
@ -1494,7 +1504,7 @@ pub fn prepare_lights(
// NOTE: For point and spotlights, we reuse the same depth attachment for all views.
// However, for directional lights, we want a new depth attachment for each view,
// so that the view is cleared for each view.
let depth_attachment = DepthAttachment::new(depth_texture_view, Some(0.0));
let depth_attachment = DepthAttachment::new(depth_texture_view.clone(), Some(0.0));
directional_depth_texture_array_index += 1;
@ -1543,6 +1553,18 @@ pub fn prepare_lights(
view_lights.push(view_light_entity);
// If this light is using occlusion culling, add the appropriate components.
if light.occlusion_culling {
commands.entity(view_light_entity).insert((
OcclusionCulling,
OcclusionCullingSubview {
depth_texture_view,
depth_texture_size: directional_light_shadow_map.size as u32,
},
));
view_occlusion_culling_lights.push(view_light_entity);
}
// Subsequent views with the same light entity will **NOT** reuse the same shadow map
// (Because the cascades are unique to each view)
// TODO: Implement GPU culling for shadow passes.
@ -1566,6 +1588,16 @@ pub fn prepare_lights(
offset: view_gpu_lights_writer.write(&gpu_lights),
},
));
// Make a link from the camera to all shadow cascades with occlusion
// culling enabled.
if !view_occlusion_culling_lights.is_empty() {
commands
.entity(entity)
.insert(OcclusionCullingSubviewEntities(
view_occlusion_culling_lights,
));
}
}
// Despawn light-view entities for views that no longer exist
@ -2115,13 +2147,44 @@ impl CachedRenderPipelinePhaseItem for Shadow {
}
}
/// The rendering node that renders meshes that were "visible" (so to speak)
/// from a light last frame.
///
/// If occlusion culling for a light is disabled, then this node simply renders
/// all meshes in range of the light.
#[derive(Deref, DerefMut)]
pub struct EarlyShadowPassNode(ShadowPassNode);
/// The rendering node that renders meshes that became newly "visible" (so to
/// speak) from a light this frame.
///
/// If occlusion culling for a light is disabled, then this node does nothing.
#[derive(Deref, DerefMut)]
pub struct LateShadowPassNode(ShadowPassNode);
/// Encapsulates rendering logic shared between the early and late shadow pass
/// nodes.
pub struct ShadowPassNode {
/// The query that finds cameras in which shadows are visible.
main_view_query: QueryState<Read<ViewLightEntities>>,
view_light_query: QueryState<(Read<ShadowView>, Read<ExtractedView>)>,
/// The query that finds shadow cascades.
view_light_query: QueryState<(Read<ShadowView>, Read<ExtractedView>, Has<OcclusionCulling>)>,
}
impl ShadowPassNode {
pub fn new(world: &mut World) -> Self {
impl FromWorld for EarlyShadowPassNode {
fn from_world(world: &mut World) -> Self {
Self(ShadowPassNode::from_world(world))
}
}
impl FromWorld for LateShadowPassNode {
fn from_world(world: &mut World) -> Self {
Self(ShadowPassNode::from_world(world))
}
}
impl FromWorld for ShadowPassNode {
fn from_world(world: &mut World) -> Self {
Self {
main_view_query: QueryState::new(world),
view_light_query: QueryState::new(world),
@ -2129,10 +2192,9 @@ impl ShadowPassNode {
}
}
impl Node for ShadowPassNode {
impl Node for EarlyShadowPassNode {
fn update(&mut self, world: &mut World) {
self.main_view_query.update_archetypes(world);
self.view_light_query.update_archetypes(world);
self.0.update(world);
}
fn run<'w>(
@ -2140,6 +2202,42 @@ impl Node for ShadowPassNode {
graph: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
world: &'w World,
) -> Result<(), NodeRunError> {
self.0.run(graph, render_context, world, false)
}
}
impl Node for LateShadowPassNode {
fn update(&mut self, world: &mut World) {
self.0.update(world);
}
fn run<'w>(
&self,
graph: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
world: &'w World,
) -> Result<(), NodeRunError> {
self.0.run(graph, render_context, world, true)
}
}
impl ShadowPassNode {
fn update(&mut self, world: &mut World) {
self.main_view_query.update_archetypes(world);
self.view_light_query.update_archetypes(world);
}
/// Runs the node logic.
///
/// `is_late` is true if this is the late shadow pass or false if this is
/// the early shadow pass.
fn run<'w>(
&self,
graph: &mut RenderGraphContext,
render_context: &mut RenderContext<'w>,
world: &'w World,
is_late: bool,
) -> Result<(), NodeRunError> {
let diagnostics = render_context.diagnostic_recorder();
@ -2154,12 +2252,18 @@ impl Node for ShadowPassNode {
if let Ok(view_lights) = self.main_view_query.get_manual(world, view_entity) {
for view_light_entity in view_lights.lights.iter().copied() {
let Ok((view_light, extracted_light_view)) =
let Ok((view_light, extracted_light_view, occlusion_culling)) =
self.view_light_query.get_manual(world, view_light_entity)
else {
continue;
};
// There's no need for a late shadow pass if the light isn't
// using occlusion culling.
if is_late && !occlusion_culling {
continue;
}
let Some(shadow_phase) =
shadow_render_phases.get(&extracted_light_view.retained_view_entity)
else {

View File

@ -5,10 +5,13 @@
use bevy_app::{App, Plugin};
use bevy_asset::{load_internal_asset, weak_handle, Handle};
use bevy_ecs::{component::Component, prelude::ReflectComponent};
use bevy_ecs::{component::Component, entity::Entity, prelude::ReflectComponent};
use bevy_reflect::{prelude::ReflectDefault, Reflect};
use crate::{extract_component::ExtractComponent, render_resource::Shader};
use crate::{
extract_component::ExtractComponent,
render_resource::{Shader, TextureView},
};
/// The handle to the `mesh_preprocess_types.wgsl` compute shader.
pub const MESH_PREPROCESS_TYPES_SHADER_HANDLE: Handle<Shader> =
@ -85,3 +88,29 @@ impl Plugin for OcclusionCullingPlugin {
#[derive(Component, ExtractComponent, Clone, Copy, Default, Reflect)]
#[reflect(Component, Default)]
pub struct OcclusionCulling;
/// A render-world component that contains resources necessary to perform
/// occlusion culling on any view other than a camera.
///
/// Bevy automatically places this component on views created for shadow
/// mapping. You don't ordinarily need to add this component yourself.
#[derive(Clone, Component)]
pub struct OcclusionCullingSubview {
/// A texture view of the Z-buffer.
pub depth_texture_view: TextureView,
/// The size of the texture along both dimensions.
///
/// Because [`OcclusionCullingSubview`] is only currently used for shadow
/// maps, they're guaranteed to have sizes equal to a power of two, so we
/// don't have to store the two dimensions individually here.
pub depth_texture_size: u32,
}
/// A render-world component placed on each camera that stores references to all
/// entities other than cameras that need occlusion culling.
///
/// Bevy automatically places this component on cameras that are drawing
/// shadows, when those shadows come from lights with occlusion culling enabled.
/// You don't ordinarily need to add this component yourself.
#[derive(Clone, Component)]
pub struct OcclusionCullingSubviewEntities(pub Vec<Entity>);

View File

@ -209,10 +209,13 @@ fn setup_scene_after_load(
// Spawn a default light if the scene does not have one
if !scene_handle.has_light || args.add_light == Some(true) {
info!("Spawning a directional light");
commands.spawn((
let mut light = commands.spawn((
DirectionalLight::default(),
Transform::from_xyz(1.0, 1.0, 0.0).looking_at(Vec3::ZERO, Vec3::Y),
));
if args.occlusion_culling == Some(true) {
light.insert(OcclusionCulling);
}
scene_handle.has_light = true;
}