bevy/crates/bevy_sprite/src/dynamic_texture_atlas_builder.rs
Félix Lescaudey de Maneville 135c7240f1
Texture Atlas rework (#5103)
# 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](https://github.com/bevyengine/bevy/pull/5103#discussion_r917281844))
- 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](https://github.com/bevyengine/bevy/pull/5072#issuecomment-1165836139))

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>
2024-01-16 13:59:08 +00:00

105 lines
3.7 KiB
Rust

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 [`TextureAtlasLayout`] on the fly.
///
/// Helpful in cases when texture is created procedurally,
/// 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,
}
impl DynamicTextureAtlasBuilder {
/// Create a new [`DynamicTextureAtlasBuilder`]
///
/// # Arguments
///
/// * `size` - total size for the atlas
/// * `padding` - gap added between textures in the atlas, both in x axis and y axis
pub fn new(size: Vec2, padding: i32) -> Self {
Self {
atlas_allocator: AtlasAllocator::new(to_size2(size)),
padding,
}
}
/// 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,
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(atlas_texture_handle).unwrap();
assert_eq!(
atlas_texture.cpu_persistent_access,
RenderAssetPersistencePolicy::Keep
);
self.place_texture(atlas_texture, allocation, texture);
let mut rect: Rect = to_rect(allocation.rectangle);
rect.max -= self.padding as f32;
Some(atlas_layout.add_texture(rect))
} else {
None
}
}
fn place_texture(
&mut self,
atlas_texture: &mut Image,
allocation: Allocation,
texture: &Image,
) {
let mut rect = allocation.rectangle;
rect.max.x -= self.padding;
rect.max.y -= self.padding;
let atlas_width = atlas_texture.width() as usize;
let rect_width = rect.width() as usize;
let format_size = atlas_texture.texture_descriptor.format.pixel_size();
for (texture_y, bound_y) in (rect.min.y..rect.max.y).map(|i| i as usize).enumerate() {
let begin = (bound_y * atlas_width + rect.min.x as usize) * format_size;
let end = begin + rect_width * format_size;
let texture_begin = texture_y * rect_width * format_size;
let texture_end = texture_begin + rect_width * format_size;
atlas_texture.data[begin..end]
.copy_from_slice(&texture.data[texture_begin..texture_end]);
}
}
}
fn to_rect(rectangle: guillotiere::Rectangle) -> Rect {
Rect {
min: IVec2::new(rectangle.min.x, rectangle.min.y).as_vec2(),
max: IVec2::new(rectangle.max.x, rectangle.max.y).as_vec2(),
}
}
fn to_size2(vec2: Vec2) -> guillotiere::Size {
guillotiere::Size::new(vec2.x as i32, vec2.y as i32)
}