-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
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
Proportional scaling for the sprite's texture. #17258
Changes from 6 commits
cc47c93
baf755f
296174f
22879d4
2672332
1a8fa2a
dbfa421
6fda406
954fead
bea348f
bf1b769
a876f00
837e0f7
9a2329e
75176c8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,6 +1,6 @@ | ||
use core::ops::Range; | ||
|
||
use crate::{ComputedTextureSlices, Sprite, 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,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<Entity>, | ||
pub scaling_mode: Option<ScalingMode>, | ||
} | ||
|
||
#[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), | ||
scaling_mode: sprite.image_mode.scale(), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd prefer it if the sprite geometry was calculated during extraction rather than in prepare. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All the geometry logic happens in |
||
}, | ||
); | ||
} | ||
|
@@ -700,21 +702,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(scaling_mode) = extracted_sprite.scaling_mode { | ||
apply_scaling( | ||
scaling_mode, | ||
batch_image_size, | ||
&mut quad_size, | ||
&mut quad_translation, | ||
&mut uv_offset_scale, | ||
); | ||
} | ||
|
||
if extracted_sprite.flip_x { | ||
|
@@ -726,15 +744,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 | ||
|
@@ -875,3 +891,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 apply_scaling( | ||
scaling_mode: ScalingMode, | ||
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 scaling_mode { | ||
ScalingMode::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); | ||
} | ||
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); | ||
} 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); | ||
} | ||
} | ||
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); | ||
} 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); | ||
} | ||
} | ||
ScalingMode::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; | ||
} | ||
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 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; | ||
} | ||
} | ||
ScalingMode::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; | ||
} | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. These are identical except for 1 minus sign, and there are similar duplication in the other Start/End cases. On one hand it would be nice if the repetition could be factored out so e.g. FitEnd is just FitStart and set the minus. On the other hand this code is pretty simple and easy to read. It might also become simpler if you match on There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I understand your concern about code duplication, and I agree that maintaining clarity is important. However, considering the minimal amount of code duplication we currently have, I believe it's reasonable to keep things as they are. The existing structure seems to be functioning well without introducing unnecessary complexity. |
||
} | ||
} |
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -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. | ||||||
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` | ||||||
|
@@ -185,6 +188,51 @@ impl SpriteImageMode { | |||||
SpriteImageMode::Sliced(..) | SpriteImageMode::Tiled { .. } | ||||||
) | ||||||
} | ||||||
|
||||||
/// Returns [`ScalingMode`] if scale is presented or [`Option::None`] otherwise. | ||||||
#[inline] | ||||||
#[must_use] | ||||||
pub const fn scale(&self) -> Option<ScalingMode> { | ||||||
if let SpriteImageMode::Scale(scale) = self { | ||||||
Some(*scale) | ||||||
} else { | ||||||
None | ||||||
} | ||||||
} | ||||||
} | ||||||
|
||||||
/// 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 ScalingMode { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think maybe this type might be better split up into different two enums, one There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
I don't think that will be convenient for the end user. There are only six variants and no more are planned to add I guess. That led to the single field in Sprite that corresponds to scaling, adding one more enum adds more complexity. Then |
||||||
/// 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. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess you mean the |
||||||
/// Fill rect with a centered texture. | ||||||
#[default] | ||||||
FillCenter, | ||||||
/// Scale the texture to fill the rect with a start of the texture, | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
This isn't really very clear about what the "start" of the texture is. |
||||||
/// maintaining the aspect ratio. | ||||||
FillStart, | ||||||
/// Scale the texture to fill the rect with a end of the texture, | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
/// 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. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
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. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
/// 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. | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
/// Aligns the result to the right and bottom edges of rect. | ||||||
FitEnd, | ||||||
} | ||||||
|
||||||
/// How a sprite is positioned relative to its [`Transform`]. | ||||||
|
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
|
@@ -813,7 +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 ImageScaleMode::Stretch") | ||||||
unreachable!("Slices should not be computed for SpriteImageMode::Stretch") | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
} | ||||||
SpriteImageMode::Scale(_) => { | ||||||
unreachable!("Slices should not be computed for SpriteImageMode::Scale") | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
} | ||||||
} | ||||||
} | ||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.