Skip to content

Commit 5504942

Browse files
committed
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.
1 parent b7d784d commit 5504942

File tree

2 files changed

+53
-58
lines changed

2 files changed

+53
-58
lines changed

crates/bevy_render/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ once_cell = "1.4.1" # TODO: replace once_cell with std equivalent if/when this l
5656
downcast-rs = "1.2.0"
5757
thiserror = "1.0"
5858
futures-lite = "1.4.0"
59+
crossbeam-channel = "0.5.0"
5960
anyhow = "1.0"
6061
hex = "0.4.2"
6162
hexasphere = "7.0.0"

crates/bevy_render/src/view/visibility/mod.rs

Lines changed: 52 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -149,71 +149,65 @@ pub fn update_frusta<T: Component + CameraProjection + Send + Sync + 'static>(
149149

150150
pub fn check_visibility(
151151
mut view_query: Query<(&mut VisibleEntities, &Frustum, Option<&RenderLayers>), With<Camera>>,
152-
mut visible_entity_query: ParamSet<(
153-
Query<&mut ComputedVisibility>,
154-
Query<(
155-
Entity,
156-
&Visibility,
157-
&mut ComputedVisibility,
158-
Option<&RenderLayers>,
159-
Option<&Aabb>,
160-
Option<&NoFrustumCulling>,
161-
Option<&GlobalTransform>,
162-
)>,
152+
mut visible_entity_query: Query<(
153+
Entity,
154+
&Visibility,
155+
&mut ComputedVisibility,
156+
Option<&RenderLayers>,
157+
Option<&Aabb>,
158+
Option<&NoFrustumCulling>,
159+
Option<&GlobalTransform>,
163160
)>,
164161
) {
165-
// Reset the computed visibility to false
166-
for mut computed_visibility in visible_entity_query.p0().iter_mut() {
167-
computed_visibility.is_visible = false;
168-
}
169-
170162
for (mut visible_entities, frustum, maybe_view_mask) in view_query.iter_mut() {
171-
visible_entities.entities.clear();
172163
let view_mask = maybe_view_mask.copied().unwrap_or_default();
173-
174-
for (
175-
entity,
176-
visibility,
177-
mut computed_visibility,
178-
maybe_entity_mask,
179-
maybe_aabb,
180-
maybe_no_frustum_culling,
181-
maybe_transform,
182-
) in visible_entity_query.p1().iter_mut()
183-
{
184-
if !visibility.is_visible {
185-
continue;
186-
}
187-
188-
let entity_mask = maybe_entity_mask.copied().unwrap_or_default();
189-
if !view_mask.intersects(&entity_mask) {
190-
continue;
191-
}
192-
193-
// If we have an aabb and transform, do frustum culling
194-
if let (Some(model_aabb), None, Some(transform)) =
195-
(maybe_aabb, maybe_no_frustum_culling, maybe_transform)
196-
{
197-
let model = transform.compute_matrix();
198-
let model_sphere = Sphere {
199-
center: model.transform_point3a(model_aabb.center),
200-
radius: (Vec3A::from(transform.scale) * model_aabb.half_extents).length(),
201-
};
202-
// Do quick sphere-based frustum culling
203-
if !frustum.intersects_sphere(&model_sphere, false) {
204-
continue;
164+
let (visible_entity_sender, visible_entity_receiver) = crossbeam_channel::unbounded();
165+
166+
visible_entity_query.par_for_each_mut(
167+
1024,
168+
|(
169+
entity,
170+
visibility,
171+
mut computed_visibility,
172+
maybe_entity_mask,
173+
maybe_aabb,
174+
maybe_no_frustum_culling,
175+
maybe_transform,
176+
)| {
177+
// Reset visibility
178+
computed_visibility.is_visible = false;
179+
180+
if !visibility.is_visible {
181+
return;
205182
}
206-
// If we have an aabb, do aabb-based frustum culling
207-
if !frustum.intersects_obb(model_aabb, &model, false) {
208-
continue;
183+
let entity_mask = maybe_entity_mask.copied().unwrap_or_default();
184+
if !view_mask.intersects(&entity_mask) {
185+
return;
209186
}
210-
}
211187

212-
computed_visibility.is_visible = true;
213-
visible_entities.entities.push(entity);
214-
}
188+
// If we have an aabb and transform, do frustum culling
189+
if let (Some(model_aabb), None, Some(transform)) =
190+
(maybe_aabb, maybe_no_frustum_culling, maybe_transform)
191+
{
192+
let model = transform.compute_matrix();
193+
let model_sphere = Sphere {
194+
center: model.transform_point3a(model_aabb.center),
195+
radius: (Vec3A::from(transform.scale) * model_aabb.half_extents).length(),
196+
};
197+
// Do quick sphere-based frustum culling
198+
if !frustum.intersects_sphere(&model_sphere, false) {
199+
return;
200+
}
201+
// If we have an aabb, do aabb-based frustum culling
202+
if !frustum.intersects_obb(model_aabb, &model, false) {
203+
return;
204+
}
205+
}
215206

216-
// TODO: check for big changes in visible entities len() vs capacity() (ex: 2x) and resize
217-
// to prevent holding unneeded memory
207+
computed_visibility.is_visible = true;
208+
visible_entity_sender.send(entity).ok();
209+
},
210+
);
211+
visible_entities.entities = visible_entity_receiver.try_iter().collect();
218212
}
219213
}

0 commit comments

Comments
 (0)