//! Spatial clustering of objects, currently just point and spot lights.12use bevy_asset::Handle;3use bevy_camera::{4visibility::{self, Visibility, VisibilityClass},5Camera, Camera3d,6};7use bevy_ecs::{8component::Component,9entity::Entity,10query::{With, Without},11reflect::ReflectComponent,12resource::Resource,13system::{Commands, Query},14};15use bevy_image::Image;16use bevy_math::{AspectRatio, UVec2, UVec3, Vec3Swizzles as _};17use bevy_platform::collections::HashSet;18use bevy_reflect::{std_traits::ReflectDefault, Reflect};19use bevy_transform::components::Transform;20use tracing::warn;2122pub mod assign;2324#[cfg(test)]25mod test;2627// Clustered-forward rendering notes28// The main initial reference material used was this rather accessible article:29// http://www.aortiz.me/2018/12/21/CG.html30// Some inspiration was taken from “Practical Clustered Shading” which is part 2 of:31// https://efficientshading.com/2015/01/01/real-time-many-light-management-and-shadows-with-clustered-shading/32// (Also note that Part 3 of the above shows how we could support the shadow mapping for many lights.)33// The z-slicing method mentioned in the aortiz article is originally from Tiago Sousa's Siggraph 2016 talk about Doom 2016:34// http://advances.realtimerendering.com/s2016/Siggraph2016_idTech6.pdf3536#[derive(Resource)]37pub struct GlobalClusterSettings {38pub supports_storage_buffers: bool,39pub clustered_decals_are_usable: bool,40pub max_uniform_buffer_clusterable_objects: usize,41pub view_cluster_bindings_max_indices: usize,42}4344/// Configure the far z-plane mode used for the furthest depth slice for clustered forward45/// rendering46#[derive(Debug, Copy, Clone, Reflect)]47#[reflect(Clone)]48pub enum ClusterFarZMode {49/// Calculate the required maximum z-depth based on currently visible50/// clusterable objects. Makes better use of available clusters, speeding51/// up GPU lighting operations at the expense of some CPU time and using52/// more indices in the clusterable object index lists.53MaxClusterableObjectRange,54/// Constant max z-depth55Constant(f32),56}5758/// Configure the depth-slicing strategy for clustered forward rendering59#[derive(Debug, Copy, Clone, Reflect)]60#[reflect(Default, Clone)]61pub struct ClusterZConfig {62/// Far `Z` plane of the first depth slice63pub first_slice_depth: f32,64/// Strategy for how to evaluate the far `Z` plane of the furthest depth slice65pub far_z_mode: ClusterFarZMode,66}6768/// Configuration of the clustering strategy for clustered forward rendering69#[derive(Debug, Copy, Clone, Component, Reflect)]70#[reflect(Component, Debug, Default, Clone)]71pub enum ClusterConfig {72/// Disable cluster calculations for this view73None,74/// One single cluster. Optimal for low-light complexity scenes or scenes where75/// most lights affect the entire scene.76Single,77/// Explicit `X`, `Y` and `Z` counts (may yield non-square `X/Y` clusters depending on the aspect ratio)78XYZ {79dimensions: UVec3,80z_config: ClusterZConfig,81/// Specify if clusters should automatically resize in `X/Y` if there is a risk of exceeding82/// the available cluster-object index limit83dynamic_resizing: bool,84},85/// Fixed number of `Z` slices, `X` and `Y` calculated to give square clusters86/// with at most total clusters. For top-down games where lights will generally always be within a87/// short depth range, it may be useful to use this configuration with 1 or few `Z` slices. This88/// would reduce the number of lights per cluster by distributing more clusters in screen space89/// `X/Y` which matches how lights are distributed in the scene.90FixedZ {91total: u32,92z_slices: u32,93z_config: ClusterZConfig,94/// Specify if clusters should automatically resize in `X/Y` if there is a risk of exceeding95/// the available clusterable object index limit96dynamic_resizing: bool,97},98}99100#[derive(Component, Debug, Default)]101pub struct Clusters {102/// Tile size103pub tile_size: UVec2,104/// Number of clusters in `X` / `Y` / `Z` in the view frustum105pub dimensions: UVec3,106/// Distance to the far plane of the first depth slice. The first depth slice is special107/// and explicitly-configured to avoid having unnecessarily many slices close to the camera.108pub near: f32,109pub far: f32,110pub clusterable_objects: Vec<VisibleClusterableObjects>,111}112113/// The [`VisibilityClass`] used for clusterables (decals, point lights, directional lights, and spot lights).114///115/// [`VisibilityClass`]: bevy_camera::visibility::VisibilityClass116pub struct ClusterVisibilityClass;117118#[derive(Clone, Component, Debug, Default)]119pub struct VisibleClusterableObjects {120pub entities: Vec<Entity>,121pub counts: ClusterableObjectCounts,122}123124#[derive(Resource, Default)]125pub struct GlobalVisibleClusterableObjects {126pub(crate) entities: HashSet<Entity>,127}128129/// Stores the number of each type of clusterable object in a single cluster.130///131/// Note that `reflection_probes` and `irradiance_volumes` won't be clustered if132/// fewer than 3 SSBOs are available, which usually means on WebGL 2.133#[derive(Clone, Copy, Default, Debug)]134pub struct ClusterableObjectCounts {135/// The number of point lights in the cluster.136pub point_lights: u32,137/// The number of spot lights in the cluster.138pub spot_lights: u32,139/// The number of reflection probes in the cluster.140pub reflection_probes: u32,141/// The number of irradiance volumes in the cluster.142pub irradiance_volumes: u32,143/// The number of decals in the cluster.144pub decals: u32,145}146147/// An object that projects a decal onto surfaces within its bounds.148///149/// Conceptually, a clustered decal is a 1×1×1 cube centered on its origin. It150/// projects the given [`Self::image`] onto surfaces in the -Z direction (thus151/// you may find [`Transform::looking_at`] useful).152///153/// Clustered decals are the highest-quality types of decals that Bevy supports,154/// but they require bindless textures. This means that they presently can't be155/// used on WebGL 2, WebGPU, macOS, or iOS. Bevy's clustered decals can be used156/// with forward or deferred rendering and don't require a prepass.157#[derive(Component, Debug, Clone, Reflect)]158#[reflect(Component, Debug, Clone)]159#[require(Transform, Visibility, VisibilityClass)]160#[component(on_add = visibility::add_visibility_class::<ClusterVisibilityClass>)]161pub struct ClusteredDecal {162/// The image that the clustered decal projects.163///164/// This must be a 2D image. If it has an alpha channel, it'll be alpha165/// blended with the underlying surface and/or other decals. All decal166/// images in the scene must use the same sampler.167pub image: Handle<Image>,168169/// An application-specific tag you can use for any purpose you want.170///171/// See the `clustered_decals` example for an example of use.172pub tag: u32,173}174175impl Default for ClusterZConfig {176fn default() -> Self {177Self {178first_slice_depth: 5.0,179far_z_mode: ClusterFarZMode::MaxClusterableObjectRange,180}181}182}183184impl Default for ClusterConfig {185fn default() -> Self {186// 24 depth slices, square clusters with at most 4096 total clusters187// use max light distance as clusters max `Z`-depth, first slice extends to 5.0188Self::FixedZ {189total: 4096,190z_slices: 24,191z_config: ClusterZConfig::default(),192dynamic_resizing: true,193}194}195}196197impl ClusterConfig {198fn dimensions_for_screen_size(&self, screen_size: UVec2) -> UVec3 {199match &self {200ClusterConfig::None => UVec3::ZERO,201ClusterConfig::Single => UVec3::ONE,202ClusterConfig::XYZ { dimensions, .. } => *dimensions,203ClusterConfig::FixedZ {204total, z_slices, ..205} => {206let aspect_ratio: f32 = AspectRatio::try_from_pixels(screen_size.x, screen_size.y)207.expect("Failed to calculate aspect ratio for Cluster: screen dimensions must be positive, non-zero values")208.ratio();209let mut z_slices = *z_slices;210if *total < z_slices {211warn!("ClusterConfig has more z-slices than total clusters!");212z_slices = *total;213}214let per_layer = *total as f32 / z_slices as f32;215216let y = f32::sqrt(per_layer / aspect_ratio);217218let mut x = (y * aspect_ratio) as u32;219let mut y = y as u32;220221// check extremes222if x == 0 {223x = 1;224y = per_layer as u32;225}226if y == 0 {227x = per_layer as u32;228y = 1;229}230231UVec3::new(x, y, z_slices)232}233}234}235236fn first_slice_depth(&self) -> f32 {237match self {238ClusterConfig::None | ClusterConfig::Single => 0.0,239ClusterConfig::XYZ { z_config, .. } | ClusterConfig::FixedZ { z_config, .. } => {240z_config.first_slice_depth241}242}243}244245fn far_z_mode(&self) -> ClusterFarZMode {246match self {247ClusterConfig::None => ClusterFarZMode::Constant(0.0),248ClusterConfig::Single => ClusterFarZMode::MaxClusterableObjectRange,249ClusterConfig::XYZ { z_config, .. } | ClusterConfig::FixedZ { z_config, .. } => {250z_config.far_z_mode251}252}253}254255fn dynamic_resizing(&self) -> bool {256match self {257ClusterConfig::None | ClusterConfig::Single => false,258ClusterConfig::XYZ {259dynamic_resizing, ..260}261| ClusterConfig::FixedZ {262dynamic_resizing, ..263} => *dynamic_resizing,264}265}266}267268impl Clusters {269fn update(&mut self, screen_size: UVec2, requested_dimensions: UVec3) {270debug_assert!(271requested_dimensions.x > 0 && requested_dimensions.y > 0 && requested_dimensions.z > 0272);273274let tile_size = (screen_size.as_vec2() / requested_dimensions.xy().as_vec2())275.ceil()276.as_uvec2()277.max(UVec2::ONE);278self.tile_size = tile_size;279self.dimensions = (screen_size.as_vec2() / tile_size.as_vec2())280.ceil()281.as_uvec2()282.extend(requested_dimensions.z)283.max(UVec3::ONE);284285// NOTE: Maximum 4096 clusters due to uniform buffer size constraints286debug_assert!(self.dimensions.x * self.dimensions.y * self.dimensions.z <= 4096);287}288fn clear(&mut self) {289self.tile_size = UVec2::ONE;290self.dimensions = UVec3::ZERO;291self.near = 0.0;292self.far = 0.0;293self.clusterable_objects.clear();294}295}296297pub fn add_clusters(298mut commands: Commands,299cameras: Query<(Entity, Option<&ClusterConfig>, &Camera), (Without<Clusters>, With<Camera3d>)>,300) {301for (entity, config, camera) in &cameras {302if !camera.is_active {303continue;304}305306let config = config.copied().unwrap_or_default();307// actual settings here don't matter - they will be overwritten in308// `assign_objects_to_clusters``309commands310.entity(entity)311.insert((Clusters::default(), config));312}313}314315impl VisibleClusterableObjects {316#[inline]317pub fn iter(&self) -> impl DoubleEndedIterator<Item = &Entity> {318self.entities.iter()319}320321#[inline]322pub fn len(&self) -> usize {323self.entities.len()324}325326#[inline]327pub fn is_empty(&self) -> bool {328self.entities.is_empty()329}330}331332impl GlobalVisibleClusterableObjects {333#[inline]334pub fn iter(&self) -> impl Iterator<Item = &Entity> {335self.entities.iter()336}337338#[inline]339pub fn contains(&self, entity: Entity) -> bool {340self.entities.contains(&entity)341}342}343344345