Skip to content

Commit

Permalink
Add TextureScale modes for sprites
Browse files Browse the repository at this point in the history
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
  • Loading branch information
silvestrpredko committed Jan 9, 2025
1 parent 3578f9e commit 84d4a25
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 19 deletions.
2 changes: 1 addition & 1 deletion crates/bevy_sprite/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ pub mod prelude {
sprite::{Sprite, SpriteImageMode},
texture_atlas::{TextureAtlas, TextureAtlasLayout, TextureAtlasSources},
texture_slice::{BorderRect, SliceScaleMode, TextureSlice, TextureSlicer},
ColorMaterial, MeshMaterial2d, TextureAtlasBuilder,
ColorMaterial, MeshMaterial2d, TextureAtlasBuilder, TextureScale,
};
}

Expand Down
138 changes: 124 additions & 14 deletions crates/bevy_sprite/src/render/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use core::ops::Range;

use crate::{
texture_atlas::TextureAtlasLayout, ComputedTextureSlices, Sprite, SPRITE_SHADER_HANDLE,
texture_atlas::TextureAtlasLayout, ComputedTextureSlices, Sprite, TextureScale,
SPRITE_SHADER_HANDLE,
};
use bevy_asset::{AssetEvent, AssetId, Assets};
use bevy_color::{ColorToComponents, LinearRgba};
Expand Down Expand Up @@ -341,6 +342,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<Entity>,
pub texture_scale: Option<TextureScale>,
}

#[derive(Resource, Default)]
Expand Down Expand Up @@ -432,6 +434,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(),
},
);
}
Expand Down Expand Up @@ -713,21 +716,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 {
Expand All @@ -739,15 +758,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
Expand Down Expand Up @@ -888,3 +905,96 @@ impl<P: PhaseItem> RenderCommand<P> 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;
}
}
}
}
46 changes: 46 additions & 0 deletions crates/bevy_sprite/src/sprite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand All @@ -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<TextureScale> {
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`].
Expand Down
5 changes: 3 additions & 2 deletions crates/bevy_sprite/src/texture_slice/computed_slices.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}
})
}
Expand Down Expand Up @@ -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))
Expand Down
1 change: 1 addition & 0 deletions crates/bevy_text/src/text2d.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
},
);
}
Expand Down
4 changes: 2 additions & 2 deletions crates/bevy_ui/src/render/ui_texture_slice_pipeline.rs
Original file line number Diff line number Diff line change
Expand Up @@ -802,8 +802,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")
}
}
}
Expand Down

0 comments on commit 84d4a25

Please sign in to comment.