Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
bevyengine
GitHub Repository: bevyengine/bevy
Path: blob/main/crates/bevy_image/src/texture_atlas_builder.rs
6595 views
1
use bevy_asset::{AssetId, RenderAssetUsages};
2
use bevy_math::{URect, UVec2};
3
use bevy_platform::collections::HashMap;
4
use rectangle_pack::{
5
contains_smallest_box, pack_rects, volume_heuristic, GroupedRectsToPlace, PackedLocation,
6
RectToInsert, TargetBin,
7
};
8
use thiserror::Error;
9
use tracing::{debug, error, warn};
10
use wgpu_types::{Extent3d, TextureDimension, TextureFormat};
11
12
use crate::{Image, TextureAccessError, TextureFormatPixelInfo};
13
use crate::{TextureAtlasLayout, TextureAtlasSources};
14
15
#[derive(Debug, Error)]
16
pub enum TextureAtlasBuilderError {
17
#[error("could not pack textures into an atlas within the given bounds")]
18
NotEnoughSpace,
19
#[error("added a texture with the wrong format in an atlas")]
20
WrongFormat,
21
/// Attempted to add a texture to an uninitialized atlas
22
#[error("cannot add texture to uninitialized atlas texture")]
23
UninitializedAtlas,
24
/// Attempted to add an uninitialized texture to an atlas
25
#[error("cannot add uninitialized texture to atlas")]
26
UninitializedSourceTexture,
27
/// A texture access error occurred
28
#[error("texture access error: {0}")]
29
TextureAccess(#[from] TextureAccessError),
30
}
31
32
#[derive(Debug)]
33
#[must_use]
34
/// A builder which is used to create a texture atlas from many individual
35
/// sprites.
36
pub struct TextureAtlasBuilder<'a> {
37
/// Collection of texture's asset id (optional) and image data to be packed into an atlas
38
textures_to_place: Vec<(Option<AssetId<Image>>, &'a Image)>,
39
/// The initial atlas size in pixels.
40
initial_size: UVec2,
41
/// The absolute maximum size of the texture atlas in pixels.
42
max_size: UVec2,
43
/// The texture format for the textures that will be loaded in the atlas.
44
format: TextureFormat,
45
/// Enable automatic format conversion for textures if they are not in the atlas format.
46
auto_format_conversion: bool,
47
/// The amount of padding in pixels to add along the right and bottom edges of the texture rects.
48
padding: UVec2,
49
}
50
51
impl Default for TextureAtlasBuilder<'_> {
52
fn default() -> Self {
53
Self {
54
textures_to_place: Vec::new(),
55
initial_size: UVec2::splat(256),
56
max_size: UVec2::splat(2048),
57
format: TextureFormat::Rgba8UnormSrgb,
58
auto_format_conversion: true,
59
padding: UVec2::ZERO,
60
}
61
}
62
}
63
64
pub type TextureAtlasBuilderResult<T> = Result<T, TextureAtlasBuilderError>;
65
66
impl<'a> TextureAtlasBuilder<'a> {
67
/// Sets the initial size of the atlas in pixels.
68
pub fn initial_size(&mut self, size: UVec2) -> &mut Self {
69
self.initial_size = size;
70
self
71
}
72
73
/// Sets the max size of the atlas in pixels.
74
pub fn max_size(&mut self, size: UVec2) -> &mut Self {
75
self.max_size = size;
76
self
77
}
78
79
/// Sets the texture format for textures in the atlas.
80
pub fn format(&mut self, format: TextureFormat) -> &mut Self {
81
self.format = format;
82
self
83
}
84
85
/// Control whether the added texture should be converted to the atlas format, if different.
86
pub fn auto_format_conversion(&mut self, auto_format_conversion: bool) -> &mut Self {
87
self.auto_format_conversion = auto_format_conversion;
88
self
89
}
90
91
/// Adds a texture to be copied to the texture atlas.
92
///
93
/// Optionally an asset id can be passed that can later be used with the texture layout to retrieve the index of this texture.
94
/// The insertion order will reflect the index of the added texture in the finished texture atlas.
95
pub fn add_texture(
96
&mut self,
97
image_id: Option<AssetId<Image>>,
98
texture: &'a Image,
99
) -> &mut Self {
100
self.textures_to_place.push((image_id, texture));
101
self
102
}
103
104
/// Sets the amount of padding in pixels to add between the textures in the texture atlas.
105
///
106
/// The `x` value provide will be added to the right edge, while the `y` value will be added to the bottom edge.
107
pub fn padding(&mut self, padding: UVec2) -> &mut Self {
108
self.padding = padding;
109
self
110
}
111
112
fn copy_texture_to_atlas(
113
atlas_texture: &mut Image,
114
texture: &Image,
115
packed_location: &PackedLocation,
116
padding: UVec2,
117
) -> TextureAtlasBuilderResult<()> {
118
let rect_width = (packed_location.width() - padding.x) as usize;
119
let rect_height = (packed_location.height() - padding.y) as usize;
120
let rect_x = packed_location.x() as usize;
121
let rect_y = packed_location.y() as usize;
122
let atlas_width = atlas_texture.width() as usize;
123
let format_size = atlas_texture.texture_descriptor.format.pixel_size()?;
124
125
let Some(ref mut atlas_data) = atlas_texture.data else {
126
return Err(TextureAtlasBuilderError::UninitializedAtlas);
127
};
128
let Some(ref data) = texture.data else {
129
return Err(TextureAtlasBuilderError::UninitializedSourceTexture);
130
};
131
for (texture_y, bound_y) in (rect_y..rect_y + rect_height).enumerate() {
132
let begin = (bound_y * atlas_width + rect_x) * format_size;
133
let end = begin + rect_width * format_size;
134
let texture_begin = texture_y * rect_width * format_size;
135
let texture_end = texture_begin + rect_width * format_size;
136
atlas_data[begin..end].copy_from_slice(&data[texture_begin..texture_end]);
137
}
138
Ok(())
139
}
140
141
fn copy_converted_texture(
142
&self,
143
atlas_texture: &mut Image,
144
texture: &Image,
145
packed_location: &PackedLocation,
146
) -> TextureAtlasBuilderResult<()> {
147
if self.format == texture.texture_descriptor.format {
148
Self::copy_texture_to_atlas(atlas_texture, texture, packed_location, self.padding)?;
149
} else if let Some(converted_texture) = texture.convert(self.format) {
150
debug!(
151
"Converting texture from '{:?}' to '{:?}'",
152
texture.texture_descriptor.format, self.format
153
);
154
Self::copy_texture_to_atlas(
155
atlas_texture,
156
&converted_texture,
157
packed_location,
158
self.padding,
159
)?;
160
} else {
161
error!(
162
"Error converting texture from '{:?}' to '{:?}', ignoring",
163
texture.texture_descriptor.format, self.format
164
);
165
}
166
Ok(())
167
}
168
169
/// Consumes the builder, and returns the newly created texture atlas and
170
/// the associated atlas layout.
171
///
172
/// Assigns indices to the textures based on the insertion order.
173
/// Internally it copies all rectangles from the textures and copies them
174
/// into a new texture.
175
///
176
/// # Usage
177
///
178
/// ```rust
179
/// # use bevy_ecs::prelude::*;
180
/// # use bevy_asset::*;
181
/// # use bevy_image::prelude::*;
182
///
183
/// fn my_system(mut textures: ResMut<Assets<Image>>, mut layouts: ResMut<Assets<TextureAtlasLayout>>) {
184
/// // Declare your builder
185
/// let mut builder = TextureAtlasBuilder::default();
186
/// // Customize it
187
/// // ...
188
/// // Build your texture and the atlas layout
189
/// let (atlas_layout, atlas_sources, texture) = builder.build().unwrap();
190
/// let texture = textures.add(texture);
191
/// let layout = layouts.add(atlas_layout);
192
/// }
193
/// ```
194
///
195
/// # Errors
196
///
197
/// If there is not enough space in the atlas texture, an error will
198
/// be returned. It is then recommended to make a larger sprite sheet.
199
pub fn build(
200
&mut self,
201
) -> Result<(TextureAtlasLayout, TextureAtlasSources, Image), TextureAtlasBuilderError> {
202
let max_width = self.max_size.x;
203
let max_height = self.max_size.y;
204
205
let mut current_width = self.initial_size.x;
206
let mut current_height = self.initial_size.y;
207
let mut rect_placements = None;
208
let mut atlas_texture = Image::default();
209
let mut rects_to_place = GroupedRectsToPlace::<usize>::new();
210
211
// Adds textures to rectangle group packer
212
for (index, (_, texture)) in self.textures_to_place.iter().enumerate() {
213
rects_to_place.push_rect(
214
index,
215
None,
216
RectToInsert::new(
217
texture.width() + self.padding.x,
218
texture.height() + self.padding.y,
219
1,
220
),
221
);
222
}
223
224
while rect_placements.is_none() {
225
if current_width > max_width || current_height > max_height {
226
break;
227
}
228
229
let last_attempt = current_height == max_height && current_width == max_width;
230
231
let mut target_bins = alloc::collections::BTreeMap::new();
232
target_bins.insert(0, TargetBin::new(current_width, current_height, 1));
233
rect_placements = match pack_rects(
234
&rects_to_place,
235
&mut target_bins,
236
&volume_heuristic,
237
&contains_smallest_box,
238
) {
239
Ok(rect_placements) => {
240
atlas_texture = Image::new(
241
Extent3d {
242
width: current_width,
243
height: current_height,
244
depth_or_array_layers: 1,
245
},
246
TextureDimension::D2,
247
vec![
248
0;
249
self.format.pixel_size()? * (current_width * current_height) as usize
250
],
251
self.format,
252
RenderAssetUsages::MAIN_WORLD | RenderAssetUsages::RENDER_WORLD,
253
);
254
Some(rect_placements)
255
}
256
Err(rectangle_pack::RectanglePackError::NotEnoughBinSpace) => {
257
current_height = (current_height * 2).clamp(0, max_height);
258
current_width = (current_width * 2).clamp(0, max_width);
259
None
260
}
261
};
262
263
if last_attempt {
264
break;
265
}
266
}
267
268
let rect_placements = rect_placements.ok_or(TextureAtlasBuilderError::NotEnoughSpace)?;
269
270
let mut texture_rects = Vec::with_capacity(rect_placements.packed_locations().len());
271
let mut texture_ids = <HashMap<_, _>>::default();
272
// We iterate through the textures to place to respect the insertion order for the texture indices
273
for (index, (image_id, texture)) in self.textures_to_place.iter().enumerate() {
274
let (_, packed_location) = rect_placements.packed_locations().get(&index).unwrap();
275
276
let min = UVec2::new(packed_location.x(), packed_location.y());
277
let max =
278
min + UVec2::new(packed_location.width(), packed_location.height()) - self.padding;
279
if let Some(image_id) = image_id {
280
texture_ids.insert(*image_id, index);
281
}
282
texture_rects.push(URect { min, max });
283
if texture.texture_descriptor.format != self.format && !self.auto_format_conversion {
284
warn!(
285
"Loading a texture of format '{:?}' in an atlas with format '{:?}'",
286
texture.texture_descriptor.format, self.format
287
);
288
return Err(TextureAtlasBuilderError::WrongFormat);
289
}
290
self.copy_converted_texture(&mut atlas_texture, texture, packed_location)?;
291
}
292
293
Ok((
294
TextureAtlasLayout {
295
size: atlas_texture.size(),
296
textures: texture_rects,
297
},
298
TextureAtlasSources { texture_ids },
299
atlas_texture,
300
))
301
}
302
}
303
304