mirror of
https://github.com/bevyengine/bevy
synced 2024-11-22 04:33:37 +00:00
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:
parent
ae4f809a52
commit
115b170d1f
11 changed files with 711 additions and 37 deletions
18
Cargo.toml
18
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"
|
||||
|
|
98
examples/2d/pipelined_texture_atlas.rs
Normal file
98
examples/2d/pipelined_texture_atlas.rs
Normal 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()
|
||||
});
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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"] }
|
||||
|
|
|
@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
101
pipelined/bevy_sprite2/src/dynamic_texture_atlas_builder.rs
Normal file
101
pipelined/bevy_sprite2/src/dynamic_texture_atlas_builder.rs
Normal 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)
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
145
pipelined/bevy_sprite2/src/texture_atlas.rs
Normal file
145
pipelined/bevy_sprite2/src/texture_atlas.rs
Normal 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())
|
||||
}
|
||||
}
|
236
pipelined/bevy_sprite2/src/texture_atlas_builder.rs
Normal file
236
pipelined/bevy_sprite2/src/texture_atlas_builder.rs
Normal 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),
|
||||
})
|
||||
}
|
||||
}
|
Loading…
Reference in a new issue