diff --git a/assets/shaders/irradiance_volume_voxel_visualization.wgsl b/assets/shaders/irradiance_volume_voxel_visualization.wgsl index 26a8deb87e..f34e6f8453 100644 --- a/assets/shaders/irradiance_volume_voxel_visualization.wgsl +++ b/assets/shaders/irradiance_volume_voxel_visualization.wgsl @@ -1,6 +1,7 @@ #import bevy_pbr::forward_io::VertexOutput #import bevy_pbr::irradiance_volume #import bevy_pbr::mesh_view_bindings +#import bevy_pbr::clustered_forward struct VoxelVisualizationIrradianceVolumeInfo { world_from_voxel: mat4x4, @@ -25,11 +26,24 @@ fn fragment(mesh: VertexOutput) -> @location(0) vec4 { let stp_rounded = round(stp - 0.5f) + 0.5f; let rounded_world_pos = (irradiance_volume_info.world_from_voxel * vec4(stp_rounded, 1.0f)).xyz; + // Look up the irradiance volume range in the cluster list. + let view_z = dot(vec4( + mesh_view_bindings::view.view_from_world[0].z, + mesh_view_bindings::view.view_from_world[1].z, + mesh_view_bindings::view.view_from_world[2].z, + mesh_view_bindings::view.view_from_world[3].z + ), mesh.world_position); + let cluster_index = clustered_forward::fragment_cluster_index(mesh.position.xy, view_z, false); + var clusterable_object_index_ranges = + clustered_forward::unpack_clusterable_object_index_ranges(cluster_index); + // `irradiance_volume_light()` multiplies by intensity, so cancel it out. // If we take intensity into account, the cubes will be way too bright. let rgb = irradiance_volume::irradiance_volume_light( mesh.world_position.xyz, - mesh.world_normal) / irradiance_volume_info.intensity; + mesh.world_normal, + &clusterable_object_index_ranges, + ) / irradiance_volume_info.intensity; return vec4(rgb, 1.0f); } diff --git a/crates/bevy_pbr/src/cluster/assign.rs b/crates/bevy_pbr/src/cluster/assign.rs index 4c3e22febe..69de548b57 100644 --- a/crates/bevy_pbr/src/cluster/assign.rs +++ b/crates/bevy_pbr/src/cluster/assign.rs @@ -2,9 +2,13 @@ use bevy_ecs::{ entity::Entity, + query::{Has, With}, system::{Commands, Local, Query, Res, ResMut}, }; -use bevy_math::{ops, Mat4, UVec3, Vec2, Vec3, Vec3A, Vec3Swizzles as _, Vec4, Vec4Swizzles as _}; +use bevy_math::{ + ops::{self, sin_cos}, + Mat4, UVec3, Vec2, Vec3, Vec3A, Vec3Swizzles as _, Vec4, Vec4Swizzles as _, +}; use bevy_render::{ camera::Camera, primitives::{Aabb, Frustum, HalfSpace, Sphere}, @@ -13,12 +17,13 @@ use bevy_render::{ view::{RenderLayers, ViewVisibility}, }; use bevy_transform::components::GlobalTransform; -use bevy_utils::tracing::warn; +use bevy_utils::{prelude::default, tracing::warn}; use crate::{ - ClusterConfig, ClusterFarZMode, Clusters, GlobalVisibleClusterableObjects, PointLight, - SpotLight, ViewClusterBindings, VisibleClusterableObjects, VolumetricLight, - CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT, MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS, + prelude::EnvironmentMapLight, ClusterConfig, ClusterFarZMode, Clusters, ExtractedPointLight, + GlobalVisibleClusterableObjects, LightProbe, PointLight, SpotLight, ViewClusterBindings, + VisibleClusterableObjects, VolumetricLight, CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT, + MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS, }; use super::ClusterableObjectOrderData; @@ -29,15 +34,13 @@ const NDC_MAX: Vec2 = Vec2::ONE; const VEC2_HALF: Vec2 = Vec2::splat(0.5); const VEC2_HALF_NEGATIVE_Y: Vec2 = Vec2::new(0.5, -0.5); -#[derive(Clone)] -// data required for assigning objects to clusters +/// Data required for assigning objects to clusters. +#[derive(Clone, Debug)] pub(crate) struct ClusterableObjectAssignmentData { entity: Entity, transform: GlobalTransform, range: f32, - shadows_enabled: bool, - volumetric: bool, - spot_light_angle: Option, + object_type: ClusterableObjectType, render_layers: RenderLayers, } @@ -50,6 +53,88 @@ impl ClusterableObjectAssignmentData { } } +/// Data needed to assign objects to clusters that's specific to the type of +/// clusterable object. +#[derive(Clone, Copy, Debug)] +pub(crate) enum ClusterableObjectType { + /// Data needed to assign point lights to clusters. + PointLight { + /// Whether shadows are enabled for this point light. + /// + /// This is used for sorting the light list. + shadows_enabled: bool, + + /// Whether this light interacts with volumetrics. + /// + /// This is used for sorting the light list. + volumetric: bool, + }, + + /// Data needed to assign spot lights to clusters. + SpotLight { + /// Whether shadows are enabled for this spot light. + /// + /// This is used for sorting the light list. + shadows_enabled: bool, + + /// Whether this light interacts with volumetrics. + /// + /// This is used for sorting the light list. + volumetric: bool, + + /// The outer angle of the light cone in radians. + outer_angle: f32, + }, + + /// Marks that the clusterable object is a reflection probe. + ReflectionProbe, + + /// Marks that the clusterable object is an irradiance volume. + IrradianceVolume, +} + +impl ClusterableObjectType { + /// Returns a tuple that can be sorted to obtain the order in which indices + /// to clusterable objects must be stored in the cluster offsets and counts + /// list. + /// + /// Generally, we sort first by type, then, for lights, by whether shadows + /// are enabled (enabled before disabled), and then whether volumetrics are + /// enabled (enabled before disabled). + pub(crate) fn ordering(&self) -> (u8, bool, bool) { + match *self { + ClusterableObjectType::PointLight { + shadows_enabled, + volumetric, + } => (0, !shadows_enabled, !volumetric), + ClusterableObjectType::SpotLight { + shadows_enabled, + volumetric, + .. + } => (1, !shadows_enabled, !volumetric), + ClusterableObjectType::ReflectionProbe => (2, false, false), + ClusterableObjectType::IrradianceVolume => (3, false, false), + } + } + + /// Creates the [`ClusterableObjectType`] data for a point or spot light. + pub(crate) fn from_point_or_spot_light( + point_light: &ExtractedPointLight, + ) -> ClusterableObjectType { + match point_light.spot_light_angles { + Some((_, outer_angle)) => ClusterableObjectType::SpotLight { + outer_angle, + shadows_enabled: point_light.shadows_enabled, + volumetric: point_light.volumetric, + }, + None => ClusterableObjectType::PointLight { + shadows_enabled: point_light.shadows_enabled, + volumetric: point_light.volumetric, + }, + } + } +} + // NOTE: Run this before update_point_light_frusta! #[allow(clippy::too_many_arguments)] pub(crate) fn assign_objects_to_clusters( @@ -81,6 +166,10 @@ pub(crate) fn assign_objects_to_clusters( Option<&VolumetricLight>, &ViewVisibility, )>, + light_probes_query: Query< + (Entity, &GlobalTransform, Has), + With, + >, mut clusterable_objects: Local>, mut cluster_aabb_spheres: Local>>, mut max_clusterable_objects_warning_emitted: Local, @@ -102,10 +191,11 @@ pub(crate) fn assign_objects_to_clusters( ClusterableObjectAssignmentData { entity, transform: GlobalTransform::from_translation(transform.translation()), - shadows_enabled: point_light.shadows_enabled, - volumetric: volumetric.is_some(), range: point_light.range, - spot_light_angle: None, + object_type: ClusterableObjectType::PointLight { + shadows_enabled: point_light.shadows_enabled, + volumetric: volumetric.is_some(), + }, render_layers: maybe_layers.unwrap_or_default().clone(), } }, @@ -120,10 +210,12 @@ pub(crate) fn assign_objects_to_clusters( ClusterableObjectAssignmentData { entity, transform: *transform, - shadows_enabled: spot_light.shadows_enabled, - volumetric: volumetric.is_some(), range: spot_light.range, - spot_light_angle: Some(spot_light.outer_angle), + object_type: ClusterableObjectType::SpotLight { + outer_angle: spot_light.outer_angle, + shadows_enabled: spot_light.shadows_enabled, + volumetric: volumetric.is_some(), + }, render_layers: maybe_layers.unwrap_or_default().clone(), } }, @@ -136,6 +228,29 @@ pub(crate) fn assign_objects_to_clusters( clustered_forward_buffer_binding_type, BufferBindingType::Storage { .. } ); + + // Gather up light probes, but only if we're clustering them. + // + // UBOs aren't large enough to hold indices for light probes, so we can't + // cluster light probes on such platforms (mainly WebGL 2). Besides, those + // platforms typically lack bindless textures, so multiple light probes + // wouldn't be supported anyhow. + if supports_storage_buffers { + clusterable_objects.extend(light_probes_query.iter().map( + |(entity, transform, is_reflection_probe)| ClusterableObjectAssignmentData { + entity, + transform: *transform, + range: transform.radius_vec3a(Vec3A::ONE), + object_type: if is_reflection_probe { + ClusterableObjectType::ReflectionProbe + } else { + ClusterableObjectType::IrradianceVolume + }, + render_layers: RenderLayers::default(), + }, + )); + } + if clusterable_objects.len() > MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS && !supports_storage_buffers { @@ -143,15 +258,11 @@ pub(crate) fn assign_objects_to_clusters( crate::clusterable_object_order( ClusterableObjectOrderData { entity: &clusterable_object_1.entity, - shadows_enabled: &clusterable_object_1.shadows_enabled, - is_volumetric_light: &clusterable_object_1.volumetric, - is_spot_light: &clusterable_object_1.spot_light_angle.is_some(), + object_type: &clusterable_object_1.object_type, }, ClusterableObjectOrderData { entity: &clusterable_object_2.entity, - shadows_enabled: &clusterable_object_2.shadows_enabled, - is_volumetric_light: &clusterable_object_2.volumetric, - is_spot_light: &clusterable_object_2.spot_light_angle.is_some(), + object_type: &clusterable_object_2.object_type, }, ) }); @@ -361,8 +472,7 @@ pub(crate) fn assign_objects_to_clusters( for clusterable_objects in &mut clusters.clusterable_objects { clusterable_objects.entities.clear(); - clusterable_objects.point_light_count = 0; - clusterable_objects.spot_light_count = 0; + clusterable_objects.counts = default(); } let cluster_count = (clusters.dimensions.x * clusters.dimensions.y * clusters.dimensions.z) as usize; @@ -495,16 +605,21 @@ pub(crate) fn assign_objects_to_clusters( ), radius: clusterable_object_sphere.radius * view_from_world_scale_max, }; - let spot_light_dir_sin_cos = clusterable_object.spot_light_angle.map(|angle| { - let (angle_sin, angle_cos) = ops::sin_cos(angle); - ( - (view_from_world * clusterable_object.transform.back().extend(0.0)) - .truncate() - .normalize(), - angle_sin, - angle_cos, - ) - }); + let spot_light_dir_sin_cos = match clusterable_object.object_type { + ClusterableObjectType::SpotLight { outer_angle, .. } => { + let (angle_sin, angle_cos) = sin_cos(outer_angle); + Some(( + (view_from_world * clusterable_object.transform.back().extend(0.0)) + .truncate() + .normalize(), + angle_sin, + angle_cos, + )) + } + ClusterableObjectType::PointLight { .. } + | ClusterableObjectType::ReflectionProbe + | ClusterableObjectType::IrradianceVolume => None, + }; let clusterable_object_center_clip = camera.clip_from_view() * view_clusterable_object_sphere.center.extend(1.0); let object_center_ndc = @@ -602,72 +717,114 @@ pub(crate) fn assign_objects_to_clusters( * clusters.dimensions.z + z) as usize; - if let Some((view_light_direction, angle_sin, angle_cos)) = - spot_light_dir_sin_cos - { - for x in min_x..=max_x { - // further culling for spot lights - // get or initialize cluster bounding sphere - let cluster_aabb_sphere = &mut cluster_aabb_spheres[cluster_index]; - let cluster_aabb_sphere = if let Some(sphere) = cluster_aabb_sphere - { - &*sphere - } else { - let aabb = compute_aabb_for_cluster( - first_slice_depth, - far_z, - clusters.tile_size.as_vec2(), - screen_size.as_vec2(), - view_from_clip, - is_orthographic, - clusters.dimensions, - UVec3::new(x, y, z), + match clusterable_object.object_type { + ClusterableObjectType::SpotLight { .. } => { + let (view_light_direction, angle_sin, angle_cos) = + spot_light_dir_sin_cos.unwrap(); + for x in min_x..=max_x { + // further culling for spot lights + // get or initialize cluster bounding sphere + let cluster_aabb_sphere = + &mut cluster_aabb_spheres[cluster_index]; + let cluster_aabb_sphere = + if let Some(sphere) = cluster_aabb_sphere { + &*sphere + } else { + let aabb = compute_aabb_for_cluster( + first_slice_depth, + far_z, + clusters.tile_size.as_vec2(), + screen_size.as_vec2(), + view_from_clip, + is_orthographic, + clusters.dimensions, + UVec3::new(x, y, z), + ); + let sphere = Sphere { + center: aabb.center, + radius: aabb.half_extents.length(), + }; + *cluster_aabb_sphere = Some(sphere); + cluster_aabb_sphere.as_ref().unwrap() + }; + + // test -- based on https://bartwronski.com/2017/04/13/cull-that-cone/ + let spot_light_offset = Vec3::from( + view_clusterable_object_sphere.center + - cluster_aabb_sphere.center, ); - let sphere = Sphere { - center: aabb.center, - radius: aabb.half_extents.length(), - }; - *cluster_aabb_sphere = Some(sphere); - cluster_aabb_sphere.as_ref().unwrap() - }; + let spot_light_dist_sq = spot_light_offset.length_squared(); + let v1_len = spot_light_offset.dot(view_light_direction); - // test -- based on https://bartwronski.com/2017/04/13/cull-that-cone/ - let spot_light_offset = Vec3::from( - view_clusterable_object_sphere.center - - cluster_aabb_sphere.center, - ); - let spot_light_dist_sq = spot_light_offset.length_squared(); - let v1_len = spot_light_offset.dot(view_light_direction); + let distance_closest_point = (angle_cos + * (spot_light_dist_sq - v1_len * v1_len).sqrt()) + - v1_len * angle_sin; + let angle_cull = + distance_closest_point > cluster_aabb_sphere.radius; - let distance_closest_point = (angle_cos - * (spot_light_dist_sq - v1_len * v1_len).sqrt()) - - v1_len * angle_sin; - let angle_cull = - distance_closest_point > cluster_aabb_sphere.radius; + let front_cull = v1_len + > cluster_aabb_sphere.radius + + clusterable_object.range * view_from_world_scale_max; + let back_cull = v1_len < -cluster_aabb_sphere.radius; - let front_cull = v1_len - > cluster_aabb_sphere.radius - + clusterable_object.range * view_from_world_scale_max; - let back_cull = v1_len < -cluster_aabb_sphere.radius; + if !angle_cull && !front_cull && !back_cull { + // this cluster is affected by the spot light + clusters.clusterable_objects[cluster_index] + .entities + .push(clusterable_object.entity); + clusters.clusterable_objects[cluster_index] + .counts + .spot_lights += 1; + } + cluster_index += clusters.dimensions.z as usize; + } + } - if !angle_cull && !front_cull && !back_cull { - // this cluster is affected by the spot light + ClusterableObjectType::PointLight { .. } => { + for _ in min_x..=max_x { + // all clusters within range are affected by point lights clusters.clusterable_objects[cluster_index] .entities .push(clusterable_object.entity); - clusters.clusterable_objects[cluster_index].spot_light_count += - 1; + clusters.clusterable_objects[cluster_index] + .counts + .point_lights += 1; + cluster_index += clusters.dimensions.z as usize; } - cluster_index += clusters.dimensions.z as usize; } - } else { - for _ in min_x..=max_x { - // all clusters within range are affected by point lights - clusters.clusterable_objects[cluster_index] - .entities - .push(clusterable_object.entity); - clusters.clusterable_objects[cluster_index].point_light_count += 1; - cluster_index += clusters.dimensions.z as usize; + + ClusterableObjectType::ReflectionProbe => { + // Reflection probes currently affect all + // clusters in their bounding sphere. + // + // TODO: Cull more aggressively based on the + // probe's OBB. + for _ in min_x..=max_x { + clusters.clusterable_objects[cluster_index] + .entities + .push(clusterable_object.entity); + clusters.clusterable_objects[cluster_index] + .counts + .reflection_probes += 1; + cluster_index += clusters.dimensions.z as usize; + } + } + + ClusterableObjectType::IrradianceVolume => { + // Irradiance volumes currently affect all + // clusters in their bounding sphere. + // + // TODO: Cull more aggressively based on the + // probe's OBB. + for _ in min_x..=max_x { + clusters.clusterable_objects[cluster_index] + .entities + .push(clusterable_object.entity); + clusters.clusterable_objects[cluster_index] + .counts + .irradiance_volumes += 1; + cluster_index += clusters.dimensions.z as usize; + } } } } diff --git a/crates/bevy_pbr/src/cluster/mod.rs b/crates/bevy_pbr/src/cluster/mod.rs index f30dc0f432..bbab0dff08 100644 --- a/crates/bevy_pbr/src/cluster/mod.rs +++ b/crates/bevy_pbr/src/cluster/mod.rs @@ -2,6 +2,7 @@ use core::num::NonZero; +use self::assign::ClusterableObjectType; use bevy_core_pipeline::core_3d::Camera3d; use bevy_ecs::{ component::Component, @@ -11,7 +12,7 @@ use bevy_ecs::{ system::{Commands, Query, Res, Resource}, world::{FromWorld, World}, }; -use bevy_math::{AspectRatio, UVec2, UVec3, UVec4, Vec3Swizzles as _, Vec4}; +use bevy_math::{uvec4, AspectRatio, UVec2, UVec3, UVec4, Vec3Swizzles as _, Vec4}; use bevy_reflect::{std_traits::ReflectDefault, Reflect}; use bevy_render::{ camera::Camera, @@ -28,7 +29,7 @@ use bevy_utils::{hashbrown::HashSet, tracing::warn}; pub(crate) use crate::cluster::assign::assign_objects_to_clusters; use crate::MeshPipeline; -mod assign; +pub(crate) mod assign; #[cfg(test)] mod test; @@ -132,8 +133,7 @@ pub struct Clusters { #[derive(Clone, Component, Debug, Default)] pub struct VisibleClusterableObjects { pub(crate) entities: Vec, - pub point_light_count: usize, - pub spot_light_count: usize, + counts: ClusterableObjectCounts, } #[derive(Resource, Default)] @@ -189,8 +189,24 @@ pub struct ExtractedClusterConfig { pub(crate) dimensions: UVec3, } +/// Stores the number of each type of clusterable object in a single cluster. +/// +/// Note that `reflection_probes` and `irradiance_volumes` won't be clustered if +/// fewer than 3 SSBOs are available, which usually means on WebGL 2. +#[derive(Clone, Copy, Default, Debug)] +struct ClusterableObjectCounts { + /// The number of point lights in the cluster. + point_lights: u32, + /// The number of spot lights in the cluster. + spot_lights: u32, + /// The number of reflection probes in the cluster. + reflection_probes: u32, + /// The number of irradiance volumes in the cluster. + irradiance_volumes: u32, +} + enum ExtractedClusterableObjectElement { - ClusterHeader(u32, u32), + ClusterHeader(ClusterableObjectCounts), ClusterableObjectEntity(Entity), } @@ -212,8 +228,11 @@ struct GpuClusterableObjectIndexListsStorage { #[derive(ShaderType, Default)] struct GpuClusterOffsetsAndCountsStorage { + /// The starting offset, followed by the number of point lights, spot + /// lights, reflection probes, and irradiance volumes in each cluster, in + /// that order. The remaining fields are filled with zeroes. #[size(runtime)] - data: Vec, + data: Vec<[UVec4; 2]>, } enum ViewClusterBuffers { @@ -499,16 +518,14 @@ impl Default for GpuClusterableObjectsUniform { pub(crate) struct ClusterableObjectOrderData<'a> { pub(crate) entity: &'a Entity, - pub(crate) shadows_enabled: &'a bool, - pub(crate) is_volumetric_light: &'a bool, - pub(crate) is_spot_light: &'a bool, + pub(crate) object_type: &'a ClusterableObjectType, } #[allow(clippy::too_many_arguments)] // Sort clusterable objects by: // -// * point-light vs spot-light, so that we can iterate point lights and spot -// lights in contiguous blocks in the fragment shader, +// * object type, so that we can iterate point lights, spot lights, etc. in +// contiguous blocks in the fragment shader, // // * then those with shadows enabled first, so that the index can be used to // render at most `point_light_shadow_maps_count` point light shadows and @@ -521,10 +538,9 @@ pub(crate) fn clusterable_object_order( a: ClusterableObjectOrderData, b: ClusterableObjectOrderData, ) -> core::cmp::Ordering { - a.is_spot_light - .cmp(b.is_spot_light) // pointlights before spot lights - .then_with(|| b.shadows_enabled.cmp(a.shadows_enabled)) // shadow casters before non-casters - .then_with(|| b.is_volumetric_light.cmp(a.is_volumetric_light)) // volumetric lights before non-volumetric lights + a.object_type + .ordering() + .cmp(&b.object_type.ordering()) .then_with(|| a.entity.cmp(b.entity)) // stable } @@ -551,8 +567,7 @@ pub fn extract_clusters( let mut data = Vec::with_capacity(clusters.clusterable_objects.len() + num_entities); for cluster_objects in &clusters.clusterable_objects { data.push(ExtractedClusterableObjectElement::ClusterHeader( - cluster_objects.point_light_count as u32, - cluster_objects.spot_light_count as u32, + cluster_objects.counts, )); for clusterable_entity in &cluster_objects.entities { if let Ok(entity) = mapper.get(*clusterable_entity) { @@ -594,16 +609,9 @@ pub fn prepare_clusters( for record in &extracted_clusters.data { match record { - ExtractedClusterableObjectElement::ClusterHeader( - point_light_count, - spot_light_count, - ) => { + ExtractedClusterableObjectElement::ClusterHeader(counts) => { let offset = view_clusters_bindings.n_indices(); - view_clusters_bindings.push_offset_and_counts( - offset, - *point_light_count as usize, - *spot_light_count as usize, - ); + view_clusters_bindings.push_offset_and_counts(offset, counts); } ExtractedClusterableObjectElement::ClusterableObjectEntity(entity) => { if let Some(clusterable_object_index) = @@ -664,7 +672,7 @@ impl ViewClusterBindings { } } - pub fn push_offset_and_counts(&mut self, offset: usize, point_count: usize, spot_count: usize) { + fn push_offset_and_counts(&mut self, offset: usize, counts: &ClusterableObjectCounts) { match &mut self.buffers { ViewClusterBuffers::Uniform { cluster_offsets_and_counts, @@ -676,7 +684,8 @@ impl ViewClusterBindings { return; } let component = self.n_offsets & ((1 << 2) - 1); - let packed = pack_offset_and_counts(offset, point_count, spot_count); + let packed = + pack_offset_and_counts(offset, counts.point_lights, counts.spot_lights); cluster_offsets_and_counts.get_mut().data[array_index][component] = packed; } @@ -684,12 +693,15 @@ impl ViewClusterBindings { cluster_offsets_and_counts, .. } => { - cluster_offsets_and_counts.get_mut().data.push(UVec4::new( - offset as u32, - point_count as u32, - spot_count as u32, - 0, - )); + cluster_offsets_and_counts.get_mut().data.push([ + uvec4( + offset as u32, + counts.point_lights, + counts.spot_lights, + counts.reflection_probes, + ), + uvec4(counts.irradiance_volumes, 0, 0, 0), + ]); } } @@ -815,6 +827,12 @@ impl ViewClusterBuffers { } } +// Compresses the offset and counts of point and spot lights so that they fit in +// a UBO. +// +// This function is only used if storage buffers are unavailable on this +// platform: typically, on WebGL 2. +// // NOTE: With uniform buffer max binding size as 16384 bytes // that means we can fit 204 clusterable objects in one uniform // buffer, which means the count can be at most 204 so it @@ -827,12 +845,16 @@ impl ViewClusterBuffers { // the point light count into bits 9-17, and the spot light count into bits 0-8. // [ 31 .. 18 | 17 .. 9 | 8 .. 0 ] // [ offset | point light count | spot light count ] +// // NOTE: This assumes CPU and GPU endianness are the same which is true // for all common and tested x86/ARM CPUs and AMD/NVIDIA/Intel/Apple/etc GPUs -fn pack_offset_and_counts(offset: usize, point_count: usize, spot_count: usize) -> u32 { +// +// NOTE: On platforms that use this function, we don't cluster light probes, so +// the number of light probes is irrelevant. +fn pack_offset_and_counts(offset: usize, point_count: u32, spot_count: u32) -> u32 { ((offset as u32 & CLUSTER_OFFSET_MASK) << (CLUSTER_COUNT_SIZE * 2)) - | (point_count as u32 & CLUSTER_COUNT_MASK) << CLUSTER_COUNT_SIZE - | (spot_count as u32 & CLUSTER_COUNT_MASK) + | (point_count & CLUSTER_COUNT_MASK) << CLUSTER_COUNT_SIZE + | (spot_count & CLUSTER_COUNT_MASK) } #[derive(ShaderType)] diff --git a/crates/bevy_pbr/src/light_probe/environment_map.wgsl b/crates/bevy_pbr/src/light_probe/environment_map.wgsl index 459ced93ec..0720a30dbe 100644 --- a/crates/bevy_pbr/src/light_probe/environment_map.wgsl +++ b/crates/bevy_pbr/src/light_probe/environment_map.wgsl @@ -7,6 +7,7 @@ #import bevy_pbr::lighting::{ F_Schlick_vec, LayerLightingInput, LightingInput, LAYER_BASE, LAYER_CLEARCOAT } +#import bevy_pbr::clustered_forward::ClusterableObjectIndexRanges struct EnvironmentMapLight { diffuse: vec3, @@ -26,6 +27,7 @@ struct EnvironmentMapRadiances { fn compute_radiances( input: ptr, + clusterable_object_index_ranges: ptr, layer: u32, world_position: vec3, found_diffuse_indirect: bool, @@ -38,7 +40,11 @@ fn compute_radiances( var radiances: EnvironmentMapRadiances; // Search for a reflection probe that contains the fragment. - var query_result = query_light_probe(world_position, /*is_irradiance_volume=*/ false); + var query_result = query_light_probe( + world_position, + /*is_irradiance_volume=*/ false, + clusterable_object_index_ranges, + ); // If we didn't find a reflection probe, use the view environment map if applicable. if (query_result.texture_index < 0) { @@ -90,6 +96,7 @@ fn compute_radiances( fn compute_radiances( input: ptr, + clusterable_object_index_ranges: ptr, layer: u32, world_position: vec3, found_diffuse_indirect: bool, @@ -152,6 +159,7 @@ fn compute_radiances( fn environment_map_light_clearcoat( out: ptr, input: ptr, + clusterable_object_index_ranges: ptr, found_diffuse_indirect: bool, ) { // Unpack. @@ -166,7 +174,12 @@ fn environment_map_light_clearcoat( let inv_Fc = 1.0 - Fc; let clearcoat_radiances = compute_radiances( - input, LAYER_CLEARCOAT, world_position, found_diffuse_indirect); + input, + clusterable_object_index_ranges, + LAYER_CLEARCOAT, + world_position, + found_diffuse_indirect, + ); // Composite the clearcoat layer on top of the existing one. // These formulas are from Filament: @@ -179,6 +192,7 @@ fn environment_map_light_clearcoat( fn environment_map_light( input: ptr, + clusterable_object_index_ranges: ptr, found_diffuse_indirect: bool, ) -> EnvironmentMapLight { // Unpack. @@ -191,7 +205,14 @@ fn environment_map_light( var out: EnvironmentMapLight; - let radiances = compute_radiances(input, LAYER_BASE, world_position, found_diffuse_indirect); + let radiances = compute_radiances( + input, + clusterable_object_index_ranges, + LAYER_BASE, + world_position, + found_diffuse_indirect, + ); + if (all(radiances.irradiance == vec3(0.0)) && all(radiances.radiance == vec3(0.0))) { out.diffuse = vec3(0.0); out.specular = vec3(0.0); @@ -225,7 +246,12 @@ fn environment_map_light( out.specular = FssEss * radiances.radiance; #ifdef STANDARD_MATERIAL_CLEARCOAT - environment_map_light_clearcoat(&out, input, found_diffuse_indirect); + environment_map_light_clearcoat( + &out, + input, + clusterable_object_index_ranges, + found_diffuse_indirect, + ); #endif // STANDARD_MATERIAL_CLEARCOAT return out; diff --git a/crates/bevy_pbr/src/light_probe/irradiance_volume.wgsl b/crates/bevy_pbr/src/light_probe/irradiance_volume.wgsl index 01b2741c2a..b4ba8b2e61 100644 --- a/crates/bevy_pbr/src/light_probe/irradiance_volume.wgsl +++ b/crates/bevy_pbr/src/light_probe/irradiance_volume.wgsl @@ -7,15 +7,24 @@ irradiance_volume_sampler, light_probes, }; +#import bevy_pbr::clustered_forward::ClusterableObjectIndexRanges #ifdef IRRADIANCE_VOLUMES_ARE_USABLE // See: // https://advances.realtimerendering.com/s2006/Mitchell-ShadingInValvesSourceEngine.pdf // Slide 28, "Ambient Cube Basis" -fn irradiance_volume_light(world_position: vec3, N: vec3) -> vec3 { +fn irradiance_volume_light( + world_position: vec3, + N: vec3, + clusterable_object_index_ranges: ptr, +) -> vec3 { // Search for an irradiance volume that contains the fragment. - let query_result = query_light_probe(world_position, /*is_irradiance_volume=*/ true); + let query_result = query_light_probe( + world_position, + /*is_irradiance_volume=*/ true, + clusterable_object_index_ranges, + ); // If there was no irradiance volume found, bail out. if (query_result.texture_index < 0) { diff --git a/crates/bevy_pbr/src/light_probe/light_probe.wgsl b/crates/bevy_pbr/src/light_probe/light_probe.wgsl index e1ab710708..ab80f0dd92 100644 --- a/crates/bevy_pbr/src/light_probe/light_probe.wgsl +++ b/crates/bevy_pbr/src/light_probe/light_probe.wgsl @@ -1,5 +1,7 @@ #define_import_path bevy_pbr::light_probe +#import bevy_pbr::clustered_forward +#import bevy_pbr::clustered_forward::ClusterableObjectIndexRanges #import bevy_pbr::mesh_view_bindings::light_probes #import bevy_pbr::mesh_view_types::LightProbe @@ -25,12 +27,79 @@ fn transpose_affine_matrix(matrix: mat3x4) -> mat4x4 { return transpose(matrix4x4); } +#if AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3 + // Searches for a light probe that contains the fragment. // +// This is the version that's used when storage buffers are available and +// light probes are clustered. +// // TODO: Interpolate between multiple light probes. fn query_light_probe( world_position: vec3, is_irradiance_volume: bool, + clusterable_object_index_ranges: ptr, +) -> LightProbeQueryResult { + var result: LightProbeQueryResult; + result.texture_index = -1; + + // Reflection probe indices are followed by irradiance volume indices in the + // cluster index list. Use this fact to create our bracketing range of + // indices. + var start_offset: u32; + var end_offset: u32; + if is_irradiance_volume { + start_offset = (*clusterable_object_index_ranges).first_irradiance_volume_index_offset; + end_offset = (*clusterable_object_index_ranges).last_clusterable_object_index_offset; + } else { + start_offset = (*clusterable_object_index_ranges).first_reflection_probe_index_offset; + end_offset = (*clusterable_object_index_ranges).first_irradiance_volume_index_offset; + } + + for (var light_probe_index_offset: u32 = start_offset; + light_probe_index_offset < end_offset && result.texture_index < 0; + light_probe_index_offset += 1u) { + let light_probe_index = i32(clustered_forward::get_clusterable_object_id( + light_probe_index_offset)); + + var light_probe: LightProbe; + if is_irradiance_volume { + light_probe = light_probes.irradiance_volumes[light_probe_index]; + } else { + light_probe = light_probes.reflection_probes[light_probe_index]; + } + + // Unpack the inverse transform. + let light_from_world = + transpose_affine_matrix(light_probe.light_from_world_transposed); + + // Check to see if the transformed point is inside the unit cube + // centered at the origin. + let probe_space_pos = (light_from_world * vec4(world_position, 1.0f)).xyz; + if (all(abs(probe_space_pos) <= vec3(0.5f))) { + result.texture_index = light_probe.cubemap_index; + result.intensity = light_probe.intensity; + result.light_from_world = light_from_world; + break; + } + } + + return result; +} + +#else // AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3 + +// Searches for a light probe that contains the fragment. +// +// This is the version that's used when storage buffers aren't available and +// light probes aren't clustered. It simply does a brute force search of all +// light probes. Because platforms without sufficient SSBO bindings typically +// lack bindless shaders, there will usually only be one of each type of light +// probe present anyway. +fn query_light_probe( + world_position: vec3, + is_irradiance_volume: bool, + clusterable_object_index_ranges: ptr, ) -> LightProbeQueryResult { var result: LightProbeQueryResult; result.texture_index = -1; @@ -76,3 +145,4 @@ fn query_light_probe( return result; } +#endif // AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3 diff --git a/crates/bevy_pbr/src/render/clustered_forward.wgsl b/crates/bevy_pbr/src/render/clustered_forward.wgsl index 7b303764db..72eef607db 100644 --- a/crates/bevy_pbr/src/render/clustered_forward.wgsl +++ b/crates/bevy_pbr/src/render/clustered_forward.wgsl @@ -10,6 +10,28 @@ maths::PI_2, } +// Offsets within the `cluster_offsets_and_counts` buffer for a single cluster. +// +// These offsets must be monotonically nondecreasing. That is, indices are +// always sorted into the following order: point lights, spot lights, reflection +// probes, irradiance volumes. +struct ClusterableObjectIndexRanges { + // The offset of the index of the first point light. + first_point_light_index_offset: u32, + // The offset of the index of the first spot light, which also terminates + // the list of point lights. + first_spot_light_index_offset: u32, + // The offset of the index of the first reflection probe, which also + // terminates the list of spot lights. + first_reflection_probe_index_offset: u32, + // The offset of the index of the first irradiance volumes, which also + // terminates the list of reflection probes. + first_irradiance_volume_index_offset: u32, + // One past the offset of the index of the final clusterable object for this + // cluster. + last_clusterable_object_index_offset: u32, +} + // NOTE: Keep in sync with bevy_pbr/src/light.rs fn view_z_to_z_slice(view_z: f32, is_orthographic: bool) -> u32 { var z_slice: u32 = 0u; @@ -38,21 +60,65 @@ fn fragment_cluster_index(frag_coord: vec2, view_z: f32, is_orthographic: b // this must match CLUSTER_COUNT_SIZE in light.rs const CLUSTER_COUNT_SIZE = 9u; -fn unpack_offset_and_counts(cluster_index: u32) -> vec3 { + +// Returns the indices of clusterable objects belonging to the given cluster. +// +// Note that if fewer than 3 SSBO bindings are available (in WebGL 2, +// primarily), light probes aren't clustered, and therefore both light probe +// index ranges will be empty. +fn unpack_clusterable_object_index_ranges(cluster_index: u32) -> ClusterableObjectIndexRanges { #if AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3 - return bindings::cluster_offsets_and_counts.data[cluster_index].xyz; -#else - let offset_and_counts = bindings::cluster_offsets_and_counts.data[cluster_index >> 2u][cluster_index & ((1u << 2u) - 1u)]; + + let offset_and_counts_a = bindings::cluster_offsets_and_counts.data[cluster_index][0]; + let offset_and_counts_b = bindings::cluster_offsets_and_counts.data[cluster_index][1]; + + // Sum up the counts to produce the range brackets. + // + // We could have stored the range brackets in `cluster_offsets_and_counts` + // directly, but doing it this way makes the logic in this path more + // consistent with the WebGL 2 path below. + let point_light_offset = offset_and_counts_a.x; + let spot_light_offset = point_light_offset + offset_and_counts_a.y; + let reflection_probe_offset = spot_light_offset + offset_and_counts_a.z; + let irradiance_volume_offset = reflection_probe_offset + offset_and_counts_a.w; + let last_clusterable_offset = irradiance_volume_offset + offset_and_counts_b.x; + return ClusterableObjectIndexRanges( + point_light_offset, + spot_light_offset, + reflection_probe_offset, + irradiance_volume_offset, + last_clusterable_offset + ); + +#else // AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3 + + let raw_offset_and_counts = bindings::cluster_offsets_and_counts.data[cluster_index >> 2u][cluster_index & ((1u << 2u) - 1u)]; // [ 31 .. 18 | 17 .. 9 | 8 .. 0 ] // [ offset | point light count | spot light count ] - return vec3( - (offset_and_counts >> (CLUSTER_COUNT_SIZE * 2u)) & ((1u << (32u - (CLUSTER_COUNT_SIZE * 2u))) - 1u), - (offset_and_counts >> CLUSTER_COUNT_SIZE) & ((1u << CLUSTER_COUNT_SIZE) - 1u), - offset_and_counts & ((1u << CLUSTER_COUNT_SIZE) - 1u), + let offset_and_counts = vec3( + (raw_offset_and_counts >> (CLUSTER_COUNT_SIZE * 2u)) & ((1u << (32u - (CLUSTER_COUNT_SIZE * 2u))) - 1u), + (raw_offset_and_counts >> CLUSTER_COUNT_SIZE) & ((1u << CLUSTER_COUNT_SIZE) - 1u), + raw_offset_and_counts & ((1u << CLUSTER_COUNT_SIZE) - 1u), ); -#endif + + // We don't cluster reflection probes or irradiance volumes on this + // platform, as there's no room in the UBO. Thus, those offset ranges + // (corresponding to `offset_d` and `offset_e` above) are empty and are + // simply copies of `offset_c`. + + let offset_a = offset_and_counts.x; + let offset_b = offset_a + offset_and_counts.y; + let offset_c = offset_b + offset_and_counts.z; + + return ClusterableObjectIndexRanges(offset_a, offset_b, offset_c, offset_c, offset_c); + +#endif // AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3 } +// Returns the index of the clusterable object at the given offset. +// +// Note that, in the case of a light probe, the index refers to an element in +// one of the two `light_probes` sublists, not the `clusterable_objects` list. fn get_clusterable_object_id(index: u32) -> u32 { #if AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3 return bindings::clusterable_object_index_lists.data[index]; @@ -70,7 +136,7 @@ fn cluster_debug_visualization( input_color: vec4, view_z: f32, is_orthographic: bool, - offset_and_counts: vec3, + clusterable_object_index_ranges: ClusterableObjectIndexRanges, cluster_index: u32, ) -> vec4 { var output_color = input_color; @@ -101,16 +167,12 @@ fn cluster_debug_visualization( // complexity measure. let cluster_overlay_alpha = 0.1; let max_complexity_per_cluster = 64.0; + let object_count = clusterable_object_index_ranges.first_reflection_probe_index_offset - + clusterable_object_index_ranges.first_point_light_index_offset; output_color.r = (1.0 - cluster_overlay_alpha) * output_color.r + cluster_overlay_alpha * - smoothStep( - 0.0, - max_complexity_per_cluster, - f32(offset_and_counts[1] + offset_and_counts[2])); + smoothstep(0.0, max_complexity_per_cluster, f32(object_count)); output_color.g = (1.0 - cluster_overlay_alpha) * output_color.g + cluster_overlay_alpha * - (1.0 - smoothStep( - 0.0, - max_complexity_per_cluster, - f32(offset_and_counts[1] + offset_and_counts[2]))); + (1.0 - smoothstep(0.0, max_complexity_per_cluster, f32(object_count))); #endif // CLUSTERED_FORWARD_DEBUG_CLUSTER_COMPLEXITY #ifdef CLUSTERED_FORWARD_DEBUG_CLUSTER_COHERENCY // NOTE: Visualizes the cluster to which the fragment belongs diff --git a/crates/bevy_pbr/src/render/light.rs b/crates/bevy_pbr/src/render/light.rs index 04506591f6..9223441552 100644 --- a/crates/bevy_pbr/src/render/light.rs +++ b/crates/bevy_pbr/src/render/light.rs @@ -1,3 +1,4 @@ +use self::assign::ClusterableObjectType; use crate::material_bind_groups::MaterialBindGroupAllocator; use crate::*; use bevy_asset::UntypedAssetId; @@ -821,15 +822,11 @@ pub fn prepare_lights( clusterable_object_order( ClusterableObjectOrderData { entity: entity_1, - shadows_enabled: &light_1.shadows_enabled, - is_volumetric_light: &light_1.volumetric, - is_spot_light: &light_1.spot_light_angles.is_some(), + object_type: &ClusterableObjectType::from_point_or_spot_light(light_1), }, ClusterableObjectOrderData { entity: entity_2, - shadows_enabled: &light_2.shadows_enabled, - is_volumetric_light: &light_2.volumetric, - is_spot_light: &light_2.spot_light_angles.is_some(), + object_type: &ClusterableObjectType::from_point_or_spot_light(light_2), }, ) }); diff --git a/crates/bevy_pbr/src/render/mesh_view_types.wgsl b/crates/bevy_pbr/src/render/mesh_view_types.wgsl index d9cc61cad9..a3648340f3 100644 --- a/crates/bevy_pbr/src/render/mesh_view_types.wgsl +++ b/crates/bevy_pbr/src/render/mesh_view_types.wgsl @@ -101,7 +101,7 @@ struct ClusterLightIndexLists { data: array, }; struct ClusterOffsetsAndCounts { - data: array>, + data: array, 2>>, }; #else struct ClusterableObjects { diff --git a/crates/bevy_pbr/src/render/pbr_functions.wgsl b/crates/bevy_pbr/src/render/pbr_functions.wgsl index ef357284cb..093af38e59 100644 --- a/crates/bevy_pbr/src/render/pbr_functions.wgsl +++ b/crates/bevy_pbr/src/render/pbr_functions.wgsl @@ -413,10 +413,13 @@ fn apply_pbr_lighting( view_bindings::view.view_from_world[3].z ), in.world_position); let cluster_index = clustering::fragment_cluster_index(in.frag_coord.xy, view_z, in.is_orthographic); - let offset_and_counts = clustering::unpack_offset_and_counts(cluster_index); + var clusterable_object_index_ranges = + clustering::unpack_clusterable_object_index_ranges(cluster_index); // Point lights (direct) - for (var i: u32 = offset_and_counts[0]; i < offset_and_counts[0] + offset_and_counts[1]; i = i + 1u) { + for (var i: u32 = clusterable_object_index_ranges.first_point_light_index_offset; + i < clusterable_object_index_ranges.first_spot_light_index_offset; + i = i + 1u) { let light_id = clustering::get_clusterable_object_id(i); var shadow: f32 = 1.0; if ((in.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u @@ -450,7 +453,9 @@ fn apply_pbr_lighting( } // Spot lights (direct) - for (var i: u32 = offset_and_counts[0] + offset_and_counts[1]; i < offset_and_counts[0] + offset_and_counts[1] + offset_and_counts[2]; i = i + 1u) { + for (var i: u32 = clusterable_object_index_ranges.first_spot_light_index_offset; + i < clusterable_object_index_ranges.first_reflection_probe_index_offset; + i = i + 1u) { let light_id = clustering::get_clusterable_object_id(i); var shadow: f32 = 1.0; @@ -575,7 +580,10 @@ fn apply_pbr_lighting( // Irradiance volume light (indirect) if (!found_diffuse_indirect) { let irradiance_volume_light = irradiance_volume::irradiance_volume_light( - in.world_position.xyz, in.N); + in.world_position.xyz, + in.N, + &clusterable_object_index_ranges, + ); indirect_light += irradiance_volume_light * diffuse_color * diffuse_occlusion; found_diffuse_indirect = true; } @@ -594,7 +602,8 @@ fn apply_pbr_lighting( let environment_light = environment_map::environment_map_light( environment_map_lighting_input, - found_diffuse_indirect + &clusterable_object_index_ranges, + found_diffuse_indirect, ); // If screen space reflections are going to be used for this material, don't @@ -609,6 +618,7 @@ fn apply_pbr_lighting( if (!use_ssr) { let environment_light = environment_map::environment_map_light( &lighting_input, + &clusterable_object_index_ranges, found_diffuse_indirect ); @@ -667,8 +677,11 @@ fn apply_pbr_lighting( transmissive_environment_light_input.layers[LAYER_CLEARCOAT].roughness = 0.0; #endif // STANDARD_MATERIAL_CLEARCOAT - let transmitted_environment_light = - environment_map::environment_map_light(&transmissive_environment_light_input, false); + let transmitted_environment_light = environment_map::environment_map_light( + &transmissive_environment_light_input, + &clusterable_object_index_ranges, + false, + ); #ifdef STANDARD_MATERIAL_DIFFUSE_TRANSMISSION transmitted_light += transmitted_environment_light.diffuse * diffuse_transmissive_color; @@ -722,7 +735,7 @@ fn apply_pbr_lighting( output_color, view_z, in.is_orthographic, - offset_and_counts, + clusterable_object_index_ranges, cluster_index, ); diff --git a/crates/bevy_pbr/src/ssr/ssr.wgsl b/crates/bevy_pbr/src/ssr/ssr.wgsl index 4f2c4eafcc..3dddfa1ba3 100644 --- a/crates/bevy_pbr/src/ssr/ssr.wgsl +++ b/crates/bevy_pbr/src/ssr/ssr.wgsl @@ -4,6 +4,7 @@ #import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput #import bevy_pbr::{ + clustered_forward, lighting, lighting::{LAYER_BASE, LAYER_CLEARCOAT}, mesh_view_bindings::{view, depth_prepass_texture, deferred_prepass_texture, ssr_settings}, @@ -171,8 +172,16 @@ fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4 { lighting_input.clearcoat_strength = clearcoat; #endif // STANDARD_MATERIAL_CLEARCOAT + // Determine which cluster we're in. We'll need this to find the right + // reflection probe. + let cluster_index = clustered_forward::fragment_cluster_index( + frag_coord.xy, frag_coord.z, false); + var clusterable_object_index_ranges = + clustered_forward::unpack_clusterable_object_index_ranges(cluster_index); + // Sample the environment map. - let environment_light = environment_map::environment_map_light(&lighting_input, false); + let environment_light = environment_map::environment_map_light( + &lighting_input, &clusterable_object_index_ranges, false); // Accumulate the environment map light. indirect_light += view.exposure *