Parallel Frustum Culling (#4489)

# Objective

Working with a large number of entities with `Aabbs`, rendered with an instanced shader, I found the bottleneck became the frustum culling system. The goal of this PR is to significantly improve culling performance without any major changes. We should consider constructing a BVH for more substantial improvements.

## Solution

- Convert the inner entity query to a parallel iterator with `par_for_each_mut` using a batch size of 1,024. 
- This outperforms single threaded culling when there are more than 1,000 entities. 
  - Below this they are approximately equal, with <= 10 microseconds of multithreading overhead.
  - Above this, the multithreaded version is significantly faster, scaling linearly with core count.
- In my million-entity-workload, this PR improves my framerate by 200% - 300%.

## log-log of `check_visibility` time vs. entities for single/multithreaded
![image](https://user-images.githubusercontent.com/2632925/163709007-7eab4437-e9f9-4c06-bac0-250073885110.png)

---

## Changelog

Frustum culling is now run with a parallel query. When culling more than a thousand entities, this is faster than the previous method, scaling proportionally with the number of available cores.
This commit is contained in:
Aevyrie 2022-06-14 02:07:40 +00:00
parent c6222f1acc
commit 915fa69b66
2 changed files with 51 additions and 56 deletions

View File

@ -56,6 +56,7 @@ once_cell = "1.4.1" # TODO: replace once_cell with std equivalent if/when this l
downcast-rs = "1.2.0" downcast-rs = "1.2.0"
thiserror = "1.0" thiserror = "1.0"
futures-lite = "1.4.0" futures-lite = "1.4.0"
crossbeam-channel = "0.5.0"
anyhow = "1.0" anyhow = "1.0"
hex = "0.4.2" hex = "0.4.2"
hexasphere = "7.0.0" hexasphere = "7.0.0"

View File

@ -149,71 +149,65 @@ pub fn update_frusta<T: Component + CameraProjection + Send + Sync + 'static>(
pub fn check_visibility( pub fn check_visibility(
mut view_query: Query<(&mut VisibleEntities, &Frustum, Option<&RenderLayers>), With<Camera>>, mut view_query: Query<(&mut VisibleEntities, &Frustum, Option<&RenderLayers>), With<Camera>>,
mut visible_entity_query: ParamSet<( mut visible_entity_query: Query<(
Query<&mut ComputedVisibility>, Entity,
Query<( &Visibility,
Entity, &mut ComputedVisibility,
&Visibility, Option<&RenderLayers>,
&mut ComputedVisibility, Option<&Aabb>,
Option<&RenderLayers>, Option<&NoFrustumCulling>,
Option<&Aabb>, Option<&GlobalTransform>,
Option<&NoFrustumCulling>,
Option<&GlobalTransform>,
)>,
)>, )>,
) { ) {
// Reset the computed visibility to false
for mut computed_visibility in visible_entity_query.p0().iter_mut() {
computed_visibility.is_visible = false;
}
for (mut visible_entities, frustum, maybe_view_mask) in view_query.iter_mut() { for (mut visible_entities, frustum, maybe_view_mask) in view_query.iter_mut() {
visible_entities.entities.clear();
let view_mask = maybe_view_mask.copied().unwrap_or_default(); let view_mask = maybe_view_mask.copied().unwrap_or_default();
let (visible_entity_sender, visible_entity_receiver) = crossbeam_channel::unbounded();
for ( visible_entity_query.par_for_each_mut(
entity, 1024,
visibility, |(
mut computed_visibility, entity,
maybe_entity_mask, visibility,
maybe_aabb, mut computed_visibility,
maybe_no_frustum_culling, maybe_entity_mask,
maybe_transform, maybe_aabb,
) in visible_entity_query.p1().iter_mut() maybe_no_frustum_culling,
{ maybe_transform,
if !visibility.is_visible { )| {
continue; // Reset visibility
} computed_visibility.is_visible = false;
let entity_mask = maybe_entity_mask.copied().unwrap_or_default(); if !visibility.is_visible {
if !view_mask.intersects(&entity_mask) { return;
continue;
}
// If we have an aabb and transform, do frustum culling
if let (Some(model_aabb), None, Some(transform)) =
(maybe_aabb, maybe_no_frustum_culling, maybe_transform)
{
let model = transform.compute_matrix();
let model_sphere = Sphere {
center: model.transform_point3a(model_aabb.center),
radius: (Vec3A::from(transform.scale) * model_aabb.half_extents).length(),
};
// Do quick sphere-based frustum culling
if !frustum.intersects_sphere(&model_sphere, false) {
continue;
} }
// If we have an aabb, do aabb-based frustum culling let entity_mask = maybe_entity_mask.copied().unwrap_or_default();
if !frustum.intersects_obb(model_aabb, &model, false) { if !view_mask.intersects(&entity_mask) {
continue; return;
} }
}
computed_visibility.is_visible = true; // If we have an aabb and transform, do frustum culling
visible_entities.entities.push(entity); if let (Some(model_aabb), None, Some(transform)) =
} (maybe_aabb, maybe_no_frustum_culling, maybe_transform)
{
let model = transform.compute_matrix();
let model_sphere = Sphere {
center: model.transform_point3a(model_aabb.center),
radius: (Vec3A::from(transform.scale) * model_aabb.half_extents).length(),
};
// Do quick sphere-based frustum culling
if !frustum.intersects_sphere(&model_sphere, false) {
return;
}
// If we have an aabb, do aabb-based frustum culling
if !frustum.intersects_obb(model_aabb, &model, false) {
return;
}
}
// TODO: check for big changes in visible entities len() vs capacity() (ex: 2x) and resize computed_visibility.is_visible = true;
// to prevent holding unneeded memory visible_entity_sender.send(entity).ok();
},
);
visible_entities.entities = visible_entity_receiver.try_iter().collect();
} }
} }