Skip to content

Commit

Permalink
Frustum Culling (for Sprites) (#1492)
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
Byteron and cart committed Mar 24, 2021
1 parent d3e020a commit b65ec82
Show file tree
Hide file tree
Showing 12 changed files with 231 additions and 28 deletions.
18 changes: 16 additions & 2 deletions crates/bevy_render/src/camera/active_cameras.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use bevy_utils::HashMap;

#[derive(Debug, Default)]
pub struct ActiveCamera {
pub name: String,
pub entity: Option<Entity>,
pub bindings: RenderResourceBindings,
}
Expand All @@ -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> {
Expand All @@ -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(
Expand Down
8 changes: 4 additions & 4 deletions crates/bevy_render/src/camera/visible_entities.rs
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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()
Expand Down
11 changes: 11 additions & 0 deletions crates/bevy_render/src/draw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
4 changes: 3 additions & 1 deletion crates/bevy_render/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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>()
Expand Down
8 changes: 6 additions & 2 deletions crates/bevy_render/src/pipeline/render_pipelines.rs
Original file line number Diff line number Diff line change
@@ -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},
};
Expand Down Expand Up @@ -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 {
Expand Down
11 changes: 7 additions & 4 deletions crates/bevy_render/src/shader/shader_defs.rs
Original file line number Diff line number Diff line change
@@ -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"
Expand Down Expand Up @@ -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,
{
Expand Down Expand Up @@ -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,
{
Expand Down
1 change: 1 addition & 0 deletions crates/bevy_sprite/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
120 changes: 120 additions & 0 deletions crates/bevy_sprite/src/frustum_culling.rs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
}
}
}
}
55 changes: 48 additions & 7 deletions crates/bevy_sprite/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ pub mod entity;

mod color_material;
mod dynamic_texture_atlas_builder;
mod frustum_culling;
mod rect;
mod render;
mod sprite;
Expand All @@ -26,17 +27,34 @@ 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,
shader::{asset_shader_defs_system, Shader},
};
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;

Expand All @@ -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,
Expand Down
8 changes: 6 additions & 2 deletions crates/bevy_sprite/src/sprite.rs
Original file line number Diff line number Diff line change
@@ -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,
};
Expand Down Expand Up @@ -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 {
Expand Down
Loading

0 comments on commit b65ec82

Please sign in to comment.