Skip to content

Commit b65ec82

Browse files
Byteroncart
andcommitted
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 <[email protected]>
1 parent d3e020a commit b65ec82

File tree

12 files changed

+231
-28
lines changed

12 files changed

+231
-28
lines changed

crates/bevy_render/src/camera/active_cameras.rs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ use bevy_utils::HashMap;
99

1010
#[derive(Debug, Default)]
1111
pub struct ActiveCamera {
12+
pub name: String,
1213
pub entity: Option<Entity>,
1314
pub bindings: RenderResourceBindings,
1415
}
@@ -20,8 +21,13 @@ pub struct ActiveCameras {
2021

2122
impl ActiveCameras {
2223
pub fn add(&mut self, name: &str) {
23-
self.cameras
24-
.insert(name.to_string(), ActiveCamera::default());
24+
self.cameras.insert(
25+
name.to_string(),
26+
ActiveCamera {
27+
name: name.to_string(),
28+
..Default::default()
29+
},
30+
);
2531
}
2632

2733
pub fn get(&self, name: &str) -> Option<&ActiveCamera> {
@@ -31,6 +37,14 @@ impl ActiveCameras {
3137
pub fn get_mut(&mut self, name: &str) -> Option<&mut ActiveCamera> {
3238
self.cameras.get_mut(name)
3339
}
40+
41+
pub fn iter(&self) -> impl Iterator<Item = &ActiveCamera> {
42+
self.cameras.values()
43+
}
44+
45+
pub fn iter_mut(&mut self) -> impl Iterator<Item = &mut ActiveCamera> {
46+
self.cameras.values_mut()
47+
}
3448
}
3549

3650
pub fn active_cameras_system(

crates/bevy_render/src/camera/visible_entities.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use super::{Camera, DepthCalculation};
2-
use crate::prelude::Visible;
2+
use crate::{draw::OutsideFrustum, prelude::Visible};
33
use bevy_core::FloatOrd;
4-
use bevy_ecs::{entity::Entity, query::With, reflect::ReflectComponent, system::Query};
4+
use bevy_ecs::{entity::Entity, query::Without, reflect::ReflectComponent, system::Query};
55
use bevy_reflect::Reflect;
66
use bevy_transform::prelude::GlobalTransform;
77

@@ -204,8 +204,8 @@ pub fn visible_entities_system(
204204
&mut VisibleEntities,
205205
Option<&RenderLayers>,
206206
)>,
207-
visible_query: Query<(Entity, &Visible, Option<&RenderLayers>)>,
208-
visible_transform_query: Query<&GlobalTransform, With<Visible>>,
207+
visible_query: Query<(Entity, &Visible, Option<&RenderLayers>), Without<OutsideFrustum>>,
208+
visible_transform_query: Query<&GlobalTransform, Without<OutsideFrustum>>,
209209
) {
210210
for (camera, camera_global_transform, mut visible_entities, maybe_camera_mask) in
211211
camera_query.iter_mut()

crates/bevy_render/src/draw.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,17 @@ impl Default for Visible {
6666
}
6767
}
6868

69+
/// A component that indicates that an entity is outside the view frustum.
70+
/// Any entity with this component will be ignored during rendering.
71+
///
72+
/// # Note
73+
/// This does not handle multiple "views" properly as it is a "global" filter.
74+
/// This will be resolved in the future. For now, disable frustum culling if you
75+
/// need to support multiple views (ex: set the `SpriteSettings::frustum_culling_enabled` resource).
76+
#[derive(Debug, Default, Clone, Reflect)]
77+
#[reflect(Component)]
78+
pub struct OutsideFrustum;
79+
6980
/// A component that indicates how to draw an entity.
7081
#[derive(Debug, Clone, Reflect)]
7182
#[reflect(Component)]

crates/bevy_render/src/lib.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ use bevy_ecs::{
1717
system::{IntoExclusiveSystem, IntoSystem},
1818
};
1919
use bevy_transform::TransformSystem;
20-
use draw::Visible;
20+
use draw::{OutsideFrustum, Visible};
21+
2122
pub use once_cell;
2223

2324
pub mod prelude {
@@ -137,6 +138,7 @@ impl Plugin for RenderPlugin {
137138
.register_type::<DepthCalculation>()
138139
.register_type::<Draw>()
139140
.register_type::<Visible>()
141+
.register_type::<OutsideFrustum>()
140142
.register_type::<RenderPipelines>()
141143
.register_type::<OrthographicProjection>()
142144
.register_type::<PerspectiveProjection>()

crates/bevy_render/src/pipeline/render_pipelines.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
use super::{PipelineDescriptor, PipelineSpecialization};
22
use crate::{
3-
draw::{Draw, DrawContext},
3+
draw::{Draw, DrawContext, OutsideFrustum},
44
mesh::{Indices, Mesh},
55
prelude::{Msaa, Visible},
66
renderer::RenderResourceBindings,
77
};
88
use bevy_asset::{Assets, Handle};
99
use bevy_ecs::{
10+
query::Without,
1011
reflect::ReflectComponent,
1112
system::{Query, Res, ResMut},
1213
};
@@ -86,7 +87,10 @@ pub fn draw_render_pipelines_system(
8687
mut render_resource_bindings: ResMut<RenderResourceBindings>,
8788
msaa: Res<Msaa>,
8889
meshes: Res<Assets<Mesh>>,
89-
mut query: Query<(&mut Draw, &mut RenderPipelines, &Handle<Mesh>, &Visible)>,
90+
mut query: Query<
91+
(&mut Draw, &mut RenderPipelines, &Handle<Mesh>, &Visible),
92+
Without<OutsideFrustum>,
93+
>,
9094
) {
9195
for (mut draw, mut render_pipelines, mesh_handle, visible) in query.iter_mut() {
9296
if !visible.is_visible {

crates/bevy_render/src/shader/shader_defs.rs

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,11 @@
11
use bevy_asset::{Asset, Assets, Handle};
22

3-
use crate::{pipeline::RenderPipelines, Texture};
3+
use crate::{draw::OutsideFrustum, pipeline::RenderPipelines, Texture};
44
pub use bevy_derive::ShaderDefs;
5-
use bevy_ecs::system::{Query, Res};
5+
use bevy_ecs::{
6+
query::Without,
7+
system::{Query, Res},
8+
};
69

710
/// Something that can either be "defined" or "not defined". This is used to determine if a "shader
811
/// def" should be considered "defined"
@@ -61,7 +64,7 @@ impl ShaderDef for Option<Handle<Texture>> {
6164
}
6265

6366
/// Updates [RenderPipelines] with the latest [ShaderDefs]
64-
pub fn shader_defs_system<T>(mut query: Query<(&T, &mut RenderPipelines)>)
67+
pub fn shader_defs_system<T>(mut query: Query<(&T, &mut RenderPipelines), Without<OutsideFrustum>>)
6568
where
6669
T: ShaderDefs + Send + Sync + 'static,
6770
{
@@ -94,7 +97,7 @@ pub fn clear_shader_defs_system(mut query: Query<&mut RenderPipelines>) {
9497
/// Updates [RenderPipelines] with the latest [ShaderDefs] from a given asset type
9598
pub fn asset_shader_defs_system<T: Asset>(
9699
assets: Res<Assets<T>>,
97-
mut query: Query<(&Handle<T>, &mut RenderPipelines)>,
100+
mut query: Query<(&Handle<T>, &mut RenderPipelines), Without<OutsideFrustum>>,
98101
) where
99102
T: ShaderDefs + Send + Sync + 'static,
100103
{

crates/bevy_sprite/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ bevy_reflect = { path = "../bevy_reflect", version = "0.4.0", features = ["bevy"
2424
bevy_render = { path = "../bevy_render", version = "0.4.0" }
2525
bevy_transform = { path = "../bevy_transform", version = "0.4.0" }
2626
bevy_utils = { path = "../bevy_utils", version = "0.4.0" }
27+
bevy_window = { path = "../bevy_window", version = "0.4.0" }
2728

2829
# other
2930
rectangle-pack = "0.3"
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
use bevy_asset::{Assets, Handle};
2+
use bevy_ecs::prelude::{Commands, Entity, Query, Res, With};
3+
use bevy_math::Vec2;
4+
use bevy_render::{
5+
camera::{ActiveCameras, Camera},
6+
draw::OutsideFrustum,
7+
};
8+
use bevy_transform::components::Transform;
9+
use bevy_window::Windows;
10+
11+
use crate::{Sprite, TextureAtlas, TextureAtlasSprite};
12+
13+
struct Rect {
14+
position: Vec2,
15+
size: Vec2,
16+
}
17+
18+
impl Rect {
19+
#[inline]
20+
pub fn is_intersecting(&self, other: Rect) -> bool {
21+
self.position.distance(other.position) < (self.get_radius() + other.get_radius())
22+
}
23+
24+
#[inline]
25+
pub fn get_radius(&self) -> f32 {
26+
let half_size = self.size / Vec2::splat(2.0);
27+
(half_size.x.powf(2.0) + half_size.y.powf(2.0)).sqrt()
28+
}
29+
}
30+
31+
pub fn sprite_frustum_culling_system(
32+
mut commands: Commands,
33+
windows: Res<Windows>,
34+
active_cameras: Res<ActiveCameras>,
35+
camera_transforms: Query<&Transform, With<Camera>>,
36+
culled_sprites: Query<&OutsideFrustum, With<Sprite>>,
37+
sprites: Query<(Entity, &Transform, &Sprite)>,
38+
) {
39+
let window_size = if let Some(window) = windows.get_primary() {
40+
Vec2::new(window.width(), window.height())
41+
} else {
42+
return;
43+
};
44+
45+
for active_camera_entity in active_cameras.iter().filter_map(|a| a.entity) {
46+
if let Ok(camera_transform) = camera_transforms.get(active_camera_entity) {
47+
let camera_size = window_size * camera_transform.scale.truncate();
48+
49+
let rect = Rect {
50+
position: camera_transform.translation.truncate(),
51+
size: camera_size,
52+
};
53+
54+
for (entity, drawable_transform, sprite) in sprites.iter() {
55+
let sprite_rect = Rect {
56+
position: drawable_transform.translation.truncate(),
57+
size: sprite.size,
58+
};
59+
60+
if rect.is_intersecting(sprite_rect) {
61+
if culled_sprites.get(entity).is_ok() {
62+
commands.entity(entity).remove::<OutsideFrustum>();
63+
}
64+
} else if culled_sprites.get(entity).is_err() {
65+
commands.entity(entity).insert(OutsideFrustum);
66+
}
67+
}
68+
}
69+
}
70+
}
71+
72+
pub fn atlas_frustum_culling_system(
73+
mut commands: Commands,
74+
windows: Res<Windows>,
75+
active_cameras: Res<ActiveCameras>,
76+
textures: Res<Assets<TextureAtlas>>,
77+
camera_transforms: Query<&Transform, With<Camera>>,
78+
culled_sprites: Query<&OutsideFrustum, With<TextureAtlasSprite>>,
79+
sprites: Query<(
80+
Entity,
81+
&Transform,
82+
&TextureAtlasSprite,
83+
&Handle<TextureAtlas>,
84+
)>,
85+
) {
86+
let window = windows.get_primary().unwrap();
87+
let window_size = Vec2::new(window.width(), window.height());
88+
89+
for active_camera_entity in active_cameras.iter().filter_map(|a| a.entity) {
90+
if let Ok(camera_transform) = camera_transforms.get(active_camera_entity) {
91+
let camera_size = window_size * camera_transform.scale.truncate();
92+
93+
let rect = Rect {
94+
position: camera_transform.translation.truncate(),
95+
size: camera_size,
96+
};
97+
98+
for (entity, drawable_transform, sprite, atlas_handle) in sprites.iter() {
99+
if let Some(atlas) = textures.get(atlas_handle) {
100+
if let Some(sprite) = atlas.textures.get(sprite.index as usize) {
101+
let size = Vec2::new(sprite.width(), sprite.height());
102+
103+
let sprite_rect = Rect {
104+
position: drawable_transform.translation.truncate(),
105+
size,
106+
};
107+
108+
if rect.is_intersecting(sprite_rect) {
109+
if culled_sprites.get(entity).is_ok() {
110+
commands.entity(entity).remove::<OutsideFrustum>();
111+
}
112+
} else if culled_sprites.get(entity).is_err() {
113+
commands.entity(entity).insert(OutsideFrustum);
114+
}
115+
}
116+
}
117+
}
118+
}
119+
}
120+
}

crates/bevy_sprite/src/lib.rs

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ pub mod entity;
33

44
mod color_material;
55
mod dynamic_texture_atlas_builder;
6+
mod frustum_culling;
67
mod rect;
78
mod render;
89
mod sprite;
@@ -26,17 +27,34 @@ pub use texture_atlas_builder::*;
2627

2728
use bevy_app::prelude::*;
2829
use bevy_asset::{AddAsset, Assets, Handle, HandleUntyped};
29-
use bevy_ecs::system::IntoSystem;
30+
use bevy_ecs::{
31+
component::{ComponentDescriptor, StorageType},
32+
system::IntoSystem,
33+
};
3034
use bevy_math::Vec2;
3135
use bevy_reflect::TypeUuid;
3236
use bevy_render::{
37+
draw::OutsideFrustum,
3338
mesh::{shape, Mesh},
3439
pipeline::PipelineDescriptor,
3540
render_graph::RenderGraph,
3641
shader::{asset_shader_defs_system, Shader},
3742
};
3843
use sprite::sprite_system;
3944

45+
#[derive(Debug, Clone)]
46+
pub struct SpriteSettings {
47+
pub frustum_culling_enabled: bool,
48+
}
49+
50+
impl Default for SpriteSettings {
51+
fn default() -> Self {
52+
Self {
53+
frustum_culling_enabled: true,
54+
}
55+
}
56+
}
57+
4058
#[derive(Default)]
4159
pub struct SpritePlugin;
4260

@@ -59,16 +77,39 @@ impl Plugin for SpritePlugin {
5977
asset_shader_defs_system::<ColorMaterial>.system(),
6078
);
6179

62-
let world = app.world_mut().cell();
63-
let mut render_graph = world.get_resource_mut::<RenderGraph>().unwrap();
64-
let mut pipelines = world
80+
let sprite_settings = app
81+
.world_mut()
82+
.get_resource_or_insert_with(SpriteSettings::default)
83+
.clone();
84+
if sprite_settings.frustum_culling_enabled {
85+
app.add_system_to_stage(
86+
CoreStage::PostUpdate,
87+
frustum_culling::sprite_frustum_culling_system.system(),
88+
)
89+
.add_system_to_stage(
90+
CoreStage::PostUpdate,
91+
frustum_culling::atlas_frustum_culling_system.system(),
92+
);
93+
}
94+
let world = app.world_mut();
95+
world
96+
.register_component(ComponentDescriptor::new::<OutsideFrustum>(
97+
StorageType::SparseSet,
98+
))
99+
.unwrap();
100+
101+
let world_cell = world.cell();
102+
let mut render_graph = world_cell.get_resource_mut::<RenderGraph>().unwrap();
103+
let mut pipelines = world_cell
65104
.get_resource_mut::<Assets<PipelineDescriptor>>()
66105
.unwrap();
67-
let mut shaders = world.get_resource_mut::<Assets<Shader>>().unwrap();
106+
let mut shaders = world_cell.get_resource_mut::<Assets<Shader>>().unwrap();
68107
crate::render::add_sprite_graph(&mut render_graph, &mut pipelines, &mut shaders);
69108

70-
let mut meshes = world.get_resource_mut::<Assets<Mesh>>().unwrap();
71-
let mut color_materials = world.get_resource_mut::<Assets<ColorMaterial>>().unwrap();
109+
let mut meshes = world_cell.get_resource_mut::<Assets<Mesh>>().unwrap();
110+
let mut color_materials = world_cell
111+
.get_resource_mut::<Assets<ColorMaterial>>()
112+
.unwrap();
72113
color_materials.set_untracked(Handle::<ColorMaterial>::default(), ColorMaterial::default());
73114
meshes.set_untracked(
74115
QUAD_HANDLE,

crates/bevy_sprite/src/sprite.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
11
use crate::ColorMaterial;
22
use bevy_asset::{Assets, Handle};
33
use bevy_core::Bytes;
4-
use bevy_ecs::system::{Query, Res};
4+
use bevy_ecs::{
5+
query::Without,
6+
system::{Query, Res},
7+
};
58
use bevy_math::Vec2;
69
use bevy_reflect::{Reflect, ReflectDeserialize, TypeUuid};
710
use bevy_render::{
11+
draw::OutsideFrustum,
812
renderer::{RenderResource, RenderResourceType, RenderResources},
913
texture::Texture,
1014
};
@@ -76,7 +80,7 @@ impl Sprite {
7680
pub fn sprite_system(
7781
materials: Res<Assets<ColorMaterial>>,
7882
textures: Res<Assets<Texture>>,
79-
mut query: Query<(&mut Sprite, &Handle<ColorMaterial>)>,
83+
mut query: Query<(&mut Sprite, &Handle<ColorMaterial>), Without<OutsideFrustum>>,
8084
) {
8185
for (mut sprite, handle) in query.iter_mut() {
8286
match sprite.resize_mode {

0 commit comments

Comments
 (0)