Cluster light probes using conservative spherical bounds. (#13746)

This commit allows the Bevy renderer to use the clustering
infrastructure for light probes (reflection probes and irradiance
volumes) on platforms where at least 3 storage buffers are available. On
such platforms (the vast majority), we stop performing brute-force
searches of light probes for each fragment and instead only search the
light probes with bounding spheres that intersect the current cluster.
This should dramatically improve scalability of irradiance volumes and
reflection probes.

The primary platform that doesn't support 3 storage buffers is WebGL 2,
and we continue using a brute-force search of light probes on that
platform, as the UBO that stores per-cluster indices is too small to fit
the light probe counts. Note, however, that that platform also doesn't
support bindless textures (indeed, it would be very odd for a platform
to support bindless textures but not SSBOs), so we only support one of
each type of light probe per drawcall there in the first place.
Consequently, this isn't a performance problem, as the search will only
have one light probe to consider. (In fact, clustering would probably
end up being a performance loss.)

Known potential improvements include:

1. We currently cull based on a conservative bounding sphere test and
not based on the oriented bounding box (OBB) of the light probe. This is
improvable, but in the interests of simplicity, I opted to keep the
bounding sphere test for now. The OBB improvement can be a follow-up.

2. This patch doesn't change the fact that each fragment only takes a
single light probe into account. Typical light probe implementations
detect the case in which multiple light probes cover the current
fragment and perform some sort of weighted blend between them. As the
light probe fetch function presently returns only a single light probe,
implementing that feature would require more code restructuring, so I
left it out for now. It can be added as a follow-up.

3. Light probe implementations typically have a falloff range. Although
this is a wanted feature in Bevy, this particular commit also doesn't
implement that feature, as it's out of scope.

4. This commit doesn't raise the maximum number of light probes past its
current value of 8 for each type. This should be addressed later, but
would possibly require more bindings on platforms with storage buffers,
which would increase this patch's complexity. Even without raising the
limit, this patch should constitute a significant performance
improvement for scenes that get anywhere close to this limit. In the
interest of keeping this patch small, I opted to leave raising the limit
to a follow-up.

## Changelog

### Changed

* Light probes (reflection probes and irradiance volumes) are now
clustered on most platforms, improving performance when many light
probes are present.

---------

Co-authored-by: Benjamin Brienen <Benjamin.Brienen@outlook.com>
Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
This commit is contained in:
Patrick Walton 2024-12-05 05:07:10 -08:00 committed by GitHub
parent d2a07f9f72
commit b7bcd313ca
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
11 changed files with 547 additions and 168 deletions

View file

@ -1,6 +1,7 @@
#import bevy_pbr::forward_io::VertexOutput #import bevy_pbr::forward_io::VertexOutput
#import bevy_pbr::irradiance_volume #import bevy_pbr::irradiance_volume
#import bevy_pbr::mesh_view_bindings #import bevy_pbr::mesh_view_bindings
#import bevy_pbr::clustered_forward
struct VoxelVisualizationIrradianceVolumeInfo { struct VoxelVisualizationIrradianceVolumeInfo {
world_from_voxel: mat4x4<f32>, world_from_voxel: mat4x4<f32>,
@ -25,11 +26,24 @@ fn fragment(mesh: VertexOutput) -> @location(0) vec4<f32> {
let stp_rounded = round(stp - 0.5f) + 0.5f; 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; 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<f32>(
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. // `irradiance_volume_light()` multiplies by intensity, so cancel it out.
// If we take intensity into account, the cubes will be way too bright. // If we take intensity into account, the cubes will be way too bright.
let rgb = irradiance_volume::irradiance_volume_light( let rgb = irradiance_volume::irradiance_volume_light(
mesh.world_position.xyz, mesh.world_position.xyz,
mesh.world_normal) / irradiance_volume_info.intensity; mesh.world_normal,
&clusterable_object_index_ranges,
) / irradiance_volume_info.intensity;
return vec4<f32>(rgb, 1.0f); return vec4<f32>(rgb, 1.0f);
} }

View file

@ -2,9 +2,13 @@
use bevy_ecs::{ use bevy_ecs::{
entity::Entity, entity::Entity,
query::{Has, With},
system::{Commands, Local, Query, Res, ResMut}, 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::{ use bevy_render::{
camera::Camera, camera::Camera,
primitives::{Aabb, Frustum, HalfSpace, Sphere}, primitives::{Aabb, Frustum, HalfSpace, Sphere},
@ -13,12 +17,13 @@ use bevy_render::{
view::{RenderLayers, ViewVisibility}, view::{RenderLayers, ViewVisibility},
}; };
use bevy_transform::components::GlobalTransform; use bevy_transform::components::GlobalTransform;
use bevy_utils::tracing::warn; use bevy_utils::{prelude::default, tracing::warn};
use crate::{ use crate::{
ClusterConfig, ClusterFarZMode, Clusters, GlobalVisibleClusterableObjects, PointLight, prelude::EnvironmentMapLight, ClusterConfig, ClusterFarZMode, Clusters, ExtractedPointLight,
SpotLight, ViewClusterBindings, VisibleClusterableObjects, VolumetricLight, GlobalVisibleClusterableObjects, LightProbe, PointLight, SpotLight, ViewClusterBindings,
CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT, MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS, VisibleClusterableObjects, VolumetricLight, CLUSTERED_FORWARD_STORAGE_BUFFER_COUNT,
MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS,
}; };
use super::ClusterableObjectOrderData; use super::ClusterableObjectOrderData;
@ -29,15 +34,13 @@ const NDC_MAX: Vec2 = Vec2::ONE;
const VEC2_HALF: Vec2 = Vec2::splat(0.5); const VEC2_HALF: Vec2 = Vec2::splat(0.5);
const VEC2_HALF_NEGATIVE_Y: Vec2 = Vec2::new(0.5, -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 { pub(crate) struct ClusterableObjectAssignmentData {
entity: Entity, entity: Entity,
transform: GlobalTransform, transform: GlobalTransform,
range: f32, range: f32,
shadows_enabled: bool, object_type: ClusterableObjectType,
volumetric: bool,
spot_light_angle: Option<f32>,
render_layers: RenderLayers, 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! // NOTE: Run this before update_point_light_frusta!
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub(crate) fn assign_objects_to_clusters( pub(crate) fn assign_objects_to_clusters(
@ -81,6 +166,10 @@ pub(crate) fn assign_objects_to_clusters(
Option<&VolumetricLight>, Option<&VolumetricLight>,
&ViewVisibility, &ViewVisibility,
)>, )>,
light_probes_query: Query<
(Entity, &GlobalTransform, Has<EnvironmentMapLight>),
With<LightProbe>,
>,
mut clusterable_objects: Local<Vec<ClusterableObjectAssignmentData>>, mut clusterable_objects: Local<Vec<ClusterableObjectAssignmentData>>,
mut cluster_aabb_spheres: Local<Vec<Option<Sphere>>>, mut cluster_aabb_spheres: Local<Vec<Option<Sphere>>>,
mut max_clusterable_objects_warning_emitted: Local<bool>, mut max_clusterable_objects_warning_emitted: Local<bool>,
@ -102,10 +191,11 @@ pub(crate) fn assign_objects_to_clusters(
ClusterableObjectAssignmentData { ClusterableObjectAssignmentData {
entity, entity,
transform: GlobalTransform::from_translation(transform.translation()), transform: GlobalTransform::from_translation(transform.translation()),
shadows_enabled: point_light.shadows_enabled,
volumetric: volumetric.is_some(),
range: point_light.range, 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(), render_layers: maybe_layers.unwrap_or_default().clone(),
} }
}, },
@ -120,10 +210,12 @@ pub(crate) fn assign_objects_to_clusters(
ClusterableObjectAssignmentData { ClusterableObjectAssignmentData {
entity, entity,
transform: *transform, transform: *transform,
shadows_enabled: spot_light.shadows_enabled,
volumetric: volumetric.is_some(),
range: spot_light.range, 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(), render_layers: maybe_layers.unwrap_or_default().clone(),
} }
}, },
@ -136,6 +228,29 @@ pub(crate) fn assign_objects_to_clusters(
clustered_forward_buffer_binding_type, clustered_forward_buffer_binding_type,
BufferBindingType::Storage { .. } 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 if clusterable_objects.len() > MAX_UNIFORM_BUFFER_CLUSTERABLE_OBJECTS
&& !supports_storage_buffers && !supports_storage_buffers
{ {
@ -143,15 +258,11 @@ pub(crate) fn assign_objects_to_clusters(
crate::clusterable_object_order( crate::clusterable_object_order(
ClusterableObjectOrderData { ClusterableObjectOrderData {
entity: &clusterable_object_1.entity, entity: &clusterable_object_1.entity,
shadows_enabled: &clusterable_object_1.shadows_enabled, object_type: &clusterable_object_1.object_type,
is_volumetric_light: &clusterable_object_1.volumetric,
is_spot_light: &clusterable_object_1.spot_light_angle.is_some(),
}, },
ClusterableObjectOrderData { ClusterableObjectOrderData {
entity: &clusterable_object_2.entity, entity: &clusterable_object_2.entity,
shadows_enabled: &clusterable_object_2.shadows_enabled, object_type: &clusterable_object_2.object_type,
is_volumetric_light: &clusterable_object_2.volumetric,
is_spot_light: &clusterable_object_2.spot_light_angle.is_some(),
}, },
) )
}); });
@ -361,8 +472,7 @@ pub(crate) fn assign_objects_to_clusters(
for clusterable_objects in &mut clusters.clusterable_objects { for clusterable_objects in &mut clusters.clusterable_objects {
clusterable_objects.entities.clear(); clusterable_objects.entities.clear();
clusterable_objects.point_light_count = 0; clusterable_objects.counts = default();
clusterable_objects.spot_light_count = 0;
} }
let cluster_count = let cluster_count =
(clusters.dimensions.x * clusters.dimensions.y * clusters.dimensions.z) as usize; (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, radius: clusterable_object_sphere.radius * view_from_world_scale_max,
}; };
let spot_light_dir_sin_cos = clusterable_object.spot_light_angle.map(|angle| { let spot_light_dir_sin_cos = match clusterable_object.object_type {
let (angle_sin, angle_cos) = ops::sin_cos(angle); ClusterableObjectType::SpotLight { outer_angle, .. } => {
( let (angle_sin, angle_cos) = sin_cos(outer_angle);
(view_from_world * clusterable_object.transform.back().extend(0.0)) Some((
.truncate() (view_from_world * clusterable_object.transform.back().extend(0.0))
.normalize(), .truncate()
angle_sin, .normalize(),
angle_cos, angle_sin,
) angle_cos,
}); ))
}
ClusterableObjectType::PointLight { .. }
| ClusterableObjectType::ReflectionProbe
| ClusterableObjectType::IrradianceVolume => None,
};
let clusterable_object_center_clip = let clusterable_object_center_clip =
camera.clip_from_view() * view_clusterable_object_sphere.center.extend(1.0); camera.clip_from_view() * view_clusterable_object_sphere.center.extend(1.0);
let object_center_ndc = let object_center_ndc =
@ -602,72 +717,114 @@ pub(crate) fn assign_objects_to_clusters(
* clusters.dimensions.z * clusters.dimensions.z
+ z) as usize; + z) as usize;
if let Some((view_light_direction, angle_sin, angle_cos)) = match clusterable_object.object_type {
spot_light_dir_sin_cos ClusterableObjectType::SpotLight { .. } => {
{ let (view_light_direction, angle_sin, angle_cos) =
for x in min_x..=max_x { spot_light_dir_sin_cos.unwrap();
// further culling for spot lights for x in min_x..=max_x {
// get or initialize cluster bounding sphere // further culling for spot lights
let cluster_aabb_sphere = &mut cluster_aabb_spheres[cluster_index]; // get or initialize cluster bounding sphere
let cluster_aabb_sphere = if let Some(sphere) = cluster_aabb_sphere let cluster_aabb_sphere =
{ &mut cluster_aabb_spheres[cluster_index];
&*sphere let cluster_aabb_sphere =
} else { if let Some(sphere) = cluster_aabb_sphere {
let aabb = compute_aabb_for_cluster( &*sphere
first_slice_depth, } else {
far_z, let aabb = compute_aabb_for_cluster(
clusters.tile_size.as_vec2(), first_slice_depth,
screen_size.as_vec2(), far_z,
view_from_clip, clusters.tile_size.as_vec2(),
is_orthographic, screen_size.as_vec2(),
clusters.dimensions, view_from_clip,
UVec3::new(x, y, z), 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 { let spot_light_dist_sq = spot_light_offset.length_squared();
center: aabb.center, let v1_len = spot_light_offset.dot(view_light_direction);
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 distance_closest_point = (angle_cos
let spot_light_offset = Vec3::from( * (spot_light_dist_sq - v1_len * v1_len).sqrt())
view_clusterable_object_sphere.center - v1_len * angle_sin;
- cluster_aabb_sphere.center, let angle_cull =
); distance_closest_point > cluster_aabb_sphere.radius;
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 let front_cull = v1_len
* (spot_light_dist_sq - v1_len * v1_len).sqrt()) > cluster_aabb_sphere.radius
- v1_len * angle_sin; + clusterable_object.range * view_from_world_scale_max;
let angle_cull = let back_cull = v1_len < -cluster_aabb_sphere.radius;
distance_closest_point > cluster_aabb_sphere.radius;
let front_cull = v1_len if !angle_cull && !front_cull && !back_cull {
> cluster_aabb_sphere.radius // this cluster is affected by the spot light
+ clusterable_object.range * view_from_world_scale_max; clusters.clusterable_objects[cluster_index]
let back_cull = v1_len < -cluster_aabb_sphere.radius; .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 { ClusterableObjectType::PointLight { .. } => {
// this cluster is affected by the spot light for _ in min_x..=max_x {
// all clusters within range are affected by point lights
clusters.clusterable_objects[cluster_index] clusters.clusterable_objects[cluster_index]
.entities .entities
.push(clusterable_object.entity); .push(clusterable_object.entity);
clusters.clusterable_objects[cluster_index].spot_light_count += clusters.clusterable_objects[cluster_index]
1; .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 { ClusterableObjectType::ReflectionProbe => {
// all clusters within range are affected by point lights // Reflection probes currently affect all
clusters.clusterable_objects[cluster_index] // clusters in their bounding sphere.
.entities //
.push(clusterable_object.entity); // TODO: Cull more aggressively based on the
clusters.clusterable_objects[cluster_index].point_light_count += 1; // probe's OBB.
cluster_index += clusters.dimensions.z as usize; 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;
}
} }
} }
} }

View file

@ -2,6 +2,7 @@
use core::num::NonZero; use core::num::NonZero;
use self::assign::ClusterableObjectType;
use bevy_core_pipeline::core_3d::Camera3d; use bevy_core_pipeline::core_3d::Camera3d;
use bevy_ecs::{ use bevy_ecs::{
component::Component, component::Component,
@ -11,7 +12,7 @@ use bevy_ecs::{
system::{Commands, Query, Res, Resource}, system::{Commands, Query, Res, Resource},
world::{FromWorld, World}, 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_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{ use bevy_render::{
camera::Camera, camera::Camera,
@ -28,7 +29,7 @@ use bevy_utils::{hashbrown::HashSet, tracing::warn};
pub(crate) use crate::cluster::assign::assign_objects_to_clusters; pub(crate) use crate::cluster::assign::assign_objects_to_clusters;
use crate::MeshPipeline; use crate::MeshPipeline;
mod assign; pub(crate) mod assign;
#[cfg(test)] #[cfg(test)]
mod test; mod test;
@ -132,8 +133,7 @@ pub struct Clusters {
#[derive(Clone, Component, Debug, Default)] #[derive(Clone, Component, Debug, Default)]
pub struct VisibleClusterableObjects { pub struct VisibleClusterableObjects {
pub(crate) entities: Vec<Entity>, pub(crate) entities: Vec<Entity>,
pub point_light_count: usize, counts: ClusterableObjectCounts,
pub spot_light_count: usize,
} }
#[derive(Resource, Default)] #[derive(Resource, Default)]
@ -189,8 +189,24 @@ pub struct ExtractedClusterConfig {
pub(crate) dimensions: UVec3, 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 { enum ExtractedClusterableObjectElement {
ClusterHeader(u32, u32), ClusterHeader(ClusterableObjectCounts),
ClusterableObjectEntity(Entity), ClusterableObjectEntity(Entity),
} }
@ -212,8 +228,11 @@ struct GpuClusterableObjectIndexListsStorage {
#[derive(ShaderType, Default)] #[derive(ShaderType, Default)]
struct GpuClusterOffsetsAndCountsStorage { 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)] #[size(runtime)]
data: Vec<UVec4>, data: Vec<[UVec4; 2]>,
} }
enum ViewClusterBuffers { enum ViewClusterBuffers {
@ -499,16 +518,14 @@ impl Default for GpuClusterableObjectsUniform {
pub(crate) struct ClusterableObjectOrderData<'a> { pub(crate) struct ClusterableObjectOrderData<'a> {
pub(crate) entity: &'a Entity, pub(crate) entity: &'a Entity,
pub(crate) shadows_enabled: &'a bool, pub(crate) object_type: &'a ClusterableObjectType,
pub(crate) is_volumetric_light: &'a bool,
pub(crate) is_spot_light: &'a bool,
} }
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
// Sort clusterable objects by: // Sort clusterable objects by:
// //
// * point-light vs spot-light, so that we can iterate point lights and spot // * object type, so that we can iterate point lights, spot lights, etc. in
// lights in contiguous blocks in the fragment shader, // contiguous blocks in the fragment shader,
// //
// * then those with shadows enabled first, so that the index can be used to // * 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 // render at most `point_light_shadow_maps_count` point light shadows and
@ -521,10 +538,9 @@ pub(crate) fn clusterable_object_order(
a: ClusterableObjectOrderData, a: ClusterableObjectOrderData,
b: ClusterableObjectOrderData, b: ClusterableObjectOrderData,
) -> core::cmp::Ordering { ) -> core::cmp::Ordering {
a.is_spot_light a.object_type
.cmp(b.is_spot_light) // pointlights before spot lights .ordering()
.then_with(|| b.shadows_enabled.cmp(a.shadows_enabled)) // shadow casters before non-casters .cmp(&b.object_type.ordering())
.then_with(|| b.is_volumetric_light.cmp(a.is_volumetric_light)) // volumetric lights before non-volumetric lights
.then_with(|| a.entity.cmp(b.entity)) // stable .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); let mut data = Vec::with_capacity(clusters.clusterable_objects.len() + num_entities);
for cluster_objects in &clusters.clusterable_objects { for cluster_objects in &clusters.clusterable_objects {
data.push(ExtractedClusterableObjectElement::ClusterHeader( data.push(ExtractedClusterableObjectElement::ClusterHeader(
cluster_objects.point_light_count as u32, cluster_objects.counts,
cluster_objects.spot_light_count as u32,
)); ));
for clusterable_entity in &cluster_objects.entities { for clusterable_entity in &cluster_objects.entities {
if let Ok(entity) = mapper.get(*clusterable_entity) { if let Ok(entity) = mapper.get(*clusterable_entity) {
@ -594,16 +609,9 @@ pub fn prepare_clusters(
for record in &extracted_clusters.data { for record in &extracted_clusters.data {
match record { match record {
ExtractedClusterableObjectElement::ClusterHeader( ExtractedClusterableObjectElement::ClusterHeader(counts) => {
point_light_count,
spot_light_count,
) => {
let offset = view_clusters_bindings.n_indices(); let offset = view_clusters_bindings.n_indices();
view_clusters_bindings.push_offset_and_counts( view_clusters_bindings.push_offset_and_counts(offset, counts);
offset,
*point_light_count as usize,
*spot_light_count as usize,
);
} }
ExtractedClusterableObjectElement::ClusterableObjectEntity(entity) => { ExtractedClusterableObjectElement::ClusterableObjectEntity(entity) => {
if let Some(clusterable_object_index) = 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 { match &mut self.buffers {
ViewClusterBuffers::Uniform { ViewClusterBuffers::Uniform {
cluster_offsets_and_counts, cluster_offsets_and_counts,
@ -676,7 +684,8 @@ impl ViewClusterBindings {
return; return;
} }
let component = self.n_offsets & ((1 << 2) - 1); 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; 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,
.. ..
} => { } => {
cluster_offsets_and_counts.get_mut().data.push(UVec4::new( cluster_offsets_and_counts.get_mut().data.push([
offset as u32, uvec4(
point_count as u32, offset as u32,
spot_count as u32, counts.point_lights,
0, 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 // NOTE: With uniform buffer max binding size as 16384 bytes
// that means we can fit 204 clusterable objects in one uniform // that means we can fit 204 clusterable objects in one uniform
// buffer, which means the count can be at most 204 so it // 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. // the point light count into bits 9-17, and the spot light count into bits 0-8.
// [ 31 .. 18 | 17 .. 9 | 8 .. 0 ] // [ 31 .. 18 | 17 .. 9 | 8 .. 0 ]
// [ offset | point light count | spot light count ] // [ offset | point light count | spot light count ]
//
// NOTE: This assumes CPU and GPU endianness are the same which is true // 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 // 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)) ((offset as u32 & CLUSTER_OFFSET_MASK) << (CLUSTER_COUNT_SIZE * 2))
| (point_count as u32 & CLUSTER_COUNT_MASK) << CLUSTER_COUNT_SIZE | (point_count & CLUSTER_COUNT_MASK) << CLUSTER_COUNT_SIZE
| (spot_count as u32 & CLUSTER_COUNT_MASK) | (spot_count & CLUSTER_COUNT_MASK)
} }
#[derive(ShaderType)] #[derive(ShaderType)]

View file

@ -7,6 +7,7 @@
#import bevy_pbr::lighting::{ #import bevy_pbr::lighting::{
F_Schlick_vec, LayerLightingInput, LightingInput, LAYER_BASE, LAYER_CLEARCOAT F_Schlick_vec, LayerLightingInput, LightingInput, LAYER_BASE, LAYER_CLEARCOAT
} }
#import bevy_pbr::clustered_forward::ClusterableObjectIndexRanges
struct EnvironmentMapLight { struct EnvironmentMapLight {
diffuse: vec3<f32>, diffuse: vec3<f32>,
@ -26,6 +27,7 @@ struct EnvironmentMapRadiances {
fn compute_radiances( fn compute_radiances(
input: ptr<function, LightingInput>, input: ptr<function, LightingInput>,
clusterable_object_index_ranges: ptr<function, ClusterableObjectIndexRanges>,
layer: u32, layer: u32,
world_position: vec3<f32>, world_position: vec3<f32>,
found_diffuse_indirect: bool, found_diffuse_indirect: bool,
@ -38,7 +40,11 @@ fn compute_radiances(
var radiances: EnvironmentMapRadiances; var radiances: EnvironmentMapRadiances;
// Search for a reflection probe that contains the fragment. // 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 we didn't find a reflection probe, use the view environment map if applicable.
if (query_result.texture_index < 0) { if (query_result.texture_index < 0) {
@ -90,6 +96,7 @@ fn compute_radiances(
fn compute_radiances( fn compute_radiances(
input: ptr<function, LightingInput>, input: ptr<function, LightingInput>,
clusterable_object_index_ranges: ptr<function, ClusterableObjectIndexRanges>,
layer: u32, layer: u32,
world_position: vec3<f32>, world_position: vec3<f32>,
found_diffuse_indirect: bool, found_diffuse_indirect: bool,
@ -152,6 +159,7 @@ fn compute_radiances(
fn environment_map_light_clearcoat( fn environment_map_light_clearcoat(
out: ptr<function, EnvironmentMapLight>, out: ptr<function, EnvironmentMapLight>,
input: ptr<function, LightingInput>, input: ptr<function, LightingInput>,
clusterable_object_index_ranges: ptr<function, ClusterableObjectIndexRanges>,
found_diffuse_indirect: bool, found_diffuse_indirect: bool,
) { ) {
// Unpack. // Unpack.
@ -166,7 +174,12 @@ fn environment_map_light_clearcoat(
let inv_Fc = 1.0 - Fc; let inv_Fc = 1.0 - Fc;
let clearcoat_radiances = compute_radiances( 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. // Composite the clearcoat layer on top of the existing one.
// These formulas are from Filament: // These formulas are from Filament:
@ -179,6 +192,7 @@ fn environment_map_light_clearcoat(
fn environment_map_light( fn environment_map_light(
input: ptr<function, LightingInput>, input: ptr<function, LightingInput>,
clusterable_object_index_ranges: ptr<function, ClusterableObjectIndexRanges>,
found_diffuse_indirect: bool, found_diffuse_indirect: bool,
) -> EnvironmentMapLight { ) -> EnvironmentMapLight {
// Unpack. // Unpack.
@ -191,7 +205,14 @@ fn environment_map_light(
var out: EnvironmentMapLight; 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))) { if (all(radiances.irradiance == vec3(0.0)) && all(radiances.radiance == vec3(0.0))) {
out.diffuse = vec3(0.0); out.diffuse = vec3(0.0);
out.specular = vec3(0.0); out.specular = vec3(0.0);
@ -225,7 +246,12 @@ fn environment_map_light(
out.specular = FssEss * radiances.radiance; out.specular = FssEss * radiances.radiance;
#ifdef STANDARD_MATERIAL_CLEARCOAT #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 #endif // STANDARD_MATERIAL_CLEARCOAT
return out; return out;

View file

@ -7,15 +7,24 @@
irradiance_volume_sampler, irradiance_volume_sampler,
light_probes, light_probes,
}; };
#import bevy_pbr::clustered_forward::ClusterableObjectIndexRanges
#ifdef IRRADIANCE_VOLUMES_ARE_USABLE #ifdef IRRADIANCE_VOLUMES_ARE_USABLE
// See: // See:
// https://advances.realtimerendering.com/s2006/Mitchell-ShadingInValvesSourceEngine.pdf // https://advances.realtimerendering.com/s2006/Mitchell-ShadingInValvesSourceEngine.pdf
// Slide 28, "Ambient Cube Basis" // Slide 28, "Ambient Cube Basis"
fn irradiance_volume_light(world_position: vec3<f32>, N: vec3<f32>) -> vec3<f32> { fn irradiance_volume_light(
world_position: vec3<f32>,
N: vec3<f32>,
clusterable_object_index_ranges: ptr<function, ClusterableObjectIndexRanges>,
) -> vec3<f32> {
// Search for an irradiance volume that contains the fragment. // 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 there was no irradiance volume found, bail out.
if (query_result.texture_index < 0) { if (query_result.texture_index < 0) {

View file

@ -1,5 +1,7 @@
#define_import_path bevy_pbr::light_probe #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_bindings::light_probes
#import bevy_pbr::mesh_view_types::LightProbe #import bevy_pbr::mesh_view_types::LightProbe
@ -25,12 +27,79 @@ fn transpose_affine_matrix(matrix: mat3x4<f32>) -> mat4x4<f32> {
return transpose(matrix4x4); return transpose(matrix4x4);
} }
#if AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3
// Searches for a light probe that contains the fragment. // 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. // TODO: Interpolate between multiple light probes.
fn query_light_probe( fn query_light_probe(
world_position: vec3<f32>, world_position: vec3<f32>,
is_irradiance_volume: bool, is_irradiance_volume: bool,
clusterable_object_index_ranges: ptr<function, ClusterableObjectIndexRanges>,
) -> 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<f32>(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<f32>,
is_irradiance_volume: bool,
clusterable_object_index_ranges: ptr<function, ClusterableObjectIndexRanges>,
) -> LightProbeQueryResult { ) -> LightProbeQueryResult {
var result: LightProbeQueryResult; var result: LightProbeQueryResult;
result.texture_index = -1; result.texture_index = -1;
@ -76,3 +145,4 @@ fn query_light_probe(
return result; return result;
} }
#endif // AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3

View file

@ -10,6 +10,28 @@
maths::PI_2, 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 // NOTE: Keep in sync with bevy_pbr/src/light.rs
fn view_z_to_z_slice(view_z: f32, is_orthographic: bool) -> u32 { fn view_z_to_z_slice(view_z: f32, is_orthographic: bool) -> u32 {
var z_slice: u32 = 0u; var z_slice: u32 = 0u;
@ -38,21 +60,65 @@ fn fragment_cluster_index(frag_coord: vec2<f32>, view_z: f32, is_orthographic: b
// this must match CLUSTER_COUNT_SIZE in light.rs // this must match CLUSTER_COUNT_SIZE in light.rs
const CLUSTER_COUNT_SIZE = 9u; const CLUSTER_COUNT_SIZE = 9u;
fn unpack_offset_and_counts(cluster_index: u32) -> vec3<u32> {
// 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 #if AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3
return bindings::cluster_offsets_and_counts.data[cluster_index].xyz;
#else let offset_and_counts_a = bindings::cluster_offsets_and_counts.data[cluster_index][0];
let offset_and_counts = bindings::cluster_offsets_and_counts.data[cluster_index >> 2u][cluster_index & ((1u << 2u) - 1u)]; 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 ] // [ 31 .. 18 | 17 .. 9 | 8 .. 0 ]
// [ offset | point light count | spot light count ] // [ offset | point light count | spot light count ]
return vec3<u32>( let offset_and_counts = vec3<u32>(
(offset_and_counts >> (CLUSTER_COUNT_SIZE * 2u)) & ((1u << (32u - (CLUSTER_COUNT_SIZE * 2u))) - 1u), (raw_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), (raw_offset_and_counts >> CLUSTER_COUNT_SIZE) & ((1u << CLUSTER_COUNT_SIZE) - 1u),
offset_and_counts & ((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 { fn get_clusterable_object_id(index: u32) -> u32 {
#if AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3 #if AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3
return bindings::clusterable_object_index_lists.data[index]; return bindings::clusterable_object_index_lists.data[index];
@ -70,7 +136,7 @@ fn cluster_debug_visualization(
input_color: vec4<f32>, input_color: vec4<f32>,
view_z: f32, view_z: f32,
is_orthographic: bool, is_orthographic: bool,
offset_and_counts: vec3<u32>, clusterable_object_index_ranges: ClusterableObjectIndexRanges,
cluster_index: u32, cluster_index: u32,
) -> vec4<f32> { ) -> vec4<f32> {
var output_color = input_color; var output_color = input_color;
@ -101,16 +167,12 @@ fn cluster_debug_visualization(
// complexity measure. // complexity measure.
let cluster_overlay_alpha = 0.1; let cluster_overlay_alpha = 0.1;
let max_complexity_per_cluster = 64.0; 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 * output_color.r = (1.0 - cluster_overlay_alpha) * output_color.r + cluster_overlay_alpha *
smoothStep( smoothstep(0.0, max_complexity_per_cluster, f32(object_count));
0.0,
max_complexity_per_cluster,
f32(offset_and_counts[1] + offset_and_counts[2]));
output_color.g = (1.0 - cluster_overlay_alpha) * output_color.g + cluster_overlay_alpha * output_color.g = (1.0 - cluster_overlay_alpha) * output_color.g + cluster_overlay_alpha *
(1.0 - smoothStep( (1.0 - smoothstep(0.0, max_complexity_per_cluster, f32(object_count)));
0.0,
max_complexity_per_cluster,
f32(offset_and_counts[1] + offset_and_counts[2])));
#endif // CLUSTERED_FORWARD_DEBUG_CLUSTER_COMPLEXITY #endif // CLUSTERED_FORWARD_DEBUG_CLUSTER_COMPLEXITY
#ifdef CLUSTERED_FORWARD_DEBUG_CLUSTER_COHERENCY #ifdef CLUSTERED_FORWARD_DEBUG_CLUSTER_COHERENCY
// NOTE: Visualizes the cluster to which the fragment belongs // NOTE: Visualizes the cluster to which the fragment belongs

View file

@ -1,3 +1,4 @@
use self::assign::ClusterableObjectType;
use crate::material_bind_groups::MaterialBindGroupAllocator; use crate::material_bind_groups::MaterialBindGroupAllocator;
use crate::*; use crate::*;
use bevy_asset::UntypedAssetId; use bevy_asset::UntypedAssetId;
@ -821,15 +822,11 @@ pub fn prepare_lights(
clusterable_object_order( clusterable_object_order(
ClusterableObjectOrderData { ClusterableObjectOrderData {
entity: entity_1, entity: entity_1,
shadows_enabled: &light_1.shadows_enabled, object_type: &ClusterableObjectType::from_point_or_spot_light(light_1),
is_volumetric_light: &light_1.volumetric,
is_spot_light: &light_1.spot_light_angles.is_some(),
}, },
ClusterableObjectOrderData { ClusterableObjectOrderData {
entity: entity_2, entity: entity_2,
shadows_enabled: &light_2.shadows_enabled, object_type: &ClusterableObjectType::from_point_or_spot_light(light_2),
is_volumetric_light: &light_2.volumetric,
is_spot_light: &light_2.spot_light_angles.is_some(),
}, },
) )
}); });

View file

@ -101,7 +101,7 @@ struct ClusterLightIndexLists {
data: array<u32>, data: array<u32>,
}; };
struct ClusterOffsetsAndCounts { struct ClusterOffsetsAndCounts {
data: array<vec4<u32>>, data: array<array<vec4<u32>, 2>>,
}; };
#else #else
struct ClusterableObjects { struct ClusterableObjects {

View file

@ -413,10 +413,13 @@ fn apply_pbr_lighting(
view_bindings::view.view_from_world[3].z view_bindings::view.view_from_world[3].z
), in.world_position); ), in.world_position);
let cluster_index = clustering::fragment_cluster_index(in.frag_coord.xy, view_z, in.is_orthographic); 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) // 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); let light_id = clustering::get_clusterable_object_id(i);
var shadow: f32 = 1.0; var shadow: f32 = 1.0;
if ((in.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u if ((in.flags & MESH_FLAGS_SHADOW_RECEIVER_BIT) != 0u
@ -450,7 +453,9 @@ fn apply_pbr_lighting(
} }
// Spot lights (direct) // 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); let light_id = clustering::get_clusterable_object_id(i);
var shadow: f32 = 1.0; var shadow: f32 = 1.0;
@ -575,7 +580,10 @@ fn apply_pbr_lighting(
// Irradiance volume light (indirect) // Irradiance volume light (indirect)
if (!found_diffuse_indirect) { if (!found_diffuse_indirect) {
let irradiance_volume_light = irradiance_volume::irradiance_volume_light( 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; indirect_light += irradiance_volume_light * diffuse_color * diffuse_occlusion;
found_diffuse_indirect = true; found_diffuse_indirect = true;
} }
@ -594,7 +602,8 @@ fn apply_pbr_lighting(
let environment_light = environment_map::environment_map_light( let environment_light = environment_map::environment_map_light(
environment_map_lighting_input, 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 // 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) { if (!use_ssr) {
let environment_light = environment_map::environment_map_light( let environment_light = environment_map::environment_map_light(
&lighting_input, &lighting_input,
&clusterable_object_index_ranges,
found_diffuse_indirect found_diffuse_indirect
); );
@ -667,8 +677,11 @@ fn apply_pbr_lighting(
transmissive_environment_light_input.layers[LAYER_CLEARCOAT].roughness = 0.0; transmissive_environment_light_input.layers[LAYER_CLEARCOAT].roughness = 0.0;
#endif // STANDARD_MATERIAL_CLEARCOAT #endif // STANDARD_MATERIAL_CLEARCOAT
let transmitted_environment_light = let transmitted_environment_light = environment_map::environment_map_light(
environment_map::environment_map_light(&transmissive_environment_light_input, false); &transmissive_environment_light_input,
&clusterable_object_index_ranges,
false,
);
#ifdef STANDARD_MATERIAL_DIFFUSE_TRANSMISSION #ifdef STANDARD_MATERIAL_DIFFUSE_TRANSMISSION
transmitted_light += transmitted_environment_light.diffuse * diffuse_transmissive_color; transmitted_light += transmitted_environment_light.diffuse * diffuse_transmissive_color;
@ -722,7 +735,7 @@ fn apply_pbr_lighting(
output_color, output_color,
view_z, view_z,
in.is_orthographic, in.is_orthographic,
offset_and_counts, clusterable_object_index_ranges,
cluster_index, cluster_index,
); );

View file

@ -4,6 +4,7 @@
#import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput #import bevy_core_pipeline::fullscreen_vertex_shader::FullscreenVertexOutput
#import bevy_pbr::{ #import bevy_pbr::{
clustered_forward,
lighting, lighting,
lighting::{LAYER_BASE, LAYER_CLEARCOAT}, lighting::{LAYER_BASE, LAYER_CLEARCOAT},
mesh_view_bindings::{view, depth_prepass_texture, deferred_prepass_texture, ssr_settings}, mesh_view_bindings::{view, depth_prepass_texture, deferred_prepass_texture, ssr_settings},
@ -171,8 +172,16 @@ fn fragment(in: FullscreenVertexOutput) -> @location(0) vec4<f32> {
lighting_input.clearcoat_strength = clearcoat; lighting_input.clearcoat_strength = clearcoat;
#endif // STANDARD_MATERIAL_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. // 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. // Accumulate the environment map light.
indirect_light += view.exposure * indirect_light += view.exposure *