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

Sprite slicing and tiling #10588

Merged
merged 11 commits into from
Jan 15, 2024
Merged
Show file tree
Hide file tree
Changes from 9 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
20 changes: 20 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -455,6 +455,26 @@ description = "Renders an animated 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"

[package.metadata.example.sprite_slice]
name = "Sprite Slice"
description = "Showcases slicing sprites into sections that can be scaled independently via the 9-patch technique"
category = "2D Rendering"
wasm = true

[[example]]
name = "text2d"
path = "examples/2d/text2d.rs"
Expand Down
Binary file added assets/textures/slice_square.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added assets/textures/slice_square_2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 5 additions & 1 deletion crates/bevy_sprite/src/bundle.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use crate::{
texture_atlas::{TextureAtlas, TextureAtlasSprite},
Sprite,
ImageScaleMode, Sprite,
};
use bevy_asset::Handle;
use bevy_ecs::bundle::Bundle;
Expand All @@ -15,6 +15,8 @@ use bevy_transform::components::{GlobalTransform, Transform};
pub struct SpriteBundle {
/// Specifies the rendering properties of the sprite, such as color tint and flip.
pub sprite: Sprite,
/// Controls how the image is altered when scaled.
pub scale_mode: ImageScaleMode,
/// The local transform of the sprite, relative to its parent.
pub transform: Transform,
/// The absolute transform of the sprite. This should generally not be written to directly.
Expand All @@ -35,6 +37,8 @@ pub struct SpriteBundle {
pub struct SpriteSheetBundle {
/// The specific sprite from the texture atlas to be drawn, defaulting to the sprite at index 0.
pub sprite: TextureAtlasSprite,
/// Controls how the image is altered when scaled.
pub scale_mode: ImageScaleMode,
/// 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
Expand Down
17 changes: 15 additions & 2 deletions crates/bevy_sprite/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,17 @@ mod render;
mod sprite;
mod texture_atlas;
mod texture_atlas_builder;
mod texture_slice;

pub mod collide_aabb;

pub mod prelude {
#[doc(hidden)]
pub use crate::{
bundle::{SpriteBundle, SpriteSheetBundle},
sprite::Sprite,
sprite::{ImageScaleMode, Sprite},
texture_atlas::{TextureAtlas, TextureAtlasSprite},
texture_slice::{BorderRect, SliceScaleMode, TextureSlicer},
ColorMaterial, ColorMesh2dBundle, TextureAtlasBuilder,
};
}
Expand All @@ -26,6 +28,7 @@ pub use render::*;
pub use sprite::*;
pub use texture_atlas::*;
pub use texture_atlas_builder::*;
pub use texture_slice::*;

use bevy_app::prelude::*;
use bevy_asset::{load_internal_asset, AssetApp, Assets, Handle};
Expand All @@ -51,6 +54,7 @@ pub const SPRITE_SHADER_HANDLE: Handle<Shader> = Handle::weak_from_u128(27633439
#[derive(Debug, Hash, PartialEq, Eq, Clone, SystemSet)]
pub enum SpriteSystem {
ExtractSprites,
ComputeSlices,
}

impl Plugin for SpritePlugin {
Expand All @@ -64,13 +68,22 @@ impl Plugin for SpritePlugin {
app.init_asset::<TextureAtlas>()
.register_asset_reflect::<TextureAtlas>()
.register_type::<Sprite>()
.register_type::<ImageScaleMode>()
.register_type::<TextureSlicer>()
.register_type::<TextureAtlasSprite>()
.register_type::<Anchor>()
.register_type::<Mesh2dHandle>()
.add_plugins((Mesh2dRenderPlugin, ColorMaterialPlugin))
.add_systems(
PostUpdate,
calculate_bounds_2d.in_set(VisibilitySystems::CalculateBounds),
(
calculate_bounds_2d.in_set(VisibilitySystems::CalculateBounds),
(
compute_slices_on_asset_event,
compute_slices_on_sprite_change,
)
.in_set(SpriteSystem::ComputeSlices),
),
);

if let Ok(render_app) = app.get_sub_app_mut(RenderApp) {
Expand Down
46 changes: 28 additions & 18 deletions crates/bevy_sprite/src/render/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use std::ops::Range;

use crate::{
texture_atlas::{TextureAtlas, TextureAtlasSprite},
Sprite, SPRITE_SHADER_HANDLE,
ComputedTextureSlices, Sprite, SPRITE_SHADER_HANDLE,
};
use bevy_asset::{AssetEvent, AssetId, Assets, Handle};
use bevy_core_pipeline::{
Expand Down Expand Up @@ -333,6 +333,7 @@ pub fn extract_sprite_events(
}

pub fn extract_sprites(
mut commands: Commands,
mut extracted_sprites: ResMut<ExtractedSprites>,
texture_atlases: Extract<Res<Assets<TextureAtlas>>>,
sprite_query: Extract<
Expand All @@ -342,6 +343,7 @@ pub fn extract_sprites(
&Sprite,
&GlobalTransform,
&Handle<Image>,
Option<&ComputedTextureSlices>,
)>,
>,
atlas_query: Extract<
Expand All @@ -356,26 +358,34 @@ pub fn extract_sprites(
) {
extracted_sprites.sprites.clear();

for (entity, view_visibility, sprite, transform, handle) in sprite_query.iter() {
for (entity, view_visibility, sprite, transform, handle, slices) in sprite_query.iter() {
if !view_visibility.get() {
continue;
}
// 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
extracted_sprites.sprites.insert(
entity,
ExtractedSprite {
color: sprite.color,
transform: *transform,
rect: sprite.rect,
// Pass the custom size
custom_size: sprite.custom_size,
flip_x: sprite.flip_x,
flip_y: sprite.flip_y,
image_handle_id: handle.id(),
anchor: sprite.anchor.as_vec(),
original_entity: None,
},
);
if let Some(slices) = slices {
extracted_sprites.sprites.extend(
slices
.extract_sprites(transform, entity, sprite, handle)
.map(|e| (commands.spawn_empty().id(), e)),
alice-i-cecile marked this conversation as resolved.
Show resolved Hide resolved
Comment on lines +368 to +369
Copy link
Contributor

@ickshonpe ickshonpe Jan 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could use a custom shader here that draws all the slices in one pass, then only need one entity id is needed.

With a shader based solution, the whole texture_slice module wouldn't be needed even. All it would need is just the ImageScaleMode component, extraction function, some pipeline specialisation stuff, and a fragment shader.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also it isn't very difficult either to modify batching to group sprites together with the same entity id when there isn't any need to sort the group internally. I thought I even wrote a PR somewhere that implemented it for use with Text2d.

);
} 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
extracted_sprites.sprites.insert(
entity,
ExtractedSprite {
color: sprite.color,
transform: *transform,
rect: sprite.rect,
// Pass the custom size
custom_size: sprite.custom_size,
flip_x: sprite.flip_x,
flip_y: sprite.flip_y,
image_handle_id: handle.id(),
anchor: sprite.anchor.as_vec(),
original_entity: None,
},
);
}
}
for (entity, view_visibility, atlas_sprite, transform, texture_atlas_handle) in
atlas_query.iter()
Expand Down
23 changes: 23 additions & 0 deletions crates/bevy_sprite/src/sprite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ use bevy_math::{Rect, Vec2};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_render::color::Color;

use crate::TextureSlicer;

/// Specifies the rendering properties of a sprite.
///
/// This is commonly used as a component within [`SpriteBundle`](crate::bundle::SpriteBundle).
Expand All @@ -26,6 +28,27 @@ pub struct Sprite {
pub anchor: Anchor,
}

/// Controls how the image is altered when scaled.
#[derive(Component, Debug, Default, Clone, Reflect)]
#[reflect(Component, Default)]
pub enum ImageScaleMode {
Copy link
Contributor

@ickshonpe ickshonpe Jan 13, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure about the name ImageScaleMode but it's not that much a problem I guess, I can't think of anything better.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ImageScalingMode or ImageScalingBehavior is a marginal improvement I think.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe instead of scale, because the other variants aren't really scalings, something like ImageExtendMode or ImageExtensionMode

/// The entire texture stretches when its dimensions change. This is the default option.
#[default]
Stretched,
/// 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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

change to:

/// The image will repeat horizontally.
...
/// The image will repeat vertically.

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).
/// It defaults to `Anchor::Center`.
#[derive(Component, Debug, Clone, Copy, PartialEq, Default, Reflect)]
Expand Down
59 changes: 59 additions & 0 deletions crates/bevy_sprite/src/texture_slice/border_rect.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
use bevy_reflect::Reflect;

/// Struct defining a [`Sprite`](crate::Sprite) border with padding values
#[derive(Default, Copy, Clone, PartialEq, Debug, Reflect)]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't this type reflect Default and PartialEq?

#[reflect(Default, PartialEq)]

pub struct BorderRect {
/// Pixel padding to the left
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These comments probably aren't necessary (like the UiRect fields).

pub left: f32,
/// Pixel padding to the right
pub right: f32,
/// Pixel padding to the top
pub top: f32,
/// Pixel padding to the bottom
pub bottom: f32,
}

impl BorderRect {
/// Creates a new border as a square, with identical pixel padding values on every direction
#[must_use]
#[inline]
pub const fn square(value: f32) -> Self {
Self {
left: value,
right: value,
top: value,
bottom: value,
}
}

/// Creates a new border as a rectangle, with:
/// - `horizontal` for left and right pixel padding
/// - `vertical` for top and bottom pixel padding
#[must_use]
#[inline]
pub const fn rectangle(horizontal: f32, vertical: f32) -> Self {
Self {
left: horizontal,
right: horizontal,
top: vertical,
bottom: vertical,
}
}
}

impl From<f32> for BorderRect {
fn from(v: f32) -> Self {
Self::square(v)
}
}

impl From<[f32; 4]> for BorderRect {
fn from([left, right, top, bottom]: [f32; 4]) -> Self {
Self {
left,
right,
top,
bottom,
}
}
}
Loading
Loading