From cc47c933ceac65bd6090fe699530fc4fe7c18cf1 Mon Sep 17 00:00:00 2001 From: Silvestr Predko Date: Thu, 9 Jan 2025 15:02:51 +0200 Subject: [PATCH 01/10] Add TextureScale modes for sprites This change introduces new texture scaling options for sprites that maintain aspect ratio: - Add `TextureScale` enum with `FillCenter`, `FillStart`, `FillEnd`, `FitCenter`, `FitStart`, and `FitEnd` modes - Extend `SpriteImageMode` with `ScaleMode` variant to support the new scaling options --- crates/bevy_sprite/src/lib.rs | 2 +- crates/bevy_sprite/src/render/mod.rs | 137 ++++++++++++++++-- crates/bevy_sprite/src/sprite.rs | 46 ++++++ .../src/texture_slice/computed_slices.rs | 5 +- crates/bevy_text/src/text2d.rs | 1 + .../src/render/ui_texture_slice_pipeline.rs | 4 +- 6 files changed, 176 insertions(+), 19 deletions(-) diff --git a/crates/bevy_sprite/src/lib.rs b/crates/bevy_sprite/src/lib.rs index fc6c4b5d5dfe9..3a173e2d7a794 100644 --- a/crates/bevy_sprite/src/lib.rs +++ b/crates/bevy_sprite/src/lib.rs @@ -30,7 +30,7 @@ pub mod prelude { pub use crate::{ sprite::{Sprite, SpriteImageMode}, texture_slice::{BorderRect, SliceScaleMode, TextureSlice, TextureSlicer}, - ColorMaterial, MeshMaterial2d, + ColorMaterial, MeshMaterial2d, TextureScale, }; } diff --git a/crates/bevy_sprite/src/render/mod.rs b/crates/bevy_sprite/src/render/mod.rs index 6d697dd64ab43..c53db77edb967 100644 --- a/crates/bevy_sprite/src/render/mod.rs +++ b/crates/bevy_sprite/src/render/mod.rs @@ -1,6 +1,6 @@ use core::ops::Range; -use crate::{ComputedTextureSlices, Sprite, SPRITE_SHADER_HANDLE}; +use crate::{ComputedTextureSlices, Sprite, TextureScale, SPRITE_SHADER_HANDLE}; use bevy_asset::{AssetEvent, AssetId, Assets}; use bevy_color::{ColorToComponents, LinearRgba}; use bevy_core_pipeline::{ @@ -339,6 +339,7 @@ pub struct ExtractedSprite { /// For cases where additional [`ExtractedSprites`] are created during extraction, this stores the /// entity that caused that creation for use in determining visibility. pub original_entity: Option, + pub texture_scale: Option, } #[derive(Resource, Default)] @@ -430,6 +431,7 @@ pub fn extract_sprites( image_handle_id: sprite.image.id(), anchor: sprite.anchor.as_vec(), original_entity: Some(original_entity), + texture_scale: sprite.image_mode.scale(), }, ); } @@ -699,21 +701,37 @@ pub fn prepare_sprite_image_bind_groups( // By default, the size of the quad is the size of the texture let mut quad_size = batch_image_size; - // Calculate vertex data for this item - let mut uv_offset_scale: Vec4; - // If a rect is specified, adjust UVs and the size of the quad - if let Some(rect) = extracted_sprite.rect { + let mut uv_offset_scale = if let Some(rect) = extracted_sprite.rect { let rect_size = rect.size(); - uv_offset_scale = Vec4::new( + quad_size = rect_size; + Vec4::new( rect.min.x / batch_image_size.x, rect.max.y / batch_image_size.y, rect_size.x / batch_image_size.x, -rect_size.y / batch_image_size.y, - ); - quad_size = rect_size; + ) } else { - uv_offset_scale = Vec4::new(0.0, 1.0, 1.0, -1.0); + Vec4::new(0.0, 1.0, 1.0, -1.0) + }; + + // Override the size if a custom one is specified + if let Some(custom_size) = extracted_sprite.custom_size { + quad_size = custom_size; + } + + // Used for translation of the quad if `TextureScale::Fit...` is specified. + let mut quad_translation = Vec2::ZERO; + + // Scales the texture based on the `texture_scale` field. + if let Some(texture_scale) = extracted_sprite.texture_scale { + scale_texture( + texture_scale, + batch_image_size, + &mut quad_size, + &mut quad_translation, + &mut uv_offset_scale, + ); } if extracted_sprite.flip_x { @@ -725,15 +743,13 @@ pub fn prepare_sprite_image_bind_groups( uv_offset_scale.w *= -1.0; } - // Override the size if a custom one is specified - if let Some(custom_size) = extracted_sprite.custom_size { - quad_size = custom_size; - } let transform = extracted_sprite.transform.affine() * Affine3A::from_scale_rotation_translation( quad_size.extend(1.0), Quat::IDENTITY, - (quad_size * (-extracted_sprite.anchor - Vec2::splat(0.5))).extend(0.0), + ((quad_size + quad_translation) + * (-extracted_sprite.anchor - Vec2::splat(0.5))) + .extend(0.0), ); // Store the vertex data and add the item to the render phase @@ -874,3 +890,96 @@ impl RenderCommand

for DrawSpriteBatch { RenderCommandResult::Success } } + +/// Scales a texture to fit within a given quad size with keeping the aspect ratio. +fn scale_texture( + texture_scale: TextureScale, + texture_size: Vec2, + quad_size: &mut Vec2, + quad_translation: &mut Vec2, + uv_offset_scale: &mut Vec4, +) { + let quad_ratio = quad_size.x / quad_size.y; + let texture_ratio = texture_size.x / texture_size.y; + + match texture_scale { + TextureScale::FillCenter => { + let scale = if quad_ratio > texture_ratio { + // Quad is wider than the image + Vec2::new(1., -texture_ratio / quad_ratio) + } else { + // Quad is taller than the image + Vec2::new(quad_ratio / texture_ratio, -1.) + }; + let offset = (1.0 - scale) * 0.5; + + // override all previous scaling and offset + *uv_offset_scale = Vec4::new(offset.x, offset.y, scale.x, scale.y); + } + TextureScale::FillStart => { + if quad_ratio > texture_ratio { + let scale = Vec2::new(1., -texture_ratio / quad_ratio); + let offset = (1.0 - scale) * 0.5; + *uv_offset_scale = Vec4::new(offset.x, scale.y.abs(), scale.x, scale.y); + } else { + let scale = Vec2::new(quad_ratio / texture_ratio, -1.); + let offset = (1.0 - scale) * 0.5; + *uv_offset_scale = Vec4::new(0.0, offset.y, scale.x, scale.y); + } + } + TextureScale::FillEnd => { + if quad_ratio > texture_ratio { + let scale = Vec2::new(1., -texture_ratio / quad_ratio); + let offset = (1.0 - scale) * 0.5; + *uv_offset_scale = Vec4::new(offset.x, 1.0, scale.x, scale.y); + } else { + let scale = Vec2::new(quad_ratio / texture_ratio, -1.); + let offset = (1.0 - scale) * 0.5; + *uv_offset_scale = Vec4::new(1.0 - scale.x, offset.y, scale.x, scale.y); + } + } + TextureScale::FitCenter => { + let scale = if texture_ratio > quad_ratio { + // Scale based on width + Vec2::new(1.0, quad_ratio / texture_ratio) + } else { + // Scale based on height + Vec2::new(texture_ratio / quad_ratio, 1.0) + }; + + *quad_size *= scale; + } + TextureScale::FitStart => { + if texture_ratio > quad_ratio { + // The quad is scaled to match the image ratio, and the quad translation is adjusted + // to start of the quad within the original quad size. + let scale = Vec2::new(1.0, quad_ratio / texture_ratio); + let new_quad = *quad_size * scale; + let offset = *quad_size - new_quad; + *quad_translation = Vec2::new(0.0, -offset.y); + *quad_size = new_quad; + } else { + let scale = Vec2::new(texture_ratio / quad_ratio, 1.0); + let new_quad = *quad_size * scale; + let offset = *quad_size - new_quad; + *quad_translation = Vec2::new(offset.x, 0.0); + *quad_size = new_quad; + } + } + TextureScale::FitEnd => { + if texture_ratio > quad_ratio { + let scale = Vec2::new(1.0, quad_ratio / texture_ratio); + let new_quad = *quad_size * scale; + let offset = *quad_size - new_quad; + *quad_translation = Vec2::new(0.0, offset.y); + *quad_size = new_quad; + } else { + let scale = Vec2::new(texture_ratio / quad_ratio, 1.0); + let new_quad = *quad_size * scale; + let offset = *quad_size - new_quad; + *quad_translation = Vec2::new(-offset.x, 0.0); + *quad_size = new_quad; + } + } + } +} diff --git a/crates/bevy_sprite/src/sprite.rs b/crates/bevy_sprite/src/sprite.rs index 82101a8b38024..ea1b7e0b92af3 100644 --- a/crates/bevy_sprite/src/sprite.rs +++ b/crates/bevy_sprite/src/sprite.rs @@ -162,6 +162,9 @@ pub enum SpriteImageMode { /// The sprite will take on the size of the image by default, and will be stretched or shrunk if [`Sprite::custom_size`] is set. #[default] Auto, + /// The texture will be scaled to fit the rect bounds defined in [`Sprite::custom_size`]. + /// Otherwise no scaling will be applied. + ScaleMode(TextureScale), /// 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` @@ -185,6 +188,49 @@ impl SpriteImageMode { SpriteImageMode::Sliced(..) | SpriteImageMode::Tiled { .. } ) } + + /// Returns [`TextureScale`] if scale is presented or [`Option::None`] otherwise + #[inline] + #[must_use] + pub const fn scale(&self) -> Option { + if let SpriteImageMode::ScaleMode(scale) = self { + Some(*scale) + } else { + None + } + } +} + +/// Represents various modes for proportional scaling of a texture +#[derive(Debug, Clone, Copy, PartialEq, Default, Reflect)] +#[reflect(Debug)] +pub enum TextureScale { + /// Scale the texture uniformly (maintain the texture's aspect ratio) + /// so that both dimensions (width and height) of the texture will be equal + /// to or larger than the corresponding dimension of the rect. + /// Fill rect with a centered texture. + #[default] + FillCenter, + /// Scale the texture to fill the rect with a start of the texture, + /// maintaining the aspect ratio. + FillStart, + /// Scale the texture to fill the rect with a end of the texture, + /// maintaining the aspect ratio. + FillEnd, + /// Scaling the texture will maintain the original aspect ratio + /// and ensure that the original texture fits entirely inside the rect. + /// At least one axis (X or Y) will fit exactly. The result is centered inside the rect. + FitCenter, + /// Scaling the texture will maintain the original aspect ratio + /// and ensure that the original texture fits entirely inside rect. + /// At least one axis (X or Y) will fit exactly. + /// Aligns the result to the left and top edges of rect. + FitStart, + /// Scaling the texture will maintain the original aspect ratio + /// and ensure that the original texture fits entirely inside rect. + /// At least one axis (X or Y) will fit exactly. + /// Aligns the result to the right and bottom edges of rect. + FitEnd, } /// How a sprite is positioned relative to its [`Transform`]. diff --git a/crates/bevy_sprite/src/texture_slice/computed_slices.rs b/crates/bevy_sprite/src/texture_slice/computed_slices.rs index 490071a6005ed..210136b99fdba 100644 --- a/crates/bevy_sprite/src/texture_slice/computed_slices.rs +++ b/crates/bevy_sprite/src/texture_slice/computed_slices.rs @@ -53,6 +53,7 @@ impl ComputedTextureSlices { flip_y, image_handle_id: sprite.image.id(), anchor: Self::redepend_anchor_from_sprite_to_slice(sprite, slice), + texture_scale: sprite.image_mode.scale(), } }) } @@ -120,8 +121,8 @@ fn compute_sprite_slices( }; slice.tiled(*stretch_value, (*tile_x, *tile_y)) } - SpriteImageMode::Auto => { - unreachable!("Slices should not be computed for SpriteImageMode::Stretch") + SpriteImageMode::Auto | SpriteImageMode::ScaleMode(_) => { + unreachable!("Slices should not be computed for SpriteImageMode::Stretch or SpriteImageMode::ScaleMode") } }; Some(ComputedTextureSlices(slices)) diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index 547ed6bfce745..925303339e818 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -208,6 +208,7 @@ pub fn extract_text2d_sprite( flip_y: false, anchor: Anchor::Center.as_vec(), original_entity: Some(original_entity), + texture_scale: None, }, ); } diff --git a/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs b/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs index f2d5eb243ed8d..6c0bdfc436dc1 100644 --- a/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs @@ -799,8 +799,8 @@ fn compute_texture_slices( let ry = compute_tiled_axis(*tile_y, image_size.y, target_size.y, *stretch_value); [[0., 0., 1., 1.], [0., 0., 1., 1.], [1., 1., rx, ry]] } - SpriteImageMode::Auto => { - unreachable!("Slices should not be computed for ImageScaleMode::Stretch") + SpriteImageMode::Auto | SpriteImageMode::ScaleMode(_) => { + unreachable!("Slices should not be computed for SpriteImageMode::Stretch or SpriteImageMode::ScaleMode") } } } From baf755f0e236d94436869874bbb64302590eab86 Mon Sep 17 00:00:00 2001 From: Silvestr Predko Date: Thu, 9 Jan 2025 15:06:37 +0200 Subject: [PATCH 02/10] Add `sprite_scale.rs` example to demonstrate the new functionality Update `Cargo.toml` accordingly. --- Cargo.toml | 11 +++ examples/2d/sprite_scale.rs | 139 ++++++++++++++++++++++++++++++++++++ 2 files changed, 150 insertions(+) create mode 100644 examples/2d/sprite_scale.rs diff --git a/Cargo.toml b/Cargo.toml index b8fb95fb2da4b..0e0d7b386e049 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -657,6 +657,17 @@ description = "Animates a sprite in response to an event" category = "2D Rendering" wasm = true +[[example]] +name = "sprite_scale" +path = "examples/2d/sprite_scale.rs" +doc-scrape-examples = true + +[package.metadata.example.sprite_scale] +name = "Sprite Scale" +description = "Shows how a sprite could be scaled into a rectangle while keeping the aspect ratio" +category = "2D Rendering" +wasm = true + [[example]] name = "sprite_flipping" path = "examples/2d/sprite_flipping.rs" diff --git a/examples/2d/sprite_scale.rs b/examples/2d/sprite_scale.rs new file mode 100644 index 0000000000000..8735df6e2d611 --- /dev/null +++ b/examples/2d/sprite_scale.rs @@ -0,0 +1,139 @@ +//! Shows how to use sprite scaling modes to fill and fit textures into the sprite. + +use bevy::prelude::*; + +fn main() { + App::new() + .add_plugins(DefaultPlugins) + .add_systems(Startup, setup) + .run(); +} + +fn setup(mut commands: Commands, asset_server: Res) { + commands.spawn(Camera2d); + let font = asset_server.load("fonts/FiraSans-Bold.ttf"); + let style = TextFont { + font: font.clone(), + ..default() + }; + + let square = asset_server.load("textures/slice_square_2.png"); + let banner = asset_server.load("branding/banner.png"); + + let rects = vec![ + Rect { + size: Vec2::new(100., 300.), + text: "Stretched".to_string(), + transform: Transform::from_translation(Vec3::new(-550.0, 200.0, 0.0)), + texture: square.clone(), + image_mode: SpriteImageMode::Auto, + }, + Rect { + size: Vec2::new(100., 300.), + text: "Fill Center".to_string(), + transform: Transform::from_translation(Vec3::new(-400.0, 200.0, 0.0)), + texture: square.clone(), + image_mode: SpriteImageMode::ScaleMode(TextureScale::FillCenter), + }, + Rect { + size: Vec2::new(100., 300.), + text: "Fill Start".to_string(), + transform: Transform::from_translation(Vec3::new(-250.0, 200.0, 0.0)), + texture: square.clone(), + image_mode: SpriteImageMode::ScaleMode(TextureScale::FillStart), + }, + Rect { + size: Vec2::new(100., 300.), + text: "Fill End".to_string(), + transform: Transform::from_translation(Vec3::new(-100.0, 200.0, 0.0)), + texture: square.clone(), + image_mode: SpriteImageMode::ScaleMode(TextureScale::FillEnd), + }, + Rect { + size: Vec2::new(300., 100.), + text: "Fill Start Horizontal".to_string(), + transform: Transform::from_translation(Vec3::new(150.0, 300.0, 0.0)), + texture: square.clone(), + image_mode: SpriteImageMode::ScaleMode(TextureScale::FillStart), + }, + Rect { + size: Vec2::new(300., 100.), + text: "Fill End Horizontal".to_string(), + transform: Transform::from_translation(Vec3::new(150.0, 100.0, 0.0)), + texture: square.clone(), + image_mode: SpriteImageMode::ScaleMode(TextureScale::FillEnd), + }, + Rect { + size: Vec2::new(200., 200.), + text: "Fill Center".to_string(), + transform: Transform::from_translation(Vec3::new(450.0, 200.0, 0.0)), + texture: banner.clone(), + image_mode: SpriteImageMode::ScaleMode(TextureScale::FillCenter), + }, + Rect { + size: Vec2::new(100., 100.), + text: "Stretched".to_string(), + transform: Transform::from_translation(Vec3::new(-550.0, -200.0, 0.0)), + texture: banner.clone(), + image_mode: SpriteImageMode::Auto, + }, + Rect { + size: Vec2::new(200., 200.), + text: "Fit Center".to_string(), + transform: Transform::from_translation(Vec3::new(-350.0, -200.0, 0.0)), + texture: banner.clone(), + image_mode: SpriteImageMode::ScaleMode(TextureScale::FitCenter), + }, + Rect { + size: Vec2::new(200., 200.), + text: "Fit Start".to_string(), + transform: Transform::from_translation(Vec3::new(-100.0, -200.0, 0.0)), + texture: banner.clone(), + image_mode: SpriteImageMode::ScaleMode(TextureScale::FitStart), + }, + Rect { + size: Vec2::new(200., 200.), + text: "Fit End".to_string(), + transform: Transform::from_translation(Vec3::new(150.0, -200.0, 0.0)), + texture: banner.clone(), + image_mode: SpriteImageMode::ScaleMode(TextureScale::FitEnd), + }, + Rect { + size: Vec2::new(100., 200.), + text: "Fit Center".to_string(), + transform: Transform::from_translation(Vec3::new(350.0, -200.0, 0.0)), + texture: banner.clone(), + image_mode: SpriteImageMode::ScaleMode(TextureScale::FitCenter), + }, + ]; + + for rect in rects { + let mut cmd = commands.spawn(( + Sprite { + image: rect.texture, + custom_size: Some(rect.size), + image_mode: rect.image_mode, + ..default() + }, + rect.transform, + )); + + cmd.with_children(|builder| { + builder.spawn(( + Text2d::new(rect.text), + style.clone(), + TextLayout::new_with_justify(JustifyText::Center), + Transform::from_xyz(0., -0.5 * rect.size.y - 10., 0.0), + bevy::sprite::Anchor::TopCenter, + )); + }); + } +} + +struct Rect { + size: Vec2, + text: String, + transform: Transform, + texture: Handle, + image_mode: SpriteImageMode, +} From 296174f81cd30c9b725ce6ce38d59cb9bc87b0eb Mon Sep 17 00:00:00 2001 From: Silvestr Predko Date: Thu, 9 Jan 2025 15:49:14 +0200 Subject: [PATCH 03/10] Update examples/README.md --- examples/README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/README.md b/examples/README.md index 49a90a422423f..0473bae36b748 100644 --- a/examples/README.md +++ b/examples/README.md @@ -120,6 +120,7 @@ Example | Description [Sprite](../examples/2d/sprite.rs) | Renders a sprite [Sprite Animation](../examples/2d/sprite_animation.rs) | Animates a sprite in response to an event [Sprite Flipping](../examples/2d/sprite_flipping.rs) | Renders a sprite flipped along an axis +[Sprite Scale](../examples/2d/sprite_scale.rs) | Shows how a sprite could be scaled into a rectangle while keeping the aspect ratio [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 From 1a8fa2a67eac9bae18c23335bc8610d1fb3f5247 Mon Sep 17 00:00:00 2001 From: Silvestr Predko Date: Wed, 15 Jan 2025 16:53:17 +0200 Subject: [PATCH 04/10] - Rename TextureScale enum to ScalingMode for better clarity - Update SpriteImageMode::ScaleMode variant to Scale - Fix unreachable error messages to be more specific - Clean up sprite scale example code - Update documentation comments This change improves naming consistency and makes the sprite scaling API more intuitive while maintaining all existing functionality. --- crates/bevy_sprite/src/lib.rs | 2 +- crates/bevy_sprite/src/render/mod.rs | 30 +++++++++---------- crates/bevy_sprite/src/sprite.rs | 14 +++++---- .../src/texture_slice/computed_slices.rs | 9 ++++-- crates/bevy_text/src/text2d.rs | 2 +- .../src/render/ui_texture_slice_pipeline.rs | 7 +++-- examples/2d/sprite_scale.rs | 30 ++++++++----------- 7 files changed, 48 insertions(+), 46 deletions(-) diff --git a/crates/bevy_sprite/src/lib.rs b/crates/bevy_sprite/src/lib.rs index e85134286f727..2bd354ac8b263 100644 --- a/crates/bevy_sprite/src/lib.rs +++ b/crates/bevy_sprite/src/lib.rs @@ -25,7 +25,7 @@ pub mod prelude { pub use crate::{ sprite::{Sprite, SpriteImageMode}, texture_slice::{BorderRect, SliceScaleMode, TextureSlice, TextureSlicer}, - ColorMaterial, MeshMaterial2d, TextureScale, + ColorMaterial, MeshMaterial2d, ScalingMode, }; } diff --git a/crates/bevy_sprite/src/render/mod.rs b/crates/bevy_sprite/src/render/mod.rs index 8c144884bf58f..b1ab59e741c42 100644 --- a/crates/bevy_sprite/src/render/mod.rs +++ b/crates/bevy_sprite/src/render/mod.rs @@ -1,6 +1,6 @@ use core::ops::Range; -use crate::{ComputedTextureSlices, Sprite, TextureScale, SPRITE_SHADER_HANDLE}; +use crate::{ComputedTextureSlices, ScalingMode, Sprite, SPRITE_SHADER_HANDLE}; use bevy_asset::{AssetEvent, AssetId, Assets}; use bevy_color::{ColorToComponents, LinearRgba}; use bevy_core_pipeline::{ @@ -339,7 +339,7 @@ pub struct ExtractedSprite { /// For cases where additional [`ExtractedSprites`] are created during extraction, this stores the /// entity that caused that creation for use in determining visibility. pub original_entity: Option, - pub texture_scale: Option, + pub scaling_mode: Option, } #[derive(Resource, Default)] @@ -431,7 +431,7 @@ pub fn extract_sprites( image_handle_id: sprite.image.id(), anchor: sprite.anchor.as_vec(), original_entity: Some(original_entity), - texture_scale: sprite.image_mode.scale(), + scaling_mode: sprite.image_mode.scale(), }, ); } @@ -725,9 +725,9 @@ pub fn prepare_sprite_image_bind_groups( let mut quad_translation = Vec2::ZERO; // Scales the texture based on the `texture_scale` field. - if let Some(texture_scale) = extracted_sprite.texture_scale { - scale_texture( - texture_scale, + if let Some(scaling_mode) = extracted_sprite.scaling_mode { + apply_scaling( + scaling_mode, batch_image_size, &mut quad_size, &mut quad_translation, @@ -893,8 +893,8 @@ impl RenderCommand

for DrawSpriteBatch { } /// Scales a texture to fit within a given quad size with keeping the aspect ratio. -fn scale_texture( - texture_scale: TextureScale, +fn apply_scaling( + scaling_mode: ScalingMode, texture_size: Vec2, quad_size: &mut Vec2, quad_translation: &mut Vec2, @@ -903,8 +903,8 @@ fn scale_texture( let quad_ratio = quad_size.x / quad_size.y; let texture_ratio = texture_size.x / texture_size.y; - match texture_scale { - TextureScale::FillCenter => { + match scaling_mode { + ScalingMode::FillCenter => { let scale = if quad_ratio > texture_ratio { // Quad is wider than the image Vec2::new(1., -texture_ratio / quad_ratio) @@ -917,7 +917,7 @@ fn scale_texture( // override all previous scaling and offset *uv_offset_scale = Vec4::new(offset.x, offset.y, scale.x, scale.y); } - TextureScale::FillStart => { + ScalingMode::FillStart => { if quad_ratio > texture_ratio { let scale = Vec2::new(1., -texture_ratio / quad_ratio); let offset = (1.0 - scale) * 0.5; @@ -928,7 +928,7 @@ fn scale_texture( *uv_offset_scale = Vec4::new(0.0, offset.y, scale.x, scale.y); } } - TextureScale::FillEnd => { + ScalingMode::FillEnd => { if quad_ratio > texture_ratio { let scale = Vec2::new(1., -texture_ratio / quad_ratio); let offset = (1.0 - scale) * 0.5; @@ -939,7 +939,7 @@ fn scale_texture( *uv_offset_scale = Vec4::new(1.0 - scale.x, offset.y, scale.x, scale.y); } } - TextureScale::FitCenter => { + ScalingMode::FitCenter => { let scale = if texture_ratio > quad_ratio { // Scale based on width Vec2::new(1.0, quad_ratio / texture_ratio) @@ -950,7 +950,7 @@ fn scale_texture( *quad_size *= scale; } - TextureScale::FitStart => { + ScalingMode::FitStart => { if texture_ratio > quad_ratio { // The quad is scaled to match the image ratio, and the quad translation is adjusted // to start of the quad within the original quad size. @@ -967,7 +967,7 @@ fn scale_texture( *quad_size = new_quad; } } - TextureScale::FitEnd => { + ScalingMode::FitEnd => { if texture_ratio > quad_ratio { let scale = Vec2::new(1.0, quad_ratio / texture_ratio); let new_quad = *quad_size * scale; diff --git a/crates/bevy_sprite/src/sprite.rs b/crates/bevy_sprite/src/sprite.rs index ea1b7e0b92af3..88b4cb6f79ee0 100644 --- a/crates/bevy_sprite/src/sprite.rs +++ b/crates/bevy_sprite/src/sprite.rs @@ -164,7 +164,7 @@ pub enum SpriteImageMode { Auto, /// The texture will be scaled to fit the rect bounds defined in [`Sprite::custom_size`]. /// Otherwise no scaling will be applied. - ScaleMode(TextureScale), + Scale(ScalingMode), /// 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` @@ -189,11 +189,11 @@ impl SpriteImageMode { ) } - /// Returns [`TextureScale`] if scale is presented or [`Option::None`] otherwise + /// Returns [`ScalingMode`] if scale is presented or [`Option::None`] otherwise. #[inline] #[must_use] - pub const fn scale(&self) -> Option { - if let SpriteImageMode::ScaleMode(scale) = self { + pub const fn scale(&self) -> Option { + if let SpriteImageMode::Scale(scale) = self { Some(*scale) } else { None @@ -201,10 +201,12 @@ impl SpriteImageMode { } } -/// Represents various modes for proportional scaling of a texture +/// Represents various modes for proportional scaling of a texture. +/// +/// Can be used in [`SpriteImageMode::Scale`]. #[derive(Debug, Clone, Copy, PartialEq, Default, Reflect)] #[reflect(Debug)] -pub enum TextureScale { +pub enum ScalingMode { /// Scale the texture uniformly (maintain the texture's aspect ratio) /// so that both dimensions (width and height) of the texture will be equal /// to or larger than the corresponding dimension of the rect. diff --git a/crates/bevy_sprite/src/texture_slice/computed_slices.rs b/crates/bevy_sprite/src/texture_slice/computed_slices.rs index 210136b99fdba..573b1ccea6de9 100644 --- a/crates/bevy_sprite/src/texture_slice/computed_slices.rs +++ b/crates/bevy_sprite/src/texture_slice/computed_slices.rs @@ -53,7 +53,7 @@ impl ComputedTextureSlices { flip_y, image_handle_id: sprite.image.id(), anchor: Self::redepend_anchor_from_sprite_to_slice(sprite, slice), - texture_scale: sprite.image_mode.scale(), + scaling_mode: sprite.image_mode.scale(), } }) } @@ -121,8 +121,11 @@ fn compute_sprite_slices( }; slice.tiled(*stretch_value, (*tile_x, *tile_y)) } - SpriteImageMode::Auto | SpriteImageMode::ScaleMode(_) => { - unreachable!("Slices should not be computed for SpriteImageMode::Stretch or SpriteImageMode::ScaleMode") + SpriteImageMode::Auto => { + unreachable!("Slices should not be computed for SpriteImageMode::Stretch") + } + SpriteImageMode::Scale(_) => { + unreachable!("Slices should not be computed for SpriteImageMode::Scale") } }; Some(ComputedTextureSlices(slices)) diff --git a/crates/bevy_text/src/text2d.rs b/crates/bevy_text/src/text2d.rs index b0fe4468413e5..6970731b88443 100644 --- a/crates/bevy_text/src/text2d.rs +++ b/crates/bevy_text/src/text2d.rs @@ -208,7 +208,7 @@ pub fn extract_text2d_sprite( flip_y: false, anchor: Anchor::Center.as_vec(), original_entity: Some(original_entity), - texture_scale: None, + scaling_mode: None, }, ); } diff --git a/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs b/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs index d2c4dfd3d1782..8309e47e14cb6 100644 --- a/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs @@ -812,8 +812,11 @@ fn compute_texture_slices( let ry = compute_tiled_axis(*tile_y, image_size.y, target_size.y, *stretch_value); [[0., 0., 1., 1.], [0., 0., 1., 1.], [1., 1., rx, ry]] } - SpriteImageMode::Auto | SpriteImageMode::ScaleMode(_) => { - unreachable!("Slices should not be computed for SpriteImageMode::Stretch or SpriteImageMode::ScaleMode") + SpriteImageMode::Auto => { + unreachable!("Slices should not be computed for SpriteImageMode::Stretch") + } + SpriteImageMode::Scale(_) => { + unreachable!("Slices should not be computed for SpriteImageMode::Scale") } } } diff --git a/examples/2d/sprite_scale.rs b/examples/2d/sprite_scale.rs index 8735df6e2d611..53c02cb77e5d0 100644 --- a/examples/2d/sprite_scale.rs +++ b/examples/2d/sprite_scale.rs @@ -1,4 +1,4 @@ -//! Shows how to use sprite scaling modes to fill and fit textures into the sprite. +//! Shows how to use sprite scaling to fill and fit textures into the sprite. use bevy::prelude::*; @@ -11,16 +11,11 @@ fn main() { fn setup(mut commands: Commands, asset_server: Res) { commands.spawn(Camera2d); - let font = asset_server.load("fonts/FiraSans-Bold.ttf"); - let style = TextFont { - font: font.clone(), - ..default() - }; let square = asset_server.load("textures/slice_square_2.png"); let banner = asset_server.load("branding/banner.png"); - let rects = vec![ + let rects = [ Rect { size: Vec2::new(100., 300.), text: "Stretched".to_string(), @@ -33,42 +28,42 @@ fn setup(mut commands: Commands, asset_server: Res) { text: "Fill Center".to_string(), transform: Transform::from_translation(Vec3::new(-400.0, 200.0, 0.0)), texture: square.clone(), - image_mode: SpriteImageMode::ScaleMode(TextureScale::FillCenter), + image_mode: SpriteImageMode::Scale(ScalingMode::FillCenter), }, Rect { size: Vec2::new(100., 300.), text: "Fill Start".to_string(), transform: Transform::from_translation(Vec3::new(-250.0, 200.0, 0.0)), texture: square.clone(), - image_mode: SpriteImageMode::ScaleMode(TextureScale::FillStart), + image_mode: SpriteImageMode::Scale(ScalingMode::FillStart), }, Rect { size: Vec2::new(100., 300.), text: "Fill End".to_string(), transform: Transform::from_translation(Vec3::new(-100.0, 200.0, 0.0)), texture: square.clone(), - image_mode: SpriteImageMode::ScaleMode(TextureScale::FillEnd), + image_mode: SpriteImageMode::Scale(ScalingMode::FillEnd), }, Rect { size: Vec2::new(300., 100.), text: "Fill Start Horizontal".to_string(), transform: Transform::from_translation(Vec3::new(150.0, 300.0, 0.0)), texture: square.clone(), - image_mode: SpriteImageMode::ScaleMode(TextureScale::FillStart), + image_mode: SpriteImageMode::Scale(ScalingMode::FillStart), }, Rect { size: Vec2::new(300., 100.), text: "Fill End Horizontal".to_string(), transform: Transform::from_translation(Vec3::new(150.0, 100.0, 0.0)), texture: square.clone(), - image_mode: SpriteImageMode::ScaleMode(TextureScale::FillEnd), + image_mode: SpriteImageMode::Scale(ScalingMode::FillEnd), }, Rect { size: Vec2::new(200., 200.), text: "Fill Center".to_string(), transform: Transform::from_translation(Vec3::new(450.0, 200.0, 0.0)), texture: banner.clone(), - image_mode: SpriteImageMode::ScaleMode(TextureScale::FillCenter), + image_mode: SpriteImageMode::Scale(ScalingMode::FillCenter), }, Rect { size: Vec2::new(100., 100.), @@ -82,28 +77,28 @@ fn setup(mut commands: Commands, asset_server: Res) { text: "Fit Center".to_string(), transform: Transform::from_translation(Vec3::new(-350.0, -200.0, 0.0)), texture: banner.clone(), - image_mode: SpriteImageMode::ScaleMode(TextureScale::FitCenter), + image_mode: SpriteImageMode::Scale(ScalingMode::FitCenter), }, Rect { size: Vec2::new(200., 200.), text: "Fit Start".to_string(), transform: Transform::from_translation(Vec3::new(-100.0, -200.0, 0.0)), texture: banner.clone(), - image_mode: SpriteImageMode::ScaleMode(TextureScale::FitStart), + image_mode: SpriteImageMode::Scale(ScalingMode::FitStart), }, Rect { size: Vec2::new(200., 200.), text: "Fit End".to_string(), transform: Transform::from_translation(Vec3::new(150.0, -200.0, 0.0)), texture: banner.clone(), - image_mode: SpriteImageMode::ScaleMode(TextureScale::FitEnd), + image_mode: SpriteImageMode::Scale(ScalingMode::FitEnd), }, Rect { size: Vec2::new(100., 200.), text: "Fit Center".to_string(), transform: Transform::from_translation(Vec3::new(350.0, -200.0, 0.0)), texture: banner.clone(), - image_mode: SpriteImageMode::ScaleMode(TextureScale::FitCenter), + image_mode: SpriteImageMode::Scale(ScalingMode::FitCenter), }, ]; @@ -121,7 +116,6 @@ fn setup(mut commands: Commands, asset_server: Res) { cmd.with_children(|builder| { builder.spawn(( Text2d::new(rect.text), - style.clone(), TextLayout::new_with_justify(JustifyText::Center), Transform::from_xyz(0., -0.5 * rect.size.y - 10., 0.0), bevy::sprite::Anchor::TopCenter, From dbfa421589c28016862156863f8122853fefaaf8 Mon Sep 17 00:00:00 2001 From: Silvestr Predko Date: Sun, 19 Jan 2025 00:31:45 +0200 Subject: [PATCH 05/10] Update documentation for the sprite scale --- Cargo.toml | 2 +- crates/bevy_sprite/src/sprite.rs | 30 +++++++++++++------ .../src/render/ui_texture_slice_pipeline.rs | 4 +-- examples/README.md | 2 +- 4 files changed, 25 insertions(+), 13 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 39af4b708aab4..ffbe0d4b0471c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -670,7 +670,7 @@ doc-scrape-examples = true [package.metadata.example.sprite_scale] name = "Sprite Scale" -description = "Shows how a sprite could be scaled into a rectangle while keeping the aspect ratio" +description = "Shows how a sprite can be scaled into a rectangle while keeping the aspect ratio" category = "2D Rendering" wasm = true diff --git a/crates/bevy_sprite/src/sprite.rs b/crates/bevy_sprite/src/sprite.rs index 88b4cb6f79ee0..6a3f213e6d733 100644 --- a/crates/bevy_sprite/src/sprite.rs +++ b/crates/bevy_sprite/src/sprite.rs @@ -164,6 +164,10 @@ pub enum SpriteImageMode { Auto, /// The texture will be scaled to fit the rect bounds defined in [`Sprite::custom_size`]. /// Otherwise no scaling will be applied. + /// + /// Note: [`TextureAtlas`] cannot be used with scaling applied to a sprite's rectangle. + /// Additionally, the custom rectangle portion of the texture cannot be used with the scaling option too. + /// It will be completely ignored during the scaling. Scale(ScalingMode), /// The texture will be cut in 9 slices, keeping the texture in proportions on resize Sliced(TextureSlicer), @@ -209,28 +213,36 @@ impl SpriteImageMode { pub enum ScalingMode { /// Scale the texture uniformly (maintain the texture's aspect ratio) /// so that both dimensions (width and height) of the texture will be equal - /// to or larger than the corresponding dimension of the rect. - /// Fill rect with a centered texture. + /// to or larger than the corresponding dimension of the target rectangle. + /// Fill sprite with a centered texture. #[default] FillCenter, - /// Scale the texture to fill the rect with a start of the texture, - /// maintaining the aspect ratio. + /// Scales the texture to fill the target rectangle while maintaining its aspect ratio. + /// One dimension of the texture will match the rectangle's size, + /// while the other dimension may exceed it. + /// The exceeding portion is aligned to the start: + /// * Horizontal overflow is left-aligned if the width exceeds the rectangle. + /// * Vertical overflow is top-aligned if the height exceeds the rectangle. FillStart, - /// Scale the texture to fill the rect with a end of the texture, - /// maintaining the aspect ratio. + /// Scales the texture to fill the target rectangle while maintaining its aspect ratio. + /// One dimension of the texture will match the rectangle's size, + /// while the other dimension may exceed it. + /// The exceeding portion is aligned to the end: + /// * Horizontal overflow is right-aligned if the width exceeds the rectangle. + /// * Vertical overflow is bottom-aligned if the height exceeds the rectangle. FillEnd, /// Scaling the texture will maintain the original aspect ratio /// and ensure that the original texture fits entirely inside the rect. - /// At least one axis (X or Y) will fit exactly. The result is centered inside the rect. + /// At least one axis (x or y) will fit exactly. The result is centered inside the rect. FitCenter, /// Scaling the texture will maintain the original aspect ratio /// and ensure that the original texture fits entirely inside rect. - /// At least one axis (X or Y) will fit exactly. + /// At least one axis (x or y) will fit exactly. /// Aligns the result to the left and top edges of rect. FitStart, /// Scaling the texture will maintain the original aspect ratio /// and ensure that the original texture fits entirely inside rect. - /// At least one axis (X or Y) will fit exactly. + /// At least one axis (x or y) will fit exactly. /// Aligns the result to the right and bottom edges of rect. FitEnd, } diff --git a/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs b/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs index 8309e47e14cb6..cacc386a3be1c 100644 --- a/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs +++ b/crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs @@ -813,10 +813,10 @@ fn compute_texture_slices( [[0., 0., 1., 1.], [0., 0., 1., 1.], [1., 1., rx, ry]] } SpriteImageMode::Auto => { - unreachable!("Slices should not be computed for SpriteImageMode::Stretch") + unreachable!("Slices can not be computed for SpriteImageMode::Stretch") } SpriteImageMode::Scale(_) => { - unreachable!("Slices should not be computed for SpriteImageMode::Scale") + unreachable!("Slices can not be computed for SpriteImageMode::Scale") } } } diff --git a/examples/README.md b/examples/README.md index 8d919332a5f39..324b3465affc6 100644 --- a/examples/README.md +++ b/examples/README.md @@ -120,7 +120,7 @@ Example | Description [Sprite](../examples/2d/sprite.rs) | Renders a sprite [Sprite Animation](../examples/2d/sprite_animation.rs) | Animates a sprite in response to an event [Sprite Flipping](../examples/2d/sprite_flipping.rs) | Renders a sprite flipped along an axis -[Sprite Scale](../examples/2d/sprite_scale.rs) | Shows how a sprite could be scaled into a rectangle while keeping the aspect ratio +[Sprite Scale](../examples/2d/sprite_scale.rs) | Shows how a sprite can be scaled into a rectangle while keeping the aspect ratio [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 From 6fda4067547abf7d104b67ef6a78abc2bb636c63 Mon Sep 17 00:00:00 2001 From: Silvestr Predko Date: Sun, 19 Jan 2025 00:47:30 +0200 Subject: [PATCH 06/10] Fix formatting --- crates/bevy_sprite/src/sprite.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/bevy_sprite/src/sprite.rs b/crates/bevy_sprite/src/sprite.rs index 6a3f213e6d733..c7e610d091ba0 100644 --- a/crates/bevy_sprite/src/sprite.rs +++ b/crates/bevy_sprite/src/sprite.rs @@ -222,7 +222,7 @@ pub enum ScalingMode { /// while the other dimension may exceed it. /// The exceeding portion is aligned to the start: /// * Horizontal overflow is left-aligned if the width exceeds the rectangle. - /// * Vertical overflow is top-aligned if the height exceeds the rectangle. + /// * Vertical overflow is top-aligned if the height exceeds the rectangle. FillStart, /// Scales the texture to fill the target rectangle while maintaining its aspect ratio. /// One dimension of the texture will match the rectangle's size, From bea348fa532ceb8dab1a7c280ca855f90aef423a Mon Sep 17 00:00:00 2001 From: Silvestr Predko Date: Mon, 20 Jan 2025 21:48:26 +0200 Subject: [PATCH 07/10] Implement scaling for TextureAtlas or rect of the texture --- crates/bevy_sprite/src/render/mod.rs | 63 ++++++++++++++-------------- crates/bevy_sprite/src/sprite.rs | 4 -- 2 files changed, 31 insertions(+), 36 deletions(-) diff --git a/crates/bevy_sprite/src/render/mod.rs b/crates/bevy_sprite/src/render/mod.rs index b1ab59e741c42..f24a48c4f7227 100644 --- a/crates/bevy_sprite/src/render/mod.rs +++ b/crates/bevy_sprite/src/render/mod.rs @@ -702,10 +702,16 @@ pub fn prepare_sprite_image_bind_groups( // By default, the size of the quad is the size of the texture let mut quad_size = batch_image_size; + // Texture size is the size of the image + let mut texture_size = batch_image_size; + // If a rect is specified, adjust UVs and the size of the quad let mut uv_offset_scale = if let Some(rect) = extracted_sprite.rect { let rect_size = rect.size(); quad_size = rect_size; + // Update texture size to the rect size + // It will help scale properly only portion of the image + texture_size = rect_size; Vec4::new( rect.min.x / batch_image_size.x, rect.max.y / batch_image_size.y, @@ -728,7 +734,7 @@ pub fn prepare_sprite_image_bind_groups( if let Some(scaling_mode) = extracted_sprite.scaling_mode { apply_scaling( scaling_mode, - batch_image_size, + texture_size, &mut quad_size, &mut quad_translation, &mut uv_offset_scale, @@ -902,65 +908,58 @@ fn apply_scaling( ) { let quad_ratio = quad_size.x / quad_size.y; let texture_ratio = texture_size.x / texture_size.y; + let tex_quad_scale = texture_ratio / quad_ratio; + let quad_tex_scale = quad_ratio / texture_ratio; match scaling_mode { ScalingMode::FillCenter => { - let scale = if quad_ratio > texture_ratio { - // Quad is wider than the image - Vec2::new(1., -texture_ratio / quad_ratio) + if quad_ratio > texture_ratio { + // offset texture to center by y coordinate + uv_offset_scale.y += (uv_offset_scale.w - uv_offset_scale.w * tex_quad_scale) * 0.5; + // sum up scales + uv_offset_scale.w *= tex_quad_scale; } else { - // Quad is taller than the image - Vec2::new(quad_ratio / texture_ratio, -1.) + // offset texture to center by x coordinate + uv_offset_scale.x += (uv_offset_scale.z - uv_offset_scale.z * quad_tex_scale) * 0.5; + uv_offset_scale.z *= quad_tex_scale; }; - let offset = (1.0 - scale) * 0.5; - - // override all previous scaling and offset - *uv_offset_scale = Vec4::new(offset.x, offset.y, scale.x, scale.y); } ScalingMode::FillStart => { if quad_ratio > texture_ratio { - let scale = Vec2::new(1., -texture_ratio / quad_ratio); - let offset = (1.0 - scale) * 0.5; - *uv_offset_scale = Vec4::new(offset.x, scale.y.abs(), scale.x, scale.y); + uv_offset_scale.y += uv_offset_scale.w - uv_offset_scale.w * tex_quad_scale; + uv_offset_scale.w *= tex_quad_scale; } else { - let scale = Vec2::new(quad_ratio / texture_ratio, -1.); - let offset = (1.0 - scale) * 0.5; - *uv_offset_scale = Vec4::new(0.0, offset.y, scale.x, scale.y); + uv_offset_scale.z *= quad_tex_scale; } } ScalingMode::FillEnd => { if quad_ratio > texture_ratio { - let scale = Vec2::new(1., -texture_ratio / quad_ratio); - let offset = (1.0 - scale) * 0.5; - *uv_offset_scale = Vec4::new(offset.x, 1.0, scale.x, scale.y); + uv_offset_scale.w *= tex_quad_scale; } else { - let scale = Vec2::new(quad_ratio / texture_ratio, -1.); - let offset = (1.0 - scale) * 0.5; - *uv_offset_scale = Vec4::new(1.0 - scale.x, offset.y, scale.x, scale.y); + uv_offset_scale.x += uv_offset_scale.z - uv_offset_scale.z * quad_tex_scale; + uv_offset_scale.z *= quad_tex_scale; } } ScalingMode::FitCenter => { - let scale = if texture_ratio > quad_ratio { + if texture_ratio > quad_ratio { // Scale based on width - Vec2::new(1.0, quad_ratio / texture_ratio) + quad_size.y *= quad_tex_scale; } else { // Scale based on height - Vec2::new(texture_ratio / quad_ratio, 1.0) - }; - - *quad_size *= scale; + quad_size.x *= tex_quad_scale; + } } ScalingMode::FitStart => { if texture_ratio > quad_ratio { // The quad is scaled to match the image ratio, and the quad translation is adjusted // to start of the quad within the original quad size. - let scale = Vec2::new(1.0, quad_ratio / texture_ratio); + let scale = Vec2::new(1.0, quad_tex_scale); let new_quad = *quad_size * scale; let offset = *quad_size - new_quad; *quad_translation = Vec2::new(0.0, -offset.y); *quad_size = new_quad; } else { - let scale = Vec2::new(texture_ratio / quad_ratio, 1.0); + let scale = Vec2::new(tex_quad_scale, 1.0); let new_quad = *quad_size * scale; let offset = *quad_size - new_quad; *quad_translation = Vec2::new(offset.x, 0.0); @@ -969,13 +968,13 @@ fn apply_scaling( } ScalingMode::FitEnd => { if texture_ratio > quad_ratio { - let scale = Vec2::new(1.0, quad_ratio / texture_ratio); + let scale = Vec2::new(1.0, quad_tex_scale); let new_quad = *quad_size * scale; let offset = *quad_size - new_quad; *quad_translation = Vec2::new(0.0, offset.y); *quad_size = new_quad; } else { - let scale = Vec2::new(texture_ratio / quad_ratio, 1.0); + let scale = Vec2::new(tex_quad_scale, 1.0); let new_quad = *quad_size * scale; let offset = *quad_size - new_quad; *quad_translation = Vec2::new(-offset.x, 0.0); diff --git a/crates/bevy_sprite/src/sprite.rs b/crates/bevy_sprite/src/sprite.rs index c7e610d091ba0..59c60071a0293 100644 --- a/crates/bevy_sprite/src/sprite.rs +++ b/crates/bevy_sprite/src/sprite.rs @@ -164,10 +164,6 @@ pub enum SpriteImageMode { Auto, /// The texture will be scaled to fit the rect bounds defined in [`Sprite::custom_size`]. /// Otherwise no scaling will be applied. - /// - /// Note: [`TextureAtlas`] cannot be used with scaling applied to a sprite's rectangle. - /// Additionally, the custom rectangle portion of the texture cannot be used with the scaling option too. - /// It will be completely ignored during the scaling. Scale(ScalingMode), /// The texture will be cut in 9 slices, keeping the texture in proportions on resize Sliced(TextureSlicer), From bf1b7697d0faf24adb9c65f63ab32f162884467a Mon Sep 17 00:00:00 2001 From: Silvestr Predko Date: Mon, 20 Jan 2025 21:48:43 +0200 Subject: [PATCH 08/10] Add animated sprite sheet examples with various scaling modes (Auto, Fill, Fit) --- examples/2d/sprite_scale.rs | 235 +++++++++++++++++++++++++++++++++--- 1 file changed, 217 insertions(+), 18 deletions(-) diff --git a/examples/2d/sprite_scale.rs b/examples/2d/sprite_scale.rs index 53c02cb77e5d0..f7557d65355bd 100644 --- a/examples/2d/sprite_scale.rs +++ b/examples/2d/sprite_scale.rs @@ -5,98 +5,111 @@ use bevy::prelude::*; fn main() { App::new() .add_plugins(DefaultPlugins) - .add_systems(Startup, setup) + .add_systems( + Startup, + (setup_sprites, setup_texture_atlas).after(setup_camera), + ) + .add_systems(Update, animate_sprite) .run(); } -fn setup(mut commands: Commands, asset_server: Res) { +fn setup_camera(mut commands: Commands) { commands.spawn(Camera2d); +} +fn setup_sprites(mut commands: Commands, asset_server: Res) { let square = asset_server.load("textures/slice_square_2.png"); let banner = asset_server.load("branding/banner.png"); let rects = [ Rect { - size: Vec2::new(100., 300.), + size: Vec2::new(100., 225.), text: "Stretched".to_string(), - transform: Transform::from_translation(Vec3::new(-550.0, 200.0, 0.0)), + transform: Transform::from_translation(Vec3::new(-570.0, 230., 0.)), texture: square.clone(), image_mode: SpriteImageMode::Auto, }, Rect { - size: Vec2::new(100., 300.), + size: Vec2::new(100., 225.), text: "Fill Center".to_string(), - transform: Transform::from_translation(Vec3::new(-400.0, 200.0, 0.0)), + transform: Transform::from_translation(Vec3::new(-450.0, 230., 0.)), texture: square.clone(), image_mode: SpriteImageMode::Scale(ScalingMode::FillCenter), }, Rect { - size: Vec2::new(100., 300.), + size: Vec2::new(100., 225.), text: "Fill Start".to_string(), - transform: Transform::from_translation(Vec3::new(-250.0, 200.0, 0.0)), + transform: Transform::from_translation(Vec3::new(-330.0, 230.0, 0.0)), texture: square.clone(), image_mode: SpriteImageMode::Scale(ScalingMode::FillStart), }, Rect { - size: Vec2::new(100., 300.), + size: Vec2::new(100., 225.), text: "Fill End".to_string(), - transform: Transform::from_translation(Vec3::new(-100.0, 200.0, 0.0)), + transform: Transform::from_translation(Vec3::new(-210.0, 230.0, 0.0)), texture: square.clone(), image_mode: SpriteImageMode::Scale(ScalingMode::FillEnd), }, Rect { size: Vec2::new(300., 100.), text: "Fill Start Horizontal".to_string(), - transform: Transform::from_translation(Vec3::new(150.0, 300.0, 0.0)), + transform: Transform::from_translation(Vec3::new(10.0, 290.0, 0.0)), texture: square.clone(), image_mode: SpriteImageMode::Scale(ScalingMode::FillStart), }, Rect { size: Vec2::new(300., 100.), text: "Fill End Horizontal".to_string(), - transform: Transform::from_translation(Vec3::new(150.0, 100.0, 0.0)), + transform: Transform::from_translation(Vec3::new(10.0, 155.0, 0.0)), texture: square.clone(), image_mode: SpriteImageMode::Scale(ScalingMode::FillEnd), }, Rect { size: Vec2::new(200., 200.), text: "Fill Center".to_string(), - transform: Transform::from_translation(Vec3::new(450.0, 200.0, 0.0)), + transform: Transform::from_translation(Vec3::new(280.0, 230.0, 0.0)), texture: banner.clone(), image_mode: SpriteImageMode::Scale(ScalingMode::FillCenter), }, + Rect { + size: Vec2::new(200., 100.), + text: "Fill Center".to_string(), + transform: Transform::from_translation(Vec3::new(500.0, 230.0, 0.0)), + texture: square.clone(), + image_mode: SpriteImageMode::Scale(ScalingMode::FillCenter), + }, Rect { size: Vec2::new(100., 100.), text: "Stretched".to_string(), - transform: Transform::from_translation(Vec3::new(-550.0, -200.0, 0.0)), + transform: Transform::from_translation(Vec3::new(-570.0, -40., 0.0)), texture: banner.clone(), image_mode: SpriteImageMode::Auto, }, Rect { size: Vec2::new(200., 200.), text: "Fit Center".to_string(), - transform: Transform::from_translation(Vec3::new(-350.0, -200.0, 0.0)), + transform: Transform::from_translation(Vec3::new(-400.0, -40., 0.0)), texture: banner.clone(), image_mode: SpriteImageMode::Scale(ScalingMode::FitCenter), }, Rect { size: Vec2::new(200., 200.), text: "Fit Start".to_string(), - transform: Transform::from_translation(Vec3::new(-100.0, -200.0, 0.0)), + transform: Transform::from_translation(Vec3::new(-180.0, -40.0, 0.0)), texture: banner.clone(), image_mode: SpriteImageMode::Scale(ScalingMode::FitStart), }, Rect { size: Vec2::new(200., 200.), text: "Fit End".to_string(), - transform: Transform::from_translation(Vec3::new(150.0, -200.0, 0.0)), + transform: Transform::from_translation(Vec3::new(40.0, -40.0, 0.0)), texture: banner.clone(), image_mode: SpriteImageMode::Scale(ScalingMode::FitEnd), }, Rect { size: Vec2::new(100., 200.), text: "Fit Center".to_string(), - transform: Transform::from_translation(Vec3::new(350.0, -200.0, 0.0)), + transform: Transform::from_translation(Vec3::new(210.0, -40.0, 0.0)), texture: banner.clone(), image_mode: SpriteImageMode::Scale(ScalingMode::FitCenter), }, @@ -117,6 +130,7 @@ fn setup(mut commands: Commands, asset_server: Res) { builder.spawn(( Text2d::new(rect.text), TextLayout::new_with_justify(JustifyText::Center), + TextFont::from_font_size(15.), Transform::from_xyz(0., -0.5 * rect.size.y - 10., 0.0), bevy::sprite::Anchor::TopCenter, )); @@ -124,6 +138,152 @@ fn setup(mut commands: Commands, asset_server: Res) { } } +fn setup_texture_atlas( + mut commands: Commands, + asset_server: Res, + mut texture_atlas_layouts: ResMut>, +) { + commands.spawn(Camera2d); + let gabe = asset_server.load("textures/rpg/chars/gabe/gabe-idle-run.png"); + let animation_indices_gabe = AnimationIndices { first: 0, last: 6 }; + let gabe_atlas = TextureAtlas { + layout: texture_atlas_layouts.add(TextureAtlasLayout::from_grid( + UVec2::splat(24), + 7, + 1, + None, + None, + )), + index: animation_indices_gabe.first, + }; + + let sheets = [ + SpriteSheet { + size: Vec2::new(120., 50.), + text: "Stretched".to_string(), + transform: Transform::from_translation(Vec3::new(-570., -200., 0.)), + texture: gabe.clone(), + image_mode: SpriteImageMode::Auto, + atlas: gabe_atlas.clone(), + indices: animation_indices_gabe.clone(), + timer: AnimationTimer(Timer::from_seconds(0.1, TimerMode::Repeating)), + }, + SpriteSheet { + size: Vec2::new(120., 50.), + text: "Fill Center".to_string(), + transform: Transform::from_translation(Vec3::new(-570., -300., 0.)), + texture: gabe.clone(), + image_mode: SpriteImageMode::Scale(ScalingMode::FillCenter), + atlas: gabe_atlas.clone(), + indices: animation_indices_gabe.clone(), + timer: AnimationTimer(Timer::from_seconds(0.1, TimerMode::Repeating)), + }, + SpriteSheet { + size: Vec2::new(120., 50.), + text: "Fill Start".to_string(), + transform: Transform::from_translation(Vec3::new(-430., -200., 0.)), + texture: gabe.clone(), + image_mode: SpriteImageMode::Scale(ScalingMode::FillStart), + atlas: gabe_atlas.clone(), + indices: animation_indices_gabe.clone(), + timer: AnimationTimer(Timer::from_seconds(0.1, TimerMode::Repeating)), + }, + SpriteSheet { + size: Vec2::new(120., 50.), + text: "Fill End".to_string(), + transform: Transform::from_translation(Vec3::new(-430., -300., 0.)), + texture: gabe.clone(), + image_mode: SpriteImageMode::Scale(ScalingMode::FillEnd), + atlas: gabe_atlas.clone(), + indices: animation_indices_gabe.clone(), + timer: AnimationTimer(Timer::from_seconds(0.1, TimerMode::Repeating)), + }, + SpriteSheet { + size: Vec2::new(50., 120.), + text: "Fill Center".to_string(), + transform: Transform::from_translation(Vec3::new(-300., -250., 0.)), + texture: gabe.clone(), + image_mode: SpriteImageMode::Scale(ScalingMode::FillCenter), + atlas: gabe_atlas.clone(), + indices: animation_indices_gabe.clone(), + timer: AnimationTimer(Timer::from_seconds(0.1, TimerMode::Repeating)), + }, + SpriteSheet { + size: Vec2::new(50., 120.), + text: "Fill Start".to_string(), + transform: Transform::from_translation(Vec3::new(-190., -250., 0.)), + texture: gabe.clone(), + image_mode: SpriteImageMode::Scale(ScalingMode::FillStart), + atlas: gabe_atlas.clone(), + indices: animation_indices_gabe.clone(), + timer: AnimationTimer(Timer::from_seconds(0.1, TimerMode::Repeating)), + }, + SpriteSheet { + size: Vec2::new(50., 120.), + text: "Fill End".to_string(), + transform: Transform::from_translation(Vec3::new(-90., -250., 0.)), + texture: gabe.clone(), + image_mode: SpriteImageMode::Scale(ScalingMode::FillEnd), + atlas: gabe_atlas.clone(), + indices: animation_indices_gabe.clone(), + timer: AnimationTimer(Timer::from_seconds(0.1, TimerMode::Repeating)), + }, + SpriteSheet { + size: Vec2::new(120., 50.), + text: "Fit Center".to_string(), + transform: Transform::from_translation(Vec3::new(20., -200., 0.)), + texture: gabe.clone(), + image_mode: SpriteImageMode::Scale(ScalingMode::FitCenter), + atlas: gabe_atlas.clone(), + indices: animation_indices_gabe.clone(), + timer: AnimationTimer(Timer::from_seconds(0.1, TimerMode::Repeating)), + }, + SpriteSheet { + size: Vec2::new(120., 50.), + text: "Fit Start".to_string(), + transform: Transform::from_translation(Vec3::new(20., -300., 0.)), + texture: gabe.clone(), + image_mode: SpriteImageMode::Scale(ScalingMode::FitStart), + atlas: gabe_atlas.clone(), + indices: animation_indices_gabe.clone(), + timer: AnimationTimer(Timer::from_seconds(0.1, TimerMode::Repeating)), + }, + SpriteSheet { + size: Vec2::new(120., 50.), + text: "Fit End".to_string(), + transform: Transform::from_translation(Vec3::new(160., -200., 0.)), + texture: gabe.clone(), + image_mode: SpriteImageMode::Scale(ScalingMode::FitEnd), + atlas: gabe_atlas.clone(), + indices: animation_indices_gabe.clone(), + timer: AnimationTimer(Timer::from_seconds(0.1, TimerMode::Repeating)), + }, + ]; + + for sheet in sheets { + let mut cmd = commands.spawn(( + Sprite { + image_mode: sheet.image_mode, + custom_size: Some(sheet.size), + ..Sprite::from_atlas_image(sheet.texture.clone(), sheet.atlas.clone()) + }, + sheet.indices, + sheet.timer, + sheet.transform, + )); + + cmd.with_children(|builder| { + builder.spawn(( + Text2d::new(sheet.text), + TextLayout::new_with_justify(JustifyText::Center), + TextFont::from_font_size(15.), + Transform::from_xyz(0., -0.5 * sheet.size.y - 10., 0.0), + bevy::sprite::Anchor::TopCenter, + )); + }); + } +} + struct Rect { size: Vec2, text: String, @@ -131,3 +291,42 @@ struct Rect { texture: Handle, image_mode: SpriteImageMode, } + +struct SpriteSheet { + size: Vec2, + text: String, + transform: Transform, + texture: Handle, + image_mode: SpriteImageMode, + atlas: TextureAtlas, + indices: AnimationIndices, + timer: AnimationTimer, +} + +#[derive(Component, Clone)] +struct AnimationIndices { + first: usize, + last: usize, +} + +#[derive(Component, Deref, DerefMut)] +struct AnimationTimer(Timer); + +fn animate_sprite( + time: Res