Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Merged by Bors] - Add sprite atlases into the new renderer. #2560

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 14 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down Expand Up @@ -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"

Expand Down Expand Up @@ -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"
Expand Down
98 changes: 98 additions & 0 deletions examples/2d/pipelined_texture_atlas.rs
Original file line number Diff line number Diff line change
@@ -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()
});
}
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
43 changes: 41 additions & 2 deletions pipelined/bevy_render2/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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) =
Expand All @@ -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::<ScratchRenderWorld>();

let mut render_app = App::empty();
let mut extract_stage = SystemStage::parallel();
Expand All @@ -89,14 +115,15 @@ impl Plugin for RenderPlugin {
.init_resource::<RenderGraph>()
.init_resource::<DrawFunctions>();

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();
render_app
.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.
Expand Down Expand Up @@ -156,6 +183,18 @@ fn extract(app_world: &mut World, render_app: &mut App) {
.schedule
.get_stage_mut::<SystemStage>(&RenderStage::Extract)
.unwrap();

// temporarily add the render world to the app world as a resource
let scratch_world = app_world.remove_resource::<ScratchRenderWorld>().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::<RenderWorld>().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);
}
8 changes: 6 additions & 2 deletions pipelined/bevy_sprite2/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
29 changes: 28 additions & 1 deletion pipelined/bevy_sprite2/src/bundle.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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 changes: 101 additions & 0 deletions pipelined/bevy_sprite2/src/dynamic_texture_atlas_builder.rs
Original file line number Diff line number Diff line change
@@ -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)
}
Loading