From 2705e5cbb49040a120636097a48e9591c1d72561 Mon Sep 17 00:00:00 2001 From: Carter Anderson Date: Sat, 6 Jun 2020 00:12:38 -0700 Subject: [PATCH] add texture atlases --- Cargo.toml | 4 + crates/bevy_gltf/src/loader.rs | 1 + crates/bevy_render/src/camera/projection.rs | 1 + crates/bevy_render/src/lib.rs | 5 +- crates/bevy_sprite/Cargo.toml | 4 +- crates/bevy_sprite/src/entity.rs | 10 +-- crates/bevy_sprite/src/lib.rs | 8 +- crates/bevy_sprite/src/render/mod.rs | 6 +- .../bevy_sprite/src/render/sprite_sheet.frag | 6 +- .../bevy_sprite/src/render/sprite_sheet.vert | 23 ++--- .../src/{sprite_sheet.rs => texture_atlas.rs} | 34 ++++--- .../bevy_sprite/src/texture_atlas_builder.rs | 88 +++++++++++++++++++ examples/2d/sprite_sheet.rs | 20 ++--- examples/2d/texture_atlas.rs | 79 +++++++++++++++++ examples/ui/ui.rs | 8 +- src/prelude.rs | 2 +- 16 files changed, 242 insertions(+), 57 deletions(-) rename crates/bevy_sprite/src/{sprite_sheet.rs => texture_atlas.rs} (55%) create mode 100644 crates/bevy_sprite/src/texture_atlas_builder.rs create mode 100644 examples/2d/texture_atlas.rs diff --git a/Cargo.toml b/Cargo.toml index 9112c8a4dd3c7..8ee510cd2ee1b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -64,6 +64,10 @@ path = "examples/2d/sprite.rs" name = "sprite_sheet" path = "examples/2d/sprite_sheet.rs" +[[example]] +name = "texture_atlas" +path = "examples/2d/texture_atlas.rs" + [[example]] name = "load_model" path = "examples/3d/load_model.rs" diff --git a/crates/bevy_gltf/src/loader.rs b/crates/bevy_gltf/src/loader.rs index 14c1bed04f587..a01716c060eba 100644 --- a/crates/bevy_gltf/src/loader.rs +++ b/crates/bevy_gltf/src/loader.rs @@ -46,6 +46,7 @@ fn get_primitive_topology(mode: Mode) -> Result { } } +// TODO: this should return a scene pub fn load_gltf(asset_path: &Path, bytes: Vec) -> Result { let gltf = gltf::Gltf::from_slice(&bytes)?; let buffer_data = load_buffers(gltf.buffers(), asset_path)?; diff --git a/crates/bevy_render/src/camera/projection.rs b/crates/bevy_render/src/camera/projection.rs index 3e35afdfccbfa..8cc17ff7f6a72 100644 --- a/crates/bevy_render/src/camera/projection.rs +++ b/crates/bevy_render/src/camera/projection.rs @@ -36,6 +36,7 @@ impl Default for PerspectiveProjection { } } +// TODO: make this a component instead of a property #[derive(Debug, Clone, Property, Serialize, Deserialize)] pub enum WindowOrigin { Center, diff --git a/crates/bevy_render/src/lib.rs b/crates/bevy_render/src/lib.rs index aa3ea494e5ee1..05b393b5bda5b 100644 --- a/crates/bevy_render/src/lib.rs +++ b/crates/bevy_render/src/lib.rs @@ -91,7 +91,10 @@ impl AppPlugin for RenderPlugin { .init_resource::() .init_resource::() .init_resource::() - .add_system(entity_render_resource_assignments_system()) + .add_system_to_stage( + bevy_app::stage::POST_UPDATE, + entity_render_resource_assignments_system(), + ) .init_system_to_stage( bevy_app::stage::POST_UPDATE, camera::camera_system::, diff --git a/crates/bevy_sprite/Cargo.toml b/crates/bevy_sprite/Cargo.toml index dead3e84c2e05..e43dd9819d158 100644 --- a/crates/bevy_sprite/Cargo.toml +++ b/crates/bevy_sprite/Cargo.toml @@ -14,5 +14,7 @@ bevy_type_registry = { path = "../bevy_type_registry" } bevy_derive = { path = "../bevy_derive" } bevy_render = { path = "../bevy_render" } bevy_transform = { path = "../bevy_transform" } + +legion = { path = "../bevy_legion", features = ["serialize"] } glam = "0.8.7" -legion = { path = "../bevy_legion", features = ["serialize"] } \ No newline at end of file +guillotiere = "0.5.2" \ No newline at end of file diff --git a/crates/bevy_sprite/src/entity.rs b/crates/bevy_sprite/src/entity.rs index 75115dcbe3d1c..e3d42f4b4e752 100644 --- a/crates/bevy_sprite/src/entity.rs +++ b/crates/bevy_sprite/src/entity.rs @@ -1,6 +1,6 @@ use crate::{ - render::SPRITE_PIPELINE_HANDLE, sprite::Sprite, ColorMaterial, Quad, SpriteSheet, - SpriteSheetSprite, QUAD_HANDLE, SPRITE_SHEET_PIPELINE_HANDLE, + render::SPRITE_PIPELINE_HANDLE, sprite::Sprite, ColorMaterial, Quad, TextureAtlas, + TextureAtlasSprite, QUAD_HANDLE, SPRITE_SHEET_PIPELINE_HANDLE, }; use bevy_asset::Handle; use bevy_derive::EntityArchetype; @@ -32,8 +32,8 @@ impl Default for SpriteEntity { #[derive(EntityArchetype)] pub struct SpriteSheetEntity { - pub sprite: SpriteSheetSprite, - pub sprite_sheet: Handle, + pub sprite: TextureAtlasSprite, + pub texture_atlas: Handle, pub renderable: Renderable, pub mesh: Handle, // TODO: maybe abstract this out // pub local_to_world: LocalToWorld, @@ -46,7 +46,7 @@ impl Default for SpriteSheetEntity { fn default() -> Self { Self { sprite: Default::default(), - sprite_sheet: Default::default(), + texture_atlas: Default::default(), renderable: Renderable { pipelines: vec![SPRITE_SHEET_PIPELINE_HANDLE], ..Default::default() diff --git a/crates/bevy_sprite/src/lib.rs b/crates/bevy_sprite/src/lib.rs index c88a64be102d6..55718938f5464 100644 --- a/crates/bevy_sprite/src/lib.rs +++ b/crates/bevy_sprite/src/lib.rs @@ -4,14 +4,16 @@ mod quad; mod rect; mod render; mod sprite; -mod sprite_sheet; +mod texture_atlas; +mod texture_atlas_builder; pub use color_material::*; pub use quad::*; pub use rect::*; pub use render::*; pub use sprite::*; -pub use sprite_sheet::*; +pub use texture_atlas::*; +pub use texture_atlas_builder::*; use bevy_app::{stage, AppBuilder, AppPlugin}; use bevy_asset::{AddAsset, Assets, Handle}; @@ -32,7 +34,7 @@ pub const QUAD_HANDLE: Handle = Handle::from_u128(142404619811301375266013 impl AppPlugin for SpritePlugin { fn build(&self, app: &mut AppBuilder) { app.add_asset::() - .add_asset::() + .add_asset::() .add_system_to_stage(stage::POST_UPDATE, sprite_system()) .add_system_to_stage( stage::POST_UPDATE, diff --git a/crates/bevy_sprite/src/render/mod.rs b/crates/bevy_sprite/src/render/mod.rs index ddb4a68a3fccf..7465786a1b7f6 100644 --- a/crates/bevy_sprite/src/render/mod.rs +++ b/crates/bevy_sprite/src/render/mod.rs @@ -1,4 +1,4 @@ -use crate::{ColorMaterial, Quad, SpriteSheet, SpriteSheetSprite}; +use crate::{ColorMaterial, Quad, TextureAtlas, TextureAtlasSprite}; use bevy_asset::{Assets, Handle}; use bevy_render::{ base_render_graph, @@ -135,12 +135,12 @@ impl SpriteRenderGraphBuilder for RenderGraph { self.add_system_node( node::SPRITE_SHEET, - AssetUniformNode::::new(false), + AssetUniformNode::::new(false), ); self.add_system_node( node::SPRITE_SHEET_SPRITE, - UniformNode::::new(true), + UniformNode::::new(true), ); let mut pipelines = resources.get_mut::>().unwrap(); diff --git a/crates/bevy_sprite/src/render/sprite_sheet.frag b/crates/bevy_sprite/src/render/sprite_sheet.frag index 289ba6729a041..d2bcdbe6e468b 100644 --- a/crates/bevy_sprite/src/render/sprite_sheet.frag +++ b/crates/bevy_sprite/src/render/sprite_sheet.frag @@ -4,11 +4,11 @@ layout(location = 0) in vec2 v_Uv; layout(location = 0) out vec4 o_Target; -layout(set = 1, binding = 2) uniform texture2D SpriteSheet_texture; -layout(set = 1, binding = 3) uniform sampler SpriteSheet_texture_sampler; +layout(set = 1, binding = 2) uniform texture2D TextureAtlas_texture; +layout(set = 1, binding = 3) uniform sampler TextureAtlas_texture_sampler; void main() { o_Target = texture( - sampler2D(SpriteSheet_texture, SpriteSheet_texture_sampler), + sampler2D(TextureAtlas_texture, TextureAtlas_texture_sampler), v_Uv); } diff --git a/crates/bevy_sprite/src/render/sprite_sheet.vert b/crates/bevy_sprite/src/render/sprite_sheet.vert index 5109665a6ccee..459cb2332b3d1 100644 --- a/crates/bevy_sprite/src/render/sprite_sheet.vert +++ b/crates/bevy_sprite/src/render/sprite_sheet.vert @@ -4,11 +4,6 @@ layout(location = 0) in vec3 Vertex_Position; layout(location = 1) in vec3 Vertex_Normal; layout(location = 2) in vec2 Vertex_Uv; -// TODO: uncomment when instancing is implemented -// sprite -// layout(location = 0) in vec3 Sprite_Position; -// layout(location = 1) in int Sprite_Index; - layout(location = 0) out vec2 v_Uv; layout(set = 0, binding = 0) uniform Camera2d { @@ -16,7 +11,7 @@ layout(set = 0, binding = 0) uniform Camera2d { }; // TODO: merge dimensions into "sprites" buffer when that is supported in the Uniforms derive abstraction -layout(set = 1, binding = 0) uniform SpriteSheet_dimensions { +layout(set = 1, binding = 0) uniform TextureAtlas_dimensions { vec2 Dimensions; }; @@ -25,21 +20,21 @@ struct Rect { vec2 end; }; -layout(set = 1, binding = 1) buffer SpriteSheet_sprites { - Rect[] Sprites; +layout(set = 1, binding = 1) buffer TextureAtlas_textures { + Rect[] Textures; }; -layout(set = 2, binding = 0) uniform SpriteSheetSprite { - vec3 SpriteSheetSprite_position; - float SpriteSheetSprite_scale; - uint SpriteSheetSprite_index; +layout(set = 2, binding = 0) uniform TextureAtlasSprite { + vec3 TextureAtlasSprite_position; + float TextureAtlasSprite_scale; + uint TextureAtlasSprite_index; }; void main() { - Rect sprite_rect = Sprites[SpriteSheetSprite_index]; + Rect sprite_rect = Textures[TextureAtlasSprite_index]; vec2 sprite_dimensions = sprite_rect.end - sprite_rect.begin; - vec3 vertex_position = vec3(Vertex_Position.xy * sprite_dimensions * SpriteSheetSprite_scale, 0.0) + SpriteSheetSprite_position; + vec3 vertex_position = vec3(Vertex_Position.xy * sprite_dimensions * TextureAtlasSprite_scale, 0.0) + TextureAtlasSprite_position; vec2 uvs[4] = vec2[]( vec2(sprite_rect.begin.x, sprite_rect.end.y), sprite_rect.begin, diff --git a/crates/bevy_sprite/src/sprite_sheet.rs b/crates/bevy_sprite/src/texture_atlas.rs similarity index 55% rename from crates/bevy_sprite/src/sprite_sheet.rs rename to crates/bevy_sprite/src/texture_atlas.rs index fdc0e6009c844..5a31aa7abae01 100644 --- a/crates/bevy_sprite/src/sprite_sheet.rs +++ b/crates/bevy_sprite/src/texture_atlas.rs @@ -3,50 +3,60 @@ use bevy_asset::Handle; use bevy_derive::{Bytes, Uniform, Uniforms}; use bevy_render::texture::Texture; use glam::{Vec2, Vec3}; +use std::collections::HashMap; #[derive(Uniforms)] -pub struct SpriteSheet { +pub struct TextureAtlas { pub texture: Handle, // TODO: add support to Uniforms derive to write dimensions and sprites to the same buffer pub dimensions: Vec2, #[uniform(buffer)] - pub sprites: Vec, + pub textures: Vec, + #[uniform(ignore)] + pub texture_handles: Option, usize>>, } // NOTE: cannot do `unsafe impl Byteable` here because Vec3 takes up the space of a Vec4. If/when glam changes this we can swap out // Bytes for Byteable as a micro-optimization. https://github.com/bitshifter/glam-rs/issues/36 #[derive(Uniform, Bytes, Default)] -pub struct SpriteSheetSprite { +pub struct TextureAtlasSprite { pub position: Vec3, pub scale: f32, pub index: u32, } -impl SpriteSheet { +impl TextureAtlas { pub fn from_grid( texture: Handle, size: Vec2, columns: usize, rows: usize, - ) -> SpriteSheet { - let sprite_width = size.x() / columns as f32; - let sprite_height = size.y() / rows as f32; + ) -> TextureAtlas { + let texture_width = size.x() / columns as f32; + let texture_height = size.y() / rows as f32; let mut sprites = Vec::new(); for y in 0..rows { for x in 0..columns { sprites.push(Rect { - min: Vec2::new(x as f32 * sprite_width, y as f32 * sprite_height), + min: Vec2::new(x as f32 * texture_width, y as f32 * texture_height), max: Vec2::new( - (x + 1) as f32 * sprite_width, - (y + 1) as f32 * sprite_height, + (x + 1) as f32 * texture_width, + (y + 1) as f32 * texture_height, ), }) } } - SpriteSheet { + TextureAtlas { dimensions: size, - sprites, + textures: sprites, texture, + texture_handles: None, } } + + 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/crates/bevy_sprite/src/texture_atlas_builder.rs b/crates/bevy_sprite/src/texture_atlas_builder.rs new file mode 100644 index 0000000000000..65ba01543fa04 --- /dev/null +++ b/crates/bevy_sprite/src/texture_atlas_builder.rs @@ -0,0 +1,88 @@ +use crate::{TextureAtlas, Rect}; +use bevy_asset::{Assets, Handle}; +use bevy_render::texture::Texture; +use glam::Vec2; +use guillotiere::{size2, Allocation, AtlasAllocator}; +use std::collections::HashMap; + +pub struct TextureAtlasBuilder { + pub texture_allocations: Vec<(Handle, Allocation)>, + pub atlas_allocator: AtlasAllocator, + pub texture: Texture, +} + +impl Default for TextureAtlasBuilder { + fn default() -> Self { + Self::new(Vec2::new(256., 256.)) + } +} + +const FORMAT_SIZE: usize = 4; // TODO: get this from an actual format type +impl TextureAtlasBuilder { + pub fn new(initial_size: Vec2) -> Self { + let width = initial_size.x() as usize; + let height = initial_size.y() as usize; + Self { + texture_allocations: Default::default(), + atlas_allocator: AtlasAllocator::new(size2(width as i32, height as i32)), + texture: Texture::new(vec![0; width * height * FORMAT_SIZE], initial_size), + } + } + + pub fn add_texture(&mut self, texture_handle: Handle, texture: &Texture) { + // TODO: resize if allocation fails + let allocation = self + .atlas_allocator + .allocate(size2(texture.size.x() as i32, texture.size.y() as i32)) + .unwrap(); + let rect = allocation.rectangle; + let atlas_width = self.texture.size.x() as usize; + let rect_width = rect.width() as usize; + + 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; + self.texture.data[begin..end] + .copy_from_slice(&texture.data[texture_begin..texture_end]); + } + self.texture_allocations.push((texture_handle, allocation)); + } + + pub fn remove_texture(&mut self, texture_handle: Handle) { + if let Some(position) = self.texture_allocations.iter().position(|(handle, _)| *handle == texture_handle) { + let (_, allocation) = self.texture_allocations.remove(position); + self.atlas_allocator.deallocate(allocation.id); + } + } + + pub fn finish(self, textures: &mut Assets) -> TextureAtlas { + let mut texture_rects = Vec::with_capacity(self.texture_allocations.len()); + let mut texture_handles = HashMap::with_capacity(self.texture_allocations.len()); + for (index, (handle, allocation)) in self.texture_allocations.iter().enumerate() { + texture_rects.push(allocation.rectangle.into()); + texture_handles.insert(*handle, index); + } + TextureAtlas { + dimensions: to_vec2(self.atlas_allocator.size()), + texture: textures.add(self.texture), + textures: texture_rects, + texture_handles: Some(texture_handles), + } + } +} + +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_vec2(size: guillotiere::Size) -> Vec2 { + Vec2::new(size.width as f32, size.height as f32) +} + \ No newline at end of file diff --git a/examples/2d/sprite_sheet.rs b/examples/2d/sprite_sheet.rs index bf07ed6bb467c..e8bd6db972ddc 100644 --- a/examples/2d/sprite_sheet.rs +++ b/examples/2d/sprite_sheet.rs @@ -9,14 +9,14 @@ fn main() { } fn animate_sprite_system( - sprite_sheets: Res>, + texture_atlases: Res>, mut timer: ComMut, - mut sprite: ComMut, - sprite_sheet_handle: Com>, + mut sprite: ComMut, + texture_atlas_handle: Com>, ) { if timer.finished { - let sprite_sheet = sprite_sheets.get(&sprite_sheet_handle).unwrap(); - sprite.index = ((sprite.index as usize + 1) % sprite_sheet.sprites.len()) as u32; + let texture_atlas = texture_atlases.get(&texture_atlas_handle).unwrap(); + sprite.index = ((sprite.index as usize + 1) % texture_atlas.textures.len()) as u32; timer.reset(); } } @@ -25,21 +25,21 @@ fn setup( command_buffer: &mut CommandBuffer, asset_server: Res, mut textures: ResMut>, - mut sprite_sheets: ResMut>, + mut texture_atlases: ResMut>, ) { env_logger::init(); let texture_handle = asset_server .load_sync(&mut textures, "assets/textures/rpg/chars/gabe/gabe-idle-run.png") .unwrap(); let texture = textures.get(&texture_handle).unwrap(); - let sprite_sheet = SpriteSheet::from_grid(texture_handle, texture.size, 7, 1); - let sprite_sheet_handle = sprite_sheets.add(sprite_sheet); + let texture_atlas = TextureAtlas::from_grid(texture_handle, texture.size, 7, 1); + let texture_atlas_handle = texture_atlases.add(texture_atlas); command_buffer .build() .add_entity(OrthographicCameraEntity::default()) .add_entity(SpriteSheetEntity { - sprite_sheet: sprite_sheet_handle, - sprite: SpriteSheetSprite { + texture_atlas: texture_atlas_handle, + sprite: TextureAtlasSprite { index: 0, scale: 6.0, position: Vec3::new(0.0, 0.0, 0.0), diff --git a/examples/2d/texture_atlas.rs b/examples/2d/texture_atlas.rs new file mode 100644 index 0000000000000..1e22d1af895be --- /dev/null +++ b/examples/2d/texture_atlas.rs @@ -0,0 +1,79 @@ +use bevy::prelude::*; +use bevy_asset::{HandleId, LoadState}; +use bevy_sprite::TextureAtlasBuilder; + +fn main() { + App::build() + .init_resource::() + .init_resource::() + .add_default_plugins() + .add_startup_system(setup.system()) + .add_system(load_atlas.system()) + .run(); +} + +#[derive(Default)] +pub struct RpgSpriteHandles { + handles: Vec, +} + +fn setup( + command_buffer: &mut CommandBuffer, + mut rpg_sprite_handles: ResMut, + asset_server: Res, +) { + rpg_sprite_handles.handles = asset_server + .load_asset_folder("assets/textures/rpg") + .unwrap(); + command_buffer + .build() + .add_entity(OrthographicCameraEntity::default()); +} + +#[derive(Default)] +struct State { + atlas_loaded: bool, +} + +fn load_atlas( + command_buffer: &mut CommandBuffer, + mut state: ResMut, + rpg_sprite_handles: Res, + asset_server: Res, + mut texture_atlases: ResMut>, + mut textures: ResMut>, +) { + if state.atlas_loaded { + return; + } + + let mut texture_atlas_builder = TextureAtlasBuilder::new(Vec2::new(600., 600.)); + if let Some(LoadState::Loaded(_)) = + asset_server.get_group_load_state(&rpg_sprite_handles.handles) + { + // TODO: sort by size (within atlas builder) + for texture_id in rpg_sprite_handles.handles.iter() { + let handle = Handle::from_id(*texture_id); + let texture = textures.get(&handle).unwrap(); + texture_atlas_builder.add_texture(handle, texture); + } + + let texture_atlas = texture_atlas_builder.finish(&mut textures); + let vendor_handle = asset_server + .get_handle("assets/textures/rpg/chars/vendor/generic-rpg-vendor.png") + .unwrap(); + let vendor_index = texture_atlas.get_texture_index(vendor_handle).unwrap(); + let atlas_handle = texture_atlases.add(texture_atlas); + command_buffer.build().add_entity(SpriteSheetEntity { + sprite: TextureAtlasSprite { + index: vendor_index as u32, + scale: 4.0, + ..Default::default() + }, + texture_atlas: atlas_handle, + ..Default::default() + }); + + state.atlas_loaded = true; + } +} diff --git a/examples/ui/ui.rs b/examples/ui/ui.rs index bb79b7d1b7897..5987e774b7b59 100644 --- a/examples/ui/ui.rs +++ b/examples/ui/ui.rs @@ -24,13 +24,13 @@ fn setup( // ..Default::default() // }); - let texture = Texture::load(TextureType::Png( - "assets/branding/bevy_logo_dark_big.png".to_string(), - )); + let texture_handle = asset_server + .load_sync(&mut textures, "assets/branding/bevy_logo_dark_big.png") + .unwrap(); let font_handle = asset_server.load("assets/fonts/FiraSans-Bold.ttf").unwrap(); + let texture = textures.get(&texture_handle).unwrap(); let aspect = texture.aspect(); - let texture_handle = textures.add(texture); let blue_material_handle = materials.add(Color::rgb(0.6, 0.6, 1.0).into()); diff --git a/src/prelude.rs b/src/prelude.rs index 435170f61b6ea..60ba9e385ec50 100644 --- a/src/prelude.rs +++ b/src/prelude.rs @@ -33,7 +33,7 @@ pub use crate::{ scene::{Scene, SceneSpawner}, sprite::{ entity::{SpriteEntity, SpriteSheetEntity}, - ColorMaterial, Quad, Sprite, SpriteSheet, SpriteSheetSprite, + ColorMaterial, Quad, Sprite, TextureAtlas, TextureAtlasSprite, }, text::Font, transform::prelude::*,