From 18d1daaa28d7744245ab2b6c8cd7d25b26ab2f33 Mon Sep 17 00:00:00 2001 From: StarArawn Date: Wed, 21 Jul 2021 10:22:06 -0400 Subject: [PATCH 1/7] Restored sprite atlases to working condition. --- Cargo.toml | 18 +- examples/2d/pipelined_texture_atlas.rs | 98 ++++++++ pipelined/bevy_sprite2/Cargo.toml | 8 +- pipelined/bevy_sprite2/src/bundle.rs | 29 ++- pipelined/bevy_sprite2/src/lib.rs | 7 +- pipelined/bevy_sprite2/src/rect.rs | 4 + pipelined/bevy_sprite2/src/render/mod.rs | 68 +++-- pipelined/bevy_sprite2/src/texture_atlas.rs | 146 +++++++++++ .../bevy_sprite2/src/texture_atlas_builder.rs | 236 ++++++++++++++++++ 9 files changed, 585 insertions(+), 29 deletions(-) create mode 100644 examples/2d/pipelined_texture_atlas.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 476fa13047cc5..1c329e4e21e31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,7 +41,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"] @@ -89,14 +95,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" @@ -137,6 +143,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 0000000000000..16ed1c65df08e --- /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/pipelined/bevy_sprite2/Cargo.toml b/pipelined/bevy_sprite2/Cargo.toml index 1cebd3ef06dfd..7f7c8ed685ed9 100644 --- a/pipelined/bevy_sprite2/Cargo.toml +++ b/pipelined/bevy_sprite2/Cargo.toml @@ -20,12 +20,16 @@ bevy_core = { path = "../../crates/bevy_core", 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 +bytemuck = "1.5" +guillotiere = "0.6.0" thiserror = "1.0" +rectangle-pack = "0.4" serde = { version = "1", features = ["derive"] } -bytemuck = "1.5" diff --git a/pipelined/bevy_sprite2/src/bundle.rs b/pipelined/bevy_sprite2/src/bundle.rs index 60e5f92a59e54..50f5c74432f7b 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/lib.rs b/pipelined/bevy_sprite2/src/lib.rs index 9aed799588077..b795390066abb 100644 --- a/pipelined/bevy_sprite2/src/lib.rs +++ b/pipelined/bevy_sprite2/src/lib.rs @@ -2,11 +2,16 @@ mod bundle; mod rect; mod render; mod sprite; +mod texture_atlas; +mod texture_atlas_builder; +use bevy_asset::AddAsset; pub use bundle::*; 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::{ @@ -18,7 +23,7 @@ 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 .add_system_to_stage(RenderStage::Extract, render::extract_sprites) diff --git a/pipelined/bevy_sprite2/src/rect.rs b/pipelined/bevy_sprite2/src/rect.rs index 06ac65658a51c..a90a59d71d68b 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 9f55cca8afa27..ac9c00b04f977 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_ecs::{prelude::*, system::SystemState}; use bevy_math::{Mat4, Vec2, Vec3, Vec4Swizzles}; @@ -142,8 +145,9 @@ impl FromWorld for SpriteShaders { struct ExtractedSprite { transform: Mat4, - size: Vec2, + rect: Rect, handle: Handle, + atlas_size: Option, } pub struct ExtractedSprites { @@ -153,19 +157,41 @@ pub struct ExtractedSprites { pub fn extract_sprites( mut commands: Commands, images: Res>, - query: Query<(&Sprite, &GlobalTransform, &Handle)>, + texture_atlases: Res>, + sprite_query: Query<(&Sprite, &GlobalTransform, &Handle)>, + atlas_query: Query<(&TextureAtlasSprite, &GlobalTransform, &Handle)>, ) { let mut extracted_sprites = Vec::new(); - for (sprite, transform, handle) in query.iter() { + 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(), - }) + }); + } + + 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: rect.clone(), + handle: texture_atlas.texture.clone_weak(), + }); + } } commands.insert_resource(ExtractedSprites { @@ -228,17 +254,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 +270,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(), }); } diff --git a/pipelined/bevy_sprite2/src/texture_atlas.rs b/pipelined/bevy_sprite2/src/texture_atlas.rs new file mode 100644 index 0000000000000..2ffbe43c40d35 --- /dev/null +++ b/pipelined/bevy_sprite2/src/texture_atlas.rs @@ -0,0 +1,146 @@ +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"] +#[repr(C)] +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 0000000000000..961fbebf36a55 --- /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), + }) + } +} From cccba104459cbab32296bafa67c3b8d860b258fd Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Tue, 27 Jul 2021 12:53:57 -0700 Subject: [PATCH 2/7] Add RenderWorld to Extract step --- pipelined/bevy_render2/src/lib.rs | 43 +++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/pipelined/bevy_render2/src/lib.rs b/pipelined/bevy_render2/src/lib.rs index 1500846112c3b..8083b12751458 100644 --- a/pipelined/bevy_render2/src/lib.rs +++ b/pipelined/bevy_render2/src/lib.rs @@ -11,6 +11,8 @@ pub mod shader; pub mod texture; pub mod view; +use std::ops::{Deref, DerefMut}; + pub use once_cell; use wgpu::BackendBit; @@ -54,6 +56,29 @@ pub enum RenderStage { Cleanup, } +/// The Render App World. This is only available as a resource during the Extract step. +#[derive(Default)] +pub struct RenderWorld(World); + +impl Deref for RenderWorld { + type Target = World; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for RenderWorld { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +/// A "scratch" world used to avoid allocating new worlds every frame when +// swapping out the Render World. +#[derive(Default)] +struct ScratchRenderWorld(World); + impl Plugin for RenderPlugin { fn build(&self, app: &mut App) { let (instance, device, queue) = @@ -66,7 +91,8 @@ impl Plugin for RenderPlugin { &wgpu::DeviceDescriptor::default(), )); app.insert_resource(device.clone()) - .insert_resource(queue.clone()); + .insert_resource(queue.clone()) + .init_resource::(); let mut render_app = App::empty(); let mut extract_stage = SystemStage::parallel(); @@ -89,7 +115,7 @@ impl Plugin for RenderPlugin { .init_resource::() .init_resource::(); - app.add_sub_app(render_app, |app_world, render_app| { + app.add_sub_app(render_app, move |app_world, render_app| { // reserve all existing app entities for use in render_app // they can only be spawned using `get_or_spawn()` let meta_len = app_world.entities().meta.len(); @@ -97,6 +123,7 @@ impl Plugin for RenderPlugin { .world .entities() .reserve_entities(meta_len as u32); + // flushing as "invalid" ensures that app world entities aren't added as "empty archetype" entities by default // these entities cannot be accessed without spawning directly onto them // this _only_ works as expected because clear_entities() is called at the end of every frame. @@ -156,6 +183,18 @@ fn extract(app_world: &mut World, render_app: &mut App) { .schedule .get_stage_mut::(&RenderStage::Extract) .unwrap(); + + // temporarily add the render world to the app world as a resource + let scratch_world = app_world.remove_resource::().unwrap(); + let render_world = std::mem::replace(&mut render_app.world, scratch_world.0); + app_world.insert_resource(RenderWorld(render_world)); + extract.run(app_world); + + // add the render world back to the render app + let render_world = app_world.remove_resource::().unwrap(); + let scratch_world = std::mem::replace(&mut render_app.world, render_world.0); + app_world.insert_resource(ScratchRenderWorld(scratch_world)); + extract.apply_buffers(&mut render_app.world); } From eb0235b89f24e7542f9aa9efba40884cea9e14fc Mon Sep 17 00:00:00 2001 From: StarArawn Date: Wed, 28 Jul 2021 08:36:28 -0400 Subject: [PATCH 3/7] Use RenderWorld to insert or mutate the extracted sprites. --- pipelined/bevy_sprite2/src/lib.rs | 1 + pipelined/bevy_sprite2/src/render/mod.rs | 60 +++++++++++++++--------- 2 files changed, 40 insertions(+), 21 deletions(-) diff --git a/pipelined/bevy_sprite2/src/lib.rs b/pipelined/bevy_sprite2/src/lib.rs index b795390066abb..b1daaf5f859ae 100644 --- a/pipelined/bevy_sprite2/src/lib.rs +++ b/pipelined/bevy_sprite2/src/lib.rs @@ -26,6 +26,7 @@ impl Plugin for SpritePlugin { app.add_asset::().register_type::(); let render_app = app.sub_app_mut(0); render_app + .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/render/mod.rs b/pipelined/bevy_sprite2/src/render/mod.rs index ac9c00b04f977..e42c11543da5f 100644 --- a/pipelined/bevy_sprite2/src/render/mod.rs +++ b/pipelined/bevy_sprite2/src/render/mod.rs @@ -16,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}; @@ -154,12 +155,41 @@ pub struct ExtractedSprites { sprites: Vec, } +pub fn extract_atlases( + texture_atlases: Res>, + atlas_query: Query<(&TextureAtlasSprite, &GlobalTransform, &Handle)>, + mut render_world: ResMut, +) { + let mut extracted_sprites = Vec::new(); + 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: rect.clone(), + 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); + } else { + render_world.insert_resource(ExtractedSprites { + sprites: extracted_sprites, + }); + } +} + pub fn extract_sprites( - mut commands: Commands, images: Res>, - texture_atlases: Res>, sprite_query: Query<(&Sprite, &GlobalTransform, &Handle)>, - atlas_query: Query<(&TextureAtlasSprite, &GlobalTransform, &Handle)>, + mut render_world: ResMut, ) { let mut extracted_sprites = Vec::new(); for (sprite, transform, handle) in sprite_query.iter() { @@ -178,25 +208,13 @@ pub fn extract_sprites( }); } - 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: rect.clone(), - 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); + } else { + render_world.insert_resource(ExtractedSprites { + sprites: extracted_sprites, + }); } - - commands.insert_resource(ExtractedSprites { - sprites: extracted_sprites, - }); } #[repr(C)] From 443616f4ee44cf0de6288afcf5c5d7352a0b9689 Mon Sep 17 00:00:00 2001 From: StarArawn Date: Wed, 28 Jul 2021 13:27:20 -0400 Subject: [PATCH 4/7] Added new example to readme. --- examples/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/README.md b/examples/README.md index 12856903adae3..d0e7505ec85c9 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 From 0c2b128daaaab6524d9f98c745f3972840e3796e Mon Sep 17 00:00:00 2001 From: StarArawn Date: Wed, 28 Jul 2021 13:28:45 -0400 Subject: [PATCH 5/7] Don't clone rect. --- pipelined/bevy_sprite2/src/render/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pipelined/bevy_sprite2/src/render/mod.rs b/pipelined/bevy_sprite2/src/render/mod.rs index e42c11543da5f..30ff7adc70d4f 100644 --- a/pipelined/bevy_sprite2/src/render/mod.rs +++ b/pipelined/bevy_sprite2/src/render/mod.rs @@ -171,7 +171,7 @@ pub fn extract_atlases( extracted_sprites.push(ExtractedSprite { atlas_size: Some(texture_atlas.size), transform: transform.compute_matrix(), - rect: rect.clone(), + rect, handle: texture_atlas.texture.clone_weak(), }); } From f14998f739fe020e663eac9f5c742a5b7b41cadb Mon Sep 17 00:00:00 2001 From: StarArawn Date: Wed, 28 Jul 2021 13:31:52 -0400 Subject: [PATCH 6/7] Added missing dynamic_texture_atlas_builder. --- .../src/dynamic_texture_atlas_builder.rs | 101 ++++++++++++++++++ pipelined/bevy_sprite2/src/lib.rs | 2 + 2 files changed, 103 insertions(+) create mode 100644 pipelined/bevy_sprite2/src/dynamic_texture_atlas_builder.rs 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 0000000000000..9d57adf9e01f3 --- /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 b1daaf5f859ae..6728ff8aaca4c 100644 --- a/pipelined/bevy_sprite2/src/lib.rs +++ b/pipelined/bevy_sprite2/src/lib.rs @@ -1,4 +1,5 @@ mod bundle; +mod dynamic_texture_atlas_builder; mod rect; mod render; mod sprite; @@ -7,6 +8,7 @@ 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::*; From a4b750f4058215cbc3f1f91d5791dac96926f70e Mon Sep 17 00:00:00 2001 From: StarArawn Date: Sat, 31 Jul 2021 17:08:08 -0400 Subject: [PATCH 7/7] Clear out extracted sprites resource every frame. --- pipelined/bevy_sprite2/src/lib.rs | 1 + pipelined/bevy_sprite2/src/render/mod.rs | 13 ++++--------- pipelined/bevy_sprite2/src/texture_atlas.rs | 1 - 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/pipelined/bevy_sprite2/src/lib.rs b/pipelined/bevy_sprite2/src/lib.rs index 6728ff8aaca4c..fe71512b2be13 100644 --- a/pipelined/bevy_sprite2/src/lib.rs +++ b/pipelined/bevy_sprite2/src/lib.rs @@ -28,6 +28,7 @@ impl Plugin for SpritePlugin { 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) diff --git a/pipelined/bevy_sprite2/src/render/mod.rs b/pipelined/bevy_sprite2/src/render/mod.rs index 30ff7adc70d4f..019e20f0d8d6e 100644 --- a/pipelined/bevy_sprite2/src/render/mod.rs +++ b/pipelined/bevy_sprite2/src/render/mod.rs @@ -151,6 +151,7 @@ struct ExtractedSprite { atlas_size: Option, } +#[derive(Default)] pub struct ExtractedSprites { sprites: Vec, } @@ -179,10 +180,6 @@ pub fn extract_atlases( if let Some(mut extracted_sprites_res) = render_world.get_resource_mut::() { extracted_sprites_res.sprites.extend(extracted_sprites); - } else { - render_world.insert_resource(ExtractedSprites { - sprites: extracted_sprites, - }); } } @@ -210,10 +207,6 @@ pub fn extract_sprites( if let Some(mut extracted_sprites_res) = render_world.get_resource_mut::() { extracted_sprites_res.sprites.extend(extracted_sprites); - } else { - render_world.insert_resource(ExtractedSprites { - sprites: extracted_sprites, - }); } } @@ -328,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>, ) { @@ -384,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 index 2ffbe43c40d35..58ded48a3a920 100644 --- a/pipelined/bevy_sprite2/src/texture_atlas.rs +++ b/pipelined/bevy_sprite2/src/texture_atlas.rs @@ -22,7 +22,6 @@ pub struct TextureAtlas { #[derive(Debug, Clone, TypeUuid, Reflect)] #[uuid = "7233c597-ccfa-411f-bd59-9af349432ada"] -#[repr(C)] pub struct TextureAtlasSprite { pub color: Color, pub index: u32,