use bevy_asset::Handle;
use bevy_camera::{
prelude::ViewVisibility,
primitives::Aabb,
visibility::{self, Visibility, VisibilityClass},
};
use bevy_ecs::{
component::Component,
entity::Entity,
query::{Or, With, Without},
reflect::ReflectComponent,
resource::Resource,
system::{Commands, Query},
};
use bevy_image::Image;
use bevy_math::{AspectRatio, UVec2, UVec3, Vec3A, Vec3Swizzles as _};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_transform::components::Transform;
use tracing::warn;
use crate::LightProbe;
pub mod assign;
#[cfg(test)]
mod test;
#[derive(Clone, Resource, Debug)]
#[expect(missing_docs, reason = "self explanatory")]
pub struct GlobalClusterSettings {
pub supports_storage_buffers: bool,
pub clustered_decals_are_usable: bool,
pub gpu_clustering: Option<GlobalClusterGpuSettings>,
pub max_uniform_buffer_clusterable_objects: usize,
pub view_cluster_bindings_max_indices: usize,
}
#[derive(Clone, Copy, Debug)]
pub struct GlobalClusterGpuSettings {
pub initial_z_slice_list_capacity: usize,
pub initial_index_list_capacity: usize,
}
#[derive(Debug, Copy, Clone, Reflect)]
#[reflect(Clone)]
pub enum ClusterFarZMode {
MaxClusterableObjectRange,
Constant(f32),
}
#[derive(Debug, Copy, Clone, Reflect)]
#[reflect(Default, Clone)]
pub struct ClusterZConfig {
pub first_slice_depth: f32,
pub far_z_mode: ClusterFarZMode,
}
#[derive(Debug, Copy, Clone, Component, Reflect)]
#[reflect(Component, Debug, Default, Clone)]
pub enum ClusterConfig {
None,
Single,
XYZ {
dimensions: UVec3,
z_config: ClusterZConfig,
dynamic_resizing: bool,
},
FixedZ {
total: u32,
z_slices: u32,
z_config: ClusterZConfig,
dynamic_resizing: bool,
},
}
#[derive(Component, Debug)]
pub struct Clusters {
pub tile_size: UVec2,
pub dimensions: UVec3,
pub near: f32,
pub far: f32,
pub last_frame_farthest_z: Option<f32>,
pub last_frame_total_cluster_index_count: Option<usize>,
pub clusterable_objects: ClusterableObjects,
}
#[derive(Debug)]
pub enum ClusterableObjects {
Cpu(Vec<ObjectsInClusterCpu>),
Gpu,
}
pub struct ClusterVisibilityClass;
#[derive(Clone, Default, Debug)]
pub struct ObjectsInClusterCpu {
clusterables: Vec<Entity>,
pub counts: ClusterableObjectCounts,
}
#[derive(Clone, Copy, Default, Debug)]
pub struct ClusterableObjectCounts {
pub point_lights: u32,
pub spot_lights: u32,
pub reflection_probes: u32,
pub irradiance_volumes: u32,
pub decals: u32,
}
#[derive(Component, Debug, Clone, Default, Reflect)]
#[reflect(Component, Debug, Clone, Default)]
#[require(Transform, ViewVisibility, Visibility, VisibilityClass)]
#[component(on_add = visibility::add_visibility_class::<ClusterVisibilityClass>)]
pub struct ClusteredDecal {
pub base_color_texture: Option<Handle<Image>>,
pub normal_map_texture: Option<Handle<Image>>,
pub metallic_roughness_texture: Option<Handle<Image>>,
pub emissive_texture: Option<Handle<Image>>,
pub tag: u32,
}
impl Default for ClusterZConfig {
fn default() -> Self {
Self {
first_slice_depth: 5.0,
far_z_mode: ClusterFarZMode::MaxClusterableObjectRange,
}
}
}
impl Default for ClusterConfig {
fn default() -> Self {
Self::FixedZ {
total: 4096,
z_slices: 24,
z_config: ClusterZConfig::default(),
dynamic_resizing: true,
}
}
}
impl ClusterConfig {
fn dimensions_for_screen_size(&self, screen_size: UVec2) -> UVec3 {
match &self {
ClusterConfig::None => UVec3::ZERO,
ClusterConfig::Single => UVec3::ONE,
ClusterConfig::XYZ { dimensions, .. } => *dimensions,
ClusterConfig::FixedZ {
total, z_slices, ..
} => {
let aspect_ratio: f32 = AspectRatio::try_from_pixels(screen_size.x, screen_size.y)
.expect("Failed to calculate aspect ratio for Cluster: screen dimensions must be positive, non-zero values")
.ratio();
let mut z_slices = *z_slices;
if *total < z_slices {
warn!("ClusterConfig has more z-slices than total clusters!");
z_slices = *total;
}
let per_layer = *total as f32 / z_slices as f32;
let y = f32::sqrt(per_layer / aspect_ratio);
let mut x = (y * aspect_ratio) as u32;
let mut y = y as u32;
if x == 0 {
x = 1;
y = per_layer as u32;
}
if y == 0 {
x = per_layer as u32;
y = 1;
}
UVec3::new(x, y, z_slices)
}
}
}
fn first_slice_depth(&self) -> f32 {
match self {
ClusterConfig::None | ClusterConfig::Single => 0.0,
ClusterConfig::XYZ { z_config, .. } | ClusterConfig::FixedZ { z_config, .. } => {
z_config.first_slice_depth
}
}
}
fn far_z_mode(&self) -> ClusterFarZMode {
match self {
ClusterConfig::None => ClusterFarZMode::Constant(0.0),
ClusterConfig::Single => ClusterFarZMode::MaxClusterableObjectRange,
ClusterConfig::XYZ { z_config, .. } | ClusterConfig::FixedZ { z_config, .. } => {
z_config.far_z_mode
}
}
}
fn dynamic_resizing(&self) -> bool {
match self {
ClusterConfig::None | ClusterConfig::Single => false,
ClusterConfig::XYZ {
dynamic_resizing, ..
}
| ClusterConfig::FixedZ {
dynamic_resizing, ..
} => *dynamic_resizing,
}
}
}
impl Default for Clusters {
fn default() -> Clusters {
Clusters {
tile_size: UVec2::ZERO,
dimensions: UVec3::ZERO,
near: 0.0,
far: 0.0,
last_frame_farthest_z: None,
last_frame_total_cluster_index_count: None,
clusterable_objects: ClusterableObjects::Cpu(vec![]),
}
}
}
impl Clusters {
fn update(&mut self, screen_size: UVec2, requested_dimensions: UVec3) {
debug_assert!(
requested_dimensions.x > 0 && requested_dimensions.y > 0 && requested_dimensions.z > 0
);
let tile_size = (screen_size.as_vec2() / requested_dimensions.xy().as_vec2())
.ceil()
.as_uvec2()
.max(UVec2::ONE);
self.tile_size = tile_size;
self.dimensions = (screen_size.as_vec2() / tile_size.as_vec2())
.ceil()
.as_uvec2()
.extend(requested_dimensions.z)
.max(UVec3::ONE);
debug_assert!(self.dimensions.x * self.dimensions.y * self.dimensions.z <= 4096);
}
fn clear(&mut self, global_cluster_settings: &GlobalClusterSettings) {
self.tile_size = UVec2::ONE;
self.dimensions = UVec3::ZERO;
self.near = 0.0;
self.far = 0.0;
match (
&mut self.clusterable_objects,
&global_cluster_settings.gpu_clustering,
) {
(ClusterableObjects::Cpu(_), Some(_)) => {
self.clusterable_objects = ClusterableObjects::Gpu;
}
(ClusterableObjects::Cpu(objects_in_cluster_cpu), None) => {
objects_in_cluster_cpu.clear();
}
(ClusterableObjects::Gpu, Some(_)) => {
self.clusterable_objects = ClusterableObjects::Cpu(vec![]);
}
(ClusterableObjects::Gpu, None) => {}
}
}
fn reset_for_new_frame(
&mut self,
cluster_count: usize,
global_cluster_settings: &GlobalClusterSettings,
) {
match (
&mut self.clusterable_objects,
&global_cluster_settings.gpu_clustering,
) {
(ClusterableObjects::Cpu(_), Some(_)) => {
self.clusterable_objects = ClusterableObjects::Gpu;
}
(ClusterableObjects::Cpu(objects_in_cluster_cpu), None) => {
for clusterable_objects in objects_in_cluster_cpu.iter_mut() {
clusterable_objects.clear();
}
objects_in_cluster_cpu.resize_with(cluster_count, ObjectsInClusterCpu::default);
}
(ClusterableObjects::Gpu, Some(_)) => {}
(ClusterableObjects::Gpu, None) => {
self.clusterable_objects =
ClusterableObjects::Cpu(vec![ObjectsInClusterCpu::default(); cluster_count]);
}
}
}
}
impl ObjectsInClusterCpu {
pub fn clear(&mut self) {
self.clusterables.clear();
self.counts = ClusterableObjectCounts::default();
}
pub fn add_spot_light(&mut self, entity: Entity) {
self.clusterables.push(entity);
self.counts.spot_lights += 1;
}
pub fn add_point_light(&mut self, entity: Entity) {
self.clusterables.push(entity);
self.counts.point_lights += 1;
}
pub fn add_reflection_probe(&mut self, entity: Entity) {
self.clusterables.push(entity);
self.counts.reflection_probes += 1;
}
pub fn add_irradiance_volume(&mut self, entity: Entity) {
self.clusterables.push(entity);
self.counts.irradiance_volumes += 1;
}
pub fn add_decal(&mut self, entity: Entity) {
self.clusterables.push(entity);
self.counts.decals += 1;
}
pub fn iter(&self) -> impl DoubleEndedIterator<Item = &Entity> {
self.clusterables.iter()
}
}
pub fn add_light_probe_and_decal_aabbs(
mut commands: Commands,
light_probes_and_decals_query: Query<
Entity,
(Or<(With<ClusteredDecal>, With<LightProbe>)>, Without<Aabb>),
>,
) {
for entity in &light_probes_and_decals_query {
commands.entity(entity).insert(Aabb {
center: Vec3A::ZERO,
half_extents: Vec3A::splat(0.5),
});
}
}