Path: blob/main/crates/bevy_image/src/texture_atlas_builder.rs
6595 views
use bevy_asset::{AssetId, RenderAssetUsages};1use bevy_math::{URect, UVec2};2use bevy_platform::collections::HashMap;3use rectangle_pack::{4contains_smallest_box, pack_rects, volume_heuristic, GroupedRectsToPlace, PackedLocation,5RectToInsert, TargetBin,6};7use thiserror::Error;8use tracing::{debug, error, warn};9use wgpu_types::{Extent3d, TextureDimension, TextureFormat};1011use crate::{Image, TextureAccessError, TextureFormatPixelInfo};12use crate::{TextureAtlasLayout, TextureAtlasSources};1314#[derive(Debug, Error)]15pub enum TextureAtlasBuilderError {16#[error("could not pack textures into an atlas within the given bounds")]17NotEnoughSpace,18#[error("added a texture with the wrong format in an atlas")]19WrongFormat,20/// Attempted to add a texture to an uninitialized atlas21#[error("cannot add texture to uninitialized atlas texture")]22UninitializedAtlas,23/// Attempted to add an uninitialized texture to an atlas24#[error("cannot add uninitialized texture to atlas")]25UninitializedSourceTexture,26/// A texture access error occurred27#[error("texture access error: {0}")]28TextureAccess(#[from] TextureAccessError),29}3031#[derive(Debug)]32#[must_use]33/// A builder which is used to create a texture atlas from many individual34/// sprites.35pub struct TextureAtlasBuilder<'a> {36/// Collection of texture's asset id (optional) and image data to be packed into an atlas37textures_to_place: Vec<(Option<AssetId<Image>>, &'a Image)>,38/// The initial atlas size in pixels.39initial_size: UVec2,40/// The absolute maximum size of the texture atlas in pixels.41max_size: UVec2,42/// The texture format for the textures that will be loaded in the atlas.43format: TextureFormat,44/// Enable automatic format conversion for textures if they are not in the atlas format.45auto_format_conversion: bool,46/// The amount of padding in pixels to add along the right and bottom edges of the texture rects.47padding: UVec2,48}4950impl Default for TextureAtlasBuilder<'_> {51fn default() -> Self {52Self {53textures_to_place: Vec::new(),54initial_size: UVec2::splat(256),55max_size: UVec2::splat(2048),56format: TextureFormat::Rgba8UnormSrgb,57auto_format_conversion: true,58padding: UVec2::ZERO,59}60}61}6263pub type TextureAtlasBuilderResult<T> = Result<T, TextureAtlasBuilderError>;6465impl<'a> TextureAtlasBuilder<'a> {66/// Sets the initial size of the atlas in pixels.67pub fn initial_size(&mut self, size: UVec2) -> &mut Self {68self.initial_size = size;69self70}7172/// Sets the max size of the atlas in pixels.73pub fn max_size(&mut self, size: UVec2) -> &mut Self {74self.max_size = size;75self76}7778/// Sets the texture format for textures in the atlas.79pub fn format(&mut self, format: TextureFormat) -> &mut Self {80self.format = format;81self82}8384/// Control whether the added texture should be converted to the atlas format, if different.85pub fn auto_format_conversion(&mut self, auto_format_conversion: bool) -> &mut Self {86self.auto_format_conversion = auto_format_conversion;87self88}8990/// Adds a texture to be copied to the texture atlas.91///92/// Optionally an asset id can be passed that can later be used with the texture layout to retrieve the index of this texture.93/// The insertion order will reflect the index of the added texture in the finished texture atlas.94pub fn add_texture(95&mut self,96image_id: Option<AssetId<Image>>,97texture: &'a Image,98) -> &mut Self {99self.textures_to_place.push((image_id, texture));100self101}102103/// Sets the amount of padding in pixels to add between the textures in the texture atlas.104///105/// The `x` value provide will be added to the right edge, while the `y` value will be added to the bottom edge.106pub fn padding(&mut self, padding: UVec2) -> &mut Self {107self.padding = padding;108self109}110111fn copy_texture_to_atlas(112atlas_texture: &mut Image,113texture: &Image,114packed_location: &PackedLocation,115padding: UVec2,116) -> TextureAtlasBuilderResult<()> {117let rect_width = (packed_location.width() - padding.x) as usize;118let rect_height = (packed_location.height() - padding.y) as usize;119let rect_x = packed_location.x() as usize;120let rect_y = packed_location.y() as usize;121let atlas_width = atlas_texture.width() as usize;122let format_size = atlas_texture.texture_descriptor.format.pixel_size()?;123124let Some(ref mut atlas_data) = atlas_texture.data else {125return Err(TextureAtlasBuilderError::UninitializedAtlas);126};127let Some(ref data) = texture.data else {128return Err(TextureAtlasBuilderError::UninitializedSourceTexture);129};130for (texture_y, bound_y) in (rect_y..rect_y + rect_height).enumerate() {131let begin = (bound_y * atlas_width + rect_x) * format_size;132let end = begin + rect_width * format_size;133let texture_begin = texture_y * rect_width * format_size;134let texture_end = texture_begin + rect_width * format_size;135atlas_data[begin..end].copy_from_slice(&data[texture_begin..texture_end]);136}137Ok(())138}139140fn copy_converted_texture(141&self,142atlas_texture: &mut Image,143texture: &Image,144packed_location: &PackedLocation,145) -> TextureAtlasBuilderResult<()> {146if self.format == texture.texture_descriptor.format {147Self::copy_texture_to_atlas(atlas_texture, texture, packed_location, self.padding)?;148} else if let Some(converted_texture) = texture.convert(self.format) {149debug!(150"Converting texture from '{:?}' to '{:?}'",151texture.texture_descriptor.format, self.format152);153Self::copy_texture_to_atlas(154atlas_texture,155&converted_texture,156packed_location,157self.padding,158)?;159} else {160error!(161"Error converting texture from '{:?}' to '{:?}', ignoring",162texture.texture_descriptor.format, self.format163);164}165Ok(())166}167168/// Consumes the builder, and returns the newly created texture atlas and169/// the associated atlas layout.170///171/// Assigns indices to the textures based on the insertion order.172/// Internally it copies all rectangles from the textures and copies them173/// into a new texture.174///175/// # Usage176///177/// ```rust178/// # use bevy_ecs::prelude::*;179/// # use bevy_asset::*;180/// # use bevy_image::prelude::*;181///182/// fn my_system(mut textures: ResMut<Assets<Image>>, mut layouts: ResMut<Assets<TextureAtlasLayout>>) {183/// // Declare your builder184/// let mut builder = TextureAtlasBuilder::default();185/// // Customize it186/// // ...187/// // Build your texture and the atlas layout188/// let (atlas_layout, atlas_sources, texture) = builder.build().unwrap();189/// let texture = textures.add(texture);190/// let layout = layouts.add(atlas_layout);191/// }192/// ```193///194/// # Errors195///196/// If there is not enough space in the atlas texture, an error will197/// be returned. It is then recommended to make a larger sprite sheet.198pub fn build(199&mut self,200) -> Result<(TextureAtlasLayout, TextureAtlasSources, Image), TextureAtlasBuilderError> {201let max_width = self.max_size.x;202let max_height = self.max_size.y;203204let mut current_width = self.initial_size.x;205let mut current_height = self.initial_size.y;206let mut rect_placements = None;207let mut atlas_texture = Image::default();208let mut rects_to_place = GroupedRectsToPlace::<usize>::new();209210// Adds textures to rectangle group packer211for (index, (_, texture)) in self.textures_to_place.iter().enumerate() {212rects_to_place.push_rect(213index,214None,215RectToInsert::new(216texture.width() + self.padding.x,217texture.height() + self.padding.y,2181,219),220);221}222223while rect_placements.is_none() {224if current_width > max_width || current_height > max_height {225break;226}227228let last_attempt = current_height == max_height && current_width == max_width;229230let mut target_bins = alloc::collections::BTreeMap::new();231target_bins.insert(0, TargetBin::new(current_width, current_height, 1));232rect_placements = match pack_rects(233&rects_to_place,234&mut target_bins,235&volume_heuristic,236&contains_smallest_box,237) {238Ok(rect_placements) => {239atlas_texture = Image::new(240Extent3d {241width: current_width,242height: current_height,243depth_or_array_layers: 1,244},245TextureDimension::D2,246vec![2470;248self.format.pixel_size()? * (current_width * current_height) as usize249],250self.format,251RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD,252);253Some(rect_placements)254}255Err(rectangle_pack::RectanglePackError::NotEnoughBinSpace) => {256current_height = (current_height * 2).clamp(0, max_height);257current_width = (current_width * 2).clamp(0, max_width);258None259}260};261262if last_attempt {263break;264}265}266267let rect_placements = rect_placements.ok_or(TextureAtlasBuilderError::NotEnoughSpace)?;268269let mut texture_rects = Vec::with_capacity(rect_placements.packed_locations().len());270let mut texture_ids = <HashMap<_, _>>::default();271// We iterate through the textures to place to respect the insertion order for the texture indices272for (index, (image_id, texture)) in self.textures_to_place.iter().enumerate() {273let (_, packed_location) = rect_placements.packed_locations().get(&index).unwrap();274275let min = UVec2::new(packed_location.x(), packed_location.y());276let max =277min + UVec2::new(packed_location.width(), packed_location.height()) - self.padding;278if let Some(image_id) = image_id {279texture_ids.insert(*image_id, index);280}281texture_rects.push(URect { min, max });282if texture.texture_descriptor.format != self.format && !self.auto_format_conversion {283warn!(284"Loading a texture of format '{:?}' in an atlas with format '{:?}'",285texture.texture_descriptor.format, self.format286);287return Err(TextureAtlasBuilderError::WrongFormat);288}289self.copy_converted_texture(&mut atlas_texture, texture, packed_location)?;290}291292Ok((293TextureAtlasLayout {294size: atlas_texture.size(),295textures: texture_rects,296},297TextureAtlasSources { texture_ids },298atlas_texture,299))300}301}302303304