bevy_sprite/
lib.rs

1#![expect(missing_docs, reason = "Not all docs are written yet, see #3492.")]
2#![cfg_attr(docsrs, feature(doc_cfg))]
3#![forbid(unsafe_code)]
4#![doc(
5    html_logo_url = "https://bevy.org/assets/icon.png",
6    html_favicon_url = "https://bevy.org/assets/icon.png"
7)]
8
9//! Provides 2D sprite functionality.
10
11extern crate alloc;
12
13#[cfg(feature = "bevy_picking")]
14mod picking_backend;
15mod sprite;
16#[cfg(feature = "bevy_text")]
17mod text2d;
18mod texture_slice;
19
20/// The sprite prelude.
21///
22/// This includes the most common types in this crate, re-exported for your convenience.
23pub mod prelude {
24    #[cfg(feature = "bevy_picking")]
25    #[doc(hidden)]
26    pub use crate::picking_backend::{
27        SpritePickingCamera, SpritePickingMode, SpritePickingPlugin, SpritePickingSettings,
28    };
29    #[cfg(feature = "bevy_text")]
30    #[doc(hidden)]
31    pub use crate::text2d::{Text2d, Text2dReader, Text2dWriter};
32    #[doc(hidden)]
33    pub use crate::{
34        sprite::{Sprite, SpriteImageMode},
35        texture_slice::{BorderRect, SliceScaleMode, TextureSlice, TextureSlicer},
36        SpriteScalingMode,
37    };
38}
39
40use bevy_asset::Assets;
41use bevy_camera::{
42    primitives::{Aabb, MeshAabb},
43    visibility::NoFrustumCulling,
44    visibility::VisibilitySystems,
45};
46use bevy_mesh::{Mesh, Mesh2d};
47#[cfg(feature = "bevy_picking")]
48pub use picking_backend::*;
49pub use sprite::*;
50#[cfg(feature = "bevy_text")]
51pub use text2d::*;
52pub use texture_slice::*;
53
54use bevy_app::prelude::*;
55use bevy_asset::prelude::AssetChanged;
56use bevy_camera::visibility::NoAutoAabb;
57use bevy_ecs::prelude::*;
58use bevy_image::{Image, TextureAtlasLayout, TextureAtlasPlugin};
59use bevy_math::Vec2;
60
61/// Adds support for 2D sprites.
62#[derive(Default)]
63pub struct SpritePlugin;
64
65/// System set for sprite rendering.
66#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
67pub enum SpriteSystems {
68    ExtractSprites,
69    ComputeSlices,
70}
71
72impl Plugin for SpritePlugin {
73    fn build(&self, app: &mut App) {
74        if !app.is_plugin_added::<TextureAtlasPlugin>() {
75            app.add_plugins(TextureAtlasPlugin);
76        }
77        app.add_systems(
78            PostUpdate,
79            calculate_bounds_2d.in_set(VisibilitySystems::CalculateBounds),
80        );
81
82        #[cfg(feature = "bevy_text")]
83        app.add_systems(
84            PostUpdate,
85            (
86                bevy_text::detect_text_needs_rerender::<Text2d>,
87                update_text2d_layout
88                    .after(bevy_camera::CameraUpdateSystems)
89                    .after(bevy_text::free_unused_font_atlases_system),
90                calculate_bounds_text2d.in_set(VisibilitySystems::CalculateBounds),
91            )
92                .chain()
93                .in_set(bevy_text::Text2dUpdateSystems)
94                .after(bevy_app::AnimationSystems),
95        );
96
97        #[cfg(feature = "bevy_picking")]
98        app.add_plugins(SpritePickingPlugin);
99    }
100}
101
102/// System calculating and inserting an [`Aabb`] component to entities with either:
103/// - a `Mesh2d` component,
104/// - a `Sprite` and `Handle<Image>` components,
105///   and without a [`NoFrustumCulling`] component.
106///
107/// Used in system set [`VisibilitySystems::CalculateBounds`].
108pub fn calculate_bounds_2d(
109    mut commands: Commands,
110    meshes: Res<Assets<Mesh>>,
111    images: Res<Assets<Image>>,
112    atlases: Res<Assets<TextureAtlasLayout>>,
113    new_mesh_aabb: Query<
114        (Entity, &Mesh2d),
115        (
116            Without<Aabb>,
117            Without<NoFrustumCulling>,
118            Without<NoAutoAabb>,
119        ),
120    >,
121    mut update_mesh_aabb: Query<
122        (&Mesh2d, &mut Aabb),
123        (
124            Or<(AssetChanged<Mesh2d>, Changed<Mesh2d>)>,
125            Without<NoFrustumCulling>,
126            Without<NoAutoAabb>,
127            Without<Sprite>, // disjoint mutable query
128        ),
129    >,
130    new_sprite_aabb: Query<
131        (Entity, &Sprite, &Anchor),
132        (
133            Without<Aabb>,
134            Without<NoFrustumCulling>,
135            Without<NoAutoAabb>,
136        ),
137    >,
138    mut update_sprite_aabb: Query<
139        (&Sprite, &mut Aabb, &Anchor),
140        (
141            Or<(Changed<Sprite>, Changed<Anchor>)>,
142            Without<NoFrustumCulling>,
143            Without<NoAutoAabb>,
144            Without<Mesh2d>, // disjoint mutable query
145        ),
146    >,
147) {
148    // New meshes require inserting a component
149    for (entity, mesh_handle) in &new_mesh_aabb {
150        if let Some(mesh) = meshes.get(mesh_handle)
151            && let Some(aabb) = mesh.compute_aabb()
152        {
153            commands.entity(entity).try_insert(aabb);
154        }
155    }
156
157    // Updated meshes can take the fast path with parallel component mutation
158    update_mesh_aabb
159        .par_iter_mut()
160        .for_each(|(mesh_handle, mut aabb)| {
161            if let Some(new_aabb) = meshes.get(mesh_handle).and_then(MeshAabb::compute_aabb) {
162                aabb.set_if_neq(new_aabb);
163            }
164        });
165
166    // Sprite helper
167    let sprite_size = |sprite: &Sprite| -> Option<Vec2> {
168        sprite
169            .custom_size
170            .or_else(|| sprite.rect.map(|rect| rect.size()))
171            .or_else(|| match &sprite.texture_atlas {
172                // We default to the texture size for regular sprites
173                None => images.get(&sprite.image).map(Image::size_f32),
174                // We default to the drawn rect for atlas sprites
175                Some(atlas) => atlas
176                    .texture_rect(&atlases)
177                    .map(|rect| rect.size().as_vec2()),
178            })
179    };
180
181    // New sprites require inserting a component
182    for (size, (entity, anchor)) in new_sprite_aabb
183        .iter()
184        .filter_map(|(entity, sprite, anchor)| sprite_size(sprite).zip(Some((entity, anchor))))
185    {
186        let aabb = Aabb {
187            center: (-anchor.as_vec() * size).extend(0.0).into(),
188            half_extents: (0.5 * size).extend(0.0).into(),
189        };
190        commands.entity(entity).try_insert(aabb);
191    }
192
193    // Updated sprites can take the fast path with parallel component mutation
194    update_sprite_aabb
195        .par_iter_mut()
196        .for_each(|(sprite, mut aabb, anchor)| {
197            if let Some(size) = sprite_size(sprite) {
198                aabb.set_if_neq(Aabb {
199                    center: (-anchor.as_vec() * size).extend(0.0).into(),
200                    half_extents: (0.5 * size).extend(0.0).into(),
201                });
202            }
203        });
204}
205
206#[cfg(test)]
207mod test {
208    use super::*;
209    use bevy_math::{Rect, Vec2, Vec3A};
210
211    #[test]
212    fn calculate_bounds_2d_create_aabb_for_image_sprite_entity() {
213        // Setup app
214        let mut app = App::new();
215
216        // Add resources and get handle to image
217        let mut image_assets = Assets::<Image>::default();
218        let image_handle = image_assets.add(Image::default());
219        app.insert_resource(image_assets);
220        let mesh_assets = Assets::<Mesh>::default();
221        app.insert_resource(mesh_assets);
222        let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
223        app.insert_resource(texture_atlas_assets);
224
225        // Add system
226        app.add_systems(Update, calculate_bounds_2d);
227
228        // Add entities
229        let entity = app.world_mut().spawn(Sprite::from_image(image_handle)).id();
230
231        // Verify that the entity does not have an AABB
232        assert!(!app
233            .world()
234            .get_entity(entity)
235            .expect("Could not find entity")
236            .contains::<Aabb>());
237
238        // Run system
239        app.update();
240
241        // Verify the AABB exists
242        assert!(app
243            .world()
244            .get_entity(entity)
245            .expect("Could not find entity")
246            .contains::<Aabb>());
247    }
248
249    #[test]
250    fn calculate_bounds_2d_update_aabb_when_sprite_custom_size_changes_to_some() {
251        // Setup app
252        let mut app = App::new();
253
254        // Add resources and get handle to image
255        let mut image_assets = Assets::<Image>::default();
256        let image_handle = image_assets.add(Image::default());
257        app.insert_resource(image_assets);
258        let mesh_assets = Assets::<Mesh>::default();
259        app.insert_resource(mesh_assets);
260        let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
261        app.insert_resource(texture_atlas_assets);
262
263        // Add system
264        app.add_systems(Update, calculate_bounds_2d);
265
266        // Add entities
267        let entity = app
268            .world_mut()
269            .spawn(Sprite {
270                custom_size: Some(Vec2::ZERO),
271                image: image_handle,
272                ..Sprite::default()
273            })
274            .id();
275
276        // Create initial AABB
277        app.update();
278
279        // Get the initial AABB
280        let first_aabb = *app
281            .world()
282            .get_entity(entity)
283            .expect("Could not find entity")
284            .get::<Aabb>()
285            .expect("Could not find initial AABB");
286
287        // Change `custom_size` of sprite
288        let mut binding = app
289            .world_mut()
290            .get_entity_mut(entity)
291            .expect("Could not find entity");
292        let mut sprite = binding
293            .get_mut::<Sprite>()
294            .expect("Could not find sprite component of entity");
295        sprite.custom_size = Some(Vec2::ONE);
296
297        // Re-run the `calculate_bounds_2d` system to get the new AABB
298        app.update();
299
300        // Get the re-calculated AABB
301        let second_aabb = *app
302            .world()
303            .get_entity(entity)
304            .expect("Could not find entity")
305            .get::<Aabb>()
306            .expect("Could not find second AABB");
307
308        // Check that the AABBs are not equal
309        assert_ne!(first_aabb, second_aabb);
310    }
311
312    #[test]
313    fn calculate_bounds_2d_correct_aabb_for_sprite_with_custom_rect() {
314        // Setup app
315        let mut app = App::new();
316
317        // Add resources and get handle to image
318        let mut image_assets = Assets::<Image>::default();
319        let image_handle = image_assets.add(Image::default());
320        app.insert_resource(image_assets);
321        let mesh_assets = Assets::<Mesh>::default();
322        app.insert_resource(mesh_assets);
323        let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
324        app.insert_resource(texture_atlas_assets);
325
326        // Add system
327        app.add_systems(Update, calculate_bounds_2d);
328
329        // Add entities
330        let entity = app
331            .world_mut()
332            .spawn((
333                Sprite {
334                    rect: Some(Rect::new(0., 0., 0.5, 1.)),
335                    image: image_handle,
336                    ..Sprite::default()
337                },
338                Anchor::TOP_RIGHT,
339            ))
340            .id();
341
342        // Create AABB
343        app.update();
344
345        // Get the AABB
346        let aabb = *app
347            .world_mut()
348            .get_entity(entity)
349            .expect("Could not find entity")
350            .get::<Aabb>()
351            .expect("Could not find AABB");
352
353        // Verify that the AABB is at the expected position
354        assert_eq!(aabb.center, Vec3A::new(-0.25, -0.5, 0.));
355
356        // Verify that the AABB has the expected size
357        assert_eq!(aabb.half_extents, Vec3A::new(0.25, 0.5, 0.));
358    }
359}