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

Migrate bevy_sprite to required components #15489

Merged
merged 35 commits into from
Oct 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
f4a628a
migrate bevy_sprite
ecoskey Sep 28, 2024
a52c0e8
migrate engine bundles
ecoskey Sep 28, 2024
2cbeab7
Revert "migrate engine bundles"
ecoskey Sep 30, 2024
6e799ab
Revert "migrate bevy_sprite"
ecoskey Sep 30, 2024
2206617
Merge branch 'bevyengine:main' into main
ecoskey Sep 30, 2024
8657959
Merge branch 'bevyengine:main' into main
ecoskey Oct 1, 2024
da50f7d
Merge branch 'bevyengine:main' into main
ecoskey Oct 1, 2024
fdb3277
migrate bevy_sprite
ecoskey Sep 28, 2024
cf71380
migrate engine bundles
ecoskey Sep 28, 2024
b4e09ce
sync to render world
ecoskey Oct 7, 2024
61e6e5f
Merge branch 'bevyengine:main' into main
ecoskey Oct 7, 2024
1a35f0c
Merge branch 'main' into migrate_bevy_sprite
ecoskey Oct 7, 2024
183687c
Merge branch 'main' into migrate_bevy_sprite
ecoskey Oct 7, 2024
eefae1c
fix ci
ecoskey Oct 7, 2024
409858e
Merge branch 'bevyengine:main' into main
ecoskey Oct 7, 2024
dd14e1a
Merge branch 'bevyengine:main' into main
ecoskey Oct 7, 2024
e139c75
Merge branch 'main' into migrate_bevy_sprite
ecoskey Oct 7, 2024
bc2085c
Update bundle.rs
ecoskey Oct 7, 2024
5a74e6f
fix more bundles
ecoskey Oct 7, 2024
0dfc16b
Merge branch 'migrate_bevy_sprite' of github.com:ecoskey/bevy into mi…
ecoskey Oct 7, 2024
98d7111
Merge branch 'main' into migrate_bevy_sprite
ecoskey Oct 7, 2024
4c664f1
Merge branch 'main' into migrate_bevy_sprite
ecoskey Oct 7, 2024
c35f42d
Merge branch 'main' into migrate_bevy_sprite
ecoskey Oct 7, 2024
8961f00
more bundles
ecoskey Oct 7, 2024
509ef62
Merge branch 'main' into migrate_bevy_sprite
ecoskey Oct 7, 2024
c37ac2d
fix asset_decompression
ecoskey Oct 7, 2024
b01fc54
cleanup
ecoskey Oct 7, 2024
1d9b438
fix sprite_sheet
ecoskey Oct 7, 2024
dc366a7
fix sprite_animation
ecoskey Oct 7, 2024
36ad703
fix ci
ecoskey Oct 7, 2024
01bc756
Merge branch 'main' into migrate_bevy_sprite
ecoskey Oct 7, 2024
da69cf3
Merge branch 'bevyengine:main' into main
ecoskey Oct 9, 2024
ff0ca87
Merge branch 'main' into migrate_bevy_sprite
ecoskey Oct 9, 2024
80b4d9e
rename sprite.texture_atlas
ecoskey Oct 9, 2024
b4e3841
remove unneeded tuple
ecoskey Oct 9, 2024
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
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 {
Copy link
Member

@cart cart Oct 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(Making this a comment so discussion can happen in a thread)

Leaving TextureAtlas separate is a smoother upgrade

I do agree here. It would let people largely focus on the required components change as the component boundaries would remain largely untouched. But I do think the "everything is on Sprite now" narrative coupled with "moving Handle<Image> into Sprite", makes "moving TextureAtlas into Sprite " a pretty small increase in migration overhead.

This unification could come later (in the interest of making migrations easier)

Imo not breaking people across multiple releases is as big of a concern (if not bigger) than making a specific upgrade easier or harder. I generally prefer breaking people upfront in a slightly bigger way over breaking people multiple times.

This reverses the decisions made in the Texture Atlas Rework!

This change does revert the "separate-component-ization" of TextureAtlas data, but it doesn't revert the "conceptual unification" of Sprite and TextureAtlasSprite, which imo is the "bigger" motivating change in that PR.

Having a separate Sprite(Handle) allows custom pipelines to also "be" sprites by reusing the other fields, such as TextureAtlas, color, etc.

As others have mentioned, Sprites are intended to be a very specific "fixed function" thing. I think from an ecosystem perspective, trying to build an "implementation and behavior agnostic sprite-building framework" would require more thoughtful design work, would produce dubious gains, and would increase the user-facing complexity of the system. The fact that right now you can replace Handle<Image> with a different component and everything works as expected is an unintentional implementation detail (and I'm honestly not even sure "everything works as expected" is true, especially in the context of the wider ecosystem).

What about custom sprite materials via fully custom render pipelines? What are we going to do when sprite materials are added?

Our answer to "custom sprite materials" is currently Mesh2d. There are no existing "custom material" paths for Sprite. We may ultimately replace Sprite with Mesh2d. I've heard some people express that ambition (provided we can get perf to be roughly the same) and I'd very much like for us to explore that path, as it would allow us to define true shared infrastructure. If we do choose to implement a more constrained sprite materials system (which is definitely also worth considering) that will require a significant rethink of the whole API, and it would still be directly tied to the fixed function sprite system (we would just add extension hooks to the "fixed function" system). Not something we can / should design for here, given how uncertain that area is. We cannot (and should not) try to prepare for this without having a design that we're committed to.

What are we going to do if we add texture arrays? Make a Option<TextureArray> on Sprite?

Certainly an open question. My default answer is "probably". TextureArray support would be a "fixed function sprite behavior". Functionally, everything that is a sprite would need to ask itself "do i have a texture array yes or no" to select how it behaves. Components for opt-in behaviors are definitely also on the table. However TextureArray is a pretty generic concept. A "texture array" is essentially a contextless datatype, similar to something like a Vec3. All components (including floating components) should mean one specific functional thing. A component is data plus the behaviors driven by that data. Behaviors can and should be tied to a specific context. That is one of the primary reasons we want to remove impl Component for Handle<T>. Having a universal "anything that needs exactly one Handle<Image>" component context isn't particularly useful and is problematic for a variety of reasons. Imo SpriteTextureArray would make more sense as a floating component. And that again should be tied to the behaviors of the fixed function sprite system. And at that point, why are we separating it from Sprite?

What are we going to do when we add parallax backgrounds?

These feel like a higher level concept that would orchestrate existing Sprite functionality. Idk if that functionality needs to be baked into sprites themselves. And if we did tie that directly to sprites, it would once again be deeply integrated into the fixed function Sprite system. So why not add it to Sprite?

For pretty much everything (Sprite::color, Sprite::flix_x, Sprite::rect, Sprite::anchor, Sprite::custom_size), these are all tied to very specific implementation details of our Sprite system. We will likely add more of these over time. Custom pipelines can and should define their own fields, as they could easily behave in subtly different ways, or they might have gaps, such as not implementing flip functionality. For this reason I'm against something like SpriteProperties. I also generally object to any X + XProperties design/naming pattern, as that generally implies that those properties should live on the X type. How can something have X's properties without being X?

In terms of extracting things into their own components, I'm most on board for TextureAtlas, as it does feel more like a reasonably standalone set of functionality (I need a texture atlas for some arbitrary context and I want to read this specific index from it). But frankly even that seems like a stretch, as it relies on sprite-specific shader implementation details to read the indices and then sample the image. There are plenty of other valid uses for texture atlases (that have different functional requirements and different usage contexts). If you are defining a custom pipeline, it could easily have different requirements or behave differently.

Next Steps

I'm pretty strongly still on team "put everything in Sprite". A "Sprite" is inherently "the fixed function special cased sprite rendering featureset / pipeline" (a combination of backing ECS systems and render pipelines). This pipeline may eventually be extended to support custom materials, but that is not currently case. If you are defining something that does not use that fixed function pipeline it is not a Sprite as defined by Bevy. It is driven by completely different machinery (and will function differently). Therefore it should define its own component, with its own concept name, and its own fields.

Outside of cases like @merwaaan's bevy_spritesheet_animation example of "cross context custom Sprite3d vs Sprite spritesheet animation" (brought up by @mgi388), which defines a new Sprite3d and shares TextureAtlas between 3d and 2d sprites, I don't see much of a benefit to extracting out TextureAtlas in its current form. The "cross context sprite animation system" is a pretty niche concept, and in the "unified sprite" world, that crate could just split the Query<(Entity, &mut SpritesheetAnimation, &mut TextureAtlas)> into a query for &Sprite and &Sprite3d. It would amount to a few extra lines. I think this split is Good Actually, because Sprite3d is a completely different concept than Sprite backed by completely different infrastructure. The fact that they both have "texture atlas" functionality (and the fact that the behavior works similarly) is incidental. These types of shared use cases are what should motivate the discussion of splitting out components / defining a "shared context and API boundary". But given the use cases that have been enumerated so far, I don't think the few extra lines these authors need to write outweighs the conceptual simplicity and discoverability of the unified Sprite component. I think most of urge to split out TextureAtlas is fundamentally motivated by the desire to allow people to define new "sprite types" that are not Bevy's Fixed Function Sprite System, but still pretend they are / overlap with some of its infrastructure. I don't think this is the right path to encourage generally given how we have currently defined and implemented Sprites. Instead these cases should define their own names and behaviors.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For my own part, I'm happy with the explanation and justification, thanks. Seeing Sprite as a "fixed function" thing makes sense.

Will there also be a consolidation of TextureAtlas into UiImage—does this then also follow the same "fixed function" reasoning? (I could not see any reference in the hackmd's to show a plan for UiImage)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for taking the time to address the questions :)

Leaving TextureAtlas separate is a smoother upgrade

I do agree here. It would let people largely focus on the required components change as the component boundaries would remain largely untouched. But I do think the "everything is on Sprite now" narrative coupled with "moving Handle into Sprite", makes "moving TextureAtlas into Sprite " a pretty small increase in migration overhead.

This unification could come later (in the interest of making migrations easier)

Imo not breaking people across multiple releases is as big of a concern (if not bigger) than making a specific upgrade easier or harder. I generally prefer breaking people upfront in a slightly bigger way over breaking people multiple times.

Agreed. People already need to change how the use Sprite, so any further changes should be in the same release.

[...]
For pretty much everything (Sprite::color, Sprite::flix_x, Sprite::rect, Sprite::anchor, Sprite::custom_size), these are all tied to very specific implementation details of our Sprite system. We will likely add more of these over time. Custom pipelines can and should define their own fields, as they could easily behave in subtly different ways, or they might have gaps, such as not implementing flip functionality. For this reason I'm against something like SpriteProperties. I also generally object to any X + XProperties design/naming pattern, as that generally implies that those properties should live on the X type. How can something have X's properties without being X?

I had the same concern in my approach. I thought having these new components just be wrappers around Handles was a nice pattern, that might allow for some nice to have stuff in the future, like From<Handle>. I think I agree though, that for Sprites it should not be done like this though.

In terms of extracting things into their own components, I'm most on board for TextureAtlas, as it does feel more like a reasonably standalone set of functionality (I need a texture atlas for some arbitrary context and I want to read this specific index from it). But frankly even that seems like a stretch, as it relies on sprite-specific shader implementation details to read the indices and then sample the image. There are plenty of other valid uses for texture atlases (that have different functional requirements and different usage contexts). If you are defining a custom pipeline, it could easily have different requirements or behave differently.

I'm pretty strongly still on team "put everything in Sprite". A "Sprite" is inherently "the fixed function special cased sprite rendering featureset / pipeline" (a combination of backing ECS systems and render pipelines). This pipeline may eventually be extended to support custom materials, but that is not currently case. If you are defining something that does not use that fixed function pipeline it is not a Sprite as defined by Bevy. It is driven by completely different machinery (and will function differently). Therefore it should define its own component, with its own concept name, and its own fields.

Outside of cases like @merwaaan's bevy_spritesheet_animation example of "cross context custom Sprite3d vs Sprite spritesheet animation" (brought up by @mgi388), which defines a new Sprite3d and shares TextureAtlas between 3d and 2d sprites, I don't see much of a benefit to extracting out TextureAtlas in its current form. The "cross context sprite animation system" is a pretty niche concept, and in the "unified sprite" world, that crate could just split the Query<(Entity, &mut SpritesheetAnimation, &mut TextureAtlas)> into a query for &Sprite and &Sprite3d. It would amount to a few extra lines. I think this split is Good Actually, because Sprite3d is a completely different concept than Sprite backed by completely different infrastructure. The fact that they both have "texture atlas" functionality (and the fact that the behavior works similarly) is incidental. These types of shared use cases are what should motivate the discussion of splitting out components / defining a "shared context and API boundary". But given the use cases that have been enumerated so far, I don't think the few extra lines these authors need to write outweighs the conceptual simplicity and discoverability of the unified Sprite component. I think most of urge to split out TextureAtlas is fundamentally motivated by the desire to allow people to define new "sprite types" that are not Bevy's Fixed Function Sprite System, but still pretend they are / overlap with some of its infrastructure. I don't think this is the right path to encourage generally given how we have currently defined and implemented Sprites. Instead these cases should define their own names and behaviors.

The reason why I like TextureAtlas remaining a component is that (I think) it is the only/best thing to implement generic spritesheet/index-based animations. Having this be provided by bevy from a central position without any dependency to Sprite is a huge boon the plugin ecosystem in my opinion.
I can provide a plugin for Sprite3d or a plugin Backgrounds that support TextureAtlas and I don't have to think about animations. There can be a separate animation plugin that only interfaces with TextureAtlas. Your example of splitting the query only works if Sprite3d implements its own animation functionality (or the animation crate supports this specific Sprite3d crate).
Maybe that is not the right level of abstraction, but to my knowledge there is no better alternative right now. Moving TextureAtlas to Sprite breaks this usecase right now. I would like to see effort towards finding a solution to abstract over that before we split this up or for the next release if we go forward with this approach.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot for taking the time to review and weigh in. I disagree with some of the stuff mentioned but I really appreciate the fact that the change has its motivation and it has been communicated well.

/// 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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it confusing that it's called from_atlas_image and the argument order is image then atlas?

Copy link
Contributor Author

@ecoskey ecoskey Oct 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking "atlas" as an adjective (like the image belongs to a texture atlas) but I'm not attached to the name

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's probably fine, it just made me pause for a sec

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