diff --git a/Cargo.toml b/Cargo.toml index d2bdaf24868f4f..43be84ffe132ae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -200,6 +200,16 @@ description = "Renders a sprite" category = "2D Rendering" wasm = true +[[example]] +name = "sprite_tile" +path = "examples/2d/sprite_tile.rs" + +[package.metadata.example.sprite_tile] +name = "Sprite Tile" +description = "Renders a sprite tiled in a grid" +category = "2D Rendering" +wasm = true + [[example]] name = "sprite_slice" path = "examples/2d/sprite_slice.rs" diff --git a/crates/bevy_sprite/src/lib.rs b/crates/bevy_sprite/src/lib.rs index bf268d53192a59..4eb5e33074e959 100644 --- a/crates/bevy_sprite/src/lib.rs +++ b/crates/bevy_sprite/src/lib.rs @@ -15,7 +15,7 @@ pub mod prelude { pub use crate::{ bundle::{SpriteBundle, SpriteSheetBundle}, rect::{BorderRect, Rect}, - sprite::Sprite, + sprite::{Sprite, SpriteDrawMode}, texture_atlas::{TextureAtlas, TextureAtlasSprite}, texture_slice::{SliceScaleMode, TextureSlicer}, ColorMaterial, ColorMesh2dBundle, TextureAtlasBuilder, diff --git a/crates/bevy_sprite/src/rect.rs b/crates/bevy_sprite/src/rect.rs index 6353a769aab542..cfb58318fe02e9 100644 --- a/crates/bevy_sprite/src/rect.rs +++ b/crates/bevy_sprite/src/rect.rs @@ -13,7 +13,6 @@ pub struct Rect { } /// Struct defining a [`Sprite`](crate::Sprite) border with padding values -#[repr(C)] #[derive(Default, Clone, Copy, Debug, Reflect)] pub struct BorderRect { /// Pixel padding to the left diff --git a/crates/bevy_sprite/src/render/mod.rs b/crates/bevy_sprite/src/render/mod.rs index 4ae817a30fb7b9..c2e05d60018a90 100644 --- a/crates/bevy_sprite/src/render/mod.rs +++ b/crates/bevy_sprite/src/render/mod.rs @@ -2,7 +2,7 @@ use std::cmp::Ordering; use crate::{ texture_atlas::{TextureAtlas, TextureAtlasSprite}, - Rect, Sprite, TextureSlicer, SPRITE_SHADER_HANDLE, + Rect, Sprite, SpriteDrawMode, TextureSlice, SPRITE_SHADER_HANDLE, }; use bevy_asset::{AssetEvent, Assets, Handle, HandleId}; use bevy_core_pipeline::core_2d::Transparent2d; @@ -224,13 +224,7 @@ pub fn extract_sprites( mut render_world: ResMut, texture_atlases: Res>, images: Res>, - sprite_query: Query<( - &Visibility, - &Sprite, - &GlobalTransform, - &Handle, - Option<&TextureSlicer>, - )>, + sprite_query: Query<(&Visibility, &Sprite, &GlobalTransform, &Handle)>, atlas_query: Query<( &Visibility, &TextureAtlasSprite, @@ -240,43 +234,54 @@ pub fn extract_sprites( ) { let mut extracted_sprites = render_world.resource_mut::(); extracted_sprites.sprites.clear(); - for (visibility, sprite, transform, handle, slicer) in sprite_query.iter() { + for (visibility, sprite, transform, handle) in sprite_query.iter() { if !visibility.is_visible { continue; } - if let Some(slicer) = slicer { - let image_size = match images.get(handle) { - None => continue, - Some(i) => Vec2::new( - i.texture_descriptor.size.width as f32, - i.texture_descriptor.size.height as f32, - ), - }; - let slices = - slicer.compute_slices(image_size, sprite.custom_size.unwrap_or(image_size)); - for slice in slices { - let mut transform: GlobalTransform = *transform; - transform.translation = transform.mul_vec3(slice.offset.extend(0.0)); - extracted_sprites.sprites.alloc().init(ExtractedSprite { - color: sprite.color, - transform, - rect: Some(slice.texture_rect), - custom_size: Some(slice.draw_size), - flip_x: sprite.flip_x, - flip_y: sprite.flip_y, - image_handle_id: handle.id, - anchor: sprite.anchor.as_vec(), - }); + let image_size = match images.get(handle) { + None => continue, + Some(i) => Vec2::new( + i.texture_descriptor.size.width as f32, + i.texture_descriptor.size.height as f32, + ), + }; + + let slices = match &sprite.draw_mode { + SpriteDrawMode::Simple => vec![TextureSlice { + texture_rect: Rect { + min: Vec2::ZERO, + max: image_size, + }, + draw_size: sprite.custom_size.unwrap_or(image_size), + offset: Vec2::ZERO, + }], + SpriteDrawMode::Sliced(slicer) => { + slicer.compute_slices(image_size, sprite.custom_size.unwrap_or(image_size)) } - } else { - // PERF: we don't check in this function that the `Image` asset is ready, since it should be in most cases and hashing the handle is expensive + SpriteDrawMode::Tiled { + tile_x, + tile_y, + stretch_value, + } => { + let slice = TextureSlice { + texture_rect: Rect { + min: Vec2::ZERO, + max: image_size, + }, + draw_size: sprite.custom_size.unwrap_or(image_size), + offset: Vec2::ZERO, + }; + slice.tiled(*stretch_value, (*tile_x, *tile_y)) + } + }; + for slice in slices { + let mut transform: GlobalTransform = *transform; + transform.translation = transform.mul_vec3(slice.offset.extend(0.0)); extracted_sprites.sprites.alloc().init(ExtractedSprite { color: sprite.color, - transform: *transform, - // Use the full texture - rect: None, - // Pass the custom size - custom_size: sprite.custom_size, + transform, + rect: Some(slice.texture_rect), + custom_size: Some(slice.draw_size), flip_x: sprite.flip_x, flip_y: sprite.flip_y, image_handle_id: handle.id, diff --git a/crates/bevy_sprite/src/sprite.rs b/crates/bevy_sprite/src/sprite.rs index e39298a649f513..5335b70cafa2af 100644 --- a/crates/bevy_sprite/src/sprite.rs +++ b/crates/bevy_sprite/src/sprite.rs @@ -1,3 +1,4 @@ +use crate::TextureSlicer; use bevy_ecs::component::Component; use bevy_math::Vec2; use bevy_reflect::Reflect; @@ -17,6 +18,27 @@ pub struct Sprite { pub custom_size: Option, /// [`Anchor`] point of the sprite in the world pub anchor: Anchor, + /// Define how the Sprite scales when its dimensions change + pub draw_mode: SpriteDrawMode, +} + +#[derive(Debug, Default, Clone, Reflect)] +pub enum SpriteDrawMode { + /// The entire texture scales when its dimensions change. This is the default option. + #[default] + Simple, + /// The texture will be cut in 9 slices, keeping the texture in proportions on resize + Sliced(TextureSlicer), + /// The texture will be repeated if stretched beyond `stretched_value` + Tiled { + /// Should the image repeat horizontally + tile_x: bool, + /// Should the image repeat vertically + tile_y: bool, + /// The texture will repeat when the ratio between the *drawing dimensions* of texture and the + /// *original texture size* are above this value + stretch_value: f32, + }, } /// How a sprite is positioned relative to its [`Transform`](bevy_transform::components::Transform). diff --git a/crates/bevy_sprite/src/texture_slice.rs b/crates/bevy_sprite/src/texture_slice.rs index ccf772c5e7ed02..78b1921093b3fa 100644 --- a/crates/bevy_sprite/src/texture_slice.rs +++ b/crates/bevy_sprite/src/texture_slice.rs @@ -1,12 +1,16 @@ use crate::{BorderRect, Rect}; -use bevy_ecs::component::Component; use bevy_math::Vec2; use bevy_reflect::Reflect; -/// Component for [9-sliced](https://en.wikipedia.org/wiki/9-slice_scaling) textures. +/// Slices a texture using the **9-slicing** technique. This allows to reuse an image at various sizes +/// without needing to prepare multiple assets. The associated texture will be split tinto nine portions, +/// so that on resize the different portions scale or tile in different ways to keep the texture in proportion. /// -/// When resizing a 9-sliced texture the corners will remain unscaled while the other sections will be scaled or tiled -#[derive(Debug, Clone, Component, Reflect)] +/// For example, when resizing a 9-sliced texture the corners will remain unscaled while the other +/// sections will be scaled or tiled. +/// +/// See [9-sliced](https://en.wikipedia.org/wiki/9-slice_scaling) textures. +#[derive(Debug, Clone, Reflect)] pub struct TextureSlicer { /// The sprite borders, defining the 9 sections of the image pub border: BorderRect, @@ -28,7 +32,7 @@ pub enum SliceScaleMode { Stretch, /// The slice will be tiled to fit the area Tile { - /// The will repeat when the ratio between the *drawing dimensions* of texture and the + /// The slice will repeat when the ratio between the *drawing dimensions* of texture and the /// *original texture size* are above `stretch_value`. /// /// Note: The value should be inferior or equal to `1.0` to avoid quality loss. @@ -108,6 +112,7 @@ impl TextureSlicer { ] } + /// Computes the 2 horizontal side slices (left and right borders) pub(crate) fn horizontal_side_slices( &self, [tl_corner, tr_corner, bl_corner, br_corner]: &[TextureSlice; 4], @@ -142,6 +147,7 @@ impl TextureSlicer { [left_side, right_side] } + /// Computes the 2 vertical side slices (top and bottom borders) pub(crate) fn vertical_side_slices( &self, [tl_corner, tr_corner, bl_corner, br_corner]: &[TextureSlice; 4], @@ -177,7 +183,7 @@ impl TextureSlicer { } pub(crate) fn compute_slices(&self, image_size: Vec2, render_size: Vec2) -> Vec { - let mut res = Vec::with_capacity(9); + let mut slices = Vec::with_capacity(9); // Corners let corners = self.corner_slices(image_size, render_size); // Sides @@ -199,38 +205,46 @@ impl TextureSlicer { offset: Vec2::ZERO, }; // Res - res.extend(corners); + slices.extend(corners); match self.center_scale_mode { SliceScaleMode::Stretch => { - res.push(center); + slices.push(center); } SliceScaleMode::Tile { stretch_value } => { - res.extend(center.tiled(stretch_value, (true, true))); + slices.extend(center.tiled(stretch_value, (true, true))); } } match self.sides_scale_mode { SliceScaleMode::Stretch => { - res.extend(horizontal_sides); - res.extend(vertical_sides); + slices.extend(horizontal_sides); + slices.extend(vertical_sides); } SliceScaleMode::Tile { stretch_value } => { - res.extend( + slices.extend( horizontal_sides .into_iter() .flat_map(|s| s.tiled(stretch_value, (false, true))), ); - res.extend( + slices.extend( vertical_sides .into_iter() .flat_map(|s| s.tiled(stretch_value, (true, false))), ); } } - res + slices } } impl TextureSlice { + /// Transforms the given slice in an collection of tiled subdivisions. + /// + /// # Arguments + /// + /// * `stretch_value` - The slice will repeat when the ratio between the *drawing dimensions* of texture and the + /// *original texture size* are above `stretch_value`. + /// - `tile_x` - should the slice be tiled horizontally + /// - `tile_y` - should the slice be tiled vertically pub fn tiled(self, stretch_value: f32, (tile_x, tile_y): (bool, bool)) -> Vec { if !tile_x && !tile_y { return vec![self]; @@ -249,7 +263,7 @@ impl TextureSlice { self.draw_size.y }, ); - let mut res = Vec::new(); + let mut slices = Vec::new(); let base_offset = -self.draw_size / 2.0; let mut offset = base_offset; @@ -264,7 +278,7 @@ impl TextureSlice { offset.x += size_x / 2.0; let draw_size = Vec2::new(size_x, size_y); let delta = draw_size / expected_size; - res.push(Self { + slices.push(Self { texture_rect: Rect { min: self.texture_rect.min, max: self.texture_rect.min + self.texture_rect.size() * delta, @@ -278,7 +292,7 @@ impl TextureSlice { offset.y += size_y / 2.0; remaining_columns -= size_y; } - res + slices } } diff --git a/examples/2d/sprite_slice.rs b/examples/2d/sprite_slice.rs index f8c1f1403c65a0..7f02f1d34f5308 100644 --- a/examples/2d/sprite_slice.rs +++ b/examples/2d/sprite_slice.rs @@ -43,75 +43,71 @@ fn spawn_sprites( }); // Stretched Scaled sliced sprite - commands - .spawn_bundle(SpriteBundle { - transform: Transform::from_translation(base_pos + Vec3::X * 300.0), - texture: texture_handle.clone(), - sprite: Sprite { - custom_size: Some(Vec2::new(100.0, 200.0)), + commands.spawn_bundle(SpriteBundle { + transform: Transform::from_translation(base_pos + Vec3::X * 300.0), + texture: texture_handle.clone(), + sprite: Sprite { + custom_size: Some(Vec2::new(100.0, 200.0)), + draw_mode: SpriteDrawMode::Sliced(TextureSlicer { + border: BorderRect::square(slice_border), + center_scale_mode: SliceScaleMode::Stretch, ..default() - }, + }), ..default() - }) - .insert(TextureSlicer { - border: BorderRect::square(slice_border), - center_scale_mode: SliceScaleMode::Stretch, - ..default() - }); + }, + ..default() + }); // Scaled sliced sprite - commands - .spawn_bundle(SpriteBundle { - transform: Transform::from_translation(base_pos + Vec3::X * 450.0), - texture: texture_handle.clone(), - sprite: Sprite { - custom_size: Some(Vec2::new(100.0, 200.0)), + commands.spawn_bundle(SpriteBundle { + transform: Transform::from_translation(base_pos + Vec3::X * 450.0), + texture: texture_handle.clone(), + sprite: Sprite { + custom_size: Some(Vec2::new(100.0, 200.0)), + draw_mode: SpriteDrawMode::Sliced(TextureSlicer { + border: BorderRect::square(slice_border), + center_scale_mode: SliceScaleMode::Tile { stretch_value: 0.5 }, + sides_scale_mode: SliceScaleMode::Tile { stretch_value: 0.2 }, ..default() - }, + }), ..default() - }) - .insert(TextureSlicer { - border: BorderRect::square(slice_border), - center_scale_mode: SliceScaleMode::Tile { stretch_value: 0.5 }, - sides_scale_mode: SliceScaleMode::Tile { stretch_value: 0.2 }, - ..default() - }); + }, + ..default() + }); // Scaled sliced sprite horizontally - commands - .spawn_bundle(SpriteBundle { - transform: Transform::from_translation(base_pos + Vec3::X * 700.0), - texture: texture_handle.clone(), - sprite: Sprite { - custom_size: Some(Vec2::new(300.0, 200.0)), + commands.spawn_bundle(SpriteBundle { + transform: Transform::from_translation(base_pos + Vec3::X * 700.0), + texture: texture_handle.clone(), + sprite: Sprite { + custom_size: Some(Vec2::new(300.0, 200.0)), + draw_mode: SpriteDrawMode::Sliced(TextureSlicer { + border: BorderRect::square(slice_border), + center_scale_mode: SliceScaleMode::Tile { stretch_value: 0.2 }, + sides_scale_mode: SliceScaleMode::Tile { stretch_value: 0.3 }, ..default() - }, + }), ..default() - }) - .insert(TextureSlicer { - border: BorderRect::square(slice_border), - center_scale_mode: SliceScaleMode::Tile { stretch_value: 0.2 }, - sides_scale_mode: SliceScaleMode::Tile { stretch_value: 0.3 }, - ..default() - }); + }, + ..default() + }); // Scaled sliced sprite horizontally with max scale - commands - .spawn_bundle(SpriteBundle { - transform: Transform::from_translation(base_pos + Vec3::X * 1050.0), - texture: texture_handle, - sprite: Sprite { - custom_size: Some(Vec2::new(300.0, 200.0)), - ..default() - }, + commands.spawn_bundle(SpriteBundle { + transform: Transform::from_translation(base_pos + Vec3::X * 1050.0), + texture: texture_handle, + sprite: Sprite { + custom_size: Some(Vec2::new(300.0, 200.0)), + draw_mode: SpriteDrawMode::Sliced(TextureSlicer { + border: BorderRect::square(slice_border), + center_scale_mode: SliceScaleMode::Tile { stretch_value: 0.1 }, + sides_scale_mode: SliceScaleMode::Tile { stretch_value: 0.2 }, + max_corner_scale: 0.2, + }), ..default() - }) - .insert(TextureSlicer { - border: BorderRect::square(slice_border), - center_scale_mode: SliceScaleMode::Tile { stretch_value: 0.1 }, - sides_scale_mode: SliceScaleMode::Tile { stretch_value: 0.2 }, - max_corner_scale: 0.2, - }); + }, + ..default() + }); } fn setup(mut commands: Commands, asset_server: Res) { diff --git a/examples/2d/sprite_tile.rs b/examples/2d/sprite_tile.rs new file mode 100644 index 00000000000000..f7cdd32cdf99ff --- /dev/null +++ b/examples/2d/sprite_tile.rs @@ -0,0 +1,27 @@ +//! Displays a single [`Sprite`] tiled in a grid + +use bevy::prelude::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_startup_system(setup) + .run(); +} + +fn setup(mut commands: Commands, asset_server: Res) { + commands.spawn_bundle(Camera2dBundle::default()); + commands.spawn_bundle(SpriteBundle { + texture: asset_server.load("branding/icon.png"), + sprite: Sprite { + custom_size: Some(Vec2::splat(512.0)), // The image size is 256px + draw_mode: SpriteDrawMode::Tiled { + tile_x: true, + tile_y: true, + stretch_value: 1.0, // The image will tile every 256px + }, + ..default() + }, + ..default() + }); +} diff --git a/examples/README.md b/examples/README.md index 8463bc57123662..8cb7fdf57d7a22 100644 --- a/examples/README.md +++ b/examples/README.md @@ -97,6 +97,7 @@ Example | Description [Sprite Flipping](../examples/2d/sprite_flipping.rs) | Renders a sprite flipped along an axis [Sprite Sheet](../examples/2d/sprite_sheet.rs) | Renders an animated sprite [Sprite Slice](../examples/2d/sprite_slice.rs) | Showcases slicing sprites into sections that can be scaled independently via the 9-patch technique +[Sprite Tile](../examples/2d/sprite_tile.rs) | Renders a sprite tiled in a grid [Text 2D](../examples/2d/text2d.rs) | Generates text in 2D [Texture Atlas](../examples/2d/texture_atlas.rs) | Generates a texture atlas (sprite sheet) from individual sprites [Transparency in 2D](../examples/2d/transparency_2d.rs) | Demonstrates transparency in 2d