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,
|
component::Component,
|
||||||
entity::Entity,
|
entity::Entity,
|
||||||
prelude::{resource_exists, Without},
|
prelude::{resource_exists, Without},
|
||||||
query::{QueryItem, With},
|
query::{Or, QueryState, With},
|
||||||
resource::Resource,
|
resource::Resource,
|
||||||
schedule::IntoSystemConfigs as _,
|
schedule::IntoSystemConfigs as _,
|
||||||
system::{lifetimeless::Read, Commands, Local, Query, Res, ResMut},
|
system::{lifetimeless::Read, Commands, Local, Query, Res, ResMut},
|
||||||
@ -22,8 +22,10 @@ use bevy_ecs::{
|
|||||||
};
|
};
|
||||||
use bevy_math::{uvec2, UVec2, Vec4Swizzles as _};
|
use bevy_math::{uvec2, UVec2, Vec4Swizzles as _};
|
||||||
use bevy_render::{
|
use bevy_render::{
|
||||||
experimental::occlusion_culling::OcclusionCulling,
|
experimental::occlusion_culling::{
|
||||||
render_graph::{NodeRunError, RenderGraphApp, RenderGraphContext, ViewNode, ViewNodeRunner},
|
OcclusionCulling, OcclusionCullingSubview, OcclusionCullingSubviewEntities,
|
||||||
|
},
|
||||||
|
render_graph::{Node, NodeRunError, RenderGraphApp, RenderGraphContext},
|
||||||
render_resource::{
|
render_resource::{
|
||||||
binding_types::{sampler, texture_2d, texture_2d_multisampled, texture_storage_2d},
|
binding_types::{sampler, texture_2d, texture_2d_multisampled, texture_storage_2d},
|
||||||
BindGroup, BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries,
|
BindGroup, BindGroupEntries, BindGroupLayout, BindGroupLayoutEntries,
|
||||||
@ -41,12 +43,9 @@ use bevy_render::{
|
|||||||
};
|
};
|
||||||
use bitflags::bitflags;
|
use bitflags::bitflags;
|
||||||
|
|
||||||
use crate::{
|
use crate::core_3d::{
|
||||||
core_3d::{
|
graph::{Core3d, Node3d},
|
||||||
graph::{Core3d, Node3d},
|
prepare_core_3d_depth_textures,
|
||||||
prepare_core_3d_depth_textures,
|
|
||||||
},
|
|
||||||
prepass::DepthPrepass,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Identifies the `downsample_depth.wgsl` shader.
|
/// Identifies the `downsample_depth.wgsl` shader.
|
||||||
@ -81,14 +80,8 @@ impl Plugin for MipGenerationPlugin {
|
|||||||
|
|
||||||
render_app
|
render_app
|
||||||
.init_resource::<SpecializedComputePipelines<DownsampleDepthPipeline>>()
|
.init_resource::<SpecializedComputePipelines<DownsampleDepthPipeline>>()
|
||||||
.add_render_graph_node::<ViewNodeRunner<DownsampleDepthNode>>(
|
.add_render_graph_node::<DownsampleDepthNode>(Core3d, Node3d::EarlyDownsampleDepth)
|
||||||
Core3d,
|
.add_render_graph_node::<DownsampleDepthNode>(Core3d, Node3d::LateDownsampleDepth)
|
||||||
Node3d::EarlyDownsampleDepth,
|
|
||||||
)
|
|
||||||
.add_render_graph_node::<ViewNodeRunner<DownsampleDepthNode>>(
|
|
||||||
Core3d,
|
|
||||||
Node3d::LateDownsampleDepth,
|
|
||||||
)
|
|
||||||
.add_render_graph_edges(
|
.add_render_graph_edges(
|
||||||
Core3d,
|
Core3d,
|
||||||
(
|
(
|
||||||
@ -137,7 +130,7 @@ impl Plugin for MipGenerationPlugin {
|
|||||||
///
|
///
|
||||||
/// This runs the single-pass downsampling (SPD) shader with the *min* filter in
|
/// 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
|
/// 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
|
/// 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
|
/// 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.
|
/// of the *next* frame will perform.
|
||||||
///
|
///
|
||||||
/// This node won't do anything if occlusion culling isn't on.
|
/// This node won't do anything if occlusion culling isn't on.
|
||||||
#[derive(Default)]
|
pub struct DownsampleDepthNode {
|
||||||
pub struct DownsampleDepthNode;
|
/// The query that we use to find views that need occlusion culling for
|
||||||
|
/// their Z-buffer.
|
||||||
impl ViewNode for DownsampleDepthNode {
|
main_view_query: QueryState<(
|
||||||
type ViewQuery = (
|
|
||||||
Read<ViewDepthPyramid>,
|
Read<ViewDepthPyramid>,
|
||||||
Read<ViewDownsampleDepthBindGroup>,
|
Read<ViewDownsampleDepthBindGroup>,
|
||||||
Read<ViewDepthTexture>,
|
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>(
|
fn run<'w>(
|
||||||
&self,
|
&self,
|
||||||
render_graph_context: &mut RenderGraphContext,
|
render_graph_context: &mut RenderGraphContext,
|
||||||
render_context: &mut RenderContext<'w>,
|
render_context: &mut RenderContext<'w>,
|
||||||
(view_depth_pyramid, view_downsample_depth_bind_group, view_depth_texture): QueryItem<
|
|
||||||
'w,
|
|
||||||
Self::ViewQuery,
|
|
||||||
>,
|
|
||||||
world: &'w World,
|
world: &'w World,
|
||||||
) -> Result<(), NodeRunError> {
|
) -> Result<(), NodeRunError> {
|
||||||
// Produce a depth pyramid from the current depth buffer for a single
|
let Ok((
|
||||||
// view. The resulting depth pyramid can be used for occlusion testing.
|
view_depth_pyramid,
|
||||||
|
view_downsample_depth_bind_group,
|
||||||
let downsample_depth_pipelines = world.resource::<DownsampleDepthPipelines>();
|
view_depth_texture,
|
||||||
let pipeline_cache = world.resource::<PipelineCache>();
|
maybe_view_light_entities,
|
||||||
|
)) = self
|
||||||
// Despite the name "single-pass downsampling", we actually need two
|
.main_view_query
|
||||||
// passes because of the lack of `coherent` buffers in WGPU/WGSL.
|
.get_manual(world, render_graph_context.view_entity())
|
||||||
// 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,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
else {
|
else {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
};
|
};
|
||||||
|
|
||||||
// Fetch the pipelines for the two passes.
|
// Downsample depth for the main Z-buffer.
|
||||||
let (Some(first_downsample_depth_pipeline), Some(second_downsample_depth_pipeline)) = (
|
downsample_depth(
|
||||||
pipeline_cache.get_compute_pipeline(first_downsample_depth_pipeline_id),
|
render_graph_context,
|
||||||
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()),
|
|
||||||
render_context,
|
render_context,
|
||||||
view_size,
|
world,
|
||||||
|
view_depth_pyramid,
|
||||||
view_downsample_depth_bind_group,
|
view_downsample_depth_bind_group,
|
||||||
first_downsample_depth_pipeline,
|
uvec2(
|
||||||
second_downsample_depth_pipeline,
|
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(())
|
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.
|
/// A single depth downsample pipeline.
|
||||||
#[derive(Resource)]
|
#[derive(Resource)]
|
||||||
pub struct DownsampleDepthPipeline {
|
pub struct DownsampleDepthPipeline {
|
||||||
@ -641,19 +705,12 @@ impl ViewDepthPyramid {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Creates depth pyramids for views that have occlusion culling enabled.
|
/// Creates depth pyramids for views that have occlusion culling enabled.
|
||||||
fn prepare_view_depth_pyramids(
|
pub fn prepare_view_depth_pyramids(
|
||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
render_device: Res<RenderDevice>,
|
render_device: Res<RenderDevice>,
|
||||||
mut texture_cache: ResMut<TextureCache>,
|
mut texture_cache: ResMut<TextureCache>,
|
||||||
depth_pyramid_dummy_texture: Res<DepthPyramidDummyTexture>,
|
depth_pyramid_dummy_texture: Res<DepthPyramidDummyTexture>,
|
||||||
views: Query<
|
views: Query<(Entity, &ExtractedView), (With<OcclusionCulling>, Without<NoIndirectDrawing>)>,
|
||||||
(Entity, &ExtractedView),
|
|
||||||
(
|
|
||||||
With<OcclusionCulling>,
|
|
||||||
Without<NoIndirectDrawing>,
|
|
||||||
With<DepthPrepass>,
|
|
||||||
),
|
|
||||||
>,
|
|
||||||
) {
|
) {
|
||||||
for (view_entity, view) in &views {
|
for (view_entity, view) in &views {
|
||||||
commands.entity(view_entity).insert(ViewDepthPyramid::new(
|
commands.entity(view_entity).insert(ViewDepthPyramid::new(
|
||||||
@ -680,10 +737,21 @@ fn prepare_downsample_depth_view_bind_groups(
|
|||||||
mut commands: Commands,
|
mut commands: Commands,
|
||||||
render_device: Res<RenderDevice>,
|
render_device: Res<RenderDevice>,
|
||||||
downsample_depth_pipelines: Res<DownsampleDepthPipelines>,
|
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 {
|
for (view_entity, view_depth_pyramid, view_depth_texture, shadow_occlusion_culling) in
|
||||||
let is_multisampled = view_depth_texture.texture.sample_count() > 1;
|
&view_depth_textures
|
||||||
|
{
|
||||||
|
let is_multisampled = view_depth_texture
|
||||||
|
.is_some_and(|view_depth_texture| view_depth_texture.texture.sample_count() > 1);
|
||||||
commands
|
commands
|
||||||
.entity(view_entity)
|
.entity(view_entity)
|
||||||
.insert(ViewDownsampleDepthBindGroup(
|
.insert(ViewDownsampleDepthBindGroup(
|
||||||
@ -701,7 +769,13 @@ fn prepare_downsample_depth_view_bind_groups(
|
|||||||
} else {
|
} else {
|
||||||
&downsample_depth_pipelines.first.bind_group_layout
|
&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,
|
&downsample_depth_pipelines.sampler,
|
||||||
),
|
),
|
||||||
));
|
));
|
||||||
|
@ -87,22 +87,36 @@ pub mod prelude {
|
|||||||
pub mod graph {
|
pub mod graph {
|
||||||
use bevy_render::render_graph::RenderLabel;
|
use bevy_render::render_graph::RenderLabel;
|
||||||
|
|
||||||
|
/// Render graph nodes specific to 3D PBR rendering.
|
||||||
#[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)]
|
#[derive(Debug, Hash, PartialEq, Eq, Clone, RenderLabel)]
|
||||||
pub enum NodePbr {
|
pub enum NodePbr {
|
||||||
/// Label for the shadow pass node.
|
/// Label for the shadow pass node that draws meshes that were visible
|
||||||
ShadowPass,
|
/// 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.
|
/// Label for the screen space ambient occlusion render node.
|
||||||
ScreenSpaceAmbientOcclusion,
|
ScreenSpaceAmbientOcclusion,
|
||||||
DeferredLightingPass,
|
DeferredLightingPass,
|
||||||
/// Label for the volumetric lighting pass.
|
/// Label for the volumetric lighting pass.
|
||||||
VolumetricFog,
|
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,
|
EarlyGpuPreprocess,
|
||||||
|
/// Label for the shader that transforms and culls meshes that became
|
||||||
|
/// visible this frame.
|
||||||
LateGpuPreprocess,
|
LateGpuPreprocess,
|
||||||
/// Label for the screen space reflections pass.
|
/// Label for the screen space reflections pass.
|
||||||
ScreenSpaceReflections,
|
ScreenSpaceReflections,
|
||||||
|
/// Label for the node that builds indirect draw parameters for meshes
|
||||||
|
/// that were visible last frame.
|
||||||
EarlyPrepassBuildIndirectParameters,
|
EarlyPrepassBuildIndirectParameters,
|
||||||
|
/// Label for the node that builds indirect draw parameters for meshes
|
||||||
|
/// that became visible this frame.
|
||||||
LatePrepassBuildIndirectParameters,
|
LatePrepassBuildIndirectParameters,
|
||||||
|
/// Label for the node that builds indirect draw parameters for the main
|
||||||
|
/// rendering pass, containing all meshes that are visible this frame.
|
||||||
MainBuildIndirectParameters,
|
MainBuildIndirectParameters,
|
||||||
ClearIndirectParametersMetadata,
|
ClearIndirectParametersMetadata,
|
||||||
}
|
}
|
||||||
@ -473,11 +487,17 @@ impl Plugin for PbrPlugin {
|
|||||||
.add_observer(remove_light_view_entities);
|
.add_observer(remove_light_view_entities);
|
||||||
render_app.world_mut().add_observer(extracted_light_removed);
|
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 mut graph = render_app.world_mut().resource_mut::<RenderGraph>();
|
||||||
let draw_3d_graph = graph.get_sub_graph_mut(Core3d).unwrap();
|
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(NodePbr::EarlyShadowPass, early_shadow_pass_node);
|
||||||
draw_3d_graph.add_node_edge(NodePbr::ShadowPass, Node3d::StartMainPass);
|
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) {
|
fn finish(&self, app: &mut App) {
|
||||||
|
@ -253,7 +253,7 @@ impl Plugin for MeshletPlugin {
|
|||||||
Core3d,
|
Core3d,
|
||||||
(
|
(
|
||||||
NodeMeshlet::VisibilityBufferRasterPass,
|
NodeMeshlet::VisibilityBufferRasterPass,
|
||||||
NodePbr::ShadowPass,
|
NodePbr::EarlyShadowPass,
|
||||||
//
|
//
|
||||||
NodeMeshlet::Prepass,
|
NodeMeshlet::Prepass,
|
||||||
//
|
//
|
||||||
|
@ -20,7 +20,7 @@ use bevy_ecs::{
|
|||||||
component::Component,
|
component::Component,
|
||||||
entity::Entity,
|
entity::Entity,
|
||||||
prelude::resource_exists,
|
prelude::resource_exists,
|
||||||
query::{Has, QueryState, With, Without},
|
query::{Has, Or, QueryState, With, Without},
|
||||||
resource::Resource,
|
resource::Resource,
|
||||||
schedule::IntoSystemConfigs as _,
|
schedule::IntoSystemConfigs as _,
|
||||||
system::{lifetimeless::Read, Commands, Query, Res, ResMut},
|
system::{lifetimeless::Read, Commands, Query, Res, ResMut},
|
||||||
@ -61,7 +61,7 @@ use crate::{
|
|||||||
graph::NodePbr, MeshCullingData, MeshCullingDataBuffer, MeshInputUniform, MeshUniform,
|
graph::NodePbr, MeshCullingData, MeshCullingDataBuffer, MeshInputUniform, MeshUniform,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::ViewLightEntities;
|
use super::{ShadowView, ViewLightEntities};
|
||||||
|
|
||||||
/// The handle to the `mesh_preprocess.wgsl` compute shader.
|
/// The handle to the `mesh_preprocess.wgsl` compute shader.
|
||||||
pub const MESH_PREPROCESS_SHADER_HANDLE: Handle<Shader> =
|
pub const MESH_PREPROCESS_SHADER_HANDLE: Handle<Shader> =
|
||||||
@ -154,7 +154,7 @@ pub struct EarlyPrepassBuildIndirectParametersNode {
|
|||||||
(
|
(
|
||||||
Without<SkipGpuPreprocess>,
|
Without<SkipGpuPreprocess>,
|
||||||
Without<NoIndirectDrawing>,
|
Without<NoIndirectDrawing>,
|
||||||
With<DepthPrepass>,
|
Or<(With<DepthPrepass>, With<ShadowView>)>,
|
||||||
),
|
),
|
||||||
>,
|
>,
|
||||||
}
|
}
|
||||||
@ -173,7 +173,7 @@ pub struct LatePrepassBuildIndirectParametersNode {
|
|||||||
(
|
(
|
||||||
Without<SkipGpuPreprocess>,
|
Without<SkipGpuPreprocess>,
|
||||||
Without<NoIndirectDrawing>,
|
Without<NoIndirectDrawing>,
|
||||||
With<DepthPrepass>,
|
Or<(With<DepthPrepass>, With<ShadowView>)>,
|
||||||
With<OcclusionCulling>,
|
With<OcclusionCulling>,
|
||||||
),
|
),
|
||||||
>,
|
>,
|
||||||
@ -528,11 +528,22 @@ impl Plugin for GpuMeshPreprocessPlugin {
|
|||||||
Node3d::LatePrepass,
|
Node3d::LatePrepass,
|
||||||
Node3d::LateDeferredPrepass,
|
Node3d::LateDeferredPrepass,
|
||||||
NodePbr::MainBuildIndirectParameters,
|
NodePbr::MainBuildIndirectParameters,
|
||||||
// Shadows don't currently support occlusion culling, so we
|
Node3d::StartMainPass,
|
||||||
// treat shadows as effectively the main phase for our
|
|
||||||
// purposes.
|
|
||||||
NodePbr::ShadowPass,
|
|
||||||
),
|
),
|
||||||
|
).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_math::{ops, Mat4, UVec4, Vec2, Vec3, Vec3Swizzles, Vec4, Vec4Swizzles};
|
||||||
use bevy_platform_support::collections::{HashMap, HashSet};
|
use bevy_platform_support::collections::{HashMap, HashSet};
|
||||||
use bevy_platform_support::hash::FixedHasher;
|
use bevy_platform_support::hash::FixedHasher;
|
||||||
|
use bevy_render::experimental::occlusion_culling::{
|
||||||
|
OcclusionCulling, OcclusionCullingSubview, OcclusionCullingSubviewEntities,
|
||||||
|
};
|
||||||
use bevy_render::sync_world::MainEntityHashMap;
|
use bevy_render::sync_world::MainEntityHashMap;
|
||||||
use bevy_render::{
|
use bevy_render::{
|
||||||
batching::gpu_preprocessing::{GpuPreprocessingMode, GpuPreprocessingSupport},
|
batching::gpu_preprocessing::{GpuPreprocessingMode, GpuPreprocessingSupport},
|
||||||
@ -82,6 +85,8 @@ pub struct ExtractedDirectionalLight {
|
|||||||
pub frusta: EntityHashMap<Vec<Frustum>>,
|
pub frusta: EntityHashMap<Vec<Frustum>>,
|
||||||
pub render_layers: RenderLayers,
|
pub render_layers: RenderLayers,
|
||||||
pub soft_shadow_size: Option<f32>,
|
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!
|
// 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,
|
&ViewVisibility,
|
||||||
Option<&RenderLayers>,
|
Option<&RenderLayers>,
|
||||||
Option<&VolumetricLight>,
|
Option<&VolumetricLight>,
|
||||||
|
Has<OcclusionCulling>,
|
||||||
),
|
),
|
||||||
Without<SpotLight>,
|
Without<SpotLight>,
|
||||||
>,
|
>,
|
||||||
@ -418,6 +424,7 @@ pub fn extract_lights(
|
|||||||
view_visibility,
|
view_visibility,
|
||||||
maybe_layers,
|
maybe_layers,
|
||||||
volumetric_light,
|
volumetric_light,
|
||||||
|
occlusion_culling,
|
||||||
) in &directional_lights
|
) in &directional_lights
|
||||||
{
|
{
|
||||||
if !view_visibility.get() {
|
if !view_visibility.get() {
|
||||||
@ -483,6 +490,7 @@ pub fn extract_lights(
|
|||||||
cascades: extracted_cascades,
|
cascades: extracted_cascades,
|
||||||
frusta: extracted_frusta,
|
frusta: extracted_frusta,
|
||||||
render_layers: maybe_layers.unwrap_or_default().clone(),
|
render_layers: maybe_layers.unwrap_or_default().clone(),
|
||||||
|
occlusion_culling,
|
||||||
},
|
},
|
||||||
RenderCascadesVisibleEntities {
|
RenderCascadesVisibleEntities {
|
||||||
entities: cascade_visible_entities,
|
entities: cascade_visible_entities,
|
||||||
@ -1148,7 +1156,9 @@ pub fn prepare_lights(
|
|||||||
.filter_map(|sorted_camera| views.get(sorted_camera.entity).ok())
|
.filter_map(|sorted_camera| views.get(sorted_camera.entity).ok())
|
||||||
{
|
{
|
||||||
live_views.insert(entity);
|
live_views.insert(entity);
|
||||||
|
|
||||||
let mut view_lights = Vec::new();
|
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 {
|
let gpu_preprocessing_mode = gpu_preprocessing_support.min(if !no_indirect_drawing {
|
||||||
GpuPreprocessingMode::Culling
|
GpuPreprocessingMode::Culling
|
||||||
@ -1494,7 +1504,7 @@ pub fn prepare_lights(
|
|||||||
// NOTE: For point and spotlights, we reuse the same depth attachment for all views.
|
// 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,
|
// However, for directional lights, we want a new depth attachment for each view,
|
||||||
// so that the view is cleared 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;
|
directional_depth_texture_array_index += 1;
|
||||||
|
|
||||||
@ -1543,6 +1553,18 @@ pub fn prepare_lights(
|
|||||||
|
|
||||||
view_lights.push(view_light_entity);
|
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
|
// Subsequent views with the same light entity will **NOT** reuse the same shadow map
|
||||||
// (Because the cascades are unique to each view)
|
// (Because the cascades are unique to each view)
|
||||||
// TODO: Implement GPU culling for shadow passes.
|
// TODO: Implement GPU culling for shadow passes.
|
||||||
@ -1566,6 +1588,16 @@ pub fn prepare_lights(
|
|||||||
offset: view_gpu_lights_writer.write(&gpu_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
|
// 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 {
|
pub struct ShadowPassNode {
|
||||||
|
/// The query that finds cameras in which shadows are visible.
|
||||||
main_view_query: QueryState<Read<ViewLightEntities>>,
|
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 {
|
impl FromWorld for EarlyShadowPassNode {
|
||||||
pub fn new(world: &mut World) -> Self {
|
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 {
|
Self {
|
||||||
main_view_query: QueryState::new(world),
|
main_view_query: QueryState::new(world),
|
||||||
view_light_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) {
|
fn update(&mut self, world: &mut World) {
|
||||||
self.main_view_query.update_archetypes(world);
|
self.0.update(world);
|
||||||
self.view_light_query.update_archetypes(world);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run<'w>(
|
fn run<'w>(
|
||||||
@ -2140,6 +2202,42 @@ impl Node for ShadowPassNode {
|
|||||||
graph: &mut RenderGraphContext,
|
graph: &mut RenderGraphContext,
|
||||||
render_context: &mut RenderContext<'w>,
|
render_context: &mut RenderContext<'w>,
|
||||||
world: &'w World,
|
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> {
|
) -> Result<(), NodeRunError> {
|
||||||
let diagnostics = render_context.diagnostic_recorder();
|
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) {
|
if let Ok(view_lights) = self.main_view_query.get_manual(world, view_entity) {
|
||||||
for view_light_entity in view_lights.lights.iter().copied() {
|
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)
|
self.view_light_query.get_manual(world, view_light_entity)
|
||||||
else {
|
else {
|
||||||
continue;
|
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) =
|
let Some(shadow_phase) =
|
||||||
shadow_render_phases.get(&extracted_light_view.retained_view_entity)
|
shadow_render_phases.get(&extracted_light_view.retained_view_entity)
|
||||||
else {
|
else {
|
||||||
|
@ -5,10 +5,13 @@
|
|||||||
|
|
||||||
use bevy_app::{App, Plugin};
|
use bevy_app::{App, Plugin};
|
||||||
use bevy_asset::{load_internal_asset, weak_handle, Handle};
|
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 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.
|
/// The handle to the `mesh_preprocess_types.wgsl` compute shader.
|
||||||
pub const MESH_PREPROCESS_TYPES_SHADER_HANDLE: Handle<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)]
|
#[derive(Component, ExtractComponent, Clone, Copy, Default, Reflect)]
|
||||||
#[reflect(Component, Default)]
|
#[reflect(Component, Default)]
|
||||||
pub struct OcclusionCulling;
|
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
|
// Spawn a default light if the scene does not have one
|
||||||
if !scene_handle.has_light || args.add_light == Some(true) {
|
if !scene_handle.has_light || args.add_light == Some(true) {
|
||||||
info!("Spawning a directional light");
|
info!("Spawning a directional light");
|
||||||
commands.spawn((
|
let mut light = commands.spawn((
|
||||||
DirectionalLight::default(),
|
DirectionalLight::default(),
|
||||||
Transform::from_xyz(1.0, 1.0, 0.0).looking_at(Vec3::ZERO, Vec3::Y),
|
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;
|
scene_handle.has_light = true;
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user