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::irradiance_volume
#import bevy_pbr::mesh_view_bindings
#import bevy_pbr::clustered_forward
struct VoxelVisualizationIrradianceVolumeInfo {
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 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.
// 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<f32>(rgb, 1.0f);
}

View file

@ -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<f32>,
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<EnvironmentMapLight>),
With<LightProbe>,
>,
mut clusterable_objects: Local<Vec<ClusterableObjectAssignmentData>>,
mut cluster_aabb_spheres: Local<Vec<Option<Sphere>>>,
mut max_clusterable_objects_warning_emitted: Local<bool>,
@ -102,10 +191,11 @@ pub(crate) fn assign_objects_to_clusters(
ClusterableObjectAssignmentData {
entity,
transform: GlobalTransform::from_translation(transform.translation()),
range: point_light.range,
object_type: ClusterableObjectType::PointLight {
shadows_enabled: point_light.shadows_enabled,
volumetric: volumetric.is_some(),
range: point_light.range,
spot_light_angle: None,
},
render_layers: maybe_layers.unwrap_or_default().clone(),
}
},
@ -120,10 +210,12 @@ pub(crate) fn assign_objects_to_clusters(
ClusterableObjectAssignmentData {
entity,
transform: *transform,
range: spot_light.range,
object_type: ClusterableObjectType::SpotLight {
outer_angle: spot_light.outer_angle,
shadows_enabled: spot_light.shadows_enabled,
volumetric: volumetric.is_some(),
range: spot_light.range,
spot_light_angle: Some(spot_light.outer_angle),
},
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);
(
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,15 +717,17 @@ 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
{
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
{
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(
@ -655,21 +772,61 @@ pub(crate) fn assign_objects_to_clusters(
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
.spot_lights += 1;
}
cluster_index += clusters.dimensions.z as usize;
}
} else {
}
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].point_light_count += 1;
clusters.clusterable_objects[cluster_index]
.counts
.point_lights += 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;
}
}
}
}
}
}

View file

@ -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<Entity>,
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<UVec4>,
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(
cluster_offsets_and_counts.get_mut().data.push([
uvec4(
offset as u32,
point_count as u32,
spot_count as u32,
0,
));
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)]

View file

@ -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<f32>,
@ -26,6 +27,7 @@ struct EnvironmentMapRadiances {
fn compute_radiances(
input: ptr<function, LightingInput>,
clusterable_object_index_ranges: ptr<function, ClusterableObjectIndexRanges>,
layer: u32,
world_position: vec3<f32>,
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<function, LightingInput>,
clusterable_object_index_ranges: ptr<function, ClusterableObjectIndexRanges>,
layer: u32,
world_position: vec3<f32>,
found_diffuse_indirect: bool,
@ -152,6 +159,7 @@ fn compute_radiances(
fn environment_map_light_clearcoat(
out: ptr<function, EnvironmentMapLight>,
input: ptr<function, LightingInput>,
clusterable_object_index_ranges: ptr<function, ClusterableObjectIndexRanges>,
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<function, LightingInput>,
clusterable_object_index_ranges: ptr<function, ClusterableObjectIndexRanges>,
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;

View file

@ -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<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.
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) {

View file

@ -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<f32>) -> mat4x4<f32> {
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<f32>,
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 {
var result: LightProbeQueryResult;
result.texture_index = -1;
@ -76,3 +145,4 @@ fn query_light_probe(
return result;
}
#endif // AVAILABLE_STORAGE_BUFFER_BINDINGS >= 3

View file

@ -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<f32>, 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<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
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<u32>(
(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<u32>(
(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<f32>,
view_z: f32,
is_orthographic: bool,
offset_and_counts: vec3<u32>,
clusterable_object_index_ranges: ClusterableObjectIndexRanges,
cluster_index: u32,
) -> vec4<f32> {
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

View file

@ -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),
},
)
});

View file

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

View file

@ -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,
);

View file

@ -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<f32> {
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 *