Skip to content

Commit

Permalink
Texture Atlas rework (#5103)
Browse files Browse the repository at this point in the history
# Objective

> Old MR: #5072 
> ~~Associated UI MR: #5070~~
> Adresses #1618

Unify sprite management

## Solution

- Remove the `Handle<Image>` field in `TextureAtlas` which is the main
cause for all the boilerplate
- Remove the redundant `TextureAtlasSprite` component
- Renamed `TextureAtlas` asset to `TextureAtlasLayout`
([suggestion](#5103 (comment)))
- Add a `TextureAtlas` component, containing the atlas layout handle and
the section index

The difference between this solution and #5072 is that instead of the
`enum` approach is that we can more easily manipulate texture sheets
without any breaking changes for classic `SpriteBundle`s (@mockersf
[comment](#5072 (comment)))

Also, this approach is more *data oriented* extracting the
`Handle<Image>` and avoiding complex texture atlas manipulations to
retrieve the texture in both applicative and engine code.
With this method, the only difference between a `SpriteBundle` and a
`SpriteSheetBundle` is an **additional** component storing the atlas
handle and the index.

~~This solution can be applied to `bevy_ui` as well (see #5070).~~

EDIT: I also applied this solution to Bevy UI

## Changelog

- (**BREAKING**) Removed `TextureAtlasSprite`
- (**BREAKING**) Renamed `TextureAtlas` to `TextureAtlasLayout`
- (**BREAKING**) `SpriteSheetBundle`:
  - Uses a  `Sprite` instead of a `TextureAtlasSprite` component
- Has a `texture` field containing a `Handle<Image>` like the
`SpriteBundle`
- Has a new `TextureAtlas` component instead of a
`Handle<TextureAtlasLayout>`
- (**BREAKING**) `DynamicTextureAtlasBuilder::add_texture` takes an
additional `&Handle<Image>` parameter
- (**BREAKING**) `TextureAtlasLayout::from_grid` no longer takes a
`Handle<Image>` parameter
- (**BREAKING**) `TextureAtlasBuilder::finish` now returns a
`Result<(TextureAtlasLayout, Handle<Image>), _>`
- `bevy_text`:
  - `GlyphAtlasInfo` stores the texture `Handle<Image>`
  - `FontAtlas` stores the texture `Handle<Image>`
- `bevy_ui`:
- (**BREAKING**) Removed `UiAtlasImage` , the atlas bundle is now
identical to the `ImageBundle` with an additional `TextureAtlas`

## Migration Guide

* Sprites

```diff
fn my_system(
  mut images: ResMut<Assets<Image>>, 
-  mut atlases: ResMut<Assets<TextureAtlas>>, 
+  mut atlases: ResMut<Assets<TextureAtlasLayout>>, 
  asset_server: Res<AssetServer>
) {
    let texture_handle: asset_server.load("my_texture.png");
-   let layout = TextureAtlas::from_grid(texture_handle, Vec2::new(25.0, 25.0), 5, 5, None, None);
+   let layout = TextureAtlasLayout::from_grid(Vec2::new(25.0, 25.0), 5, 5, None, None);
    let layout_handle = atlases.add(layout);
    commands.spawn(SpriteSheetBundle {
-      sprite: TextureAtlasSprite::new(0),
-      texture_atlas: atlas_handle,
+      atlas: TextureAtlas {
+         layout: layout_handle,
+         index: 0
+      },
+      texture: texture_handle,
       ..Default::default()
     });
}
```
* UI


```diff
fn my_system(
  mut images: ResMut<Assets<Image>>, 
-  mut atlases: ResMut<Assets<TextureAtlas>>, 
+  mut atlases: ResMut<Assets<TextureAtlasLayout>>, 
  asset_server: Res<AssetServer>
) {
    let texture_handle: asset_server.load("my_texture.png");
-   let layout = TextureAtlas::from_grid(texture_handle, Vec2::new(25.0, 25.0), 5, 5, None, None);
+   let layout = TextureAtlasLayout::from_grid(Vec2::new(25.0, 25.0), 5, 5, None, None);
    let layout_handle = atlases.add(layout);
    commands.spawn(AtlasImageBundle {
-      texture_atlas_image: UiTextureAtlasImage {
-           index: 0,
-           flip_x: false,
-           flip_y: false,
-       },
-      texture_atlas: atlas_handle,
+      atlas: TextureAtlas {
+         layout: layout_handle,
+         index: 0
+      },
+      image: UiImage {
+           texture: texture_handle,
+           flip_x: false,
+           flip_y: false,
+       },
       ..Default::default()
     });
}
```

---------

Co-authored-by: Alice Cecile <alice.i.cecile@gmail.com>
Co-authored-by: François <mockersf@gmail.com>
Co-authored-by: IceSentry <IceSentry@users.noreply.github.com>
  • Loading branch information
4 people committed Jan 16, 2024
1 parent 9f8db0d commit 135c724
Show file tree
Hide file tree
Showing 22 changed files with 354 additions and 470 deletions.
24 changes: 14 additions & 10 deletions crates/bevy_sprite/src/bundle.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,4 @@
use crate::{
texture_atlas::{TextureAtlas, TextureAtlasSprite},
ImageScaleMode, Sprite,
};
use crate::{texture_atlas::TextureAtlas, ImageScaleMode, Sprite};
use bevy_asset::Handle;
use bevy_ecs::bundle::Bundle;
use bevy_render::{
Expand Down Expand Up @@ -32,18 +29,25 @@ pub struct SpriteBundle {
}

/// A [`Bundle`] of components for drawing a single sprite from a sprite sheet (also referred
/// to as a `TextureAtlas`).
/// to as a `TextureAtlas`) or for animated sprites.
///
/// Note:
/// This bundle is identical to [`SpriteBundle`] with an additional [`TextureAtlas`] component.
///
/// Check the following examples for usage:
/// - [`animated sprite sheet example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_sheet.rs)
/// - [`texture atlas example`](https://github.com/bevyengine/bevy/blob/latest/examples/2d/texture_atlas.rs)
#[derive(Bundle, Clone, Default)]
pub struct SpriteSheetBundle {
/// The specific sprite from the texture atlas to be drawn, defaulting to the sprite at index 0.
pub sprite: TextureAtlasSprite,
pub sprite: Sprite,
/// Controls how the image is altered when scaled.
pub scale_mode: ImageScaleMode,
/// A handle to the texture atlas that holds the sprite images
pub texture_atlas: Handle<TextureAtlas>,
/// Data pertaining to how the sprite is drawn on the screen
pub transform: Transform,
pub global_transform: GlobalTransform,
/// The sprite sheet base texture
pub texture: Handle<Image>,
/// The sprite sheet texture atlas, allowing to draw a custom section of `texture`.
pub atlas: TextureAtlas,
/// User indication of whether an entity is visible
pub visibility: Visibility,
pub inherited_visibility: InheritedVisibility,
Expand Down
28 changes: 18 additions & 10 deletions crates/bevy_sprite/src/dynamic_texture_atlas_builder.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
use crate::TextureAtlas;
use bevy_asset::Assets;
use crate::TextureAtlasLayout;
use bevy_asset::{Assets, Handle};
use bevy_math::{IVec2, Rect, Vec2};
use bevy_render::{
render_asset::RenderAssetPersistencePolicy,
texture::{Image, TextureFormatPixelInfo},
};
use guillotiere::{size2, Allocation, AtlasAllocator};

/// Helper utility to update [`TextureAtlas`] on the fly.
/// Helper utility to update [`TextureAtlasLayout`] on the fly.
///
/// Helpful in cases when texture is created procedurally,
/// e.g: in a font glyph [`TextureAtlas`], only add the [`Image`] texture for letters to be rendered.
/// e.g: in a font glyph [`TextureAtlasLayout`], only add the [`Image`] texture for letters to be rendered.
pub struct DynamicTextureAtlasBuilder {
atlas_allocator: AtlasAllocator,
padding: i32,
Expand All @@ -30,22 +30,30 @@ impl DynamicTextureAtlasBuilder {
}
}

/// Add a new texture to [`TextureAtlas`].
/// It is user's responsibility to pass in the correct [`TextureAtlas`],
/// and that [`TextureAtlas::texture`] has [`Image::cpu_persistent_access`]
/// Add a new texture to `atlas_layout`
/// It is the user's responsibility to pass in the correct [`TextureAtlasLayout`]
/// and that `atlas_texture_handle` has [`Image::cpu_persistent_access`]
/// set to [`RenderAssetPersistencePolicy::Keep`]
///
/// # Arguments
///
/// * `altas_layout` - The atlas to add the texture to
/// * `textures` - The texture assets container
/// * `texture` - The new texture to add to the atlas
/// * `atlas_texture_handle` - The atlas texture to edit
pub fn add_texture(
&mut self,
texture_atlas: &mut TextureAtlas,
atlas_layout: &mut TextureAtlasLayout,
textures: &mut Assets<Image>,
texture: &Image,
atlas_texture_handle: &Handle<Image>,
) -> Option<usize> {
let allocation = self.atlas_allocator.allocate(size2(
texture.width() as i32 + self.padding,
texture.height() as i32 + self.padding,
));
if let Some(allocation) = allocation {
let atlas_texture = textures.get_mut(&texture_atlas.texture).unwrap();
let atlas_texture = textures.get_mut(atlas_texture_handle).unwrap();
assert_eq!(
atlas_texture.cpu_persistent_access,
RenderAssetPersistencePolicy::Keep
Expand All @@ -54,7 +62,7 @@ impl DynamicTextureAtlasBuilder {
self.place_texture(atlas_texture, allocation, texture);
let mut rect: Rect = to_rect(allocation.rectangle);
rect.max -= self.padding as f32;
Some(texture_atlas.add_texture(rect))
Some(atlas_layout.add_texture(rect))
} else {
None
}
Expand Down
46 changes: 15 additions & 31 deletions crates/bevy_sprite/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ pub mod prelude {
pub use crate::{
bundle::{SpriteBundle, SpriteSheetBundle},
sprite::{ImageScaleMode, Sprite},
texture_atlas::{TextureAtlas, TextureAtlasSprite},
texture_atlas::{TextureAtlas, TextureAtlasLayout},
texture_slice::{BorderRect, SliceScaleMode, TextureSlicer},
ColorMaterial, ColorMesh2dBundle, TextureAtlasBuilder,
};
Expand Down Expand Up @@ -65,13 +65,13 @@ impl Plugin for SpritePlugin {
"render/sprite.wgsl",
Shader::from_wgsl
);
app.init_asset::<TextureAtlas>()
.register_asset_reflect::<TextureAtlas>()
app.init_asset::<TextureAtlasLayout>()
.register_asset_reflect::<TextureAtlasLayout>()
.register_type::<Sprite>()
.register_type::<ImageScaleMode>()
.register_type::<TextureSlicer>()
.register_type::<TextureAtlasSprite>()
.register_type::<Anchor>()
.register_type::<TextureAtlas>()
.register_type::<Mesh2dHandle>()
.add_plugins((Mesh2dRenderPlugin, ColorMaterialPlugin))
.add_systems(
Expand Down Expand Up @@ -131,19 +131,15 @@ pub fn calculate_bounds_2d(
mut commands: Commands,
meshes: Res<Assets<Mesh>>,
images: Res<Assets<Image>>,
atlases: Res<Assets<TextureAtlas>>,
atlases: Res<Assets<TextureAtlasLayout>>,
meshes_without_aabb: Query<(Entity, &Mesh2dHandle), (Without<Aabb>, Without<NoFrustumCulling>)>,
sprites_to_recalculate_aabb: Query<
(Entity, &Sprite, &Handle<Image>),
(Entity, &Sprite, &Handle<Image>, Option<&TextureAtlas>),
(
Or<(Without<Aabb>, Changed<Sprite>)>,
Without<NoFrustumCulling>,
),
>,
atlases_without_aabb: Query<
(Entity, &TextureAtlasSprite, &Handle<TextureAtlas>),
(Without<Aabb>, Without<NoFrustumCulling>),
>,
) {
for (entity, mesh_handle) in &meshes_without_aabb {
if let Some(mesh) = meshes.get(&mesh_handle.0) {
Expand All @@ -152,27 +148,15 @@ pub fn calculate_bounds_2d(
}
}
}
for (entity, sprite, texture_handle) in &sprites_to_recalculate_aabb {
if let Some(size) = sprite
.custom_size
.or_else(|| images.get(texture_handle).map(|image| image.size_f32()))
{
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);
}
}
for (entity, atlas_sprite, atlas_handle) in &atlases_without_aabb {
if let Some(size) = atlas_sprite.custom_size.or_else(|| {
atlases
.get(atlas_handle)
.and_then(|atlas| atlas.textures.get(atlas_sprite.index))
.map(|rect| (rect.min - rect.max).abs())
for (entity, sprite, texture_handle, atlas) in &sprites_to_recalculate_aabb {
if let Some(size) = sprite.custom_size.or_else(|| match atlas {
// We default to the texture size for regular sprites
None => images.get(texture_handle).map(|image| image.size_f32()),
// We default to the drawn rect for atlas sprites
Some(atlas) => atlas.texture_rect(&atlases).map(|rect| rect.size()),
}) {
let aabb = Aabb {
center: (-atlas_sprite.anchor.as_vec() * size).extend(0.0).into(),
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);
Expand All @@ -199,7 +183,7 @@ mod test {
app.insert_resource(image_assets);
let mesh_assets = Assets::<Mesh>::default();
app.insert_resource(mesh_assets);
let texture_atlas_assets = Assets::<TextureAtlas>::default();
let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
app.insert_resource(texture_atlas_assets);

// Add system
Expand Down Expand Up @@ -237,7 +221,7 @@ mod test {
app.insert_resource(image_assets);
let mesh_assets = Assets::<Mesh>::default();
app.insert_resource(mesh_assets);
let texture_atlas_assets = Assets::<TextureAtlas>::default();
let texture_atlas_assets = Assets::<TextureAtlasLayout>::default();
app.insert_resource(texture_atlas_assets);

// Add system
Expand Down
58 changes: 7 additions & 51 deletions crates/bevy_sprite/src/render/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::ops::Range;

use crate::{
texture_atlas::{TextureAtlas, TextureAtlasSprite},
texture_atlas::{TextureAtlas, TextureAtlasLayout},
ComputedTextureSlices, Sprite, SPRITE_SHADER_HANDLE,
};
use bevy_asset::{AssetEvent, AssetId, Assets, Handle};
Expand Down Expand Up @@ -335,47 +335,40 @@ pub fn extract_sprite_events(
pub fn extract_sprites(
mut commands: Commands,
mut extracted_sprites: ResMut<ExtractedSprites>,
texture_atlases: Extract<Res<Assets<TextureAtlas>>>,
texture_atlases: Extract<Res<Assets<TextureAtlasLayout>>>,
sprite_query: Extract<
Query<(
Entity,
&ViewVisibility,
&Sprite,
&GlobalTransform,
&Handle<Image>,
Option<&TextureAtlas>,
Option<&ComputedTextureSlices>,
)>,
>,
atlas_query: Extract<
Query<(
Entity,
&ViewVisibility,
&TextureAtlasSprite,
&GlobalTransform,
&Handle<TextureAtlas>,
)>,
>,
) {
extracted_sprites.sprites.clear();

for (entity, view_visibility, sprite, transform, handle, slices) in sprite_query.iter() {
for (entity, view_visibility, sprite, transform, handle, sheet, 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)
.map(|e| (commands.spawn_empty().id(), e)),
);
} else {
let rect = sheet.and_then(|s| s.texture_rect(&texture_atlases));
// PERF: we don't check in this function that the `Image` asset is ready, since it should be in most cases and hashing the handle is expensive
extracted_sprites.sprites.insert(
entity,
ExtractedSprite {
color: sprite.color,
transform: *transform,
rect: sprite.rect,
rect,
// Pass the custom size
custom_size: sprite.custom_size,
flip_x: sprite.flip_x,
Expand All @@ -387,43 +380,6 @@ pub fn extract_sprites(
);
}
}
for (entity, view_visibility, atlas_sprite, transform, texture_atlas_handle) in
atlas_query.iter()
{
if !view_visibility.get() {
continue;
}
if let Some(texture_atlas) = texture_atlases.get(texture_atlas_handle) {
let rect = Some(
*texture_atlas
.textures
.get(atlas_sprite.index)
.unwrap_or_else(|| {
panic!(
"Sprite index {:?} does not exist for texture atlas handle {:?}.",
atlas_sprite.index,
texture_atlas_handle.id(),
)
}),
);
extracted_sprites.sprites.insert(
entity,
ExtractedSprite {
color: atlas_sprite.color,
transform: *transform,
// Select the area in the texture atlas
rect,
// Pass the custom size
custom_size: atlas_sprite.custom_size,
flip_x: atlas_sprite.flip_x,
flip_y: atlas_sprite.flip_y,
image_handle_id: texture_atlas.texture.id(),
anchor: atlas_sprite.anchor.as_vec(),
original_entity: None,
},
);
}
}
}

#[repr(C)]
Expand Down
Loading

1 comment on commit 135c724

@miketwenty1
Copy link

@miketwenty1 miketwenty1 commented on 135c724 Jan 22, 2024

Choose a reason for hiding this comment

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

I'm a bit confused on the migration guide atlas_handle is not defined. Where is this coming from?
Also if I was previously using TextureAtlasSprite to pick a color, I'm guessing I still specify this in sprite, but now through Sprite -> color?

Here is an example of code I'll need to migrate:

pub fn spawn(
    texture: &Handle<TextureAtlas>,
    builder: &mut ChildBuilder,
    color: Color,
) {
    builder.spawn((
        SpriteSheetBundle {
            texture_atlas: texture.clone(),
            sprite: TextureAtlasSprite {
                color,
                index: 1,
                ..Default::default()
            },
            ..Default::default()
        },
    ));
}

Please sign in to comment.