diff --git a/crates/bevy_pbr/src/render/gpu_preprocess.rs b/crates/bevy_pbr/src/render/gpu_preprocess.rs index 33617abac86dc..3eff624d1454f 100644 --- a/crates/bevy_pbr/src/render/gpu_preprocess.rs +++ b/crates/bevy_pbr/src/render/gpu_preprocess.rs @@ -18,7 +18,7 @@ use bevy_core_pipeline::{ DeferredPrepass, DepthPrepass, MotionVectorPrepass, NormalPrepass, PreviousViewData, PreviousViewUniformOffset, PreviousViewUniforms, }, - schedule::{Core3d, Core3dSystems}, + schedule::{Core3d, Core3dSystems, RootNonCameraView}, }; use bevy_derive::{Deref, DerefMut}; use bevy_ecs::{ @@ -45,6 +45,7 @@ use bevy_render::{ PreprocessWorkItemBuffers, UntypedPhaseBatchedInstanceBuffers, UntypedPhaseIndirectParametersBuffers, }, + camera::ExtractedCamera, diagnostic::RecordDiagnostics as _, occlusion_culling::OcclusionCulling, render_phase::GpuRenderBinnedMeshInstance, @@ -61,14 +62,15 @@ use bevy_render::{ renderer::{RenderContext, RenderDevice, RenderQueue, ViewQuery}, settings::WgpuFeatures, view::{ - ExtractedView, NoIndirectDrawing, RenderVisibilityRanges, RetainedViewEntity, ViewUniform, - ViewUniformOffset, ViewUniforms, + ExtractedView, NoIndirectDrawing, PointAndSpotLightShadowPrimaryCamera, + RenderVisibilityRanges, RetainedViewEntity, ViewUniform, ViewUniformOffset, ViewUniforms, }, GpuResourceAppExt, Render, RenderApp, RenderSystems, }; use bevy_shader::Shader; use bevy_utils::{default, TypeIdMap}; use bitflags::bitflags; +use bytemuck::{Pod, Zeroable}; use smallvec::{smallvec, SmallVec}; use tracing::warn; @@ -291,7 +293,7 @@ pub enum PhasePreprocessBindGroups { }, /// The bind groups used for the compute shader when indirect drawing is - /// being used, but occlusion culling isn't being used. + /// being used and occlusion culling is being used. /// /// Because indirect drawing requires splitting the meshes into indexed and /// non-indexed meshes, and because occlusion culling requires splitting @@ -313,6 +315,13 @@ pub enum PhasePreprocessBindGroups { }, } +#[derive(Clone, Copy, Pod, Zeroable)] +#[repr(C)] +struct PreprocessImmediates { + cur_view_world_position: [f32; 3], + late_preprocess_work_item_indirect_offset: u32, +} + /// The bind groups for the compute shaders that reset indirect draw counts and /// build indirect parameters. /// @@ -600,7 +609,21 @@ pub fn unpack_bins( } pub fn early_gpu_preprocess( - current_view: ViewQuery, Without>, + current_view: ViewQuery< + ( + Option<&ViewLightEntities>, + &ExtractedView, + Has, + ), + Without, + >, + point_and_spot_light_shadow_primary_camera: Query< + &ExtractedView, + ( + With, + With, + ), + >, view_query: Query< ( &ExtractedView, @@ -630,9 +653,11 @@ pub fn early_gpu_preprocess( let pass_span = diagnostics.pass_span(&mut compute_pass, "early_mesh_preprocessing"); let view_entity = current_view.entity(); - let shadow_cascade_views = current_view.into_inner(); + let (shadow_cascade_views, extracted_view, has_non_root_view) = current_view.into_inner(); let all_views = gather_shadow_cascades_for_view(view_entity, shadow_cascade_views, &light_query); + let point_and_spot_light_shadow_camera_view = + point_and_spot_light_shadow_primary_camera.single(); // Run the compute passes. for view_entity in all_views { @@ -641,6 +666,26 @@ pub fn early_gpu_preprocess( else { continue; }; + // Set the camera position that will be used for visibility range culling. + let cur_view_world_position = if !has_non_root_view { + // Directional light shadows are made via cascaded shadow maps. + // These cascades are unique to each camera view (see RetainedViewEntity). + // For directional light shadows, the world position of the associated camera + // should be used for visibility range culling, not the world position of the shadow camera. + // The `CurrentView`'s `ExtractedView` contains the associated camera's world position. + extracted_view.world_from_view.translation().to_array() + } else { + // PointLight and SpotLight shadow views are handled in this else block. + // We could handle this case better if we can specify certain point and spot light shadow + // views to different cameras. Right now, it is all centralized to one user specified camera. + if let Ok(camera_view) = point_and_spot_light_shadow_camera_view { + camera_view.world_from_view.translation().to_array() + } else { + // There is no single designated primary camera to use for vis range culling of the shadows. + // Do not render them. + continue; + } + }; let Some(bind_groups) = bind_groups else { continue; @@ -746,10 +791,15 @@ pub fn early_gpu_preprocess( .. } = *work_item_buffers { - compute_pass.set_immediates( - 0, - bytemuck::bytes_of(&late_indirect_parameters_indexed_offset), - ); + let immediates = PreprocessImmediates { + cur_view_world_position, + late_preprocess_work_item_indirect_offset: + late_indirect_parameters_indexed_offset, + }; + compute_pass.set_immediates(0, bytemuck::bytes_of(&immediates)); + } else { + compute_pass + .set_immediates(0, bytemuck::bytes_of(&cur_view_world_position)); } compute_pass.set_bind_group(0, indexed_bind_group, &dynamic_offsets); @@ -770,10 +820,15 @@ pub fn early_gpu_preprocess( .. } = *work_item_buffers { - compute_pass.set_immediates( - 0, - bytemuck::bytes_of(&late_indirect_parameters_non_indexed_offset), - ); + let immediates = PreprocessImmediates { + cur_view_world_position, + late_preprocess_work_item_indirect_offset: + late_indirect_parameters_non_indexed_offset, + }; + compute_pass.set_immediates(0, bytemuck::bytes_of(&immediates)); + } else { + compute_pass + .set_immediates(0, bytemuck::bytes_of(&cur_view_world_position)); } compute_pass.set_bind_group(0, non_indexed_bind_group, &dynamic_offsets); @@ -1247,7 +1302,9 @@ impl SpecializedComputePipeline for PreprocessPipeline { ), layout: vec![self.bind_group_layout.clone()], immediate_size: if key.contains(PreprocessPipelineKey::OCCLUSION_CULLING) { - 4 + 16 + } else if key.contains(PreprocessPipelineKey::FRUSTUM_CULLING) { + 12 } else { 0 }, diff --git a/crates/bevy_pbr/src/render/mesh_preprocess.wgsl b/crates/bevy_pbr/src/render/mesh_preprocess.wgsl index 496cd6b6f84ab..f03f89095e85c 100644 --- a/crates/bevy_pbr/src/render/mesh_preprocess.wgsl +++ b/crates/bevy_pbr/src/render/mesh_preprocess.wgsl @@ -64,12 +64,27 @@ struct LatePreprocessWorkItemIndirectParameters { pad: vec4, } +#ifdef FRUSTUM_CULLING // These have to be in a structure because of Naga limitations on DX12. +struct Immediates { + // The world position of a camera view. + // This is important to distinguish when preprocessing shadow views, + // which is bound to `view`. Visibility range culling must use a camera view's + // position in order to function properly. + cur_view_world_position: vec3, +#ifdef OCCLUSION_CULLING + // The offset into the `late_preprocess_work_item_indirect_parameters` + // buffer. + late_preprocess_work_item_indirect_offset: u32, +#endif // OCCLUSION_CULLING +} +#else // FRUSTUM_CULLING struct Immediates { // The offset into the `late_preprocess_work_item_indirect_parameters` // buffer. late_preprocess_work_item_indirect_offset: u32, } +#endif // FRUSTUM_CULLING // The current frame's `MeshInput`. @group(0) @binding(3) var current_input: array; @@ -98,6 +113,8 @@ struct Immediates { @group(0) @binding(9) var mesh_culling_data: array; @group(0) @binding(10) var visibility_ranges: array>; + +var immediates: Immediates; #endif // FRUSTUM_CULLING #ifdef OCCLUSION_CULLING @@ -116,7 +133,9 @@ struct Immediates { array; #endif // LATE_PHASE +#ifndef FRUSTUM_CULLING var immediates: Immediates; +#endif // FRUSTUM_CULLING #endif // OCCLUSION_CULLING #ifdef FRUSTUM_CULLING @@ -224,7 +243,7 @@ fn main(@builtin(global_invocation_id) global_invocation_id: vec3) { world_pos = world_from_local[3].xyz; } - let camera_distance = length(position_world_to_view(world_pos)); + let camera_distance = length(immediates.cur_view_world_position - world_pos); // `x` is the minimum range; `w` is the largest range. if (camera_distance < lod_range.x || camera_distance >= lod_range.w) { return; diff --git a/crates/bevy_render/src/batching/gpu_preprocessing.rs b/crates/bevy_render/src/batching/gpu_preprocessing.rs index 95e0b3d804efc..c5eb0ecf98b30 100644 --- a/crates/bevy_render/src/batching/gpu_preprocessing.rs +++ b/crates/bevy_render/src/batching/gpu_preprocessing.rs @@ -1360,6 +1360,7 @@ impl FromWorld for GpuPreprocessingSupport { let max_supported_mode = if device.limits().max_compute_workgroup_size_x == 0 || is_non_supported_android_device(&adapter_info) || adapter_info.backend == wgpu::Backend::Gl + || !device.features().contains(Features::IMMEDIATES) { info_once!( "GPU preprocessing is not supported on this device. \ diff --git a/crates/bevy_render/src/camera.rs b/crates/bevy_render/src/camera.rs index 047187e2a9c2a..53203efc8ffd4 100644 --- a/crates/bevy_render/src/camera.rs +++ b/crates/bevy_render/src/camera.rs @@ -11,8 +11,9 @@ use crate::{ texture::{GpuImage, ManualTextureViews}, view::{ ColorGrading, ExtractedView, ExtractedWindows, Msaa, NoIndirectDrawing, - RenderExtractedVisibleEntities, RenderVisibleEntities, RenderVisibleEntitiesClass, - RetainedViewEntity, ViewUniformOffset, VisibilityExtractionSystemParam, + PointAndSpotLightShadowPrimaryCamera, RenderExtractedVisibleEntities, + RenderVisibleEntities, RenderVisibleEntitiesClass, RetainedViewEntity, ViewUniformOffset, + VisibilityExtractionSystemParam, }, Extract, ExtractSchedule, Render, RenderApp, RenderSystems, }; @@ -492,6 +493,7 @@ pub fn extract_cameras( Option<&MipBias>, Option<&RenderLayers>, Option<&Projection>, + Has, Has, ), )>, @@ -517,6 +519,7 @@ pub fn extract_cameras( MipBias, RenderLayers, Projection, + PointAndSpotLightShadowPrimaryCamera, NoIndirectDrawing, ViewUniformOffset, ); @@ -538,6 +541,7 @@ pub fn extract_cameras( mip_bias, render_layers, projection, + has_pasl_shadow_primary_camera, no_indirect_drawing, ), ) in query.iter() @@ -682,6 +686,12 @@ pub fn extract_cameras( commands.remove::(); } + if has_pasl_shadow_primary_camera { + commands.insert(PointAndSpotLightShadowPrimaryCamera); + } else { + commands.remove::(); + } + if no_indirect_drawing || !matches!( gpu_preprocessing_support.max_supported_mode, diff --git a/crates/bevy_render/src/view/visibility/range.rs b/crates/bevy_render/src/view/visibility/range.rs index f657ecd344741..7e3a5d11cf99c 100644 --- a/crates/bevy_render/src/view/visibility/range.rs +++ b/crates/bevy_render/src/view/visibility/range.rs @@ -4,6 +4,7 @@ use super::VisibilityRange; use bevy_app::{App, Plugin}; use bevy_ecs::{ + component::Component, entity::Entity, lifecycle::RemovedComponents, query::Changed, @@ -57,6 +58,23 @@ impl Plugin for RenderVisibilityRangePlugin { } } +/// A marker component for a `Camera` to denote that this entity should be the primary camera +/// used for GPU Visibility Range Culling on shadows produced by all `PointLight`s and +/// `SpotLight`s. There should only be one `Camera` with this marker component +/// at a given moment in the application. This culling will occur if `GpuPreprocessingMode` +/// is at least `GpuPreprocessingMode::PreprocessingOnly`. +/// +/// Unlike `DirectionalLight` shadows, `PointLight` shadows and `SpotLight` shadows are +/// not associated with any camera. They are only rendered once, regardless of the +/// number of cameras. In order for these shadows to participate in visibility range culling, +/// the user camera that distances are calculated relative to must be specified. Use this component +/// to specify that camera. +/// +/// If this marker component is not placed on a camera, or if there are multiple cameras +/// with this component, shadows made by `PointLight`s and `SpotLight`s may not render. +#[derive(Component, Default)] +pub struct PointAndSpotLightShadowPrimaryCamera; + /// Stores information related to [`VisibilityRange`]s in the render world. #[derive(Resource)] pub struct RenderVisibilityRanges { diff --git a/examples/testbed/3d.rs b/examples/testbed/3d.rs index c4553dbb09c89..bc0ae99808601 100644 --- a/examples/testbed/3d.rs +++ b/examples/testbed/3d.rs @@ -122,6 +122,7 @@ mod light { use bevy::{ color::palettes::css::{DEEP_PINK, LIME, RED}, prelude::*, + render::view::PointAndSpotLightShadowPrimaryCamera, }; const CURRENT_SCENE: super::Scene = super::Scene::Light; @@ -203,6 +204,7 @@ mod light { commands.spawn(( Camera3d::default(), + PointAndSpotLightShadowPrimaryCamera, Transform::from_xyz(-2.0, 2.5, 5.0).looking_at(Vec3::ZERO, Vec3::Y), DespawnOnExit(CURRENT_SCENE), )); @@ -732,6 +734,7 @@ mod render_layers { use bevy::{ camera::{visibility::RenderLayers, Viewport}, prelude::*, + render::view::PointAndSpotLightShadowPrimaryCamera, window::PrimaryWindow, }; @@ -805,7 +808,9 @@ mod render_layers { DespawnOnExit(CURRENT_SCENE), )); match index { - 0 => {} + 0 => { + entity_cmds.insert(PointAndSpotLightShadowPrimaryCamera); + } 1 => { entity_cmds.insert(RenderLayers::layer(1)); }