Skip to content

Commit

Permalink
Migrate bevy_sprite to required components (#15489)
Browse files Browse the repository at this point in the history
# Objective

Continue migration of bevy APIs to required components, following
guidance of https://hackmd.io/@bevy/required_components/

## Solution

- Make `Sprite` require `Transform` and `Visibility` and
`SyncToRenderWorld`
- move image and texture atlas handles into `Sprite`
- deprecate `SpriteBundle`
- remove engine uses of `SpriteBundle`

## Testing

ran cargo tests on bevy_sprite and tested several sprite examples.

---

## Migration Guide

Replace all uses of `SpriteBundle` with `Sprite`. There are several new
convenience constructors: `Sprite::from_image`,
`Sprite::from_atlas_image`, `Sprite::from_color`.

WARNING: use of `Handle<Image>` and `TextureAtlas` as components on
sprite entities will NO LONGER WORK. Use the fields on `Sprite` instead.
I would have removed the `Component` impls from `TextureAtlas` and
`Handle<Image>` except it is still used within ui. We should fix this
moving forward with the migration.
  • Loading branch information
ecoskey authored Oct 9, 2024
1 parent 219b593 commit 7d40e3e
Show file tree
Hide file tree
Showing 53 changed files with 462 additions and 682 deletions.
2 changes: 0 additions & 2 deletions crates/bevy_ecs/src/bundle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,8 +77,6 @@ use core::{any::TypeId, ptr::NonNull};
/// Additionally, [Tuples](`tuple`) of bundles are also [`Bundle`] (with up to 15 bundles).
/// These bundles contain the items of the 'inner' bundles.
/// This is a convenient shorthand which is primarily used when spawning entities.
/// For example, spawning an entity using the bundle `(SpriteBundle {...}, PlayerMarker)`
/// will spawn an entity with components required for a 2d sprite, and the `PlayerMarker` component.
///
/// [`unit`], otherwise known as [`()`](`unit`), is a [`Bundle`] containing no components (since it
/// can also be considered as the empty tuple).
Expand Down
5 changes: 5 additions & 0 deletions crates/bevy_sprite/src/bundle.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
#![expect(deprecated)]
use crate::Sprite;
use bevy_asset::Handle;
use bevy_ecs::bundle::Bundle;
Expand All @@ -16,6 +17,10 @@ use bevy_transform::components::{GlobalTransform, Transform};
/// - [`ImageScaleMode`](crate::ImageScaleMode) to enable either slicing or tiling of the texture
/// - [`TextureAtlas`](crate::TextureAtlas) to draw a specific section of the texture
#[derive(Bundle, Clone, Debug, Default)]
#[deprecated(
since = "0.15.0",
note = "Use the `Sprite` component instead. Inserting it will now also insert `Transform` and `Visibility` automatically."
)]
pub struct SpriteBundle {
/// Specifies the rendering properties of the sprite, such as color tint and flip.
pub sprite: Sprite,
Expand Down
15 changes: 6 additions & 9 deletions crates/bevy_sprite/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -185,9 +185,9 @@ pub fn calculate_bounds_2d(
atlases: Res<Assets<TextureAtlasLayout>>,
meshes_without_aabb: Query<(Entity, &Mesh2d), (Without<Aabb>, Without<NoFrustumCulling>)>,
sprites_to_recalculate_aabb: Query<
(Entity, &Sprite, &Handle<Image>, Option<&TextureAtlas>),
(Entity, &Sprite),
(
Or<(Without<Aabb>, Changed<Sprite>, Changed<TextureAtlas>)>,
Or<(Without<Aabb>, Changed<Sprite>)>,
Without<NoFrustumCulling>,
),
>,
Expand All @@ -199,13 +199,13 @@ pub fn calculate_bounds_2d(
}
}
}
for (entity, sprite, texture_handle, atlas) in &sprites_to_recalculate_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 atlas {
.or_else(|| match &sprite.texture_atlas {
// We default to the texture size for regular sprites
None => images.get(texture_handle).map(Image::size_f32),
None => images.get(&sprite.image).map(Image::size_f32),
// We default to the drawn rect for atlas sprites
Some(atlas) => atlas
.texture_rect(&atlases)
Expand Down Expand Up @@ -259,10 +259,7 @@ mod test {
app.add_systems(Update, calculate_bounds_2d);

// Add entities
let entity = app
.world_mut()
.spawn((Sprite::default(), image_handle))
.id();
let entity = app.world_mut().spawn(Sprite::from_image(image_handle)).id();

// Verify that the entity does not have an AABB
assert!(!app
Expand Down
157 changes: 76 additions & 81 deletions crates/bevy_sprite/src/picking_backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

use core::cmp::Reverse;

use crate::{Sprite, TextureAtlas, TextureAtlasLayout};
use crate::{Sprite, TextureAtlasLayout};
use bevy_app::prelude::*;
use bevy_asset::prelude::*;
use bevy_ecs::prelude::*;
Expand Down Expand Up @@ -32,8 +32,6 @@ pub fn sprite_picking(
sprite_query: Query<(
Entity,
&Sprite,
Option<&TextureAtlas>,
&Handle<Image>,
&GlobalTransform,
Option<&PickingBehavior>,
&ViewVisibility,
Expand All @@ -42,9 +40,9 @@ pub fn sprite_picking(
) {
let mut sorted_sprites: Vec<_> = sprite_query
.iter()
.filter(|x| !x.4.affine().is_nan())
.filter(|x| !x.2.affine().is_nan())
.collect();
sorted_sprites.sort_by_key(|x| Reverse(FloatOrd(x.4.translation().z)));
sorted_sprites.sort_by_key(|x| Reverse(FloatOrd(x.2.translation().z)));

let primary_window = primary_window.get_single().ok();

Expand Down Expand Up @@ -77,82 +75,79 @@ pub fn sprite_picking(
.iter()
.copied()
.filter(|(.., visibility)| visibility.get())
.filter_map(
|(entity, sprite, atlas, image, sprite_transform, picking_behavior, ..)| {
if blocked {
return None;
}

// Hit box in sprite coordinate system
let extents = match (sprite.custom_size, atlas) {
(Some(custom_size), _) => custom_size,
(None, None) => images.get(image)?.size().as_vec2(),
(None, Some(atlas)) => texture_atlas_layout
.get(&atlas.layout)
.and_then(|layout| layout.textures.get(atlas.index))
// Dropped atlas layouts and indexes out of bounds are rendered as a sprite
.map_or(images.get(image)?.size().as_vec2(), |rect| {
rect.size().as_vec2()
}),
};
let anchor = sprite.anchor.as_vec();
let center = -anchor * extents;
let rect = Rect::from_center_half_size(center, extents / 2.0);

// Transform cursor line segment to sprite coordinate system
let world_to_sprite = sprite_transform.affine().inverse();
let cursor_start_sprite =
world_to_sprite.transform_point3(cursor_ray_world.origin);
let cursor_end_sprite = world_to_sprite.transform_point3(cursor_ray_end);

// Find where the cursor segment intersects the plane Z=0 (which is the sprite's
// plane in sprite-local space). It may not intersect if, for example, we're
// viewing the sprite side-on
if cursor_start_sprite.z == cursor_end_sprite.z {
// Cursor ray is parallel to the sprite and misses it
return None;
}
let lerp_factor =
f32::inverse_lerp(cursor_start_sprite.z, cursor_end_sprite.z, 0.0);
if !(0.0..=1.0).contains(&lerp_factor) {
// Lerp factor is out of range, meaning that while an infinite line cast by
// the cursor would intersect the sprite, the sprite is not between the
// camera's near and far planes
return None;
}
// Otherwise we can interpolate the xy of the start and end positions by the
// lerp factor to get the cursor position in sprite space!
let cursor_pos_sprite = cursor_start_sprite
.lerp(cursor_end_sprite, lerp_factor)
.xy();

let is_cursor_in_sprite = rect.contains(cursor_pos_sprite);

blocked = is_cursor_in_sprite
&& picking_behavior.map(|p| p.should_block_lower) != Some(false);

is_cursor_in_sprite.then(|| {
let hit_pos_world =
sprite_transform.transform_point(cursor_pos_sprite.extend(0.0));
// Transform point from world to camera space to get the Z distance
let hit_pos_cam = cam_transform
.affine()
.inverse()
.transform_point3(hit_pos_world);
// HitData requires a depth as calculated from the camera's near clipping plane
let depth = -cam_ortho.near - hit_pos_cam.z;
(
entity,
HitData::new(
cam_entity,
depth,
Some(hit_pos_world),
Some(*sprite_transform.back()),
),
)
})
},
)
.filter_map(|(entity, sprite, sprite_transform, picking_behavior, ..)| {
if blocked {
return None;
}

// Hit box in sprite coordinate system
let extents = match (sprite.custom_size, &sprite.texture_atlas) {
(Some(custom_size), _) => custom_size,
(None, None) => images.get(&sprite.image)?.size().as_vec2(),
(None, Some(atlas)) => texture_atlas_layout
.get(&atlas.layout)
.and_then(|layout| layout.textures.get(atlas.index))
// Dropped atlas layouts and indexes out of bounds are rendered as a sprite
.map_or(images.get(&sprite.image)?.size().as_vec2(), |rect| {
rect.size().as_vec2()
}),
};
let anchor = sprite.anchor.as_vec();
let center = -anchor * extents;
let rect = Rect::from_center_half_size(center, extents / 2.0);

// Transform cursor line segment to sprite coordinate system
let world_to_sprite = sprite_transform.affine().inverse();
let cursor_start_sprite = world_to_sprite.transform_point3(cursor_ray_world.origin);
let cursor_end_sprite = world_to_sprite.transform_point3(cursor_ray_end);

// Find where the cursor segment intersects the plane Z=0 (which is the sprite's
// plane in sprite-local space). It may not intersect if, for example, we're
// viewing the sprite side-on
if cursor_start_sprite.z == cursor_end_sprite.z {
// Cursor ray is parallel to the sprite and misses it
return None;
}
let lerp_factor =
f32::inverse_lerp(cursor_start_sprite.z, cursor_end_sprite.z, 0.0);
if !(0.0..=1.0).contains(&lerp_factor) {
// Lerp factor is out of range, meaning that while an infinite line cast by
// the cursor would intersect the sprite, the sprite is not between the
// camera's near and far planes
return None;
}
// Otherwise we can interpolate the xy of the start and end positions by the
// lerp factor to get the cursor position in sprite space!
let cursor_pos_sprite = cursor_start_sprite
.lerp(cursor_end_sprite, lerp_factor)
.xy();

let is_cursor_in_sprite = rect.contains(cursor_pos_sprite);

blocked = is_cursor_in_sprite
&& picking_behavior.map(|p| p.should_block_lower) != Some(false);

is_cursor_in_sprite.then(|| {
let hit_pos_world =
sprite_transform.transform_point(cursor_pos_sprite.extend(0.0));
// Transform point from world to camera space to get the Z distance
let hit_pos_cam = cam_transform
.affine()
.inverse()
.transform_point3(hit_pos_world);
// HitData requires a depth as calculated from the camera's near clipping plane
let depth = -cam_ortho.near - hit_pos_cam.z;
(
entity,
HitData::new(
cam_entity,
depth,
Some(hit_pos_world),
Some(*sprite_transform.back()),
),
)
})
})
.collect();

let order = camera.order as f32;
Expand Down
21 changes: 10 additions & 11 deletions crates/bevy_sprite/src/render/mod.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
use core::ops::Range;

use crate::{
texture_atlas::{TextureAtlas, TextureAtlasLayout},
ComputedTextureSlices, Sprite, WithSprite, SPRITE_SHADER_HANDLE,
texture_atlas::TextureAtlasLayout, ComputedTextureSlices, Sprite, WithSprite,
SPRITE_SHADER_HANDLE,
};
use bevy_asset::{AssetEvent, AssetId, Assets, Handle};
use bevy_asset::{AssetEvent, AssetId, Assets};
use bevy_color::{ColorToComponents, LinearRgba};
use bevy_core_pipeline::{
core_2d::{Transparent2d, CORE_2D_DEPTH_FORMAT},
Expand Down Expand Up @@ -377,15 +377,12 @@ pub fn extract_sprites(
&ViewVisibility,
&Sprite,
&GlobalTransform,
&Handle<Image>,
Option<&TextureAtlas>,
Option<&ComputedTextureSlices>,
)>,
>,
) {
extracted_sprites.sprites.clear();
for (original_entity, entity, view_visibility, sprite, transform, handle, sheet, slices) in
sprite_query.iter()
for (original_entity, entity, view_visibility, sprite, transform, slices) in sprite_query.iter()
{
if !view_visibility.get() {
continue;
Expand All @@ -394,12 +391,14 @@ pub fn extract_sprites(
if let Some(slices) = slices {
extracted_sprites.sprites.extend(
slices
.extract_sprites(transform, original_entity, sprite, handle)
.extract_sprites(transform, original_entity, sprite)
.map(|e| (commands.spawn(TemporaryRenderEntity).id(), e)),
);
} else {
let atlas_rect =
sheet.and_then(|s| s.texture_rect(&texture_atlases).map(|r| r.as_rect()));
let atlas_rect = sprite
.texture_atlas
.as_ref()
.and_then(|s| s.texture_rect(&texture_atlases).map(|r| r.as_rect()));
let rect = match (atlas_rect, sprite.rect) {
(None, None) => None,
(None, Some(sprite_rect)) => Some(sprite_rect),
Expand All @@ -423,7 +422,7 @@ pub fn extract_sprites(
custom_size: sprite.custom_size,
flip_x: sprite.flip_x,
flip_y: sprite.flip_y,
image_handle_id: handle.id(),
image_handle_id: sprite.image.id(),
anchor: sprite.anchor.as_vec(),
original_entity: Some(original_entity),
},
Expand Down
52 changes: 45 additions & 7 deletions crates/bevy_sprite/src/sprite.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
use bevy_asset::Handle;
use bevy_color::Color;
use bevy_ecs::{component::Component, reflect::ReflectComponent};
use bevy_math::{Rect, Vec2};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::{sync_world::SyncToRenderWorld, texture::Image, view::Visibility};
use bevy_transform::components::Transform;

use crate::TextureSlicer;
use crate::{TextureAtlas, TextureSlicer};

/// Specifies the rendering properties of a sprite.
///
/// This is commonly used as a component within [`SpriteBundle`](crate::bundle::SpriteBundle).
/// Describes a sprite to be rendered to a 2D camera
#[derive(Component, Debug, Default, Clone, Reflect)]
#[require(Transform, Visibility, SyncToRenderWorld)]
#[reflect(Component, Default, Debug)]
pub struct Sprite {
/// The image used to render the sprite
pub image: Handle<Image>,
/// The (optional) texture atlas used to render the sprite
pub texture_atlas: Option<TextureAtlas>,
/// The sprite's color tint
pub color: Color,
/// Flip the sprite along the `X` axis
Expand All @@ -21,9 +27,9 @@ pub struct Sprite {
/// of the sprite's image
pub custom_size: Option<Vec2>,
/// An optional rectangle representing the region of the sprite's image to render, instead of rendering
/// the full image. This is an easy one-off alternative to using a [`TextureAtlas`](crate::TextureAtlas).
/// the full image. This is an easy one-off alternative to using a [`TextureAtlas`].
///
/// When used with a [`TextureAtlas`](crate::TextureAtlas), the rect
/// When used with a [`TextureAtlas`], the rect
/// is offset by the atlas's minimal (top-left) corner position.
pub rect: Option<Rect>,
/// [`Anchor`] point of the sprite in the world
Expand All @@ -38,6 +44,38 @@ impl Sprite {
..Default::default()
}
}

/// Create a sprite from an image
pub fn from_image(image: Handle<Image>) -> Self {
Self {
image,
..Default::default()
}
}

/// Create a sprite from an image, with an associated texture atlas
pub fn from_atlas_image(image: Handle<Image>, atlas: TextureAtlas) -> Self {
Self {
image,
texture_atlas: Some(atlas),
..Default::default()
}
}

/// Create a sprite from a solid color
pub fn from_color(color: impl Into<Color>, size: Vec2) -> Self {
Self {
color: color.into(),
custom_size: Some(size),
..Default::default()
}
}
}

impl From<Handle<Image>> for Sprite {
fn from(image: Handle<Image>) -> Self {
Self::from_image(image)
}
}

/// Controls how the image is altered when scaled.
Expand All @@ -58,7 +96,7 @@ pub enum ImageScaleMode {
},
}

/// How a sprite is positioned relative to its [`Transform`](bevy_transform::components::Transform).
/// How a sprite is positioned relative to its [`Transform`].
/// It defaults to `Anchor::Center`.
#[derive(Component, Debug, Clone, Copy, PartialEq, Default, Reflect)]
#[reflect(Component, Default, Debug, PartialEq)]
Expand Down
Loading

0 comments on commit 7d40e3e

Please sign in to comment.