Path: blob/main/crates/bevy_image/src/dynamic_texture_atlas_builder.rs
9374 views
use crate::{Image, TextureAccessError, TextureAtlasLayout, TextureFormatPixelInfo as _};1use bevy_asset::RenderAssetUsages;2use bevy_math::{URect, UVec2};3use guillotiere::{size2, Allocation, AtlasAllocator};4use thiserror::Error;56/// An error produced by [`DynamicTextureAtlasBuilder`] when trying to add a new7/// texture to a [`TextureAtlasLayout`].8#[derive(Debug, Error)]9pub enum DynamicTextureAtlasBuilderError {10/// Unable to allocate space within the atlas for the new texture11#[error("Couldn't allocate space to add the image requested")]12FailedToAllocateSpace,13/// Attempted to add a texture to an uninitialized atlas14#[error("cannot add texture to uninitialized atlas texture")]15UninitializedAtlas,16/// Attempted to add an uninitialized texture to an atlas17#[error("cannot add uninitialized texture to atlas")]18UninitializedSourceTexture,19/// A texture access error occurred20#[error("texture access error: {0}")]21TextureAccess(#[from] TextureAccessError),22}2324/// Helper utility to update [`TextureAtlasLayout`] on the fly.25///26/// Helpful in cases when texture is created procedurally,27/// e.g: in a font glyph [`TextureAtlasLayout`], only add the [`Image`] texture for letters to be rendered.28pub struct DynamicTextureAtlasBuilder {29atlas_allocator: AtlasAllocator,30padding: u32,31}3233impl DynamicTextureAtlasBuilder {34/// Create a new [`DynamicTextureAtlasBuilder`]35///36/// # Arguments37///38/// * `size` - total size for the atlas39/// * `padding` - gap added between textures in the atlas, both in x axis and y axis40pub fn new(size: UVec2, padding: u32) -> Self {41Self {42atlas_allocator: AtlasAllocator::new(to_size2(size)),43padding,44}45}4647/// Add a new texture to `atlas_layout`.48///49/// It is the user's responsibility to pass in the correct [`TextureAtlasLayout`].50/// Also, the asset that `atlas_texture_handle` points to must have a usage matching51/// [`RenderAssetUsages::MAIN_WORLD`].52///53/// # Arguments54///55/// * `atlas_layout` - The atlas layout to add the texture to.56/// * `texture` - The source texture to add to the atlas.57/// * `atlas_texture` - The destination atlas texture to copy the source texture to.58pub fn add_texture(59&mut self,60atlas_layout: &mut TextureAtlasLayout,61texture: &Image,62atlas_texture: &mut Image,63) -> Result<usize, DynamicTextureAtlasBuilderError> {64let allocation = self.atlas_allocator.allocate(size2(65(texture.width() + self.padding).try_into().unwrap(),66(texture.height() + self.padding).try_into().unwrap(),67));68if let Some(allocation) = allocation {69assert!(70atlas_texture.asset_usage.contains(RenderAssetUsages::MAIN_WORLD),71"The atlas_texture image must have the RenderAssetUsages::MAIN_WORLD usage flag set"72);7374self.place_texture(atlas_texture, allocation, texture)?;75let mut rect: URect = to_rect(allocation.rectangle);76rect.max = rect.max.saturating_sub(UVec2::splat(self.padding));77Ok(atlas_layout.add_texture(rect))78} else {79Err(DynamicTextureAtlasBuilderError::FailedToAllocateSpace)80}81}8283fn place_texture(84&mut self,85atlas_texture: &mut Image,86allocation: Allocation,87texture: &Image,88) -> Result<(), DynamicTextureAtlasBuilderError> {89let mut rect = allocation.rectangle;90rect.max.x -= self.padding as i32;91rect.max.y -= self.padding as i32;92let atlas_width = atlas_texture.width() as usize;93let rect_width = rect.width() as usize;94let format_size = atlas_texture.texture_descriptor.format.pixel_size()?;9596let Some(ref mut atlas_data) = atlas_texture.data else {97return Err(DynamicTextureAtlasBuilderError::UninitializedAtlas);98};99let Some(ref data) = texture.data else {100return Err(DynamicTextureAtlasBuilderError::UninitializedSourceTexture);101};102for (texture_y, bound_y) in (rect.min.y..rect.max.y).map(|i| i as usize).enumerate() {103let begin = (bound_y * atlas_width + rect.min.x as usize) * format_size;104let end = begin + rect_width * format_size;105let texture_begin = texture_y * rect_width * format_size;106let texture_end = texture_begin + rect_width * format_size;107atlas_data[begin..end].copy_from_slice(&data[texture_begin..texture_end]);108}109Ok(())110}111}112113fn to_rect(rectangle: guillotiere::Rectangle) -> URect {114URect {115min: UVec2::new(116rectangle.min.x.try_into().unwrap(),117rectangle.min.y.try_into().unwrap(),118),119max: UVec2::new(120rectangle.max.x.try_into().unwrap(),121rectangle.max.y.try_into().unwrap(),122),123}124}125126fn to_size2(vec2: UVec2) -> guillotiere::Size {127guillotiere::Size::new(vec2.x as i32, vec2.y as i32)128}129130131