From 115b170d1f11a91146bb6d6e9684dceb8b21f786 Mon Sep 17 00:00:00 2001 From: John Date: Wed, 4 Aug 2021 01:16:25 +0000 Subject: [PATCH] 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 --- Cargo.toml | 18 +- examples/2d/pipelined_texture_atlas.rs | 98 ++++++++ examples/README.md | 1 + pipelined/bevy_sprite2/Cargo.toml | 10 +- pipelined/bevy_sprite2/src/bundle.rs | 29 ++- .../src/dynamic_texture_atlas_builder.rs | 101 ++++++++ pipelined/bevy_sprite2/src/lib.rs | 11 +- pipelined/bevy_sprite2/src/rect.rs | 4 + pipelined/bevy_sprite2/src/render/mod.rs | 95 ++++--- pipelined/bevy_sprite2/src/texture_atlas.rs | 145 +++++++++++ .../bevy_sprite2/src/texture_atlas_builder.rs | 236 ++++++++++++++++++ 11 files changed, 711 insertions(+), 37 deletions(-) create mode 100644 examples/2d/pipelined_texture_atlas.rs create mode 100644 pipelined/bevy_sprite2/src/dynamic_texture_atlas_builder.rs create mode 100644 pipelined/bevy_sprite2/src/texture_atlas.rs create mode 100644 pipelined/bevy_sprite2/src/texture_atlas_builder.rs diff --git a/Cargo.toml b/Cargo.toml index 920153c08a..94cf3d143d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/examples/2d/pipelined_texture_atlas.rs b/examples/2d/pipelined_texture_atlas.rs new file mode 100644 index 0000000000..16ed1c65df --- /dev/null +++ b/examples/2d/pipelined_texture_atlas.rs @@ -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::() + .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, +} + +fn load_textures(mut rpg_sprite_handles: ResMut, asset_server: Res) { + rpg_sprite_handles.handles = asset_server.load_folder("textures/rpg").unwrap(); +} + +fn check_textures( + mut state: ResMut>, + rpg_sprite_handles: ResMut, + asset_server: Res, +) { + 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, + asset_server: Res, + mut texture_atlases: ResMut>, + mut textures: ResMut>, +) { + 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::(), 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() + }); +} diff --git a/examples/README.md b/examples/README.md index a43dffbd38..ae09301681 100644 --- a/examples/README.md +++ b/examples/README.md @@ -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 diff --git a/pipelined/bevy_sprite2/Cargo.toml b/pipelined/bevy_sprite2/Cargo.toml index 28f7e64843..11cbaca137 100644 --- a/pipelined/bevy_sprite2/Cargo.toml +++ b/pipelined/bevy_sprite2/Cargo.toml @@ -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"] } diff --git a/pipelined/bevy_sprite2/src/bundle.rs b/pipelined/bevy_sprite2/src/bundle.rs index 60e5f92a59..50f5c74432 100644 --- a/pipelined/bevy_sprite2/src/bundle.rs +++ b/pipelined/bevy_sprite2/src/bundle.rs @@ -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, + /// 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(), + } + } +} diff --git a/pipelined/bevy_sprite2/src/dynamic_texture_atlas_builder.rs b/pipelined/bevy_sprite2/src/dynamic_texture_atlas_builder.rs new file mode 100644 index 0000000000..9d57adf9e0 --- /dev/null +++ b/pipelined/bevy_sprite2/src/dynamic_texture_atlas_builder.rs @@ -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, + texture: &Image, + ) -> Option { + 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, + // 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 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) +} diff --git a/pipelined/bevy_sprite2/src/lib.rs b/pipelined/bevy_sprite2/src/lib.rs index 9e5767b8f8..6566643b18 100644 --- a/pipelined/bevy_sprite2/src/lib.rs +++ b/pipelined/bevy_sprite2/src/lib.rs @@ -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::(); + app.add_asset::().register_type::(); let render_app = app.sub_app_mut(0); render_app + .init_resource::() + .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) diff --git a/pipelined/bevy_sprite2/src/rect.rs b/pipelined/bevy_sprite2/src/rect.rs index 06ac65658a..a90a59d71d 100644 --- a/pipelined/bevy_sprite2/src/rect.rs +++ b/pipelined/bevy_sprite2/src/rect.rs @@ -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()) + } } diff --git a/pipelined/bevy_sprite2/src/render/mod.rs b/pipelined/bevy_sprite2/src/render/mod.rs index 8e83f8c711..31a9d0cd66 100644 --- a/pipelined/bevy_sprite2/src/render/mod.rs +++ b/pipelined/bevy_sprite2/src/render/mod.rs @@ -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, + atlas_size: Option, } +#[derive(Default)] pub struct ExtractedSprites { sprites: Vec, } -pub fn extract_sprites( - mut commands: Commands, - images: Res>, - query: Query<(&Sprite, &GlobalTransform, &Handle)>, +pub fn extract_atlases( + texture_atlases: Res>, + atlas_query: Query<(&TextureAtlasSprite, &GlobalTransform, &Handle)>, + mut render_world: ResMut, ) { 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::() { + extracted_sprites_res.sprites.extend(extracted_sprites); + } +} + +pub fn extract_sprites( + images: Res>, + sprite_query: Query<(&Sprite, &GlobalTransform, &Handle)>, + mut render_world: ResMut, +) { + 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::() { + 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, view_meta: Res, sprite_shaders: Res, - extracted_sprites: Res, + mut extracted_sprites: ResMut, gpu_images: Res>, mut views: Query<&mut RenderPhase>, ) { @@ -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 diff --git a/pipelined/bevy_sprite2/src/texture_atlas.rs b/pipelined/bevy_sprite2/src/texture_atlas.rs new file mode 100644 index 0000000000..58ded48a3a --- /dev/null +++ b/pipelined/bevy_sprite2/src/texture_atlas.rs @@ -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, + // 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, + pub texture_handles: Option, 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, 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, + 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, + 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) -> Option { + self.texture_handles + .as_ref() + .and_then(|texture_handles| texture_handles.get(texture).cloned()) + } +} diff --git a/pipelined/bevy_sprite2/src/texture_atlas_builder.rs b/pipelined/bevy_sprite2/src/texture_atlas_builder.rs new file mode 100644 index 0000000000..961fbebf36 --- /dev/null +++ b/pipelined/bevy_sprite2/src/texture_atlas_builder.rs @@ -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>, + /// 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 = Result; + +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, 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, + ) -> Result { + 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), + }) + } +}