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

Combine multiple sprites into a single texture atlas #10027

Open
ickshonpe opened this issue Oct 5, 2023 · 2 comments
Open

Combine multiple sprites into a single texture atlas #10027

ickshonpe opened this issue Oct 5, 2023 · 2 comments
Labels
A-Rendering Drawing game state to the screen C-Feature A new feature, making something new possible

Comments

@ickshonpe
Copy link
Contributor

ickshonpe commented Oct 5, 2023

What problem does this solve or what need does it fill?

You have two sprite sheets

  • 16 16x16 sprites
    16x16

  • 4 32x32 sprites
    32x32

For both convenience and performance, you would like to load the two sprite sheets individually and then have Bevy combine them into a single texture atlas. But unfortunately, Bevy only supports loading homogeneous sprite sheets from a single source image.

What solution would you like?

A method that allows you to take two (or more) sprite sheets and combine them into a single texture atlas:

atlas

Rough sketch of a possible API:

pub enum SpriteSheetItem {
    Grid {
        texture: Handle<Image>,
        tile_size: Vec2,
        columns: usize,
        rows: usize,
        padding: Option<Vec2>,
        offset: Option<Vec2>
    },
    SingleImage {
        texture: Handle<Image>,   
        padding: Option<Vec2>,
    },
}

impl TextureAtlas {
    pub fn from_grids(
        items: impl IntoIterator<Item=SpriteSheetItem>,
    ) {
        //.. merge the atlases
    }
}

Prior art: I wrote this function a while that merges multiple existing texture atlases:

pub fn merge_atlases<'a, I>(
    image_assets: &mut Assets<Image>,
    atlas_assets: &mut Assets<TextureAtlas>,
    atlas_handles: I,
) -> Handle<TextureAtlas>
where
    I: IntoIterator<Item = &'a Handle<TextureAtlas>>,
{
    let mut builder = TextureAtlasBuilder::default();
    let atlases = atlas_handles
        .into_iter()
        .map(|h| atlas_assets.get(h).expect("Missing texture atlas"))
        .collect::<Vec<_>>();
    for atlas in &atlases {
        let image = image_assets
            .get(&atlas.texture)
            .expect("Missing atlas texture");
        builder.add_texture(atlas.texture.clone_weak(), image);
    }
    let mut meta_atlas = builder.finish(image_assets).unwrap();
    let meta_rects = std::mem::take(&mut meta_atlas.textures);
    let meta_handles = std::mem::take(&mut meta_atlas.texture_handles).unwrap();
    for atlas in &atlases {
        let meta_rect_index = meta_handles[&atlas.texture];
        let meta_rect = meta_rects[meta_rect_index];
        for &rect in atlas.textures.iter() {
            let meta_sub_rect = Rect {
                min: meta_rect.min + rect.min,
                max: meta_rect.min + rect.max,
            };
            meta_atlas.add_texture(meta_sub_rect);
        }
    }
    atlas_assets.add(meta_atlas)
}

It has problems though:

  • It no longer works since 0.11 changed the visibility of the texture handles field to pub(crate). This wouldn't be a problem if it's added to the bevy_sprite crate.
  • It only merges already existing texture atlases. This means that to create the texture atlas in my example above you would have to generate two intermediate texture atlases from the sprite sheet images to pass into merge_atlases, which isn't ideal.

A more ambitious implementation could chop up the sprite sheets and then add the sprites individually into a TextureAtlasBuilder. This would allow you to add padding to unpadded source sprite sheet images and might reduce the size of the output texture atlas.

@ickshonpe ickshonpe added C-Feature A new feature, making something new possible S-Needs-Triage This issue needs to be labelled A-Rendering Drawing game state to the screen and removed S-Needs-Triage This issue needs to be labelled labels Oct 5, 2023
@superdump
Copy link
Contributor

I wanted to use either guillotiere or etagiere for general texture -> texture atlas preprocessing, not just for sprites / sprite sheets. Discussed this a bit with nical (Mozilla dev working on UI stuff as far as I know) on Discord in the Atlas allocation thread in #rendering-dev https://discord.com/channels/691052431525675048/1152646466788134912

@ickshonpe
Copy link
Contributor Author

ickshonpe commented Oct 5, 2023

Ah yeah, those both look good. I wrote some utilities for manipulating texture atlases last year for a 2D game I was working on but they didn't run during gameplay so performance didn't really matter.

The way we handle font texture atlases in Bevy is really terrible, maybe guillotiere could help?
At the moment for each font, we have a font-size -> TextureAtlas hashmap. There is no scaling or anything, we store a separate texture atlas for each distinct font size even if the difference is a millionth of a pixel. These are all retained in memory forever until the font asset is dropped. A naive system that adjusts the size of a text's font to match the width of a window so that it always fills 50% of the available space will add dozens of new texture atlases to the font set every time you change the size of the window until WGPU panics with a Not enough memory left error.

We want all the glyphs with the same font and font size to share the same atlas obviously but maybe with guillotiere we could dynamically insert and remove multiple sets of glyphs from different fonts together into single atlases.

I've avoided tackling any of this because I don't have that much experience with rendering and the UI and text systems always have new non-rendering issues for me to work on.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-Rendering Drawing game state to the screen C-Feature A new feature, making something new possible
Projects
None yet
Development

Successfully merging a pull request may close this issue.

2 participants