Skip to content

Commit

Permalink
migrate bevy_sprite
Browse files Browse the repository at this point in the history
  • Loading branch information
ecoskey committed Sep 28, 2024
1 parent 39d6a74 commit f4a628a
Show file tree
Hide file tree
Showing 6 changed files with 147 additions and 139 deletions.
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 @@ -15,6 +16,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
16 changes: 7 additions & 9 deletions crates/bevy_sprite/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ 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::{
Expand Down Expand Up @@ -188,9 +189,9 @@ pub fn calculate_bounds_2d(
atlases: Res<Assets<TextureAtlasLayout>>,
meshes_without_aabb: Query<(Entity, &Mesh2dHandle), (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 @@ -202,13 +203,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.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 @@ -262,10 +263,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
174 changes: 82 additions & 92 deletions crates/bevy_sprite/src/picking_backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,25 +29,20 @@ pub fn sprite_picking(
primary_window: Query<Entity, With<PrimaryWindow>>,
images: Res<Assets<Image>>,
texture_atlas_layout: Res<Assets<TextureAtlasLayout>>,
sprite_query: Query<
(
Entity,
Option<&Sprite>,
Option<&TextureAtlas>,
Option<&Handle<Image>>,
&GlobalTransform,
Option<&Pickable>,
&ViewVisibility,
),
Or<(With<Sprite>, With<TextureAtlas>)>,
>,
sprite_query: Query<(
Entity,
&Sprite,
&GlobalTransform,
Option<&Pickable>,
&ViewVisibility,
)>,
mut output: EventWriter<PointerHits>,
) {
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 @@ -80,88 +75,83 @@ pub fn sprite_picking(
.iter()
.copied()
.filter(|(.., visibility)| visibility.get())
.filter_map(
|(entity, sprite, atlas, image, sprite_transform, pickable, ..)| {
if blocked {
return None;
}

// Hit box in sprite coordinate system
let (extents, anchor) = if let Some((sprite, atlas)) = sprite.zip(atlas) {
let extents = sprite.custom_size.or_else(|| {
.filter_map(|(entity, sprite, sprite_transform, pickable, ..)| {
if blocked {
return None;
}

// Hit box in sprite coordinate system
let Some(extents) = sprite
.custom_size
.or_else(|| {
sprite.atlas.as_ref().and_then(|atlas| {
texture_atlas_layout
.get(&atlas.layout)
.map(|f| f.textures[atlas.index].size().as_vec2())
})?;
let anchor = sprite.anchor.as_vec();
(extents, anchor)
} else if let Some((sprite, image)) = sprite.zip(image) {
let extents = sprite
.custom_size
.or_else(|| images.get(image).map(|f| f.size().as_vec2()))?;
let anchor = sprite.anchor.as_vec();
(extents, anchor)
} else {
return None;
};

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
&& pickable.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()),
),
)
})
})
},
)
.or_else(|| images.get(&sprite.image).map(|f| f.size().as_vec2()))
else {
return None;
};

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 && pickable.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
14 changes: 7 additions & 7 deletions crates/bevy_sprite/src/render/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -375,27 +375,27 @@ pub fn extract_sprites(
&ViewVisibility,
&Sprite,
&GlobalTransform,
&Handle<Image>,
Option<&TextureAtlas>,
Option<&ComputedTextureSlices>,
)>,
>,
) {
extracted_sprites.sprites.clear();
for (entity, view_visibility, sprite, transform, handle, sheet, slices) in sprite_query.iter() {
for (entity, view_visibility, sprite, transform, slices) in sprite_query.iter() {
if !view_visibility.get() {
continue;
}

if let Some(slices) = slices {
extracted_sprites.sprites.extend(
slices
.extract_sprites(transform, entity, sprite, handle)
.extract_sprites(transform, entity, sprite, &sprite.image)
.map(|e| (commands.spawn_empty().id(), e)),
);
} else {
let atlas_rect =
sheet.and_then(|s| s.texture_rect(&texture_atlases).map(|r| r.as_rect()));
let atlas_rect = sprite
.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 @@ -419,7 +419,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: None,
},
Expand Down
40 changes: 36 additions & 4 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::{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)]
#[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 atlas: Option<TextureAtlas>,
/// The sprite's color tint
pub color: Color,
/// Flip the sprite along the `X` axis
Expand Down Expand Up @@ -38,6 +44,32 @@ 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,
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()
}
}
}

/// Controls how the image is altered when scaled.
Expand Down
Loading

0 comments on commit f4a628a

Please sign in to comment.