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:
parent
28441337bb
commit
4880a231de
@ -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,
|
||||
),
|
||||
));
|
||||
|
@ -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) {
|
||||
|
@ -253,7 +253,7 @@ impl Plugin for MeshletPlugin {
|
||||
Core3d,
|
||||
(
|
||||
NodeMeshlet::VisibilityBufferRasterPass,
|
||||
NodePbr::ShadowPass,
|
||||
NodePbr::EarlyShadowPass,
|
||||
//
|
||||
NodeMeshlet::Prepass,
|
||||
//
|
||||
|
@ -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,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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>);
|
||||
|
@ -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;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user