Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_light/src/cluster/assign.rs
6849 views
1
//! Assigning objects to clusters.
2
3
use bevy_camera::{
4
primitives::{Aabb, Frustum, HalfSpace, Sphere},
5
visibility::{RenderLayers, ViewVisibility},
6
Camera,
7
};
8
use bevy_ecs::{
9
entity::Entity,
10
query::{Has, With},
11
system::{Commands, Local, Query, Res, ResMut},
12
};
13
use bevy_math::{
14
ops::{self, sin_cos},
15
Mat4, UVec3, Vec2, Vec3, Vec3A, Vec3Swizzles as _, Vec4, Vec4Swizzles as _,
16
};
17
use bevy_transform::components::GlobalTransform;
18
use bevy_utils::prelude::default;
19
use tracing::warn;
20
21
use super::{
22
ClusterConfig, ClusterFarZMode, ClusteredDecal, Clusters, GlobalClusterSettings,
23
GlobalVisibleClusterableObjects, VisibleClusterableObjects,
24
};
25
use crate::{EnvironmentMapLight, LightProbe, PointLight, SpotLight, VolumetricLight};
26
27
const NDC_MIN: Vec2 = Vec2::NEG_ONE;
28
const NDC_MAX: Vec2 = Vec2::ONE;
29
30
const VEC2_HALF: Vec2 = Vec2::splat(0.5);
31
const VEC2_HALF_NEGATIVE_Y: Vec2 = Vec2::new(0.5, -0.5);
32
33
/// Data required for assigning objects to clusters.
34
#[derive(Clone, Debug)]
35
pub(crate) struct ClusterableObjectAssignmentData {
36
entity: Entity,
37
// TODO: We currently ignore the scale on the transform. This is confusing.
38
// Replace with an `Isometry3d`.
39
transform: GlobalTransform,
40
range: f32,
41
object_type: ClusterableObjectType,
42
render_layers: RenderLayers,
43
}
44
45
impl ClusterableObjectAssignmentData {
46
pub fn sphere(&self) -> Sphere {
47
Sphere {
48
center: self.transform.translation_vec3a(),
49
radius: self.range,
50
}
51
}
52
}
53
54
/// Data needed to assign objects to clusters that's specific to the type of
55
/// clusterable object.
56
#[derive(Clone, Copy, Debug)]
57
pub enum ClusterableObjectType {
58
/// Data needed to assign point lights to clusters.
59
PointLight {
60
/// Whether shadows are enabled for this point light.
61
///
62
/// This is used for sorting the light list.
63
shadows_enabled: bool,
64
65
/// Whether this light interacts with volumetrics.
66
///
67
/// This is used for sorting the light list.
68
volumetric: bool,
69
},
70
71
/// Data needed to assign spot lights to clusters.
72
SpotLight {
73
/// Whether shadows are enabled for this spot light.
74
///
75
/// This is used for sorting the light list.
76
shadows_enabled: bool,
77
78
/// Whether this light interacts with volumetrics.
79
///
80
/// This is used for sorting the light list.
81
volumetric: bool,
82
83
/// The outer angle of the light cone in radians.
84
outer_angle: f32,
85
},
86
87
/// Marks that the clusterable object is a reflection probe.
88
ReflectionProbe,
89
90
/// Marks that the clusterable object is an irradiance volume.
91
IrradianceVolume,
92
93
/// Marks that the clusterable object is a decal.
94
Decal,
95
}
96
97
impl ClusterableObjectType {
98
/// Returns a tuple that can be sorted to obtain the order in which indices
99
/// to clusterable objects must be stored in the cluster offsets and counts
100
/// list.
101
///
102
/// Generally, we sort first by type, then, for lights, by whether shadows
103
/// are enabled (enabled before disabled), and then whether volumetrics are
104
/// enabled (enabled before disabled).
105
pub fn ordering(&self) -> (u8, bool, bool) {
106
match *self {
107
ClusterableObjectType::PointLight {
108
shadows_enabled,
109
volumetric,
110
} => (0, !shadows_enabled, !volumetric),
111
ClusterableObjectType::SpotLight {
112
shadows_enabled,
113
volumetric,
114
..
115
} => (1, !shadows_enabled, !volumetric),
116
ClusterableObjectType::ReflectionProbe => (2, false, false),
117
ClusterableObjectType::IrradianceVolume => (3, false, false),
118
ClusterableObjectType::Decal => (4, false, false),
119
}
120
}
121
}
122
123
// NOTE: Run this before update_point_light_frusta!
124
pub(crate) fn assign_objects_to_clusters(
125
mut commands: Commands,
126
mut global_clusterable_objects: ResMut<GlobalVisibleClusterableObjects>,
127
mut views: Query<(
128
Entity,
129
&GlobalTransform,
130
&Camera,
131
&Frustum,
132
&ClusterConfig,
133
&mut Clusters,
134
Option<&RenderLayers>,
135
Option<&mut VisibleClusterableObjects>,
136
)>,
137
point_lights_query: Query<(
138
Entity,
139
&GlobalTransform,
140
&PointLight,
141
Option<&RenderLayers>,
142
Option<&VolumetricLight>,
143
&ViewVisibility,
144
)>,
145
spot_lights_query: Query<(
146
Entity,
147
&GlobalTransform,
148
&SpotLight,
149
Option<&RenderLayers>,
150
Option<&VolumetricLight>,
151
&ViewVisibility,
152
)>,
153
light_probes_query: Query<
154
(Entity, &GlobalTransform, Has<EnvironmentMapLight>),
155
With<LightProbe>,
156
>,
157
decals_query: Query<(Entity, &GlobalTransform), With<ClusteredDecal>>,
158
mut clusterable_objects: Local<Vec<ClusterableObjectAssignmentData>>,
159
mut cluster_aabb_spheres: Local<Vec<Option<Sphere>>>,
160
mut max_clusterable_objects_warning_emitted: Local<bool>,
161
global_cluster_settings: Option<Res<GlobalClusterSettings>>,
162
) {
163
let Some(global_cluster_settings) = global_cluster_settings else {
164
return;
165
};
166
167
global_clusterable_objects.entities.clear();
168
clusterable_objects.clear();
169
// collect just the relevant query data into a persisted vec to avoid reallocating each frame
170
clusterable_objects.extend(
171
point_lights_query
172
.iter()
173
.filter(|(.., visibility)| visibility.get())
174
.map(
175
|(entity, transform, point_light, maybe_layers, volumetric, _visibility)| {
176
ClusterableObjectAssignmentData {
177
entity,
178
transform: GlobalTransform::from_translation(transform.translation()),
179
range: point_light.range,
180
object_type: ClusterableObjectType::PointLight {
181
shadows_enabled: point_light.shadows_enabled,
182
volumetric: volumetric.is_some(),
183
},
184
render_layers: maybe_layers.unwrap_or_default().clone(),
185
}
186
},
187
),
188
);
189
clusterable_objects.extend(
190
spot_lights_query
191
.iter()
192
.filter(|(.., visibility)| visibility.get())
193
.map(
194
|(entity, transform, spot_light, maybe_layers, volumetric, _visibility)| {
195
ClusterableObjectAssignmentData {
196
entity,
197
transform: *transform,
198
range: spot_light.range,
199
object_type: ClusterableObjectType::SpotLight {
200
outer_angle: spot_light.outer_angle,
201
shadows_enabled: spot_light.shadows_enabled,
202
volumetric: volumetric.is_some(),
203
},
204
render_layers: maybe_layers.unwrap_or_default().clone(),
205
}
206
},
207
),
208
);
209
210
// Gather up light probes, but only if we're clustering them.
211
//
212
// UBOs aren't large enough to hold indices for light probes, so we can't
213
// cluster light probes on such platforms (mainly WebGL 2). Besides, those
214
// platforms typically lack bindless textures, so multiple light probes
215
// wouldn't be supported anyhow.
216
if global_cluster_settings.supports_storage_buffers {
217
clusterable_objects.extend(light_probes_query.iter().map(
218
|(entity, transform, is_reflection_probe)| ClusterableObjectAssignmentData {
219
entity,
220
transform: *transform,
221
range: transform.radius_vec3a(Vec3A::ONE),
222
object_type: if is_reflection_probe {
223
ClusterableObjectType::ReflectionProbe
224
} else {
225
ClusterableObjectType::IrradianceVolume
226
},
227
render_layers: RenderLayers::default(),
228
},
229
));
230
}
231
232
// Add decals if the current platform supports them.
233
if global_cluster_settings.clustered_decals_are_usable {
234
clusterable_objects.extend(decals_query.iter().map(|(entity, transform)| {
235
ClusterableObjectAssignmentData {
236
entity,
237
transform: *transform,
238
range: transform.scale().length(),
239
object_type: ClusterableObjectType::Decal,
240
render_layers: RenderLayers::default(),
241
}
242
}));
243
}
244
245
if clusterable_objects.len() > global_cluster_settings.max_uniform_buffer_clusterable_objects
246
&& !global_cluster_settings.supports_storage_buffers
247
{
248
clusterable_objects.sort_by_cached_key(|clusterable_object| {
249
(
250
clusterable_object.object_type.ordering(),
251
clusterable_object.entity,
252
)
253
});
254
255
// check each clusterable object against each view's frustum, keep only
256
// those that affect at least one of our views
257
let frusta: Vec<_> = views
258
.iter()
259
.map(|(_, _, _, frustum, _, _, _, _)| *frustum)
260
.collect();
261
let mut clusterable_objects_in_view_count = 0;
262
clusterable_objects.retain(|clusterable_object| {
263
// take one extra clusterable object to check if we should emit the warning
264
if clusterable_objects_in_view_count
265
== global_cluster_settings.max_uniform_buffer_clusterable_objects + 1
266
{
267
false
268
} else {
269
let clusterable_object_sphere = clusterable_object.sphere();
270
let clusterable_object_in_view = frusta
271
.iter()
272
.any(|frustum| frustum.intersects_sphere(&clusterable_object_sphere, true));
273
274
if clusterable_object_in_view {
275
clusterable_objects_in_view_count += 1;
276
}
277
278
clusterable_object_in_view
279
}
280
});
281
282
if clusterable_objects.len()
283
> global_cluster_settings.max_uniform_buffer_clusterable_objects
284
&& !*max_clusterable_objects_warning_emitted
285
{
286
warn!(
287
"max_uniform_buffer_clusterable_objects ({}) exceeded",
288
global_cluster_settings.max_uniform_buffer_clusterable_objects
289
);
290
*max_clusterable_objects_warning_emitted = true;
291
}
292
293
clusterable_objects
294
.truncate(global_cluster_settings.max_uniform_buffer_clusterable_objects);
295
}
296
297
for (
298
view_entity,
299
camera_transform,
300
camera,
301
frustum,
302
config,
303
clusters,
304
maybe_layers,
305
mut visible_clusterable_objects,
306
) in &mut views
307
{
308
let view_layers = maybe_layers.unwrap_or_default();
309
let clusters = clusters.into_inner();
310
311
if matches!(config, ClusterConfig::None) {
312
if visible_clusterable_objects.is_some() {
313
commands
314
.entity(view_entity)
315
.remove::<VisibleClusterableObjects>();
316
}
317
clusters.clear();
318
continue;
319
}
320
321
let screen_size = match camera.physical_viewport_size() {
322
Some(screen_size) if screen_size.x != 0 && screen_size.y != 0 => screen_size,
323
_ => {
324
clusters.clear();
325
continue;
326
}
327
};
328
329
let mut requested_cluster_dimensions = config.dimensions_for_screen_size(screen_size);
330
331
let world_from_view = camera_transform.affine();
332
let view_from_world_scale = camera_transform.compute_transform().scale.recip();
333
let view_from_world_scale_max = view_from_world_scale.abs().max_element();
334
let view_from_world = Mat4::from(world_from_view.inverse());
335
let is_orthographic = camera.clip_from_view().w_axis.w == 1.0;
336
337
let far_z = match config.far_z_mode() {
338
ClusterFarZMode::MaxClusterableObjectRange => {
339
let view_from_world_row_2 = view_from_world.row(2);
340
clusterable_objects
341
.iter()
342
.map(|object| {
343
-view_from_world_row_2.dot(object.transform.translation().extend(1.0))
344
+ object.range * view_from_world_scale.z
345
})
346
.reduce(f32::max)
347
.unwrap_or(0.0)
348
}
349
ClusterFarZMode::Constant(far) => far,
350
};
351
let first_slice_depth = match (is_orthographic, requested_cluster_dimensions.z) {
352
(true, _) => {
353
// NOTE: Based on glam's Mat4::orthographic_rh(), as used to calculate the orthographic projection
354
// matrix, we can calculate the projection's view-space near plane as follows:
355
// component 3,2 = r * near and 2,2 = r where r = 1.0 / (near - far)
356
// There is a caveat here that when calculating the projection matrix, near and far were swapped to give
357
// reversed z, consistent with the perspective projection. So,
358
// 3,2 = r * far and 2,2 = r where r = 1.0 / (far - near)
359
// rearranging r = 1.0 / (far - near), r * (far - near) = 1.0, r * far - 1.0 = r * near, near = (r * far - 1.0) / r
360
// = (3,2 - 1.0) / 2,2
361
(camera.clip_from_view().w_axis.z - 1.0) / camera.clip_from_view().z_axis.z
362
}
363
(false, 1) => config.first_slice_depth().max(far_z),
364
_ => config.first_slice_depth(),
365
};
366
let first_slice_depth = first_slice_depth * view_from_world_scale.z;
367
368
// NOTE: Ensure the far_z is at least as far as the first_depth_slice to avoid clustering problems.
369
let far_z = far_z.max(first_slice_depth);
370
let cluster_factors = calculate_cluster_factors(
371
first_slice_depth,
372
far_z,
373
requested_cluster_dimensions.z as f32,
374
is_orthographic,
375
);
376
377
if config.dynamic_resizing() {
378
let mut cluster_index_estimate = 0.0;
379
for clusterable_object in &clusterable_objects {
380
let clusterable_object_sphere = clusterable_object.sphere();
381
382
// Check if the clusterable object is within the view frustum
383
if !frustum.intersects_sphere(&clusterable_object_sphere, true) {
384
continue;
385
}
386
387
// calculate a conservative aabb estimate of number of clusters affected by this light
388
// this overestimates index counts by at most 50% (and typically much less) when the whole light range is in view
389
// it can overestimate more significantly when light ranges are only partially in view
390
let (clusterable_object_aabb_min, clusterable_object_aabb_max) =
391
cluster_space_clusterable_object_aabb(
392
view_from_world,
393
view_from_world_scale,
394
camera.clip_from_view(),
395
&clusterable_object_sphere,
396
);
397
398
// since we won't adjust z slices we can calculate exact number of slices required in z dimension
399
let z_cluster_min = view_z_to_z_slice(
400
cluster_factors,
401
requested_cluster_dimensions.z,
402
clusterable_object_aabb_min.z,
403
is_orthographic,
404
);
405
let z_cluster_max = view_z_to_z_slice(
406
cluster_factors,
407
requested_cluster_dimensions.z,
408
clusterable_object_aabb_max.z,
409
is_orthographic,
410
);
411
let z_count =
412
z_cluster_min.max(z_cluster_max) - z_cluster_min.min(z_cluster_max) + 1;
413
414
// calculate x/y count using floats to avoid overestimating counts due to large initial tile sizes
415
let xy_min = clusterable_object_aabb_min.xy();
416
let xy_max = clusterable_object_aabb_max.xy();
417
// multiply by 0.5 to move from [-1,1] to [-0.5, 0.5], max extent of 1 in each dimension
418
let xy_count = (xy_max - xy_min)
419
* 0.5
420
* Vec2::new(
421
requested_cluster_dimensions.x as f32,
422
requested_cluster_dimensions.y as f32,
423
);
424
425
// add up to 2 to each axis to account for overlap
426
let x_overlap = if xy_min.x <= -1.0 { 0.0 } else { 1.0 }
427
+ if xy_max.x >= 1.0 { 0.0 } else { 1.0 };
428
let y_overlap = if xy_min.y <= -1.0 { 0.0 } else { 1.0 }
429
+ if xy_max.y >= 1.0 { 0.0 } else { 1.0 };
430
cluster_index_estimate +=
431
(xy_count.x + x_overlap) * (xy_count.y + y_overlap) * z_count as f32;
432
}
433
434
if cluster_index_estimate
435
> global_cluster_settings.view_cluster_bindings_max_indices as f32
436
{
437
// scale x and y cluster count to be able to fit all our indices
438
439
// we take the ratio of the actual indices over the index estimate.
440
// this is not guaranteed to be small enough due to overlapped tiles, but
441
// the conservative estimate is more than sufficient to cover the
442
// difference
443
let index_ratio = global_cluster_settings.view_cluster_bindings_max_indices as f32
444
/ cluster_index_estimate;
445
let xy_ratio = index_ratio.sqrt();
446
447
requested_cluster_dimensions.x =
448
((requested_cluster_dimensions.x as f32 * xy_ratio).floor() as u32).max(1);
449
requested_cluster_dimensions.y =
450
((requested_cluster_dimensions.y as f32 * xy_ratio).floor() as u32).max(1);
451
}
452
}
453
454
clusters.update(screen_size, requested_cluster_dimensions);
455
clusters.near = first_slice_depth;
456
clusters.far = far_z;
457
458
// NOTE: Maximum 4096 clusters due to uniform buffer size constraints
459
debug_assert!(
460
clusters.dimensions.x * clusters.dimensions.y * clusters.dimensions.z <= 4096
461
);
462
463
let view_from_clip = camera.clip_from_view().inverse();
464
465
for clusterable_objects in &mut clusters.clusterable_objects {
466
clusterable_objects.entities.clear();
467
clusterable_objects.counts = default();
468
}
469
let cluster_count =
470
(clusters.dimensions.x * clusters.dimensions.y * clusters.dimensions.z) as usize;
471
clusters
472
.clusterable_objects
473
.resize_with(cluster_count, VisibleClusterableObjects::default);
474
475
// initialize empty cluster bounding spheres
476
cluster_aabb_spheres.clear();
477
cluster_aabb_spheres.extend(core::iter::repeat_n(None, cluster_count));
478
479
// Calculate the x/y/z cluster frustum planes in view space
480
let mut x_planes = Vec::with_capacity(clusters.dimensions.x as usize + 1);
481
let mut y_planes = Vec::with_capacity(clusters.dimensions.y as usize + 1);
482
let mut z_planes = Vec::with_capacity(clusters.dimensions.z as usize + 1);
483
484
if is_orthographic {
485
let x_slices = clusters.dimensions.x as f32;
486
for x in 0..=clusters.dimensions.x {
487
let x_proportion = x as f32 / x_slices;
488
let x_pos = x_proportion * 2.0 - 1.0;
489
let view_x = clip_to_view(view_from_clip, Vec4::new(x_pos, 0.0, 1.0, 1.0)).x;
490
let normal = Vec3::X;
491
let d = view_x * normal.x;
492
x_planes.push(HalfSpace::new(normal.extend(d)));
493
}
494
495
let y_slices = clusters.dimensions.y as f32;
496
for y in 0..=clusters.dimensions.y {
497
let y_proportion = 1.0 - y as f32 / y_slices;
498
let y_pos = y_proportion * 2.0 - 1.0;
499
let view_y = clip_to_view(view_from_clip, Vec4::new(0.0, y_pos, 1.0, 1.0)).y;
500
let normal = Vec3::Y;
501
let d = view_y * normal.y;
502
y_planes.push(HalfSpace::new(normal.extend(d)));
503
}
504
} else {
505
let x_slices = clusters.dimensions.x as f32;
506
for x in 0..=clusters.dimensions.x {
507
let x_proportion = x as f32 / x_slices;
508
let x_pos = x_proportion * 2.0 - 1.0;
509
let nb = clip_to_view(view_from_clip, Vec4::new(x_pos, -1.0, 1.0, 1.0)).xyz();
510
let nt = clip_to_view(view_from_clip, Vec4::new(x_pos, 1.0, 1.0, 1.0)).xyz();
511
let normal = nb.cross(nt);
512
let d = nb.dot(normal);
513
x_planes.push(HalfSpace::new(normal.extend(d)));
514
}
515
516
let y_slices = clusters.dimensions.y as f32;
517
for y in 0..=clusters.dimensions.y {
518
let y_proportion = 1.0 - y as f32 / y_slices;
519
let y_pos = y_proportion * 2.0 - 1.0;
520
let nl = clip_to_view(view_from_clip, Vec4::new(-1.0, y_pos, 1.0, 1.0)).xyz();
521
let nr = clip_to_view(view_from_clip, Vec4::new(1.0, y_pos, 1.0, 1.0)).xyz();
522
let normal = nr.cross(nl);
523
let d = nr.dot(normal);
524
y_planes.push(HalfSpace::new(normal.extend(d)));
525
}
526
}
527
528
let z_slices = clusters.dimensions.z;
529
for z in 0..=z_slices {
530
let view_z = z_slice_to_view_z(first_slice_depth, far_z, z_slices, z, is_orthographic);
531
let normal = -Vec3::Z;
532
let d = view_z * normal.z;
533
z_planes.push(HalfSpace::new(normal.extend(d)));
534
}
535
536
let mut update_from_object_intersections = |visible_clusterable_objects: &mut Vec<
537
Entity,
538
>| {
539
for clusterable_object in &clusterable_objects {
540
// check if the clusterable light layers overlap the view layers
541
if !view_layers.intersects(&clusterable_object.render_layers) {
542
continue;
543
}
544
545
let clusterable_object_sphere = clusterable_object.sphere();
546
547
// Check if the clusterable object is within the view frustum
548
if !frustum.intersects_sphere(&clusterable_object_sphere, true) {
549
continue;
550
}
551
552
// NOTE: The clusterable object intersects the frustum so it
553
// must be visible and part of the global set
554
global_clusterable_objects
555
.entities
556
.insert(clusterable_object.entity);
557
visible_clusterable_objects.push(clusterable_object.entity);
558
559
// note: caching seems to be slower than calling twice for this aabb calculation
560
let (
561
clusterable_object_aabb_xy_ndc_z_view_min,
562
clusterable_object_aabb_xy_ndc_z_view_max,
563
) = cluster_space_clusterable_object_aabb(
564
view_from_world,
565
view_from_world_scale,
566
camera.clip_from_view(),
567
&clusterable_object_sphere,
568
);
569
570
let min_cluster = ndc_position_to_cluster(
571
clusters.dimensions,
572
cluster_factors,
573
is_orthographic,
574
clusterable_object_aabb_xy_ndc_z_view_min,
575
clusterable_object_aabb_xy_ndc_z_view_min.z,
576
);
577
let max_cluster = ndc_position_to_cluster(
578
clusters.dimensions,
579
cluster_factors,
580
is_orthographic,
581
clusterable_object_aabb_xy_ndc_z_view_max,
582
clusterable_object_aabb_xy_ndc_z_view_max.z,
583
);
584
let (min_cluster, max_cluster) =
585
(min_cluster.min(max_cluster), min_cluster.max(max_cluster));
586
587
// What follows is the Iterative Sphere Refinement algorithm from Just Cause 3
588
// Persson et al, Practical Clustered Shading
589
// http://newq.net/dl/pub/s2015_practical.pdf
590
// NOTE: A sphere under perspective projection is no longer a sphere. It gets
591
// stretched and warped, which prevents simpler algorithms from being correct
592
// as they often assume that the widest part of the sphere under projection is the
593
// center point on the axis of interest plus the radius, and that is not true!
594
let view_clusterable_object_sphere = Sphere {
595
center: Vec3A::from_vec4(
596
view_from_world * clusterable_object_sphere.center.extend(1.0),
597
),
598
radius: clusterable_object_sphere.radius * view_from_world_scale_max,
599
};
600
let spot_light_dir_sin_cos = match clusterable_object.object_type {
601
ClusterableObjectType::SpotLight { outer_angle, .. } => {
602
let (angle_sin, angle_cos) = sin_cos(outer_angle);
603
Some((
604
(view_from_world * clusterable_object.transform.back().extend(0.0))
605
.truncate()
606
.normalize(),
607
angle_sin,
608
angle_cos,
609
))
610
}
611
ClusterableObjectType::Decal => {
612
// TODO: cull via a frustum
613
None
614
}
615
ClusterableObjectType::PointLight { .. }
616
| ClusterableObjectType::ReflectionProbe
617
| ClusterableObjectType::IrradianceVolume => None,
618
};
619
let clusterable_object_center_clip =
620
camera.clip_from_view() * view_clusterable_object_sphere.center.extend(1.0);
621
let object_center_ndc =
622
clusterable_object_center_clip.xyz() / clusterable_object_center_clip.w;
623
let cluster_coordinates = ndc_position_to_cluster(
624
clusters.dimensions,
625
cluster_factors,
626
is_orthographic,
627
object_center_ndc,
628
view_clusterable_object_sphere.center.z,
629
);
630
let z_center = if object_center_ndc.z <= 1.0 {
631
Some(cluster_coordinates.z)
632
} else {
633
None
634
};
635
let y_center = if object_center_ndc.y > 1.0 {
636
None
637
} else if object_center_ndc.y < -1.0 {
638
Some(clusters.dimensions.y + 1)
639
} else {
640
Some(cluster_coordinates.y)
641
};
642
for z in min_cluster.z..=max_cluster.z {
643
let mut z_object = view_clusterable_object_sphere.clone();
644
if z_center.is_none() || z != z_center.unwrap() {
645
// The z plane closer to the clusterable object has the
646
// larger radius circle where the light sphere
647
// intersects the z plane.
648
let z_plane = if z_center.is_some() && z < z_center.unwrap() {
649
z_planes[(z + 1) as usize]
650
} else {
651
z_planes[z as usize]
652
};
653
// Project the sphere to this z plane and use its radius as the radius of a
654
// new, refined sphere.
655
if let Some(projected) = project_to_plane_z(z_object, z_plane) {
656
z_object = projected;
657
} else {
658
continue;
659
}
660
}
661
for y in min_cluster.y..=max_cluster.y {
662
let mut y_object = z_object.clone();
663
if y_center.is_none() || y != y_center.unwrap() {
664
// The y plane closer to the clusterable object has
665
// the larger radius circle where the light sphere
666
// intersects the y plane.
667
let y_plane = if y_center.is_some() && y < y_center.unwrap() {
668
y_planes[(y + 1) as usize]
669
} else {
670
y_planes[y as usize]
671
};
672
// Project the refined sphere to this y plane and use its radius as the
673
// radius of a new, even more refined sphere.
674
if let Some(projected) =
675
project_to_plane_y(y_object, y_plane, is_orthographic)
676
{
677
y_object = projected;
678
} else {
679
continue;
680
}
681
}
682
// Loop from the left to find the first affected cluster
683
let mut min_x = min_cluster.x;
684
loop {
685
if min_x >= max_cluster.x
686
|| -get_distance_x(
687
x_planes[(min_x + 1) as usize],
688
y_object.center,
689
is_orthographic,
690
) + y_object.radius
691
> 0.0
692
{
693
break;
694
}
695
min_x += 1;
696
}
697
// Loop from the right to find the last affected cluster
698
let mut max_x = max_cluster.x;
699
loop {
700
if max_x <= min_x
701
|| get_distance_x(
702
x_planes[max_x as usize],
703
y_object.center,
704
is_orthographic,
705
) + y_object.radius
706
> 0.0
707
{
708
break;
709
}
710
max_x -= 1;
711
}
712
let mut cluster_index = ((y * clusters.dimensions.x + min_x)
713
* clusters.dimensions.z
714
+ z) as usize;
715
716
match clusterable_object.object_type {
717
ClusterableObjectType::SpotLight { .. } => {
718
let (view_light_direction, angle_sin, angle_cos) =
719
spot_light_dir_sin_cos.unwrap();
720
for x in min_x..=max_x {
721
// further culling for spot lights
722
// get or initialize cluster bounding sphere
723
let cluster_aabb_sphere =
724
&mut cluster_aabb_spheres[cluster_index];
725
let cluster_aabb_sphere =
726
if let Some(sphere) = cluster_aabb_sphere {
727
&*sphere
728
} else {
729
let aabb = compute_aabb_for_cluster(
730
first_slice_depth,
731
far_z,
732
clusters.tile_size.as_vec2(),
733
screen_size.as_vec2(),
734
view_from_clip,
735
is_orthographic,
736
clusters.dimensions,
737
UVec3::new(x, y, z),
738
);
739
let sphere = Sphere {
740
center: aabb.center,
741
radius: aabb.half_extents.length(),
742
};
743
*cluster_aabb_sphere = Some(sphere);
744
cluster_aabb_sphere.as_ref().unwrap()
745
};
746
747
// test -- based on https://bartwronski.com/2017/04/13/cull-that-cone/
748
let spot_light_offset = Vec3::from(
749
view_clusterable_object_sphere.center
750
- cluster_aabb_sphere.center,
751
);
752
let spot_light_dist_sq = spot_light_offset.length_squared();
753
let v1_len = spot_light_offset.dot(view_light_direction);
754
755
let distance_closest_point = (angle_cos
756
* (spot_light_dist_sq - v1_len * v1_len).sqrt())
757
- v1_len * angle_sin;
758
let angle_cull =
759
distance_closest_point > cluster_aabb_sphere.radius;
760
761
let front_cull = v1_len
762
> cluster_aabb_sphere.radius
763
+ clusterable_object.range * view_from_world_scale_max;
764
let back_cull = v1_len < -cluster_aabb_sphere.radius;
765
766
if !angle_cull && !front_cull && !back_cull {
767
// this cluster is affected by the spot light
768
clusters.clusterable_objects[cluster_index]
769
.entities
770
.push(clusterable_object.entity);
771
clusters.clusterable_objects[cluster_index]
772
.counts
773
.spot_lights += 1;
774
}
775
cluster_index += clusters.dimensions.z as usize;
776
}
777
}
778
779
ClusterableObjectType::PointLight { .. } => {
780
for _ in min_x..=max_x {
781
// all clusters within range are affected by point lights
782
clusters.clusterable_objects[cluster_index]
783
.entities
784
.push(clusterable_object.entity);
785
clusters.clusterable_objects[cluster_index]
786
.counts
787
.point_lights += 1;
788
cluster_index += clusters.dimensions.z as usize;
789
}
790
}
791
792
ClusterableObjectType::ReflectionProbe => {
793
// Reflection probes currently affect all
794
// clusters in their bounding sphere.
795
//
796
// TODO: Cull more aggressively based on the
797
// probe's OBB.
798
for _ in min_x..=max_x {
799
clusters.clusterable_objects[cluster_index]
800
.entities
801
.push(clusterable_object.entity);
802
clusters.clusterable_objects[cluster_index]
803
.counts
804
.reflection_probes += 1;
805
cluster_index += clusters.dimensions.z as usize;
806
}
807
}
808
809
ClusterableObjectType::IrradianceVolume => {
810
// Irradiance volumes currently affect all
811
// clusters in their bounding sphere.
812
//
813
// TODO: Cull more aggressively based on the
814
// probe's OBB.
815
for _ in min_x..=max_x {
816
clusters.clusterable_objects[cluster_index]
817
.entities
818
.push(clusterable_object.entity);
819
clusters.clusterable_objects[cluster_index]
820
.counts
821
.irradiance_volumes += 1;
822
cluster_index += clusters.dimensions.z as usize;
823
}
824
}
825
826
ClusterableObjectType::Decal => {
827
// Decals currently affect all clusters in their
828
// bounding sphere.
829
//
830
// TODO: Cull more aggressively based on the
831
// decal's OBB.
832
for _ in min_x..=max_x {
833
clusters.clusterable_objects[cluster_index]
834
.entities
835
.push(clusterable_object.entity);
836
clusters.clusterable_objects[cluster_index].counts.decals += 1;
837
cluster_index += clusters.dimensions.z as usize;
838
}
839
}
840
}
841
}
842
}
843
}
844
};
845
846
// reuse existing visible clusterable objects Vec, if it exists
847
if let Some(visible_clusterable_objects) = visible_clusterable_objects.as_mut() {
848
visible_clusterable_objects.entities.clear();
849
update_from_object_intersections(&mut visible_clusterable_objects.entities);
850
} else {
851
let mut entities = Vec::new();
852
update_from_object_intersections(&mut entities);
853
commands
854
.entity(view_entity)
855
.insert(VisibleClusterableObjects {
856
entities,
857
..Default::default()
858
});
859
}
860
}
861
}
862
863
pub fn calculate_cluster_factors(
864
near: f32,
865
far: f32,
866
z_slices: f32,
867
is_orthographic: bool,
868
) -> Vec2 {
869
if is_orthographic {
870
Vec2::new(-near, z_slices / (-far - -near))
871
} else {
872
let z_slices_of_ln_zfar_over_znear = (z_slices - 1.0) / ops::ln(far / near);
873
Vec2::new(
874
z_slices_of_ln_zfar_over_znear,
875
ops::ln(near) * z_slices_of_ln_zfar_over_znear,
876
)
877
}
878
}
879
880
fn compute_aabb_for_cluster(
881
z_near: f32,
882
z_far: f32,
883
tile_size: Vec2,
884
screen_size: Vec2,
885
view_from_clip: Mat4,
886
is_orthographic: bool,
887
cluster_dimensions: UVec3,
888
ijk: UVec3,
889
) -> Aabb {
890
let ijk = ijk.as_vec3();
891
892
// Calculate the minimum and maximum points in screen space
893
let p_min = ijk.xy() * tile_size;
894
let p_max = p_min + tile_size;
895
896
let cluster_min;
897
let cluster_max;
898
if is_orthographic {
899
// Use linear depth slicing for orthographic
900
901
// Convert to view space at the cluster near and far planes
902
// NOTE: 1.0 is the near plane due to using reverse z projections
903
let mut p_min = screen_to_view(screen_size, view_from_clip, p_min, 0.0).xyz();
904
let mut p_max = screen_to_view(screen_size, view_from_clip, p_max, 0.0).xyz();
905
906
// calculate cluster depth using z_near and z_far
907
p_min.z = -z_near + (z_near - z_far) * ijk.z / cluster_dimensions.z as f32;
908
p_max.z = -z_near + (z_near - z_far) * (ijk.z + 1.0) / cluster_dimensions.z as f32;
909
910
cluster_min = p_min.min(p_max);
911
cluster_max = p_min.max(p_max);
912
} else {
913
// Convert to view space at the near plane
914
// NOTE: 1.0 is the near plane due to using reverse z projections
915
let p_min = screen_to_view(screen_size, view_from_clip, p_min, 1.0);
916
let p_max = screen_to_view(screen_size, view_from_clip, p_max, 1.0);
917
918
let z_far_over_z_near = -z_far / -z_near;
919
let cluster_near = if ijk.z == 0.0 {
920
0.0
921
} else {
922
-z_near
923
* ops::powf(
924
z_far_over_z_near,
925
(ijk.z - 1.0) / (cluster_dimensions.z - 1) as f32,
926
)
927
};
928
// NOTE: This could be simplified to:
929
// cluster_far = cluster_near * z_far_over_z_near;
930
let cluster_far = if cluster_dimensions.z == 1 {
931
-z_far
932
} else {
933
-z_near * ops::powf(z_far_over_z_near, ijk.z / (cluster_dimensions.z - 1) as f32)
934
};
935
936
// Calculate the four intersection points of the min and max points with the cluster near and far planes
937
let p_min_near = line_intersection_to_z_plane(Vec3::ZERO, p_min.xyz(), cluster_near);
938
let p_min_far = line_intersection_to_z_plane(Vec3::ZERO, p_min.xyz(), cluster_far);
939
let p_max_near = line_intersection_to_z_plane(Vec3::ZERO, p_max.xyz(), cluster_near);
940
let p_max_far = line_intersection_to_z_plane(Vec3::ZERO, p_max.xyz(), cluster_far);
941
942
cluster_min = p_min_near.min(p_min_far).min(p_max_near.min(p_max_far));
943
cluster_max = p_min_near.max(p_min_far).max(p_max_near.max(p_max_far));
944
}
945
946
Aabb::from_min_max(cluster_min, cluster_max)
947
}
948
949
// NOTE: Keep in sync as the inverse of view_z_to_z_slice above
950
fn z_slice_to_view_z(
951
near: f32,
952
far: f32,
953
z_slices: u32,
954
z_slice: u32,
955
is_orthographic: bool,
956
) -> f32 {
957
if is_orthographic {
958
return -near - (far - near) * z_slice as f32 / z_slices as f32;
959
}
960
961
// Perspective
962
if z_slice == 0 {
963
0.0
964
} else {
965
-near * ops::powf(far / near, (z_slice - 1) as f32 / (z_slices - 1) as f32)
966
}
967
}
968
969
fn ndc_position_to_cluster(
970
cluster_dimensions: UVec3,
971
cluster_factors: Vec2,
972
is_orthographic: bool,
973
ndc_p: Vec3,
974
view_z: f32,
975
) -> UVec3 {
976
let cluster_dimensions_f32 = cluster_dimensions.as_vec3();
977
let frag_coord = (ndc_p.xy() * VEC2_HALF_NEGATIVE_Y + VEC2_HALF).clamp(Vec2::ZERO, Vec2::ONE);
978
let xy = (frag_coord * cluster_dimensions_f32.xy()).floor();
979
let z_slice = view_z_to_z_slice(
980
cluster_factors,
981
cluster_dimensions.z,
982
view_z,
983
is_orthographic,
984
);
985
xy.as_uvec2()
986
.extend(z_slice)
987
.clamp(UVec3::ZERO, cluster_dimensions - UVec3::ONE)
988
}
989
990
/// Calculate bounds for the clusterable object using a view space aabb.
991
///
992
/// Returns a `(Vec3, Vec3)` containing minimum and maximum with
993
/// `X` and `Y` in normalized device coordinates with range `[-1, 1]`
994
/// `Z` in view space, with range `[-inf, -f32::MIN_POSITIVE]`
995
fn cluster_space_clusterable_object_aabb(
996
view_from_world: Mat4,
997
view_from_world_scale: Vec3,
998
clip_from_view: Mat4,
999
clusterable_object_sphere: &Sphere,
1000
) -> (Vec3, Vec3) {
1001
let clusterable_object_aabb_view = Aabb {
1002
center: Vec3A::from_vec4(view_from_world * clusterable_object_sphere.center.extend(1.0)),
1003
half_extents: Vec3A::from(clusterable_object_sphere.radius * view_from_world_scale.abs()),
1004
};
1005
let (mut clusterable_object_aabb_view_min, mut clusterable_object_aabb_view_max) = (
1006
clusterable_object_aabb_view.min(),
1007
clusterable_object_aabb_view.max(),
1008
);
1009
1010
// Constrain view z to be negative - i.e. in front of the camera
1011
// When view z is >= 0.0 and we're using a perspective projection, bad things happen.
1012
// At view z == 0.0, ndc x,y are mathematically undefined. At view z > 0.0, i.e. behind the camera,
1013
// the perspective projection flips the directions of the axes. This breaks assumptions about
1014
// use of min/max operations as something that was to the left in view space is now returning a
1015
// coordinate that for view z in front of the camera would be on the right, but at view z behind the
1016
// camera is on the left. So, we just constrain view z to be < 0.0 and necessarily in front of the camera.
1017
clusterable_object_aabb_view_min.z = clusterable_object_aabb_view_min.z.min(-f32::MIN_POSITIVE);
1018
clusterable_object_aabb_view_max.z = clusterable_object_aabb_view_max.z.min(-f32::MIN_POSITIVE);
1019
1020
// Is there a cheaper way to do this? The problem is that because of perspective
1021
// the point at max z but min xy may be less xy in screenspace, and similar. As
1022
// such, projecting the min and max xy at both the closer and further z and taking
1023
// the min and max of those projected points addresses this.
1024
let (
1025
clusterable_object_aabb_view_xymin_near,
1026
clusterable_object_aabb_view_xymin_far,
1027
clusterable_object_aabb_view_xymax_near,
1028
clusterable_object_aabb_view_xymax_far,
1029
) = (
1030
clusterable_object_aabb_view_min,
1031
clusterable_object_aabb_view_min
1032
.xy()
1033
.extend(clusterable_object_aabb_view_max.z),
1034
clusterable_object_aabb_view_max
1035
.xy()
1036
.extend(clusterable_object_aabb_view_min.z),
1037
clusterable_object_aabb_view_max,
1038
);
1039
let (
1040
clusterable_object_aabb_clip_xymin_near,
1041
clusterable_object_aabb_clip_xymin_far,
1042
clusterable_object_aabb_clip_xymax_near,
1043
clusterable_object_aabb_clip_xymax_far,
1044
) = (
1045
clip_from_view * clusterable_object_aabb_view_xymin_near.extend(1.0),
1046
clip_from_view * clusterable_object_aabb_view_xymin_far.extend(1.0),
1047
clip_from_view * clusterable_object_aabb_view_xymax_near.extend(1.0),
1048
clip_from_view * clusterable_object_aabb_view_xymax_far.extend(1.0),
1049
);
1050
let (
1051
clusterable_object_aabb_ndc_xymin_near,
1052
clusterable_object_aabb_ndc_xymin_far,
1053
clusterable_object_aabb_ndc_xymax_near,
1054
clusterable_object_aabb_ndc_xymax_far,
1055
) = (
1056
clusterable_object_aabb_clip_xymin_near.xyz() / clusterable_object_aabb_clip_xymin_near.w,
1057
clusterable_object_aabb_clip_xymin_far.xyz() / clusterable_object_aabb_clip_xymin_far.w,
1058
clusterable_object_aabb_clip_xymax_near.xyz() / clusterable_object_aabb_clip_xymax_near.w,
1059
clusterable_object_aabb_clip_xymax_far.xyz() / clusterable_object_aabb_clip_xymax_far.w,
1060
);
1061
let (clusterable_object_aabb_ndc_min, clusterable_object_aabb_ndc_max) = (
1062
clusterable_object_aabb_ndc_xymin_near
1063
.min(clusterable_object_aabb_ndc_xymin_far)
1064
.min(clusterable_object_aabb_ndc_xymax_near)
1065
.min(clusterable_object_aabb_ndc_xymax_far),
1066
clusterable_object_aabb_ndc_xymin_near
1067
.max(clusterable_object_aabb_ndc_xymin_far)
1068
.max(clusterable_object_aabb_ndc_xymax_near)
1069
.max(clusterable_object_aabb_ndc_xymax_far),
1070
);
1071
1072
// clamp to ndc coords without depth
1073
let (aabb_min_ndc, aabb_max_ndc) = (
1074
clusterable_object_aabb_ndc_min.xy().clamp(NDC_MIN, NDC_MAX),
1075
clusterable_object_aabb_ndc_max.xy().clamp(NDC_MIN, NDC_MAX),
1076
);
1077
1078
// pack unadjusted z depth into the vecs
1079
(
1080
aabb_min_ndc.extend(clusterable_object_aabb_view_min.z),
1081
aabb_max_ndc.extend(clusterable_object_aabb_view_max.z),
1082
)
1083
}
1084
1085
// Calculate the intersection of a ray from the eye through the view space position to a z plane
1086
fn line_intersection_to_z_plane(origin: Vec3, p: Vec3, z: f32) -> Vec3 {
1087
let v = p - origin;
1088
let t = (z - Vec3::Z.dot(origin)) / Vec3::Z.dot(v);
1089
origin + t * v
1090
}
1091
1092
// NOTE: Keep in sync with bevy_pbr/src/render/pbr.wgsl
1093
fn view_z_to_z_slice(
1094
cluster_factors: Vec2,
1095
z_slices: u32,
1096
view_z: f32,
1097
is_orthographic: bool,
1098
) -> u32 {
1099
let z_slice = if is_orthographic {
1100
// NOTE: view_z is correct in the orthographic case
1101
((view_z - cluster_factors.x) * cluster_factors.y).floor() as u32
1102
} else {
1103
// NOTE: had to use -view_z to make it positive else log(negative) is nan
1104
(ops::ln(-view_z) * cluster_factors.x - cluster_factors.y + 1.0) as u32
1105
};
1106
// NOTE: We use min as we may limit the far z plane used for clustering to be closer than
1107
// the furthest thing being drawn. This means that we need to limit to the maximum cluster.
1108
z_slice.min(z_slices - 1)
1109
}
1110
1111
fn clip_to_view(view_from_clip: Mat4, clip: Vec4) -> Vec4 {
1112
let view = view_from_clip * clip;
1113
view / view.w
1114
}
1115
1116
fn screen_to_view(screen_size: Vec2, view_from_clip: Mat4, screen: Vec2, ndc_z: f32) -> Vec4 {
1117
let tex_coord = screen / screen_size;
1118
let clip = Vec4::new(
1119
tex_coord.x * 2.0 - 1.0,
1120
(1.0 - tex_coord.y) * 2.0 - 1.0,
1121
ndc_z,
1122
1.0,
1123
);
1124
clip_to_view(view_from_clip, clip)
1125
}
1126
1127
// NOTE: This exploits the fact that a x-plane normal has only x and z components
1128
fn get_distance_x(plane: HalfSpace, point: Vec3A, is_orthographic: bool) -> f32 {
1129
if is_orthographic {
1130
point.x - plane.d()
1131
} else {
1132
// Distance from a point to a plane:
1133
// signed distance to plane = (nx * px + ny * py + nz * pz + d) / n.length()
1134
// NOTE: For a x-plane, ny and d are 0 and we have a unit normal
1135
// = nx * px + nz * pz
1136
plane.normal_d().xz().dot(point.xz())
1137
}
1138
}
1139
1140
// NOTE: This exploits the fact that a z-plane normal has only a z component
1141
fn project_to_plane_z(z_object: Sphere, z_plane: HalfSpace) -> Option<Sphere> {
1142
// p = sphere center
1143
// n = plane normal
1144
// d = n.p if p is in the plane
1145
// NOTE: For a z-plane, nx and ny are both 0
1146
// d = px * nx + py * ny + pz * nz
1147
// = pz * nz
1148
// => pz = d / nz
1149
let z = z_plane.d() / z_plane.normal_d().z;
1150
let distance_to_plane = z - z_object.center.z;
1151
if distance_to_plane.abs() > z_object.radius {
1152
return None;
1153
}
1154
Some(Sphere {
1155
center: Vec3A::from(z_object.center.xy().extend(z)),
1156
// hypotenuse length = radius
1157
// pythagoras = (distance to plane)^2 + b^2 = radius^2
1158
radius: (z_object.radius * z_object.radius - distance_to_plane * distance_to_plane).sqrt(),
1159
})
1160
}
1161
1162
// NOTE: This exploits the fact that a y-plane normal has only y and z components
1163
fn project_to_plane_y(
1164
y_object: Sphere,
1165
y_plane: HalfSpace,
1166
is_orthographic: bool,
1167
) -> Option<Sphere> {
1168
let distance_to_plane = if is_orthographic {
1169
y_plane.d() - y_object.center.y
1170
} else {
1171
-y_object.center.yz().dot(y_plane.normal_d().yz())
1172
};
1173
1174
if distance_to_plane.abs() > y_object.radius {
1175
return None;
1176
}
1177
Some(Sphere {
1178
center: y_object.center + distance_to_plane * y_plane.normal(),
1179
radius: (y_object.radius * y_object.radius - distance_to_plane * distance_to_plane).sqrt(),
1180
})
1181
}
1182
1183