Path: blob/main/crates/bevy_light/src/cluster/assign.rs
6849 views
//! Assigning objects to clusters.12use bevy_camera::{3primitives::{Aabb, Frustum, HalfSpace, Sphere},4visibility::{RenderLayers, ViewVisibility},5Camera,6};7use bevy_ecs::{8entity::Entity,9query::{Has, With},10system::{Commands, Local, Query, Res, ResMut},11};12use bevy_math::{13ops::{self, sin_cos},14Mat4, UVec3, Vec2, Vec3, Vec3A, Vec3Swizzles as _, Vec4, Vec4Swizzles as _,15};16use bevy_transform::components::GlobalTransform;17use bevy_utils::prelude::default;18use tracing::warn;1920use super::{21ClusterConfig, ClusterFarZMode, ClusteredDecal, Clusters, GlobalClusterSettings,22GlobalVisibleClusterableObjects, VisibleClusterableObjects,23};24use crate::{EnvironmentMapLight, LightProbe, PointLight, SpotLight, VolumetricLight};2526const NDC_MIN: Vec2 = Vec2::NEG_ONE;27const NDC_MAX: Vec2 = Vec2::ONE;2829const VEC2_HALF: Vec2 = Vec2::splat(0.5);30const VEC2_HALF_NEGATIVE_Y: Vec2 = Vec2::new(0.5, -0.5);3132/// Data required for assigning objects to clusters.33#[derive(Clone, Debug)]34pub(crate) struct ClusterableObjectAssignmentData {35entity: Entity,36// TODO: We currently ignore the scale on the transform. This is confusing.37// Replace with an `Isometry3d`.38transform: GlobalTransform,39range: f32,40object_type: ClusterableObjectType,41render_layers: RenderLayers,42}4344impl ClusterableObjectAssignmentData {45pub fn sphere(&self) -> Sphere {46Sphere {47center: self.transform.translation_vec3a(),48radius: self.range,49}50}51}5253/// Data needed to assign objects to clusters that's specific to the type of54/// clusterable object.55#[derive(Clone, Copy, Debug)]56pub enum ClusterableObjectType {57/// Data needed to assign point lights to clusters.58PointLight {59/// Whether shadows are enabled for this point light.60///61/// This is used for sorting the light list.62shadows_enabled: bool,6364/// Whether this light interacts with volumetrics.65///66/// This is used for sorting the light list.67volumetric: bool,68},6970/// Data needed to assign spot lights to clusters.71SpotLight {72/// Whether shadows are enabled for this spot light.73///74/// This is used for sorting the light list.75shadows_enabled: bool,7677/// Whether this light interacts with volumetrics.78///79/// This is used for sorting the light list.80volumetric: bool,8182/// The outer angle of the light cone in radians.83outer_angle: f32,84},8586/// Marks that the clusterable object is a reflection probe.87ReflectionProbe,8889/// Marks that the clusterable object is an irradiance volume.90IrradianceVolume,9192/// Marks that the clusterable object is a decal.93Decal,94}9596impl ClusterableObjectType {97/// Returns a tuple that can be sorted to obtain the order in which indices98/// to clusterable objects must be stored in the cluster offsets and counts99/// list.100///101/// Generally, we sort first by type, then, for lights, by whether shadows102/// are enabled (enabled before disabled), and then whether volumetrics are103/// enabled (enabled before disabled).104pub fn ordering(&self) -> (u8, bool, bool) {105match *self {106ClusterableObjectType::PointLight {107shadows_enabled,108volumetric,109} => (0, !shadows_enabled, !volumetric),110ClusterableObjectType::SpotLight {111shadows_enabled,112volumetric,113..114} => (1, !shadows_enabled, !volumetric),115ClusterableObjectType::ReflectionProbe => (2, false, false),116ClusterableObjectType::IrradianceVolume => (3, false, false),117ClusterableObjectType::Decal => (4, false, false),118}119}120}121122// NOTE: Run this before update_point_light_frusta!123pub(crate) fn assign_objects_to_clusters(124mut commands: Commands,125mut global_clusterable_objects: ResMut<GlobalVisibleClusterableObjects>,126mut views: Query<(127Entity,128&GlobalTransform,129&Camera,130&Frustum,131&ClusterConfig,132&mut Clusters,133Option<&RenderLayers>,134Option<&mut VisibleClusterableObjects>,135)>,136point_lights_query: Query<(137Entity,138&GlobalTransform,139&PointLight,140Option<&RenderLayers>,141Option<&VolumetricLight>,142&ViewVisibility,143)>,144spot_lights_query: Query<(145Entity,146&GlobalTransform,147&SpotLight,148Option<&RenderLayers>,149Option<&VolumetricLight>,150&ViewVisibility,151)>,152light_probes_query: Query<153(Entity, &GlobalTransform, Has<EnvironmentMapLight>),154With<LightProbe>,155>,156decals_query: Query<(Entity, &GlobalTransform), With<ClusteredDecal>>,157mut clusterable_objects: Local<Vec<ClusterableObjectAssignmentData>>,158mut cluster_aabb_spheres: Local<Vec<Option<Sphere>>>,159mut max_clusterable_objects_warning_emitted: Local<bool>,160global_cluster_settings: Option<Res<GlobalClusterSettings>>,161) {162let Some(global_cluster_settings) = global_cluster_settings else {163return;164};165166global_clusterable_objects.entities.clear();167clusterable_objects.clear();168// collect just the relevant query data into a persisted vec to avoid reallocating each frame169clusterable_objects.extend(170point_lights_query171.iter()172.filter(|(.., visibility)| visibility.get())173.map(174|(entity, transform, point_light, maybe_layers, volumetric, _visibility)| {175ClusterableObjectAssignmentData {176entity,177transform: GlobalTransform::from_translation(transform.translation()),178range: point_light.range,179object_type: ClusterableObjectType::PointLight {180shadows_enabled: point_light.shadows_enabled,181volumetric: volumetric.is_some(),182},183render_layers: maybe_layers.unwrap_or_default().clone(),184}185},186),187);188clusterable_objects.extend(189spot_lights_query190.iter()191.filter(|(.., visibility)| visibility.get())192.map(193|(entity, transform, spot_light, maybe_layers, volumetric, _visibility)| {194ClusterableObjectAssignmentData {195entity,196transform: *transform,197range: spot_light.range,198object_type: ClusterableObjectType::SpotLight {199outer_angle: spot_light.outer_angle,200shadows_enabled: spot_light.shadows_enabled,201volumetric: volumetric.is_some(),202},203render_layers: maybe_layers.unwrap_or_default().clone(),204}205},206),207);208209// Gather up light probes, but only if we're clustering them.210//211// UBOs aren't large enough to hold indices for light probes, so we can't212// cluster light probes on such platforms (mainly WebGL 2). Besides, those213// platforms typically lack bindless textures, so multiple light probes214// wouldn't be supported anyhow.215if global_cluster_settings.supports_storage_buffers {216clusterable_objects.extend(light_probes_query.iter().map(217|(entity, transform, is_reflection_probe)| ClusterableObjectAssignmentData {218entity,219transform: *transform,220range: transform.radius_vec3a(Vec3A::ONE),221object_type: if is_reflection_probe {222ClusterableObjectType::ReflectionProbe223} else {224ClusterableObjectType::IrradianceVolume225},226render_layers: RenderLayers::default(),227},228));229}230231// Add decals if the current platform supports them.232if global_cluster_settings.clustered_decals_are_usable {233clusterable_objects.extend(decals_query.iter().map(|(entity, transform)| {234ClusterableObjectAssignmentData {235entity,236transform: *transform,237range: transform.scale().length(),238object_type: ClusterableObjectType::Decal,239render_layers: RenderLayers::default(),240}241}));242}243244if clusterable_objects.len() > global_cluster_settings.max_uniform_buffer_clusterable_objects245&& !global_cluster_settings.supports_storage_buffers246{247clusterable_objects.sort_by_cached_key(|clusterable_object| {248(249clusterable_object.object_type.ordering(),250clusterable_object.entity,251)252});253254// check each clusterable object against each view's frustum, keep only255// those that affect at least one of our views256let frusta: Vec<_> = views257.iter()258.map(|(_, _, _, frustum, _, _, _, _)| *frustum)259.collect();260let mut clusterable_objects_in_view_count = 0;261clusterable_objects.retain(|clusterable_object| {262// take one extra clusterable object to check if we should emit the warning263if clusterable_objects_in_view_count264== global_cluster_settings.max_uniform_buffer_clusterable_objects + 1265{266false267} else {268let clusterable_object_sphere = clusterable_object.sphere();269let clusterable_object_in_view = frusta270.iter()271.any(|frustum| frustum.intersects_sphere(&clusterable_object_sphere, true));272273if clusterable_object_in_view {274clusterable_objects_in_view_count += 1;275}276277clusterable_object_in_view278}279});280281if clusterable_objects.len()282> global_cluster_settings.max_uniform_buffer_clusterable_objects283&& !*max_clusterable_objects_warning_emitted284{285warn!(286"max_uniform_buffer_clusterable_objects ({}) exceeded",287global_cluster_settings.max_uniform_buffer_clusterable_objects288);289*max_clusterable_objects_warning_emitted = true;290}291292clusterable_objects293.truncate(global_cluster_settings.max_uniform_buffer_clusterable_objects);294}295296for (297view_entity,298camera_transform,299camera,300frustum,301config,302clusters,303maybe_layers,304mut visible_clusterable_objects,305) in &mut views306{307let view_layers = maybe_layers.unwrap_or_default();308let clusters = clusters.into_inner();309310if matches!(config, ClusterConfig::None) {311if visible_clusterable_objects.is_some() {312commands313.entity(view_entity)314.remove::<VisibleClusterableObjects>();315}316clusters.clear();317continue;318}319320let screen_size = match camera.physical_viewport_size() {321Some(screen_size) if screen_size.x != 0 && screen_size.y != 0 => screen_size,322_ => {323clusters.clear();324continue;325}326};327328let mut requested_cluster_dimensions = config.dimensions_for_screen_size(screen_size);329330let world_from_view = camera_transform.affine();331let view_from_world_scale = camera_transform.compute_transform().scale.recip();332let view_from_world_scale_max = view_from_world_scale.abs().max_element();333let view_from_world = Mat4::from(world_from_view.inverse());334let is_orthographic = camera.clip_from_view().w_axis.w == 1.0;335336let far_z = match config.far_z_mode() {337ClusterFarZMode::MaxClusterableObjectRange => {338let view_from_world_row_2 = view_from_world.row(2);339clusterable_objects340.iter()341.map(|object| {342-view_from_world_row_2.dot(object.transform.translation().extend(1.0))343+ object.range * view_from_world_scale.z344})345.reduce(f32::max)346.unwrap_or(0.0)347}348ClusterFarZMode::Constant(far) => far,349};350let first_slice_depth = match (is_orthographic, requested_cluster_dimensions.z) {351(true, _) => {352// NOTE: Based on glam's Mat4::orthographic_rh(), as used to calculate the orthographic projection353// matrix, we can calculate the projection's view-space near plane as follows:354// component 3,2 = r * near and 2,2 = r where r = 1.0 / (near - far)355// There is a caveat here that when calculating the projection matrix, near and far were swapped to give356// reversed z, consistent with the perspective projection. So,357// 3,2 = r * far and 2,2 = r where r = 1.0 / (far - near)358// rearranging r = 1.0 / (far - near), r * (far - near) = 1.0, r * far - 1.0 = r * near, near = (r * far - 1.0) / r359// = (3,2 - 1.0) / 2,2360(camera.clip_from_view().w_axis.z - 1.0) / camera.clip_from_view().z_axis.z361}362(false, 1) => config.first_slice_depth().max(far_z),363_ => config.first_slice_depth(),364};365let first_slice_depth = first_slice_depth * view_from_world_scale.z;366367// NOTE: Ensure the far_z is at least as far as the first_depth_slice to avoid clustering problems.368let far_z = far_z.max(first_slice_depth);369let cluster_factors = calculate_cluster_factors(370first_slice_depth,371far_z,372requested_cluster_dimensions.z as f32,373is_orthographic,374);375376if config.dynamic_resizing() {377let mut cluster_index_estimate = 0.0;378for clusterable_object in &clusterable_objects {379let clusterable_object_sphere = clusterable_object.sphere();380381// Check if the clusterable object is within the view frustum382if !frustum.intersects_sphere(&clusterable_object_sphere, true) {383continue;384}385386// calculate a conservative aabb estimate of number of clusters affected by this light387// this overestimates index counts by at most 50% (and typically much less) when the whole light range is in view388// it can overestimate more significantly when light ranges are only partially in view389let (clusterable_object_aabb_min, clusterable_object_aabb_max) =390cluster_space_clusterable_object_aabb(391view_from_world,392view_from_world_scale,393camera.clip_from_view(),394&clusterable_object_sphere,395);396397// since we won't adjust z slices we can calculate exact number of slices required in z dimension398let z_cluster_min = view_z_to_z_slice(399cluster_factors,400requested_cluster_dimensions.z,401clusterable_object_aabb_min.z,402is_orthographic,403);404let z_cluster_max = view_z_to_z_slice(405cluster_factors,406requested_cluster_dimensions.z,407clusterable_object_aabb_max.z,408is_orthographic,409);410let z_count =411z_cluster_min.max(z_cluster_max) - z_cluster_min.min(z_cluster_max) + 1;412413// calculate x/y count using floats to avoid overestimating counts due to large initial tile sizes414let xy_min = clusterable_object_aabb_min.xy();415let xy_max = clusterable_object_aabb_max.xy();416// multiply by 0.5 to move from [-1,1] to [-0.5, 0.5], max extent of 1 in each dimension417let xy_count = (xy_max - xy_min)418* 0.5419* Vec2::new(420requested_cluster_dimensions.x as f32,421requested_cluster_dimensions.y as f32,422);423424// add up to 2 to each axis to account for overlap425let x_overlap = if xy_min.x <= -1.0 { 0.0 } else { 1.0 }426+ if xy_max.x >= 1.0 { 0.0 } else { 1.0 };427let y_overlap = if xy_min.y <= -1.0 { 0.0 } else { 1.0 }428+ if xy_max.y >= 1.0 { 0.0 } else { 1.0 };429cluster_index_estimate +=430(xy_count.x + x_overlap) * (xy_count.y + y_overlap) * z_count as f32;431}432433if cluster_index_estimate434> global_cluster_settings.view_cluster_bindings_max_indices as f32435{436// scale x and y cluster count to be able to fit all our indices437438// we take the ratio of the actual indices over the index estimate.439// this is not guaranteed to be small enough due to overlapped tiles, but440// the conservative estimate is more than sufficient to cover the441// difference442let index_ratio = global_cluster_settings.view_cluster_bindings_max_indices as f32443/ cluster_index_estimate;444let xy_ratio = index_ratio.sqrt();445446requested_cluster_dimensions.x =447((requested_cluster_dimensions.x as f32 * xy_ratio).floor() as u32).max(1);448requested_cluster_dimensions.y =449((requested_cluster_dimensions.y as f32 * xy_ratio).floor() as u32).max(1);450}451}452453clusters.update(screen_size, requested_cluster_dimensions);454clusters.near = first_slice_depth;455clusters.far = far_z;456457// NOTE: Maximum 4096 clusters due to uniform buffer size constraints458debug_assert!(459clusters.dimensions.x * clusters.dimensions.y * clusters.dimensions.z <= 4096460);461462let view_from_clip = camera.clip_from_view().inverse();463464for clusterable_objects in &mut clusters.clusterable_objects {465clusterable_objects.entities.clear();466clusterable_objects.counts = default();467}468let cluster_count =469(clusters.dimensions.x * clusters.dimensions.y * clusters.dimensions.z) as usize;470clusters471.clusterable_objects472.resize_with(cluster_count, VisibleClusterableObjects::default);473474// initialize empty cluster bounding spheres475cluster_aabb_spheres.clear();476cluster_aabb_spheres.extend(core::iter::repeat_n(None, cluster_count));477478// Calculate the x/y/z cluster frustum planes in view space479let mut x_planes = Vec::with_capacity(clusters.dimensions.x as usize + 1);480let mut y_planes = Vec::with_capacity(clusters.dimensions.y as usize + 1);481let mut z_planes = Vec::with_capacity(clusters.dimensions.z as usize + 1);482483if is_orthographic {484let x_slices = clusters.dimensions.x as f32;485for x in 0..=clusters.dimensions.x {486let x_proportion = x as f32 / x_slices;487let x_pos = x_proportion * 2.0 - 1.0;488let view_x = clip_to_view(view_from_clip, Vec4::new(x_pos, 0.0, 1.0, 1.0)).x;489let normal = Vec3::X;490let d = view_x * normal.x;491x_planes.push(HalfSpace::new(normal.extend(d)));492}493494let y_slices = clusters.dimensions.y as f32;495for y in 0..=clusters.dimensions.y {496let y_proportion = 1.0 - y as f32 / y_slices;497let y_pos = y_proportion * 2.0 - 1.0;498let view_y = clip_to_view(view_from_clip, Vec4::new(0.0, y_pos, 1.0, 1.0)).y;499let normal = Vec3::Y;500let d = view_y * normal.y;501y_planes.push(HalfSpace::new(normal.extend(d)));502}503} else {504let x_slices = clusters.dimensions.x as f32;505for x in 0..=clusters.dimensions.x {506let x_proportion = x as f32 / x_slices;507let x_pos = x_proportion * 2.0 - 1.0;508let nb = clip_to_view(view_from_clip, Vec4::new(x_pos, -1.0, 1.0, 1.0)).xyz();509let nt = clip_to_view(view_from_clip, Vec4::new(x_pos, 1.0, 1.0, 1.0)).xyz();510let normal = nb.cross(nt);511let d = nb.dot(normal);512x_planes.push(HalfSpace::new(normal.extend(d)));513}514515let y_slices = clusters.dimensions.y as f32;516for y in 0..=clusters.dimensions.y {517let y_proportion = 1.0 - y as f32 / y_slices;518let y_pos = y_proportion * 2.0 - 1.0;519let nl = clip_to_view(view_from_clip, Vec4::new(-1.0, y_pos, 1.0, 1.0)).xyz();520let nr = clip_to_view(view_from_clip, Vec4::new(1.0, y_pos, 1.0, 1.0)).xyz();521let normal = nr.cross(nl);522let d = nr.dot(normal);523y_planes.push(HalfSpace::new(normal.extend(d)));524}525}526527let z_slices = clusters.dimensions.z;528for z in 0..=z_slices {529let view_z = z_slice_to_view_z(first_slice_depth, far_z, z_slices, z, is_orthographic);530let normal = -Vec3::Z;531let d = view_z * normal.z;532z_planes.push(HalfSpace::new(normal.extend(d)));533}534535let mut update_from_object_intersections = |visible_clusterable_objects: &mut Vec<536Entity,537>| {538for clusterable_object in &clusterable_objects {539// check if the clusterable light layers overlap the view layers540if !view_layers.intersects(&clusterable_object.render_layers) {541continue;542}543544let clusterable_object_sphere = clusterable_object.sphere();545546// Check if the clusterable object is within the view frustum547if !frustum.intersects_sphere(&clusterable_object_sphere, true) {548continue;549}550551// NOTE: The clusterable object intersects the frustum so it552// must be visible and part of the global set553global_clusterable_objects554.entities555.insert(clusterable_object.entity);556visible_clusterable_objects.push(clusterable_object.entity);557558// note: caching seems to be slower than calling twice for this aabb calculation559let (560clusterable_object_aabb_xy_ndc_z_view_min,561clusterable_object_aabb_xy_ndc_z_view_max,562) = cluster_space_clusterable_object_aabb(563view_from_world,564view_from_world_scale,565camera.clip_from_view(),566&clusterable_object_sphere,567);568569let min_cluster = ndc_position_to_cluster(570clusters.dimensions,571cluster_factors,572is_orthographic,573clusterable_object_aabb_xy_ndc_z_view_min,574clusterable_object_aabb_xy_ndc_z_view_min.z,575);576let max_cluster = ndc_position_to_cluster(577clusters.dimensions,578cluster_factors,579is_orthographic,580clusterable_object_aabb_xy_ndc_z_view_max,581clusterable_object_aabb_xy_ndc_z_view_max.z,582);583let (min_cluster, max_cluster) =584(min_cluster.min(max_cluster), min_cluster.max(max_cluster));585586// What follows is the Iterative Sphere Refinement algorithm from Just Cause 3587// Persson et al, Practical Clustered Shading588// http://newq.net/dl/pub/s2015_practical.pdf589// NOTE: A sphere under perspective projection is no longer a sphere. It gets590// stretched and warped, which prevents simpler algorithms from being correct591// as they often assume that the widest part of the sphere under projection is the592// center point on the axis of interest plus the radius, and that is not true!593let view_clusterable_object_sphere = Sphere {594center: Vec3A::from_vec4(595view_from_world * clusterable_object_sphere.center.extend(1.0),596),597radius: clusterable_object_sphere.radius * view_from_world_scale_max,598};599let spot_light_dir_sin_cos = match clusterable_object.object_type {600ClusterableObjectType::SpotLight { outer_angle, .. } => {601let (angle_sin, angle_cos) = sin_cos(outer_angle);602Some((603(view_from_world * clusterable_object.transform.back().extend(0.0))604.truncate()605.normalize(),606angle_sin,607angle_cos,608))609}610ClusterableObjectType::Decal => {611// TODO: cull via a frustum612None613}614ClusterableObjectType::PointLight { .. }615| ClusterableObjectType::ReflectionProbe616| ClusterableObjectType::IrradianceVolume => None,617};618let clusterable_object_center_clip =619camera.clip_from_view() * view_clusterable_object_sphere.center.extend(1.0);620let object_center_ndc =621clusterable_object_center_clip.xyz() / clusterable_object_center_clip.w;622let cluster_coordinates = ndc_position_to_cluster(623clusters.dimensions,624cluster_factors,625is_orthographic,626object_center_ndc,627view_clusterable_object_sphere.center.z,628);629let z_center = if object_center_ndc.z <= 1.0 {630Some(cluster_coordinates.z)631} else {632None633};634let y_center = if object_center_ndc.y > 1.0 {635None636} else if object_center_ndc.y < -1.0 {637Some(clusters.dimensions.y + 1)638} else {639Some(cluster_coordinates.y)640};641for z in min_cluster.z..=max_cluster.z {642let mut z_object = view_clusterable_object_sphere.clone();643if z_center.is_none() || z != z_center.unwrap() {644// The z plane closer to the clusterable object has the645// larger radius circle where the light sphere646// intersects the z plane.647let z_plane = if z_center.is_some() && z < z_center.unwrap() {648z_planes[(z + 1) as usize]649} else {650z_planes[z as usize]651};652// Project the sphere to this z plane and use its radius as the radius of a653// new, refined sphere.654if let Some(projected) = project_to_plane_z(z_object, z_plane) {655z_object = projected;656} else {657continue;658}659}660for y in min_cluster.y..=max_cluster.y {661let mut y_object = z_object.clone();662if y_center.is_none() || y != y_center.unwrap() {663// The y plane closer to the clusterable object has664// the larger radius circle where the light sphere665// intersects the y plane.666let y_plane = if y_center.is_some() && y < y_center.unwrap() {667y_planes[(y + 1) as usize]668} else {669y_planes[y as usize]670};671// Project the refined sphere to this y plane and use its radius as the672// radius of a new, even more refined sphere.673if let Some(projected) =674project_to_plane_y(y_object, y_plane, is_orthographic)675{676y_object = projected;677} else {678continue;679}680}681// Loop from the left to find the first affected cluster682let mut min_x = min_cluster.x;683loop {684if min_x >= max_cluster.x685|| -get_distance_x(686x_planes[(min_x + 1) as usize],687y_object.center,688is_orthographic,689) + y_object.radius690> 0.0691{692break;693}694min_x += 1;695}696// Loop from the right to find the last affected cluster697let mut max_x = max_cluster.x;698loop {699if max_x <= min_x700|| get_distance_x(701x_planes[max_x as usize],702y_object.center,703is_orthographic,704) + y_object.radius705> 0.0706{707break;708}709max_x -= 1;710}711let mut cluster_index = ((y * clusters.dimensions.x + min_x)712* clusters.dimensions.z713+ z) as usize;714715match clusterable_object.object_type {716ClusterableObjectType::SpotLight { .. } => {717let (view_light_direction, angle_sin, angle_cos) =718spot_light_dir_sin_cos.unwrap();719for x in min_x..=max_x {720// further culling for spot lights721// get or initialize cluster bounding sphere722let cluster_aabb_sphere =723&mut cluster_aabb_spheres[cluster_index];724let cluster_aabb_sphere =725if let Some(sphere) = cluster_aabb_sphere {726&*sphere727} else {728let aabb = compute_aabb_for_cluster(729first_slice_depth,730far_z,731clusters.tile_size.as_vec2(),732screen_size.as_vec2(),733view_from_clip,734is_orthographic,735clusters.dimensions,736UVec3::new(x, y, z),737);738let sphere = Sphere {739center: aabb.center,740radius: aabb.half_extents.length(),741};742*cluster_aabb_sphere = Some(sphere);743cluster_aabb_sphere.as_ref().unwrap()744};745746// test -- based on https://bartwronski.com/2017/04/13/cull-that-cone/747let spot_light_offset = Vec3::from(748view_clusterable_object_sphere.center749- cluster_aabb_sphere.center,750);751let spot_light_dist_sq = spot_light_offset.length_squared();752let v1_len = spot_light_offset.dot(view_light_direction);753754let distance_closest_point = (angle_cos755* (spot_light_dist_sq - v1_len * v1_len).sqrt())756- v1_len * angle_sin;757let angle_cull =758distance_closest_point > cluster_aabb_sphere.radius;759760let front_cull = v1_len761> cluster_aabb_sphere.radius762+ clusterable_object.range * view_from_world_scale_max;763let back_cull = v1_len < -cluster_aabb_sphere.radius;764765if !angle_cull && !front_cull && !back_cull {766// this cluster is affected by the spot light767clusters.clusterable_objects[cluster_index]768.entities769.push(clusterable_object.entity);770clusters.clusterable_objects[cluster_index]771.counts772.spot_lights += 1;773}774cluster_index += clusters.dimensions.z as usize;775}776}777778ClusterableObjectType::PointLight { .. } => {779for _ in min_x..=max_x {780// all clusters within range are affected by point lights781clusters.clusterable_objects[cluster_index]782.entities783.push(clusterable_object.entity);784clusters.clusterable_objects[cluster_index]785.counts786.point_lights += 1;787cluster_index += clusters.dimensions.z as usize;788}789}790791ClusterableObjectType::ReflectionProbe => {792// Reflection probes currently affect all793// clusters in their bounding sphere.794//795// TODO: Cull more aggressively based on the796// probe's OBB.797for _ in min_x..=max_x {798clusters.clusterable_objects[cluster_index]799.entities800.push(clusterable_object.entity);801clusters.clusterable_objects[cluster_index]802.counts803.reflection_probes += 1;804cluster_index += clusters.dimensions.z as usize;805}806}807808ClusterableObjectType::IrradianceVolume => {809// Irradiance volumes currently affect all810// clusters in their bounding sphere.811//812// TODO: Cull more aggressively based on the813// probe's OBB.814for _ in min_x..=max_x {815clusters.clusterable_objects[cluster_index]816.entities817.push(clusterable_object.entity);818clusters.clusterable_objects[cluster_index]819.counts820.irradiance_volumes += 1;821cluster_index += clusters.dimensions.z as usize;822}823}824825ClusterableObjectType::Decal => {826// Decals currently affect all clusters in their827// bounding sphere.828//829// TODO: Cull more aggressively based on the830// decal's OBB.831for _ in min_x..=max_x {832clusters.clusterable_objects[cluster_index]833.entities834.push(clusterable_object.entity);835clusters.clusterable_objects[cluster_index].counts.decals += 1;836cluster_index += clusters.dimensions.z as usize;837}838}839}840}841}842}843};844845// reuse existing visible clusterable objects Vec, if it exists846if let Some(visible_clusterable_objects) = visible_clusterable_objects.as_mut() {847visible_clusterable_objects.entities.clear();848update_from_object_intersections(&mut visible_clusterable_objects.entities);849} else {850let mut entities = Vec::new();851update_from_object_intersections(&mut entities);852commands853.entity(view_entity)854.insert(VisibleClusterableObjects {855entities,856..Default::default()857});858}859}860}861862pub fn calculate_cluster_factors(863near: f32,864far: f32,865z_slices: f32,866is_orthographic: bool,867) -> Vec2 {868if is_orthographic {869Vec2::new(-near, z_slices / (-far - -near))870} else {871let z_slices_of_ln_zfar_over_znear = (z_slices - 1.0) / ops::ln(far / near);872Vec2::new(873z_slices_of_ln_zfar_over_znear,874ops::ln(near) * z_slices_of_ln_zfar_over_znear,875)876}877}878879fn compute_aabb_for_cluster(880z_near: f32,881z_far: f32,882tile_size: Vec2,883screen_size: Vec2,884view_from_clip: Mat4,885is_orthographic: bool,886cluster_dimensions: UVec3,887ijk: UVec3,888) -> Aabb {889let ijk = ijk.as_vec3();890891// Calculate the minimum and maximum points in screen space892let p_min = ijk.xy() * tile_size;893let p_max = p_min + tile_size;894895let cluster_min;896let cluster_max;897if is_orthographic {898// Use linear depth slicing for orthographic899900// Convert to view space at the cluster near and far planes901// NOTE: 1.0 is the near plane due to using reverse z projections902let mut p_min = screen_to_view(screen_size, view_from_clip, p_min, 0.0).xyz();903let mut p_max = screen_to_view(screen_size, view_from_clip, p_max, 0.0).xyz();904905// calculate cluster depth using z_near and z_far906p_min.z = -z_near + (z_near - z_far) * ijk.z / cluster_dimensions.z as f32;907p_max.z = -z_near + (z_near - z_far) * (ijk.z + 1.0) / cluster_dimensions.z as f32;908909cluster_min = p_min.min(p_max);910cluster_max = p_min.max(p_max);911} else {912// Convert to view space at the near plane913// NOTE: 1.0 is the near plane due to using reverse z projections914let p_min = screen_to_view(screen_size, view_from_clip, p_min, 1.0);915let p_max = screen_to_view(screen_size, view_from_clip, p_max, 1.0);916917let z_far_over_z_near = -z_far / -z_near;918let cluster_near = if ijk.z == 0.0 {9190.0920} else {921-z_near922* ops::powf(923z_far_over_z_near,924(ijk.z - 1.0) / (cluster_dimensions.z - 1) as f32,925)926};927// NOTE: This could be simplified to:928// cluster_far = cluster_near * z_far_over_z_near;929let cluster_far = if cluster_dimensions.z == 1 {930-z_far931} else {932-z_near * ops::powf(z_far_over_z_near, ijk.z / (cluster_dimensions.z - 1) as f32)933};934935// Calculate the four intersection points of the min and max points with the cluster near and far planes936let p_min_near = line_intersection_to_z_plane(Vec3::ZERO, p_min.xyz(), cluster_near);937let p_min_far = line_intersection_to_z_plane(Vec3::ZERO, p_min.xyz(), cluster_far);938let p_max_near = line_intersection_to_z_plane(Vec3::ZERO, p_max.xyz(), cluster_near);939let p_max_far = line_intersection_to_z_plane(Vec3::ZERO, p_max.xyz(), cluster_far);940941cluster_min = p_min_near.min(p_min_far).min(p_max_near.min(p_max_far));942cluster_max = p_min_near.max(p_min_far).max(p_max_near.max(p_max_far));943}944945Aabb::from_min_max(cluster_min, cluster_max)946}947948// NOTE: Keep in sync as the inverse of view_z_to_z_slice above949fn z_slice_to_view_z(950near: f32,951far: f32,952z_slices: u32,953z_slice: u32,954is_orthographic: bool,955) -> f32 {956if is_orthographic {957return -near - (far - near) * z_slice as f32 / z_slices as f32;958}959960// Perspective961if z_slice == 0 {9620.0963} else {964-near * ops::powf(far / near, (z_slice - 1) as f32 / (z_slices - 1) as f32)965}966}967968fn ndc_position_to_cluster(969cluster_dimensions: UVec3,970cluster_factors: Vec2,971is_orthographic: bool,972ndc_p: Vec3,973view_z: f32,974) -> UVec3 {975let cluster_dimensions_f32 = cluster_dimensions.as_vec3();976let frag_coord = (ndc_p.xy() * VEC2_HALF_NEGATIVE_Y + VEC2_HALF).clamp(Vec2::ZERO, Vec2::ONE);977let xy = (frag_coord * cluster_dimensions_f32.xy()).floor();978let z_slice = view_z_to_z_slice(979cluster_factors,980cluster_dimensions.z,981view_z,982is_orthographic,983);984xy.as_uvec2()985.extend(z_slice)986.clamp(UVec3::ZERO, cluster_dimensions - UVec3::ONE)987}988989/// Calculate bounds for the clusterable object using a view space aabb.990///991/// Returns a `(Vec3, Vec3)` containing minimum and maximum with992/// `X` and `Y` in normalized device coordinates with range `[-1, 1]`993/// `Z` in view space, with range `[-inf, -f32::MIN_POSITIVE]`994fn cluster_space_clusterable_object_aabb(995view_from_world: Mat4,996view_from_world_scale: Vec3,997clip_from_view: Mat4,998clusterable_object_sphere: &Sphere,999) -> (Vec3, Vec3) {1000let clusterable_object_aabb_view = Aabb {1001center: Vec3A::from_vec4(view_from_world * clusterable_object_sphere.center.extend(1.0)),1002half_extents: Vec3A::from(clusterable_object_sphere.radius * view_from_world_scale.abs()),1003};1004let (mut clusterable_object_aabb_view_min, mut clusterable_object_aabb_view_max) = (1005clusterable_object_aabb_view.min(),1006clusterable_object_aabb_view.max(),1007);10081009// Constrain view z to be negative - i.e. in front of the camera1010// When view z is >= 0.0 and we're using a perspective projection, bad things happen.1011// At view z == 0.0, ndc x,y are mathematically undefined. At view z > 0.0, i.e. behind the camera,1012// the perspective projection flips the directions of the axes. This breaks assumptions about1013// use of min/max operations as something that was to the left in view space is now returning a1014// coordinate that for view z in front of the camera would be on the right, but at view z behind the1015// camera is on the left. So, we just constrain view z to be < 0.0 and necessarily in front of the camera.1016clusterable_object_aabb_view_min.z = clusterable_object_aabb_view_min.z.min(-f32::MIN_POSITIVE);1017clusterable_object_aabb_view_max.z = clusterable_object_aabb_view_max.z.min(-f32::MIN_POSITIVE);10181019// Is there a cheaper way to do this? The problem is that because of perspective1020// the point at max z but min xy may be less xy in screenspace, and similar. As1021// such, projecting the min and max xy at both the closer and further z and taking1022// the min and max of those projected points addresses this.1023let (1024clusterable_object_aabb_view_xymin_near,1025clusterable_object_aabb_view_xymin_far,1026clusterable_object_aabb_view_xymax_near,1027clusterable_object_aabb_view_xymax_far,1028) = (1029clusterable_object_aabb_view_min,1030clusterable_object_aabb_view_min1031.xy()1032.extend(clusterable_object_aabb_view_max.z),1033clusterable_object_aabb_view_max1034.xy()1035.extend(clusterable_object_aabb_view_min.z),1036clusterable_object_aabb_view_max,1037);1038let (1039clusterable_object_aabb_clip_xymin_near,1040clusterable_object_aabb_clip_xymin_far,1041clusterable_object_aabb_clip_xymax_near,1042clusterable_object_aabb_clip_xymax_far,1043) = (1044clip_from_view * clusterable_object_aabb_view_xymin_near.extend(1.0),1045clip_from_view * clusterable_object_aabb_view_xymin_far.extend(1.0),1046clip_from_view * clusterable_object_aabb_view_xymax_near.extend(1.0),1047clip_from_view * clusterable_object_aabb_view_xymax_far.extend(1.0),1048);1049let (1050clusterable_object_aabb_ndc_xymin_near,1051clusterable_object_aabb_ndc_xymin_far,1052clusterable_object_aabb_ndc_xymax_near,1053clusterable_object_aabb_ndc_xymax_far,1054) = (1055clusterable_object_aabb_clip_xymin_near.xyz() / clusterable_object_aabb_clip_xymin_near.w,1056clusterable_object_aabb_clip_xymin_far.xyz() / clusterable_object_aabb_clip_xymin_far.w,1057clusterable_object_aabb_clip_xymax_near.xyz() / clusterable_object_aabb_clip_xymax_near.w,1058clusterable_object_aabb_clip_xymax_far.xyz() / clusterable_object_aabb_clip_xymax_far.w,1059);1060let (clusterable_object_aabb_ndc_min, clusterable_object_aabb_ndc_max) = (1061clusterable_object_aabb_ndc_xymin_near1062.min(clusterable_object_aabb_ndc_xymin_far)1063.min(clusterable_object_aabb_ndc_xymax_near)1064.min(clusterable_object_aabb_ndc_xymax_far),1065clusterable_object_aabb_ndc_xymin_near1066.max(clusterable_object_aabb_ndc_xymin_far)1067.max(clusterable_object_aabb_ndc_xymax_near)1068.max(clusterable_object_aabb_ndc_xymax_far),1069);10701071// clamp to ndc coords without depth1072let (aabb_min_ndc, aabb_max_ndc) = (1073clusterable_object_aabb_ndc_min.xy().clamp(NDC_MIN, NDC_MAX),1074clusterable_object_aabb_ndc_max.xy().clamp(NDC_MIN, NDC_MAX),1075);10761077// pack unadjusted z depth into the vecs1078(1079aabb_min_ndc.extend(clusterable_object_aabb_view_min.z),1080aabb_max_ndc.extend(clusterable_object_aabb_view_max.z),1081)1082}10831084// Calculate the intersection of a ray from the eye through the view space position to a z plane1085fn line_intersection_to_z_plane(origin: Vec3, p: Vec3, z: f32) -> Vec3 {1086let v = p - origin;1087let t = (z - Vec3::Z.dot(origin)) / Vec3::Z.dot(v);1088origin + t * v1089}10901091// NOTE: Keep in sync with bevy_pbr/src/render/pbr.wgsl1092fn view_z_to_z_slice(1093cluster_factors: Vec2,1094z_slices: u32,1095view_z: f32,1096is_orthographic: bool,1097) -> u32 {1098let z_slice = if is_orthographic {1099// NOTE: view_z is correct in the orthographic case1100((view_z - cluster_factors.x) * cluster_factors.y).floor() as u321101} else {1102// NOTE: had to use -view_z to make it positive else log(negative) is nan1103(ops::ln(-view_z) * cluster_factors.x - cluster_factors.y + 1.0) as u321104};1105// NOTE: We use min as we may limit the far z plane used for clustering to be closer than1106// the furthest thing being drawn. This means that we need to limit to the maximum cluster.1107z_slice.min(z_slices - 1)1108}11091110fn clip_to_view(view_from_clip: Mat4, clip: Vec4) -> Vec4 {1111let view = view_from_clip * clip;1112view / view.w1113}11141115fn screen_to_view(screen_size: Vec2, view_from_clip: Mat4, screen: Vec2, ndc_z: f32) -> Vec4 {1116let tex_coord = screen / screen_size;1117let clip = Vec4::new(1118tex_coord.x * 2.0 - 1.0,1119(1.0 - tex_coord.y) * 2.0 - 1.0,1120ndc_z,11211.0,1122);1123clip_to_view(view_from_clip, clip)1124}11251126// NOTE: This exploits the fact that a x-plane normal has only x and z components1127fn get_distance_x(plane: HalfSpace, point: Vec3A, is_orthographic: bool) -> f32 {1128if is_orthographic {1129point.x - plane.d()1130} else {1131// Distance from a point to a plane:1132// signed distance to plane = (nx * px + ny * py + nz * pz + d) / n.length()1133// NOTE: For a x-plane, ny and d are 0 and we have a unit normal1134// = nx * px + nz * pz1135plane.normal_d().xz().dot(point.xz())1136}1137}11381139// NOTE: This exploits the fact that a z-plane normal has only a z component1140fn project_to_plane_z(z_object: Sphere, z_plane: HalfSpace) -> Option<Sphere> {1141// p = sphere center1142// n = plane normal1143// d = n.p if p is in the plane1144// NOTE: For a z-plane, nx and ny are both 01145// d = px * nx + py * ny + pz * nz1146// = pz * nz1147// => pz = d / nz1148let z = z_plane.d() / z_plane.normal_d().z;1149let distance_to_plane = z - z_object.center.z;1150if distance_to_plane.abs() > z_object.radius {1151return None;1152}1153Some(Sphere {1154center: Vec3A::from(z_object.center.xy().extend(z)),1155// hypotenuse length = radius1156// pythagoras = (distance to plane)^2 + b^2 = radius^21157radius: (z_object.radius * z_object.radius - distance_to_plane * distance_to_plane).sqrt(),1158})1159}11601161// NOTE: This exploits the fact that a y-plane normal has only y and z components1162fn project_to_plane_y(1163y_object: Sphere,1164y_plane: HalfSpace,1165is_orthographic: bool,1166) -> Option<Sphere> {1167let distance_to_plane = if is_orthographic {1168y_plane.d() - y_object.center.y1169} else {1170-y_object.center.yz().dot(y_plane.normal_d().yz())1171};11721173if distance_to_plane.abs() > y_object.radius {1174return None;1175}1176Some(Sphere {1177center: y_object.center + distance_to_plane * y_plane.normal(),1178radius: (y_object.radius * y_object.radius - distance_to_plane * distance_to_plane).sqrt(),1179})1180}118111821183