bevy/crates/bevy_sprite/src/lib.rs
Patrick Walton 40df1ea4b6
Remove the type parameter from check_visibility, and only invoke it once. (#16812)
Currently, `check_visibility` is parameterized over a query filter that
specifies the type of potentially-visible object. This has the
unfortunate side effect that we need a separate system,
`mark_view_visibility_as_changed_if_necessary`, to trigger view
visibility change detection. That system is quite slow because it must
iterate sequentially over all entities in the scene.

This PR moves the query filter from `check_visibility` to a new
component, `VisibilityClass`. `VisibilityClass` stores a list of type
IDs, each corresponding to one of the query filters we used to use.
Because `check_visibility` is no longer specialized to the query filter
at the type level, Bevy now only needs to invoke it once, leading to
better performance as `check_visibility` can do change detection on the
fly rather than delegating it to a separate system.

This commit also has ergonomic improvements, as there's no need for
applications that want to add their own custom renderable components to
add specializations of the `check_visibility` system to the schedule.
Instead, they only need to ensure that the `ViewVisibility` component is
properly kept up to date. The recommended way to do this, and the way
that's demonstrated in the `custom_phase_item` and
`specialized_mesh_pipeline` examples, is to make `ViewVisibility` a
required component and to add the type ID to it in a component add hook.
This patch does this for `Mesh3d`, `Mesh2d`, `Sprite`, `Light`, and
`Node`, which means that most app code doesn't need to change at all.

Note that, although this patch has a large impact on the performance of
visibility determination, it doesn't actually improve the end-to-end
frame time of `many_cubes`. That's because the render world was already
effectively hiding the latency from
`mark_view_visibility_as_changed_if_necessary`. This patch is, however,
necessary for *further* improvements to `many_cubes` performance.

`many_cubes` trace before:
![Screenshot 2024-12-13
015318](https://github.com/user-attachments/assets/d0b1881b-fb75-4a39-b05d-1a16eabfa2c5)

`many_cubes` trace after:
![Screenshot 2024-12-13
145735](https://github.com/user-attachments/assets/0a364289-e942-41bb-9cc2-b05d07e3722d)

## Migration Guide

* `check_visibility` no longer takes a `QueryFilter`, and there's no
need to add it manually to your app schedule anymore for custom
rendering items. Instead, entities with custom renderable components
should add the appropriate type IDs to `VisibilityClass`. See
`custom_phase_item` for an example.
2024-12-17 04:43:45 +00:00

399 lines
13 KiB
Rust

#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![forbid(unsafe_code)]
#![doc(
html_logo_url = "https://bevyengine.org/assets/icon.png",
html_favicon_url = "https://bevyengine.org/assets/icon.png"
)]
//! Provides 2D sprite rendering functionality.
extern crate alloc;
mod bundle;
mod dynamic_texture_atlas_builder;
mod mesh2d;
#[cfg(feature = "bevy_sprite_picking_backend")]
mod picking_backend;
mod render;
mod sprite;
mod texture_atlas;
mod texture_atlas_builder;
mod texture_slice;
/// The sprite prelude.
///
/// This includes the most common types in this crate, re-exported for your convenience.
#[expect(deprecated)]
pub mod prelude {
#[doc(hidden)]
pub use crate::{
bundle::SpriteBundle,
sprite::{Sprite, SpriteImageMode},
texture_atlas::{TextureAtlas, TextureAtlasLayout, TextureAtlasSources},
texture_slice::{BorderRect, SliceScaleMode, TextureSlice, TextureSlicer},
ColorMaterial, ColorMesh2dBundle, MeshMaterial2d, TextureAtlasBuilder,
};
}
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
pub use bundle::*;
pub use dynamic_texture_atlas_builder::*;
pub use mesh2d::*;
#[cfg(feature = "bevy_sprite_picking_backend")]
pub use picking_backend::*;
pub use render::*;
pub use sprite::*;
pub use texture_atlas::*;
pub use texture_atlas_builder::*;
pub use texture_slice::*;
use bevy_app::prelude::*;
use bevy_asset::{load_internal_asset, AssetApp, Assets, Handle};
use bevy_core_pipeline::core_2d::Transparent2d;
use bevy_ecs::{prelude::*, query::QueryItem};
use bevy_image::Image;
use bevy_render::{
extract_component::{ExtractComponent, ExtractComponentPlugin},
mesh::{Mesh, Mesh2d, MeshAabb},
primitives::Aabb,
render_phase::AddRenderCommand,
render_resource::{Shader, SpecializedRenderPipelines},
view::{self, NoFrustumCulling, VisibilityClass, VisibilitySystems},
ExtractSchedule, Render, RenderApp, RenderSet,
};
/// Adds support for 2D sprite rendering.
pub struct SpritePlugin {
/// Whether to add the sprite picking backend to the app.
#[cfg(feature = "bevy_sprite_picking_backend")]
pub add_picking: bool,
}
impl Default for SpritePlugin {
fn default() -> Self {
Self {
#[cfg(feature = "bevy_sprite_picking_backend")]
add_picking: true,
}
}
}
pub const SPRITE_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(2763343953151597127);
pub const SPRITE_VIEW_BINDINGS_SHADER_HANDLE: Handle<Shader> =
Handle::weak_from_u128(8846920112458963210);
/// System set for sprite rendering.
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
pub enum SpriteSystem {
ExtractSprites,
ComputeSlices,
}
/// A component that marks entities that aren't themselves sprites but become
/// sprites during rendering.
///
/// Right now, this is used for `Text`.
#[derive(Component, Reflect, Clone, Copy, Debug, Default)]
#[reflect(Component, Default, Debug)]
#[require(VisibilityClass)]
#[component(on_add = view::add_visibility_class::<Sprite>)]
pub struct SpriteSource;
impl Plugin for SpritePlugin {
fn build(&self, app: &mut App) {
load_internal_asset!(
app,
SPRITE_SHADER_HANDLE,
"render/sprite.wgsl",
Shader::from_wgsl
);
load_internal_asset!(
app,
SPRITE_VIEW_BINDINGS_SHADER_HANDLE,
"render/sprite_view_bindings.wgsl",
Shader::from_wgsl
);
app.init_asset::<TextureAtlasLayout>()
.register_asset_reflect::<TextureAtlasLayout>()
.register_type::<Sprite>()
.register_type::<SpriteImageMode>()
.register_type::<TextureSlicer>()
.register_type::<Anchor>()
.register_type::<TextureAtlas>()
.register_type::<Mesh2d>()
.register_type::<SpriteSource>()
.add_plugins((
Mesh2dRenderPlugin,
ColorMaterialPlugin,
ExtractComponentPlugin::<SpriteSource>::default(),
))
.add_systems(
PostUpdate,
(
calculate_bounds_2d.in_set(VisibilitySystems::CalculateBounds),
(
compute_slices_on_asset_event,
compute_slices_on_sprite_change,
)
.in_set(SpriteSystem::ComputeSlices),
),
);
#[cfg(feature = "bevy_sprite_picking_backend")]
if self.add_picking {
app.add_plugins(SpritePickingPlugin);
}
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
render_app
.init_resource::<ImageBindGroups>()
.init_resource::<SpecializedRenderPipelines<SpritePipeline>>()
.init_resource::<SpriteMeta>()
.init_resource::<ExtractedSprites>()
.init_resource::<SpriteAssetEvents>()
.add_render_command::<Transparent2d, DrawSprite>()
.add_systems(
ExtractSchedule,
(
extract_sprites.in_set(SpriteSystem::ExtractSprites),
extract_sprite_events,
),
)
.add_systems(
Render,
(
queue_sprites
.in_set(RenderSet::Queue)
.ambiguous_with(queue_material2d_meshes::<ColorMaterial>),
prepare_sprite_image_bind_groups.in_set(RenderSet::PrepareBindGroups),
prepare_sprite_view_bind_groups.in_set(RenderSet::PrepareBindGroups),
),
);
};
}
fn finish(&self, app: &mut App) {
if let Some(render_app) = app.get_sub_app_mut(RenderApp) {
render_app.init_resource::<SpritePipeline>();
}
}
}
/// System calculating and inserting an [`Aabb`] component to entities with either:
/// - a `Mesh2d` component,
/// - a `Sprite` and `Handle<Image>` components,
/// and without a [`NoFrustumCulling`] component.
///
/// Used in system set [`VisibilitySystems::CalculateBounds`].
pub fn calculate_bounds_2d(
mut commands: Commands,
meshes: Res<Assets<Mesh>>,
images: Res<Assets<Image>>,
atlases: Res<Assets<TextureAtlasLayout>>,
meshes_without_aabb: Query<(Entity, &Mesh2d), (Without<Aabb>, Without<NoFrustumCulling>)>,
sprites_to_recalculate_aabb: Query<
(Entity, &Sprite),
(
Or<(Without<Aabb>, Changed<Sprite>)>,
Without<NoFrustumCulling>,
),
>,
) {
for (entity, mesh_handle) in &meshes_without_aabb {
if let Some(mesh) = meshes.get(&mesh_handle.0) {
if let Some(aabb) = mesh.compute_aabb() {
commands.entity(entity).try_insert(aabb);
}
}
}
for (entity, sprite) in &sprites_to_recalculate_aabb {
if let Some(size) = sprite
.custom_size
.or_else(|| sprite.rect.map(|rect| rect.size()))
.or_else(|| match &sprite.texture_atlas {
// We default to the texture size for regular sprites
None => images.get(&sprite.image).map(Image::size_f32),
// We default to the drawn rect for atlas sprites
Some(atlas) => atlas
.texture_rect(&atlases)
.map(|rect| rect.size().as_vec2()),
})
{
let aabb = Aabb {
center: (-sprite.anchor.as_vec() * size).extend(0.0).into(),
half_extents: (0.5 * size).extend(0.0).into(),
};
commands.entity(entity).try_insert(aabb);
}
}
}
impl ExtractComponent for SpriteSource {
type QueryData = ();
type QueryFilter = With<SpriteSource>;
type Out = SpriteSource;
fn extract_component(_: QueryItem<'_, Self::QueryData>) -> Option<Self::Out> {
Some(SpriteSource)
}
}
#[cfg(test)]
mod test {
use bevy_math::{Rect, Vec2, Vec3A};
use bevy_utils::default;
use super::*;
#[test]
fn calculate_bounds_2d_create_aabb_for_image_sprite_entity() {
// Setup app
let mut app = App::new();
// Add resources and get handle to image
let mut image_assets = Assets::<Image>::default();
let image_handle = image_assets.add(Image::default());
app.insert_resource(image_assets);
let mesh_assets = Assets::<Mesh>::default();
app.insert_resource(mesh_assets);
let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
app.insert_resource(texture_atlas_assets);
// Add system
app.add_systems(Update, calculate_bounds_2d);
// Add entities
let entity = app.world_mut().spawn(Sprite::from_image(image_handle)).id();
// Verify that the entity does not have an AABB
assert!(!app
.world()
.get_entity(entity)
.expect("Could not find entity")
.contains::<Aabb>());
// Run system
app.update();
// Verify the AABB exists
assert!(app
.world()
.get_entity(entity)
.expect("Could not find entity")
.contains::<Aabb>());
}
#[test]
fn calculate_bounds_2d_update_aabb_when_sprite_custom_size_changes_to_some() {
// Setup app
let mut app = App::new();
// Add resources and get handle to image
let mut image_assets = Assets::<Image>::default();
let image_handle = image_assets.add(Image::default());
app.insert_resource(image_assets);
let mesh_assets = Assets::<Mesh>::default();
app.insert_resource(mesh_assets);
let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
app.insert_resource(texture_atlas_assets);
// Add system
app.add_systems(Update, calculate_bounds_2d);
// Add entities
let entity = app
.world_mut()
.spawn(Sprite {
custom_size: Some(Vec2::ZERO),
image: image_handle,
..default()
})
.id();
// Create initial AABB
app.update();
// Get the initial AABB
let first_aabb = *app
.world()
.get_entity(entity)
.expect("Could not find entity")
.get::<Aabb>()
.expect("Could not find initial AABB");
// Change `custom_size` of sprite
let mut binding = app
.world_mut()
.get_entity_mut(entity)
.expect("Could not find entity");
let mut sprite = binding
.get_mut::<Sprite>()
.expect("Could not find sprite component of entity");
sprite.custom_size = Some(Vec2::ONE);
// Re-run the `calculate_bounds_2d` system to get the new AABB
app.update();
// Get the re-calculated AABB
let second_aabb = *app
.world()
.get_entity(entity)
.expect("Could not find entity")
.get::<Aabb>()
.expect("Could not find second AABB");
// Check that the AABBs are not equal
assert_ne!(first_aabb, second_aabb);
}
#[test]
fn calculate_bounds_2d_correct_aabb_for_sprite_with_custom_rect() {
// Setup app
let mut app = App::new();
// Add resources and get handle to image
let mut image_assets = Assets::<Image>::default();
let image_handle = image_assets.add(Image::default());
app.insert_resource(image_assets);
let mesh_assets = Assets::<Mesh>::default();
app.insert_resource(mesh_assets);
let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
app.insert_resource(texture_atlas_assets);
// Add system
app.add_systems(Update, calculate_bounds_2d);
// Add entities
let entity = app
.world_mut()
.spawn(Sprite {
rect: Some(Rect::new(0., 0., 0.5, 1.)),
anchor: Anchor::TopRight,
image: image_handle,
..default()
})
.id();
// Create AABB
app.update();
// Get the AABB
let aabb = *app
.world_mut()
.get_entity(entity)
.expect("Could not find entity")
.get::<Aabb>()
.expect("Could not find AABB");
// Verify that the AABB is at the expected position
assert_eq!(aabb.center, Vec3A::new(-0.25, -0.5, 0.));
// Verify that the AABB has the expected size
assert_eq!(aabb.half_extents, Vec3A::new(0.25, 0.5, 0.));
}
}