Skip to content
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

Closed
wants to merge 6 commits into from
Closed
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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>>,
Copy link
Contributor

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)

Copy link
Member

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 :)

) {
for (camera, camera_global_transform, mut visible_entities, maybe_camera_mask) in
camera_query.iter_mut()
Expand Down
6 changes: 6 additions & 0 deletions crates/bevy_render/src/draw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Contributor

Choose a reason for hiding this comment

The 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)]
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.2"
Expand Down
111 changes: 111 additions & 0 deletions crates/bevy_sprite/src/frustum_culling.rs
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);
}
}
}
}
}
}
30 changes: 23 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,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,
Expand All @@ -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(
Copy link
Member

Choose a reason for hiding this comment

The 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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The 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.
IIRC they are both called on Update and that might lead to sprites popping up on the camera's borders a frame later. It's barely noticable but it is noticable.

Copy link
Member

Choose a reason for hiding this comment

The 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:

  1. Ensure all systems that rely on the frustum component live in a stage after PostUpdate. This isn't ideal.
  2. Do frustum culling later, remove With<WithinFrustum> and accept that we'll do unnecessary work for those systems (ex: we only filter out unnecessary draw calls, not the cpu work like shader management). This also isn't ideal.
  3. Move all of the steps out of the ECS (something like the "pipelined rendering" we've been discussing lately). This is a lot of work.

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(),
Expand All @@ -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,
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
6 changes: 3 additions & 3 deletions crates/bevy_text/src/text2d.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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();
Expand Down
9 changes: 6 additions & 3 deletions crates/bevy_ui/src/widget/text.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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()
Expand Down