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:
parent
d3e020a1e7
commit
b65ec82d46
@ -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(
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)]
|
||||
|
||||
@ -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>()
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
{
|
||||
|
||||
@ -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"
|
||||
|
||||
120
crates/bevy_sprite/src/frustum_culling.rs
Normal file
120
crates/bevy_sprite/src/frustum_culling.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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()
|
||||
|
||||
Loading…
Reference in New Issue
Block a user