Add sprite atlases into the new renderer. (#2560)

# Objective
Restore the functionality of sprite atlases in the new renderer.

### **Note:** This PR relies on #2555 

## Solution
Mostly just a copy paste of the existing sprite atlas implementation, however I unified the rendering between sprites and atlases.

Co-authored-by: Carter Anderson <mcanders1@gmail.com>
This commit is contained in:
John 2021-08-04 01:16:25 +00:00
parent ae4f809a52
commit 115b170d1f
11 changed files with 711 additions and 37 deletions

View file

@ -42,7 +42,13 @@ default = [
dynamic = ["bevy_dylib"]
# Rendering support (Also needs the bevy_wgpu feature or a third-party rendering backend)
render = ["bevy_internal/bevy_pbr", "bevy_internal/bevy_render", "bevy_internal/bevy_sprite", "bevy_internal/bevy_text", "bevy_internal/bevy_ui"]
render = [
"bevy_internal/bevy_pbr",
"bevy_internal/bevy_render",
"bevy_internal/bevy_sprite",
"bevy_internal/bevy_text",
"bevy_internal/bevy_ui",
]
# Optional bevy crates
bevy_audio = ["bevy_internal/bevy_audio"]
@ -92,14 +98,14 @@ subpixel_glyph_atlas = ["bevy_internal/subpixel_glyph_atlas"]
bevy_ci_testing = ["bevy_internal/bevy_ci_testing"]
[dependencies]
bevy_dylib = {path = "crates/bevy_dylib", version = "0.5.0", default-features = false, optional = true}
bevy_internal = {path = "crates/bevy_internal", version = "0.5.0", default-features = false}
bevy_dylib = { path = "crates/bevy_dylib", version = "0.5.0", default-features = false, optional = true }
bevy_internal = { path = "crates/bevy_internal", version = "0.5.0", default-features = false }
[dev-dependencies]
anyhow = "1.0.4"
rand = "0.8.0"
ron = "0.6.2"
serde = {version = "1", features = ["derive"]}
serde = { version = "1", features = ["derive"] }
# Needed to poll Task examples
futures-lite = "1.11.3"
@ -140,6 +146,10 @@ path = "examples/2d/text2d.rs"
name = "texture_atlas"
path = "examples/2d/texture_atlas.rs"
[[example]]
name = "pipelined_texture_atlas"
path = "examples/2d/pipelined_texture_atlas.rs"
# 3D Rendering
[[example]]
name = "3d_scene"

View file

@ -0,0 +1,98 @@
use bevy::{
asset::LoadState,
math::{Vec2, Vec3},
prelude::{
App, AssetServer, Assets, Commands, HandleUntyped, IntoSystem, Res, ResMut, State,
SystemSet, Transform,
},
render2::{camera::OrthographicCameraBundle, texture::Image},
sprite2::{
PipelinedSpriteBundle, PipelinedSpriteSheetBundle, Sprite, TextureAtlas,
TextureAtlasBuilder, TextureAtlasSprite,
},
PipelinedDefaultPlugins,
};
/// In this example we generate a new texture atlas (sprite sheet) from a folder containing
/// individual sprites
fn main() {
App::new()
.init_resource::<RpgSpriteHandles>()
.add_plugins(PipelinedDefaultPlugins)
.add_state(AppState::Setup)
.add_system_set(SystemSet::on_enter(AppState::Setup).with_system(load_textures.system()))
.add_system_set(SystemSet::on_update(AppState::Setup).with_system(check_textures.system()))
.add_system_set(SystemSet::on_enter(AppState::Finished).with_system(setup.system()))
.run();
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
enum AppState {
Setup,
Finished,
}
#[derive(Default)]
struct RpgSpriteHandles {
handles: Vec<HandleUntyped>,
}
fn load_textures(mut rpg_sprite_handles: ResMut<RpgSpriteHandles>, asset_server: Res<AssetServer>) {
rpg_sprite_handles.handles = asset_server.load_folder("textures/rpg").unwrap();
}
fn check_textures(
mut state: ResMut<State<AppState>>,
rpg_sprite_handles: ResMut<RpgSpriteHandles>,
asset_server: Res<AssetServer>,
) {
if let LoadState::Loaded =
asset_server.get_group_load_state(rpg_sprite_handles.handles.iter().map(|handle| handle.id))
{
state.set(AppState::Finished).unwrap();
}
}
fn setup(
mut commands: Commands,
rpg_sprite_handles: Res<RpgSpriteHandles>,
asset_server: Res<AssetServer>,
mut texture_atlases: ResMut<Assets<TextureAtlas>>,
mut textures: ResMut<Assets<Image>>,
) {
let mut texture_atlas_builder = TextureAtlasBuilder::default();
for handle in rpg_sprite_handles.handles.iter() {
let texture = textures.get(handle).unwrap();
texture_atlas_builder.add_texture(handle.clone_weak().typed::<Image>(), texture);
}
let texture_atlas = texture_atlas_builder.finish(&mut textures).unwrap();
let texture_atlas_texture = texture_atlas.texture.clone();
let vendor_handle = asset_server.get_handle("textures/rpg/chars/vendor/generic-rpg-vendor.png");
let vendor_index = texture_atlas.get_texture_index(&vendor_handle).unwrap();
let atlas_handle = texture_atlases.add(texture_atlas);
// set up a scene to display our texture atlas
commands.spawn_bundle(OrthographicCameraBundle::new_2d());
// draw a sprite from the atlas
commands.spawn_bundle(PipelinedSpriteSheetBundle {
transform: Transform {
translation: Vec3::new(150.0, 0.0, 0.0),
scale: Vec3::splat(4.0),
..Default::default()
},
sprite: TextureAtlasSprite::new(vendor_index as u32),
texture_atlas: atlas_handle,
..Default::default()
});
// draw the atlas itself
commands.spawn_bundle(PipelinedSpriteBundle {
sprite: Sprite {
size: Vec2::new(512.0, 512.0),
..Default::default()
},
texture: texture_atlas_texture,
transform: Transform::from_xyz(-300.0, 0.0, 0.0),
..Default::default()
});
}

View file

@ -85,6 +85,7 @@ Example | File | Description
`contributors` | [`2d/contributors.rs`](./2d/contributors.rs) | Displays each contributor as a bouncy bevy-ball!
`many_sprites` | [`2d/many_sprites.rs`](./2d/many_sprites.rs) | Displays many sprites in a grid arragement! Used for performance testing.
`mesh` | [`2d/mesh.rs`](./2d/mesh.rs) | Renders a custom mesh
`pipelined_texture_atlas` | [`2d/pipelined_texture_atlas.rs`](./2d/pipelined_texture_atlas.rs) | Generates a texture atlas (sprite sheet) from individual sprites
`sprite` | [`2d/sprite.rs`](./2d/sprite.rs) | Renders a sprite
`sprite_sheet` | [`2d/sprite_sheet.rs`](./2d/sprite_sheet.rs) | Renders an animated sprite
`text2d` | [`2d/text2d.rs`](./2d/text2d.rs) | Generates text in 2d

View file

@ -21,12 +21,16 @@ bevy_core_pipeline = { path = "../bevy_core_pipeline", version = "0.5.0" }
bevy_ecs = { path = "../../crates/bevy_ecs", version = "0.5.0" }
bevy_log = { path = "../../crates/bevy_log", version = "0.5.0" }
bevy_math = { path = "../../crates/bevy_math", version = "0.5.0" }
bevy_reflect = { path = "../../crates/bevy_reflect", version = "0.5.0", features = ["bevy"] }
bevy_reflect = { path = "../../crates/bevy_reflect", version = "0.5.0", features = [
"bevy",
] }
bevy_render2 = { path = "../bevy_render2", version = "0.5.0" }
bevy_transform = { path = "../../crates/bevy_transform", version = "0.5.0" }
bevy_utils = { path = "../../crates/bevy_utils", version = "0.5.0" }
# other
thiserror = "1.0"
serde = { version = "1", features = ["derive"] }
bytemuck = "1.5"
guillotiere = "0.6.0"
thiserror = "1.0"
rectangle-pack = "0.4"
serde = { version = "1", features = ["derive"] }

View file

@ -1,4 +1,7 @@
use crate::Sprite;
use crate::{
texture_atlas::{TextureAtlas, TextureAtlasSprite},
Sprite,
};
use bevy_asset::Handle;
use bevy_ecs::bundle::Bundle;
use bevy_render2::texture::Image;
@ -22,3 +25,27 @@ impl Default for PipelinedSpriteBundle {
}
}
}
/// A Bundle of components for drawing a single sprite from a sprite sheet (also referred
/// to as a `TextureAtlas`)
#[derive(Bundle, Clone)]
pub struct PipelinedSpriteSheetBundle {
/// The specific sprite from the texture atlas to be drawn
pub sprite: TextureAtlasSprite,
/// A handle to the texture atlas that holds the sprite images
pub texture_atlas: Handle<TextureAtlas>,
/// Data pertaining to how the sprite is drawn on the screen
pub transform: Transform,
pub global_transform: GlobalTransform,
}
impl Default for PipelinedSpriteSheetBundle {
fn default() -> Self {
Self {
sprite: Default::default(),
texture_atlas: Default::default(),
transform: Default::default(),
global_transform: Default::default(),
}
}
}

View file

@ -0,0 +1,101 @@
use crate::{Rect, TextureAtlas};
use bevy_asset::Assets;
use bevy_math::Vec2;
use bevy_render2::texture::{Image, TextureFormatPixelInfo};
use guillotiere::{size2, Allocation, AtlasAllocator};
pub struct DynamicTextureAtlasBuilder {
pub atlas_allocator: AtlasAllocator,
pub padding: i32,
}
impl DynamicTextureAtlasBuilder {
pub fn new(size: Vec2, padding: i32) -> Self {
Self {
atlas_allocator: AtlasAllocator::new(to_size2(size)),
padding,
}
}
pub fn add_texture(
&mut self,
texture_atlas: &mut TextureAtlas,
textures: &mut Assets<Image>,
texture: &Image,
) -> Option<u32> {
let allocation = self.atlas_allocator.allocate(size2(
texture.texture_descriptor.size.width as i32 + self.padding,
texture.texture_descriptor.size.height as i32 + self.padding,
));
if let Some(allocation) = allocation {
let atlas_texture = textures.get_mut(&texture_atlas.texture).unwrap();
self.place_texture(atlas_texture, allocation, texture);
let mut rect: Rect = allocation.rectangle.into();
rect.max.x -= self.padding as f32;
rect.max.y -= self.padding as f32;
texture_atlas.add_texture(rect);
Some((texture_atlas.len() - 1) as u32)
} else {
None
}
}
// fn resize(
// &mut self,
// texture_atlas: &mut TextureAtlas,
// textures: &mut Assets<Texture>,
// size: Vec2,
// ) {
// let new_size2 = to_size2(new_size);
// self.atlas_texture = Texture::new_fill(new_size, &[0,0,0,0]);
// let change_list = self.atlas_allocator.resize_and_rearrange(new_size2);
// for change in change_list.changes {
// if let Some(changed_texture_handle) = self.allocation_textures.remove(&change.old.id)
// { let changed_texture = textures.get(&changed_texture_handle).unwrap();
// self.place_texture(change.new, changed_texture_handle, changed_texture);
// }
// }
// for failure in change_list.failures {
// let failed_texture = self.allocation_textures.remove(&failure.id).unwrap();
// queued_textures.push(failed_texture);
// }
// }
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.texture_descriptor.size.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]);
}
}
}
impl From<guillotiere::Rectangle> for Rect {
fn from(rectangle: guillotiere::Rectangle) -> Self {
Rect {
min: Vec2::new(rectangle.min.x as f32, rectangle.min.y as f32),
max: Vec2::new(rectangle.max.x as f32, rectangle.max.y as f32),
}
}
}
fn to_size2(vec2: Vec2) -> guillotiere::Size {
guillotiere::Size::new(vec2.x as i32, vec2.y as i32)
}

View file

@ -1,12 +1,19 @@
mod bundle;
mod dynamic_texture_atlas_builder;
mod rect;
mod render;
mod sprite;
mod texture_atlas;
mod texture_atlas_builder;
use bevy_asset::AddAsset;
pub use bundle::*;
pub use dynamic_texture_atlas_builder::*;
pub use rect::*;
pub use render::*;
pub use sprite::*;
pub use texture_atlas::*;
pub use texture_atlas_builder::*;
use bevy_app::prelude::*;
use bevy_render2::{render_graph::RenderGraph, render_phase::DrawFunctions, RenderStage};
@ -16,9 +23,11 @@ pub struct SpritePlugin;
impl Plugin for SpritePlugin {
fn build(&self, app: &mut App) {
app.register_type::<Sprite>();
app.add_asset::<TextureAtlas>().register_type::<Sprite>();
let render_app = app.sub_app_mut(0);
render_app
.init_resource::<ExtractedSprites>()
.add_system_to_stage(RenderStage::Extract, render::extract_atlases)
.add_system_to_stage(RenderStage::Extract, render::extract_sprites)
.add_system_to_stage(RenderStage::Prepare, render::prepare_sprites)
.add_system_to_stage(RenderStage::Queue, queue_sprites)

View file

@ -19,4 +19,8 @@ impl Rect {
pub fn height(&self) -> f32 {
self.max.y - self.min.y
}
pub fn size(&self) -> Vec2 {
Vec2::new(self.width(), self.height())
}
}

View file

@ -1,4 +1,7 @@
use crate::Sprite;
use crate::{
texture_atlas::{TextureAtlas, TextureAtlasSprite},
Rect, Sprite,
};
use bevy_asset::{Assets, Handle};
use bevy_core_pipeline::Transparent2dPhase;
use bevy_ecs::{prelude::*, system::SystemState};
@ -13,6 +16,7 @@ use bevy_render2::{
shader::Shader,
texture::{BevyDefault, Image},
view::{ViewMeta, ViewUniform, ViewUniformOffset},
RenderWorld,
};
use bevy_transform::components::GlobalTransform;
use bevy_utils::slab::{FrameSlabMap, FrameSlabMapKey};
@ -142,35 +146,68 @@ impl FromWorld for SpriteShaders {
struct ExtractedSprite {
transform: Mat4,
size: Vec2,
rect: Rect,
handle: Handle<Image>,
atlas_size: Option<Vec2>,
}
#[derive(Default)]
pub struct ExtractedSprites {
sprites: Vec<ExtractedSprite>,
}
pub fn extract_sprites(
mut commands: Commands,
images: Res<Assets<Image>>,
query: Query<(&Sprite, &GlobalTransform, &Handle<Image>)>,
pub fn extract_atlases(
texture_atlases: Res<Assets<TextureAtlas>>,
atlas_query: Query<(&TextureAtlasSprite, &GlobalTransform, &Handle<TextureAtlas>)>,
mut render_world: ResMut<RenderWorld>,
) {
let mut extracted_sprites = Vec::new();
for (sprite, transform, handle) in query.iter() {
for (atlas_sprite, transform, texture_atlas_handle) in atlas_query.iter() {
if !texture_atlases.contains(texture_atlas_handle) {
continue;
}
if let Some(texture_atlas) = texture_atlases.get(texture_atlas_handle) {
let rect = texture_atlas.textures[atlas_sprite.index as usize];
extracted_sprites.push(ExtractedSprite {
atlas_size: Some(texture_atlas.size),
transform: transform.compute_matrix(),
rect,
handle: texture_atlas.texture.clone_weak(),
});
}
}
if let Some(mut extracted_sprites_res) = render_world.get_resource_mut::<ExtractedSprites>() {
extracted_sprites_res.sprites.extend(extracted_sprites);
}
}
pub fn extract_sprites(
images: Res<Assets<Image>>,
sprite_query: Query<(&Sprite, &GlobalTransform, &Handle<Image>)>,
mut render_world: ResMut<RenderWorld>,
) {
let mut extracted_sprites = Vec::new();
for (sprite, transform, handle) in sprite_query.iter() {
if !images.contains(handle) {
continue;
}
extracted_sprites.push(ExtractedSprite {
atlas_size: None,
transform: transform.compute_matrix(),
size: sprite.size,
rect: Rect {
min: Vec2::ZERO,
max: sprite.size,
},
handle: handle.clone_weak(),
})
});
}
commands.insert_resource(ExtractedSprites {
sprites: extracted_sprites,
});
if let Some(mut extracted_sprites_res) = render_world.get_resource_mut::<ExtractedSprites>() {
extracted_sprites_res.sprites.extend(extracted_sprites);
}
}
#[repr(C)]
@ -228,17 +265,6 @@ pub fn prepare_sprites(
panic!("expected vec3");
};
let quad_vertex_uvs = if let VertexAttributeValues::Float32x2(vertex_uvs) = sprite_meta
.quad
.attribute(Mesh::ATTRIBUTE_UV_0)
.unwrap()
.clone()
{
vertex_uvs
} else {
panic!("expected vec2");
};
let quad_indices = if let Indices::U32(indices) = sprite_meta.quad.indices().unwrap() {
indices.clone()
} else {
@ -255,14 +281,25 @@ pub fn prepare_sprites(
);
for (i, extracted_sprite) in extracted_sprites.sprites.iter().enumerate() {
for (vertex_position, vertex_uv) in quad_vertex_positions.iter().zip(quad_vertex_uvs.iter())
{
let sprite_rect = extracted_sprite.rect;
// Specify the corners of the sprite
let bottom_left = Vec2::new(sprite_rect.min.x, sprite_rect.max.y);
let top_left = sprite_rect.min;
let top_right = Vec2::new(sprite_rect.max.x, sprite_rect.min.y);
let bottom_right = sprite_rect.max;
let atlas_positions: [Vec2; 4] = [bottom_left, top_left, top_right, bottom_right];
for (index, vertex_position) in quad_vertex_positions.iter().enumerate() {
let mut final_position =
Vec3::from(*vertex_position) * extracted_sprite.size.extend(1.0);
Vec3::from(*vertex_position) * extracted_sprite.rect.size().extend(1.0);
final_position = (extracted_sprite.transform * final_position.extend(1.0)).xyz();
sprite_meta.vertices.push(SpriteVertex {
position: final_position.into(),
uv: *vertex_uv,
uv: (atlas_positions[index]
/ extracted_sprite.atlas_size.unwrap_or(sprite_rect.max))
.into(),
});
}
@ -284,7 +321,7 @@ pub fn queue_sprites(
mut sprite_meta: ResMut<SpriteMeta>,
view_meta: Res<ViewMeta>,
sprite_shaders: Res<SpriteShaders>,
extracted_sprites: Res<ExtractedSprites>,
mut extracted_sprites: ResMut<ExtractedSprites>,
gpu_images: Res<RenderAssets<Image>>,
mut views: Query<&mut RenderPhase<Transparent2dPhase>>,
) {
@ -340,6 +377,8 @@ pub fn queue_sprites(
});
}
}
extracted_sprites.sprites.clear();
}
// TODO: this logic can be moved to prepare_sprites once wgpu::Queue is exposed directly

View file

@ -0,0 +1,145 @@
use crate::Rect;
use bevy_asset::Handle;
use bevy_math::Vec2;
use bevy_reflect::{Reflect, TypeUuid};
use bevy_render2::{color::Color, texture::Image};
use bevy_utils::HashMap;
/// An atlas containing multiple textures (like a spritesheet or a tilemap).
/// [Example usage animating sprite.](https://github.com/bevyengine/bevy/blob/latest/examples/2d/sprite_sheet.rs)
/// [Example usage loading sprite sheet.](https://github.com/bevyengine/bevy/blob/latest/examples/2d/texture_atlas.rs)
#[derive(Debug, Clone, TypeUuid)]
#[uuid = "7233c597-ccfa-411f-bd59-9af349432ada"]
pub struct TextureAtlas {
/// The handle to the texture in which the sprites are stored
pub texture: Handle<Image>,
// TODO: add support to Uniforms derive to write dimensions and sprites to the same buffer
pub size: Vec2,
/// The specific areas of the atlas where each texture can be found
pub textures: Vec<Rect>,
pub texture_handles: Option<HashMap<Handle<Image>, usize>>,
}
#[derive(Debug, Clone, TypeUuid, Reflect)]
#[uuid = "7233c597-ccfa-411f-bd59-9af349432ada"]
pub struct TextureAtlasSprite {
pub color: Color,
pub index: u32,
pub flip_x: bool,
pub flip_y: bool,
}
impl Default for TextureAtlasSprite {
fn default() -> Self {
Self {
index: 0,
color: Color::WHITE,
flip_x: false,
flip_y: false,
}
}
}
impl TextureAtlasSprite {
pub fn new(index: u32) -> TextureAtlasSprite {
Self {
index,
..Default::default()
}
}
}
impl TextureAtlas {
/// Create a new `TextureAtlas` that has a texture, but does not have
/// any individual sprites specified
pub fn new_empty(texture: Handle<Image>, dimensions: Vec2) -> Self {
Self {
texture,
size: dimensions,
texture_handles: None,
textures: Vec::new(),
}
}
/// Generate a `TextureAtlas` by splitting a texture into a grid where each
/// cell of the grid of `tile_size` is one of the textures in the atlas
pub fn from_grid(
texture: Handle<Image>,
tile_size: Vec2,
columns: usize,
rows: usize,
) -> TextureAtlas {
Self::from_grid_with_padding(texture, tile_size, columns, rows, Vec2::new(0f32, 0f32))
}
/// Generate a `TextureAtlas` by splitting a texture into a grid where each
/// cell of the grid of `tile_size` is one of the textures in the atlas and is separated by
/// some `padding` in the texture
pub fn from_grid_with_padding(
texture: Handle<Image>,
tile_size: Vec2,
columns: usize,
rows: usize,
padding: Vec2,
) -> TextureAtlas {
let mut sprites = Vec::new();
let mut x_padding = 0.0;
let mut y_padding = 0.0;
for y in 0..rows {
if y > 0 {
y_padding = padding.y;
}
for x in 0..columns {
if x > 0 {
x_padding = padding.x;
}
let rect_min = Vec2::new(
(tile_size.x + x_padding) * x as f32,
(tile_size.y + y_padding) * y as f32,
);
sprites.push(Rect {
min: rect_min,
max: Vec2::new(rect_min.x + tile_size.x, rect_min.y + tile_size.y),
})
}
}
TextureAtlas {
size: Vec2::new(
((tile_size.x + x_padding) * columns as f32) - x_padding,
((tile_size.y + y_padding) * rows as f32) - y_padding,
),
textures: sprites,
texture,
texture_handles: None,
}
}
/// Add a sprite to the list of textures in the `TextureAtlas`
///
/// # Arguments
///
/// * `rect` - The section of the atlas that contains the texture to be added,
/// from the top-left corner of the texture to the bottom-right corner
pub fn add_texture(&mut self, rect: Rect) {
self.textures.push(rect);
}
/// How many textures are in the `TextureAtlas`
pub fn len(&self) -> usize {
self.textures.len()
}
pub fn is_empty(&self) -> bool {
self.textures.is_empty()
}
pub fn get_texture_index(&self, texture: &Handle<Image>) -> Option<usize> {
self.texture_handles
.as_ref()
.and_then(|texture_handles| texture_handles.get(texture).cloned())
}
}

View file

@ -0,0 +1,236 @@
use bevy_asset::{Assets, Handle};
use bevy_log::{debug, error, warn};
use bevy_math::Vec2;
use bevy_render2::{
render_resource::{Extent3d, TextureDimension, TextureFormat},
texture::{Image, TextureFormatPixelInfo},
};
use bevy_utils::HashMap;
use rectangle_pack::{
contains_smallest_box, pack_rects, volume_heuristic, GroupedRectsToPlace, PackedLocation,
RectToInsert, TargetBin,
};
use thiserror::Error;
use crate::{texture_atlas::TextureAtlas, Rect};
#[derive(Debug, Error)]
pub enum TextureAtlasBuilderError {
#[error("could not pack textures into an atlas within the given bounds")]
NotEnoughSpace,
#[error("added a texture with the wrong format in an atlas")]
WrongFormat,
}
#[derive(Debug)]
/// A builder which is used to create a texture atlas from many individual
/// sprites.
pub struct TextureAtlasBuilder {
/// The grouped rects which must be placed with a key value pair of a
/// texture handle to an index.
rects_to_place: GroupedRectsToPlace<Handle<Image>>,
/// The initial atlas size in pixels.
initial_size: Vec2,
/// The absolute maximum size of the texture atlas in pixels.
max_size: Vec2,
/// The texture format for the textures that will be loaded in the atlas.
format: TextureFormat,
/// Enable automatic format conversion for textures if they are not in the atlas format.
auto_format_conversion: bool,
}
impl Default for TextureAtlasBuilder {
fn default() -> Self {
Self {
rects_to_place: GroupedRectsToPlace::new(),
initial_size: Vec2::new(256., 256.),
max_size: Vec2::new(2048., 2048.),
format: TextureFormat::Rgba8UnormSrgb,
auto_format_conversion: true,
}
}
}
pub type TextureAtlasBuilderResult<T> = Result<T, TextureAtlasBuilderError>;
impl TextureAtlasBuilder {
/// Sets the initial size of the atlas in pixels.
pub fn initial_size(mut self, size: Vec2) -> Self {
self.initial_size = size;
self
}
/// Sets the max size of the atlas in pixels.
pub fn max_size(mut self, size: Vec2) -> Self {
self.max_size = size;
self
}
/// Sets the texture format for textures in the atlas.
pub fn format(mut self, format: TextureFormat) -> Self {
self.format = format;
self
}
/// Control whether the added texture should be converted to the atlas format, if different.
pub fn auto_format_conversion(mut self, auto_format_conversion: bool) -> Self {
self.auto_format_conversion = auto_format_conversion;
self
}
/// Adds a texture to be copied to the texture atlas.
pub fn add_texture(&mut self, texture_handle: Handle<Image>, texture: &Image) {
self.rects_to_place.push_rect(
texture_handle,
None,
RectToInsert::new(
texture.texture_descriptor.size.width,
texture.texture_descriptor.size.height,
1,
),
)
}
fn copy_texture_to_atlas(
atlas_texture: &mut Image,
texture: &Image,
packed_location: &PackedLocation,
) {
let rect_width = packed_location.width() as usize;
let rect_height = packed_location.height() as usize;
let rect_x = packed_location.x() as usize;
let rect_y = packed_location.y() as usize;
let atlas_width = atlas_texture.texture_descriptor.size.width as usize;
let format_size = atlas_texture.texture_descriptor.format.pixel_size();
for (texture_y, bound_y) in (rect_y..rect_y + rect_height).enumerate() {
let begin = (bound_y * atlas_width + rect_x) * 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 copy_converted_texture(
&self,
atlas_texture: &mut Image,
texture: &Image,
packed_location: &PackedLocation,
) {
if self.format == texture.texture_descriptor.format {
Self::copy_texture_to_atlas(atlas_texture, texture, packed_location);
} else if let Some(converted_texture) = texture.convert(self.format) {
debug!(
"Converting texture from '{:?}' to '{:?}'",
texture.texture_descriptor.format, self.format
);
Self::copy_texture_to_atlas(atlas_texture, &converted_texture, packed_location);
} else {
error!(
"Error converting texture from '{:?}' to '{:?}', ignoring",
texture.texture_descriptor.format, self.format
);
}
}
/// Consumes the builder and returns a result with a new texture atlas.
///
/// Internally it copies all rectangles from the textures and copies them
/// into a new texture which the texture atlas will use. It is not useful to
/// hold a strong handle to the texture afterwards else it will exist twice
/// in memory.
///
/// # Errors
///
/// If there is not enough space in the atlas texture, an error will
/// be returned. It is then recommended to make a larger sprite sheet.
pub fn finish(
self,
textures: &mut Assets<Image>,
) -> Result<TextureAtlas, TextureAtlasBuilderError> {
let initial_width = self.initial_size.x as u32;
let initial_height = self.initial_size.y as u32;
let max_width = self.max_size.x as u32;
let max_height = self.max_size.y as u32;
let mut current_width = initial_width;
let mut current_height = initial_height;
let mut rect_placements = None;
let mut atlas_texture = Image::default();
while rect_placements.is_none() {
if current_width > max_width || current_height > max_height {
break;
}
let last_attempt = current_height == max_height && current_width == max_width;
let mut target_bins = std::collections::BTreeMap::new();
target_bins.insert(0, TargetBin::new(current_width, current_height, 1));
rect_placements = match pack_rects(
&self.rects_to_place,
&mut target_bins,
&volume_heuristic,
&contains_smallest_box,
) {
Ok(rect_placements) => {
atlas_texture = Image::new_fill(
Extent3d {
width: current_width,
height: current_height,
depth_or_array_layers: 1,
},
TextureDimension::D2,
&[0, 0, 0, 0],
self.format,
);
Some(rect_placements)
}
Err(rectangle_pack::RectanglePackError::NotEnoughBinSpace) => {
current_height = (current_height * 2).clamp(0, max_height);
current_width = (current_width * 2).clamp(0, max_width);
None
}
};
if last_attempt {
break;
}
}
let rect_placements = rect_placements.ok_or(TextureAtlasBuilderError::NotEnoughSpace)?;
let mut texture_rects = Vec::with_capacity(rect_placements.packed_locations().len());
let mut texture_handles = HashMap::default();
for (texture_handle, (_, packed_location)) in rect_placements.packed_locations().iter() {
let texture = textures.get(texture_handle).unwrap();
let min = Vec2::new(packed_location.x() as f32, packed_location.y() as f32);
let max = min
+ Vec2::new(
packed_location.width() as f32,
packed_location.height() as f32,
);
texture_handles.insert(texture_handle.clone_weak(), texture_rects.len());
texture_rects.push(Rect { min, max });
if texture.texture_descriptor.format != self.format && !self.auto_format_conversion {
warn!(
"Loading a texture of format '{:?}' in an atlas with format '{:?}'",
texture.texture_descriptor.format, self.format
);
return Err(TextureAtlasBuilderError::WrongFormat);
}
self.copy_converted_texture(&mut atlas_texture, texture, packed_location);
}
Ok(TextureAtlas {
size: Vec2::new(
atlas_texture.texture_descriptor.size.width as f32,
atlas_texture.texture_descriptor.size.height as f32,
),
texture: textures.add(atlas_texture),
textures: texture_rects,
texture_handles: Some(texture_handles),
})
}
}