bevy/crates/bevy_sprite/src/frustum_culling.rs
Aaron Winter b65ec82d46 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>
2021-03-24 21:29:53 +00:00

121 lines
4.1 KiB
Rust

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);
}
}
}
}
}
}
}