Frustum Culling (for Sprites) (#1492)

This PR adds two systems to the sprite module that culls Sprites and AtlasSprites that are not within the camera's view.
This is achieved by removing / adding a new  `Viewable` Component dynamically.

Some of the render queries now use a `With<Viewable>` filter to only process the sprites that are actually on screen, which improves performance drastically for scene swith a large amount of sprites off-screen.

https://streamable.com/vvzh2u

This scene shows a map with a 320x320 tiles, with a grid size of 64p.
This is exactly 102400 Sprites in the entire scene.

Without this PR, this scene runs with 1 to 4 FPS.

With this PR..
.. at 720p, there are around 600 visible sprites and runs at ~215 FPS
.. at 1440p there are around 2000 visible sprites and runs at ~135 FPS

The Systems this PR adds take around 1.2ms (with 100K+ sprites in the scene)

Note:
This is only implemented for Sprites and AtlasTextureSprites.
There is no culling for 3D in this PR.

Co-authored-by: Carter Anderson <mcanders1@gmail.com>
This commit is contained in:
Aaron Winter 2021-03-24 21:29:53 +00:00
parent d3e020a1e7
commit b65ec82d46
12 changed files with 231 additions and 28 deletions

View File

@ -9,6 +9,7 @@ use bevy_utils::HashMap;
#[derive(Debug, Default)]
pub struct ActiveCamera {
pub name: String,
pub entity: Option<Entity>,
pub bindings: RenderResourceBindings,
}
@ -20,8 +21,13 @@ pub struct ActiveCameras {
impl ActiveCameras {
pub fn add(&mut self, name: &str) {
self.cameras
.insert(name.to_string(), ActiveCamera::default());
self.cameras.insert(
name.to_string(),
ActiveCamera {
name: name.to_string(),
..Default::default()
},
);
}
pub fn get(&self, name: &str) -> Option<&ActiveCamera> {
@ -31,6 +37,14 @@ impl ActiveCameras {
pub fn get_mut(&mut self, name: &str) -> Option<&mut ActiveCamera> {
self.cameras.get_mut(name)
}
pub fn iter(&self) -> impl Iterator<Item = &ActiveCamera> {
self.cameras.values()
}
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut ActiveCamera> {
self.cameras.values_mut()
}
}
pub fn active_cameras_system(

View File

@ -1,7 +1,7 @@
use super::{Camera, DepthCalculation};
use crate::prelude::Visible;
use crate::{draw::OutsideFrustum, prelude::Visible};
use bevy_core::FloatOrd;
use bevy_ecs::{entity::Entity, query::With, reflect::ReflectComponent, system::Query};
use bevy_ecs::{entity::Entity, query::Without, reflect::ReflectComponent, system::Query};
use bevy_reflect::Reflect;
use bevy_transform::prelude::GlobalTransform;
@ -204,8 +204,8 @@ pub fn visible_entities_system(
&mut VisibleEntities,
Option<&RenderLayers>,
)>,
visible_query: Query<(Entity, &Visible, Option<&RenderLayers>)>,
visible_transform_query: Query<&GlobalTransform, With<Visible>>,
visible_query: Query<(Entity, &Visible, Option<&RenderLayers>), Without<OutsideFrustum>>,
visible_transform_query: Query<&GlobalTransform, Without<OutsideFrustum>>,
) {
for (camera, camera_global_transform, mut visible_entities, maybe_camera_mask) in
camera_query.iter_mut()

View File

@ -66,6 +66,17 @@ impl Default for Visible {
}
}
/// A component that indicates that an entity is outside the view frustum.
/// Any entity with this component will be ignored during rendering.
///
/// # Note
/// This does not handle multiple "views" properly as it is a "global" filter.
/// This will be resolved in the future. For now, disable frustum culling if you
/// need to support multiple views (ex: set the `SpriteSettings::frustum_culling_enabled` resource).
#[derive(Debug, Default, Clone, Reflect)]
#[reflect(Component)]
pub struct OutsideFrustum;
/// A component that indicates how to draw an entity.
#[derive(Debug, Clone, Reflect)]
#[reflect(Component)]

View File

@ -17,7 +17,8 @@ use bevy_ecs::{
system::{IntoExclusiveSystem, IntoSystem},
};
use bevy_transform::TransformSystem;
use draw::Visible;
use draw::{OutsideFrustum, Visible};
pub use once_cell;
pub mod prelude {
@ -137,6 +138,7 @@ impl Plugin for RenderPlugin {
.register_type::<DepthCalculation>()
.register_type::<Draw>()
.register_type::<Visible>()
.register_type::<OutsideFrustum>()
.register_type::<RenderPipelines>()
.register_type::<OrthographicProjection>()
.register_type::<PerspectiveProjection>()

View File

@ -1,12 +1,13 @@
use super::{PipelineDescriptor, PipelineSpecialization};
use crate::{
draw::{Draw, DrawContext},
draw::{Draw, DrawContext, OutsideFrustum},
mesh::{Indices, Mesh},
prelude::{Msaa, Visible},
renderer::RenderResourceBindings,
};
use bevy_asset::{Assets, Handle};
use bevy_ecs::{
query::Without,
reflect::ReflectComponent,
system::{Query, Res, ResMut},
};
@ -86,7 +87,10 @@ pub fn draw_render_pipelines_system(
mut render_resource_bindings: ResMut<RenderResourceBindings>,
msaa: Res<Msaa>,
meshes: Res<Assets<Mesh>>,
mut query: Query<(&mut Draw, &mut RenderPipelines, &Handle<Mesh>, &Visible)>,
mut query: Query<
(&mut Draw, &mut RenderPipelines, &Handle<Mesh>, &Visible),
Without<OutsideFrustum>,
>,
) {
for (mut draw, mut render_pipelines, mesh_handle, visible) in query.iter_mut() {
if !visible.is_visible {

View File

@ -1,8 +1,11 @@
use bevy_asset::{Asset, Assets, Handle};
use crate::{pipeline::RenderPipelines, Texture};
use crate::{draw::OutsideFrustum, pipeline::RenderPipelines, Texture};
pub use bevy_derive::ShaderDefs;
use bevy_ecs::system::{Query, Res};
use bevy_ecs::{
query::Without,
system::{Query, Res},
};
/// Something that can either be "defined" or "not defined". This is used to determine if a "shader
/// def" should be considered "defined"
@ -61,7 +64,7 @@ impl ShaderDef for Option<Handle<Texture>> {
}
/// Updates [RenderPipelines] with the latest [ShaderDefs]
pub fn shader_defs_system<T>(mut query: Query<(&T, &mut RenderPipelines)>)
pub fn shader_defs_system<T>(mut query: Query<(&T, &mut RenderPipelines), Without<OutsideFrustum>>)
where
T: ShaderDefs + Send + Sync + 'static,
{
@ -94,7 +97,7 @@ pub fn clear_shader_defs_system(mut query: Query<&mut RenderPipelines>) {
/// Updates [RenderPipelines] with the latest [ShaderDefs] from a given asset type
pub fn asset_shader_defs_system<T: Asset>(
assets: Res<Assets<T>>,
mut query: Query<(&Handle<T>, &mut RenderPipelines)>,
mut query: Query<(&Handle<T>, &mut RenderPipelines), Without<OutsideFrustum>>,
) where
T: ShaderDefs + Send + Sync + 'static,
{

View File

@ -24,6 +24,7 @@ bevy_reflect = { path = "../bevy_reflect", version = "0.4.0", features = ["bevy"
bevy_render = { path = "../bevy_render", version = "0.4.0" }
bevy_transform = { path = "../bevy_transform", version = "0.4.0" }
bevy_utils = { path = "../bevy_utils", version = "0.4.0" }
bevy_window = { path = "../bevy_window", version = "0.4.0" }
# other
rectangle-pack = "0.3"

View File

@ -0,0 +1,120 @@
use bevy_asset::{Assets, Handle};
use bevy_ecs::prelude::{Commands, Entity, Query, Res, With};
use bevy_math::Vec2;
use bevy_render::{
camera::{ActiveCameras, Camera},
draw::OutsideFrustum,
};
use bevy_transform::components::Transform;
use bevy_window::Windows;
use crate::{Sprite, TextureAtlas, TextureAtlasSprite};
struct Rect {
position: Vec2,
size: Vec2,
}
impl Rect {
#[inline]
pub fn is_intersecting(&self, other: Rect) -> bool {
self.position.distance(other.position) < (self.get_radius() + other.get_radius())
}
#[inline]
pub fn get_radius(&self) -> f32 {
let half_size = self.size / Vec2::splat(2.0);
(half_size.x.powf(2.0) + half_size.y.powf(2.0)).sqrt()
}
}
pub fn sprite_frustum_culling_system(
mut commands: Commands,
windows: Res<Windows>,
active_cameras: Res<ActiveCameras>,
camera_transforms: Query<&Transform, With<Camera>>,
culled_sprites: Query<&OutsideFrustum, With<Sprite>>,
sprites: Query<(Entity, &Transform, &Sprite)>,
) {
let window_size = if let Some(window) = windows.get_primary() {
Vec2::new(window.width(), window.height())
} else {
return;
};
for active_camera_entity in active_cameras.iter().filter_map(|a| a.entity) {
if let Ok(camera_transform) = camera_transforms.get(active_camera_entity) {
let camera_size = window_size * camera_transform.scale.truncate();
let rect = Rect {
position: camera_transform.translation.truncate(),
size: camera_size,
};
for (entity, drawable_transform, sprite) in sprites.iter() {
let sprite_rect = Rect {
position: drawable_transform.translation.truncate(),
size: sprite.size,
};
if rect.is_intersecting(sprite_rect) {
if culled_sprites.get(entity).is_ok() {
commands.entity(entity).remove::<OutsideFrustum>();
}
} else if culled_sprites.get(entity).is_err() {
commands.entity(entity).insert(OutsideFrustum);
}
}
}
}
}
pub fn atlas_frustum_culling_system(
mut commands: Commands,
windows: Res<Windows>,
active_cameras: Res<ActiveCameras>,
textures: Res<Assets<TextureAtlas>>,
camera_transforms: Query<&Transform, With<Camera>>,
culled_sprites: Query<&OutsideFrustum, With<TextureAtlasSprite>>,
sprites: Query<(
Entity,
&Transform,
&TextureAtlasSprite,
&Handle<TextureAtlas>,
)>,
) {
let window = windows.get_primary().unwrap();
let window_size = Vec2::new(window.width(), window.height());
for active_camera_entity in active_cameras.iter().filter_map(|a| a.entity) {
if let Ok(camera_transform) = camera_transforms.get(active_camera_entity) {
let camera_size = window_size * camera_transform.scale.truncate();
let rect = Rect {
position: camera_transform.translation.truncate(),
size: camera_size,
};
for (entity, drawable_transform, sprite, atlas_handle) in sprites.iter() {
if let Some(atlas) = textures.get(atlas_handle) {
if let Some(sprite) = atlas.textures.get(sprite.index as usize) {
let size = Vec2::new(sprite.width(), sprite.height());
let sprite_rect = Rect {
position: drawable_transform.translation.truncate(),
size,
};
if rect.is_intersecting(sprite_rect) {
if culled_sprites.get(entity).is_ok() {
commands.entity(entity).remove::<OutsideFrustum>();
}
} else if culled_sprites.get(entity).is_err() {
commands.entity(entity).insert(OutsideFrustum);
}
}
}
}
}
}
}

View File

@ -3,6 +3,7 @@ pub mod entity;
mod color_material;
mod dynamic_texture_atlas_builder;
mod frustum_culling;
mod rect;
mod render;
mod sprite;
@ -26,10 +27,14 @@ pub use texture_atlas_builder::*;
use bevy_app::prelude::*;
use bevy_asset::{AddAsset, Assets, Handle, HandleUntyped};
use bevy_ecs::system::IntoSystem;
use bevy_ecs::{
component::{ComponentDescriptor, StorageType},
system::IntoSystem,
};
use bevy_math::Vec2;
use bevy_reflect::TypeUuid;
use bevy_render::{
draw::OutsideFrustum,
mesh::{shape, Mesh},
pipeline::PipelineDescriptor,
render_graph::RenderGraph,
@ -37,6 +42,19 @@ use bevy_render::{
};
use sprite::sprite_system;
#[derive(Debug, Clone)]
pub struct SpriteSettings {
pub frustum_culling_enabled: bool,
}
impl Default for SpriteSettings {
fn default() -> Self {
Self {
frustum_culling_enabled: true,
}
}
}
#[derive(Default)]
pub struct SpritePlugin;
@ -59,16 +77,39 @@ impl Plugin for SpritePlugin {
asset_shader_defs_system::<ColorMaterial>.system(),
);
let world = app.world_mut().cell();
let mut render_graph = world.get_resource_mut::<RenderGraph>().unwrap();
let mut pipelines = world
let sprite_settings = app
.world_mut()
.get_resource_or_insert_with(SpriteSettings::default)
.clone();
if sprite_settings.frustum_culling_enabled {
app.add_system_to_stage(
CoreStage::PostUpdate,
frustum_culling::sprite_frustum_culling_system.system(),
)
.add_system_to_stage(
CoreStage::PostUpdate,
frustum_culling::atlas_frustum_culling_system.system(),
);
}
let world = app.world_mut();
world
.register_component(ComponentDescriptor::new::<OutsideFrustum>(
StorageType::SparseSet,
))
.unwrap();
let world_cell = world.cell();
let mut render_graph = world_cell.get_resource_mut::<RenderGraph>().unwrap();
let mut pipelines = world_cell
.get_resource_mut::<Assets<PipelineDescriptor>>()
.unwrap();
let mut shaders = world.get_resource_mut::<Assets<Shader>>().unwrap();
let mut shaders = world_cell.get_resource_mut::<Assets<Shader>>().unwrap();
crate::render::add_sprite_graph(&mut render_graph, &mut pipelines, &mut shaders);
let mut meshes = world.get_resource_mut::<Assets<Mesh>>().unwrap();
let mut color_materials = world.get_resource_mut::<Assets<ColorMaterial>>().unwrap();
let mut meshes = world_cell.get_resource_mut::<Assets<Mesh>>().unwrap();
let mut color_materials = world_cell
.get_resource_mut::<Assets<ColorMaterial>>()
.unwrap();
color_materials.set_untracked(Handle::<ColorMaterial>::default(), ColorMaterial::default());
meshes.set_untracked(
QUAD_HANDLE,

View File

@ -1,10 +1,14 @@
use crate::ColorMaterial;
use bevy_asset::{Assets, Handle};
use bevy_core::Bytes;
use bevy_ecs::system::{Query, Res};
use bevy_ecs::{
query::Without,
system::{Query, Res},
};
use bevy_math::Vec2;
use bevy_reflect::{Reflect, ReflectDeserialize, TypeUuid};
use bevy_render::{
draw::OutsideFrustum,
renderer::{RenderResource, RenderResourceType, RenderResources},
texture::Texture,
};
@ -76,7 +80,7 @@ impl Sprite {
pub fn sprite_system(
materials: Res<Assets<ColorMaterial>>,
textures: Res<Assets<Texture>>,
mut query: Query<(&mut Sprite, &Handle<ColorMaterial>)>,
mut query: Query<(&mut Sprite, &Handle<ColorMaterial>), Without<OutsideFrustum>>,
) {
for (mut sprite, handle) in query.iter_mut() {
match sprite.resize_mode {

View File

@ -2,12 +2,12 @@ use bevy_asset::Assets;
use bevy_ecs::{
bundle::Bundle,
entity::Entity,
query::{Changed, With},
query::{Changed, With, Without},
system::{Local, Query, QuerySet, Res, ResMut},
};
use bevy_math::{Size, Vec3};
use bevy_render::{
draw::{DrawContext, Drawable},
draw::{DrawContext, Drawable, OutsideFrustum},
mesh::Mesh,
prelude::{Draw, Msaa, Texture, Visible},
render_graph::base::MainPass,
@ -72,7 +72,7 @@ pub fn draw_text2d_system(
&GlobalTransform,
&Text2dSize,
),
With<MainPass>,
(With<MainPass>, Without<OutsideFrustum>),
>,
) {
let font_quad = meshes.get(&QUAD_HANDLE).unwrap();

View File

@ -2,12 +2,12 @@ use crate::{CalculatedSize, Node, Style, Val};
use bevy_asset::Assets;
use bevy_ecs::{
entity::Entity,
query::{Changed, Or},
query::{Changed, Or, Without},
system::{Local, Query, QuerySet, Res, ResMut},
};
use bevy_math::Size;
use bevy_render::{
draw::{Draw, DrawContext, Drawable},
draw::{Draw, DrawContext, Drawable, OutsideFrustum},
mesh::Mesh,
prelude::{Msaa, Visible},
renderer::RenderResourceBindings,
@ -136,7 +136,10 @@ pub fn draw_text_system(
meshes: Res<Assets<Mesh>>,
mut render_resource_bindings: ResMut<RenderResourceBindings>,
text_pipeline: Res<DefaultTextPipeline>,
mut query: Query<(Entity, &mut Draw, &Visible, &Text, &Node, &GlobalTransform)>,
mut query: Query<
(Entity, &mut Draw, &Visible, &Text, &Node, &GlobalTransform),
Without<OutsideFrustum>,
>,
) {
let scale_factor = if let Some(window) = windows.get_primary() {
window.scale_factor()