-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Merged by Bors] - Frustum Culling (for Sprites) #1492
Changes from 3 commits
5394f13
876a1a6
42a7022
9e4598f
dcbdccc
dbb8cd1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -66,6 +66,12 @@ impl Default for Visible { | |
} | ||
} | ||
|
||
/// Viewable is used for frustum culling. | ||
/// Any Sprite or AtlasTextureSprite will have this removed if they are outside the camera frustum and thus not be rendered. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This comment needs updating with the recent name change (and inversion of logic). |
||
#[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)] | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,111 @@ | ||
use bevy_asset::{Assets, Handle}; | ||
use bevy_ecs::prelude::{Commands, Entity, Query, Res, With}; | ||
use bevy_math::Vec2; | ||
use bevy_render::{camera::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 sprites( | ||
mut commands: Commands, | ||
windows: Res<Windows>, | ||
cameras: 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 camera_transform in cameras.iter() { | ||
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() { | ||
Byteron marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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 atlases( | ||
mut commands: Commands, | ||
windows: Res<Windows>, | ||
textures: Res<Assets<TextureAtlas>>, | ||
cameras: 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 camera_transform in cameras.iter() { | ||
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() { | ||
Byteron marked this conversation as resolved.
Show resolved
Hide resolved
|
||
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); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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, | ||
|
@@ -50,6 +55,8 @@ impl Plugin for SpritePlugin { | |
.register_type::<Sprite>() | ||
.register_type::<SpriteResizeMode>() | ||
.add_system_to_stage(CoreStage::PostUpdate, sprite_system.system()) | ||
.add_system_to_stage(CoreStage::PostUpdate, frustum_culling::sprites.system()) | ||
.add_system_to_stage(CoreStage::PostUpdate, frustum_culling::atlases.system()) | ||
.add_system_to_stage( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These are probably going to need an ordering dependency on hierarchy propagation systems, but maybe that's better left out of this PR. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think they also need to be ordered with the visible_entities system in camera I think. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Unfortunately ordering won't be enough because the component insert commands won't be applied until after the PostUpdate stage finishes. There are a number of systems in PostUpdate that filter on that component's existence. I think we should add the relevant dependencies to be as correct as possible (and to ensure transform updates are applied), but we'll still have a one frame lag to cull / uncull sprites unless we either:
I'm hesitant to do (1) as it feels short sighted. Given how much cpu work is a bottleneck right now (2) doesn't feel like its "worth it". So imo our only real options are "don't include frustum culling in 0.5 and wait for pipelined rendering" or "accept one frame of cull lag" |
||
CoreStage::PostUpdate, | ||
material_texture_detection_system.system(), | ||
|
@@ -59,16 +66,25 @@ 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 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, | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Double negatives 🤯
(nothing we can do here, but always causes me to pause for a moment when reading logic like this)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I like to avoid double negatives in apis whenever possible. But yeah the tradeoff here for the inverse is too much :)