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  --- ## 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
					
				| @ -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…
	
		Reference in New Issue
	
	Block a user
	 Aevyrie
						Aevyrie