mirror of
https://github.com/bevyengine/bevy
synced 2025-02-16 22:18:33 +00:00
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:
parent
c6222f1acc
commit
915fa69b66
2 changed files with 51 additions and 56 deletions
|
@ -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"
|
||||
thiserror = "1.0"
|
||||
futures-lite = "1.4.0"
|
||||
crossbeam-channel = "0.5.0"
|
||||
anyhow = "1.0"
|
||||
hex = "0.4.2"
|
||||
hexasphere = "7.0.0"
|
||||
|
|
|
@ -149,71 +149,65 @@ pub fn update_frusta<T: Component + CameraProjection + Send + Sync + 'static>(
|
|||
|
||||
pub fn check_visibility(
|
||||
mut view_query: Query<(&mut VisibleEntities, &Frustum, Option<&RenderLayers>), With<Camera>>,
|
||||
mut visible_entity_query: ParamSet<(
|
||||
Query<&mut ComputedVisibility>,
|
||||
Query<(
|
||||
Entity,
|
||||
&Visibility,
|
||||
&mut ComputedVisibility,
|
||||
Option<&RenderLayers>,
|
||||
Option<&Aabb>,
|
||||
Option<&NoFrustumCulling>,
|
||||
Option<&GlobalTransform>,
|
||||
)>,
|
||||
mut visible_entity_query: Query<(
|
||||
Entity,
|
||||
&Visibility,
|
||||
&mut ComputedVisibility,
|
||||
Option<&RenderLayers>,
|
||||
Option<&Aabb>,
|
||||
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() {
|
||||
visible_entities.entities.clear();
|
||||
let view_mask = maybe_view_mask.copied().unwrap_or_default();
|
||||
let (visible_entity_sender, visible_entity_receiver) = crossbeam_channel::unbounded();
|
||||
|
||||
for (
|
||||
entity,
|
||||
visibility,
|
||||
mut computed_visibility,
|
||||
maybe_entity_mask,
|
||||
maybe_aabb,
|
||||
maybe_no_frustum_culling,
|
||||
maybe_transform,
|
||||
) in visible_entity_query.p1().iter_mut()
|
||||
{
|
||||
if !visibility.is_visible {
|
||||
continue;
|
||||
}
|
||||
visible_entity_query.par_for_each_mut(
|
||||
1024,
|
||||
|(
|
||||
entity,
|
||||
visibility,
|
||||
mut computed_visibility,
|
||||
maybe_entity_mask,
|
||||
maybe_aabb,
|
||||
maybe_no_frustum_culling,
|
||||
maybe_transform,
|
||||
)| {
|
||||
// Reset visibility
|
||||
computed_visibility.is_visible = false;
|
||||
|
||||
let entity_mask = maybe_entity_mask.copied().unwrap_or_default();
|
||||
if !view_mask.intersects(&entity_mask) {
|
||||
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 !visibility.is_visible {
|
||||
return;
|
||||
}
|
||||
// If we have an aabb, do aabb-based frustum culling
|
||||
if !frustum.intersects_obb(model_aabb, &model, false) {
|
||||
continue;
|
||||
let entity_mask = maybe_entity_mask.copied().unwrap_or_default();
|
||||
if !view_mask.intersects(&entity_mask) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
computed_visibility.is_visible = true;
|
||||
visible_entities.entities.push(entity);
|
||||
}
|
||||
// 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) {
|
||||
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
|
||||
// to prevent holding unneeded memory
|
||||
computed_visibility.is_visible = true;
|
||||
visible_entity_sender.send(entity).ok();
|
||||
},
|
||||
);
|
||||
visible_entities.entities = visible_entity_receiver.try_iter().collect();
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Reference in a new issue