Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
47ba277
created SpriteMesh
PVDoriginal Jan 7, 2026
785bc69
Image size integration
PVDoriginal Jan 7, 2026
4b21f6b
sprite rect and custom_size
PVDoriginal Jan 8, 2026
b1859f8
fully implemented ImageScaleMode
PVDoriginal Jan 9, 2026
b6d5e22
flipping
PVDoriginal Jan 9, 2026
3ab97d8
tiling
PVDoriginal Jan 9, 2026
a15c728
fixed tiling
PVDoriginal Jan 9, 2026
dead63e
sprite slicing (unfinished)
PVDoriginal Jan 11, 2026
c18425b
fixed top edge stretch
PVDoriginal Jan 11, 2026
aa277f1
slicing finished
PVDoriginal Jan 11, 2026
45c3436
texture atlas
PVDoriginal Jan 11, 2026
eccb8a8
reversed example changes
PVDoriginal Jan 12, 2026
f02c9c8
Update mod.rs
PVDoriginal Jan 12, 2026
224c254
cleanup
PVDoriginal Jan 12, 2026
76eb410
fixed sprite mesh aabb
PVDoriginal Jan 12, 2026
0eafa60
removed test example
PVDoriginal Jan 12, 2026
2021210
Update lib.rs
PVDoriginal Jan 12, 2026
a9d427c
Update settings.json
PVDoriginal Jan 12, 2026
0c8e3f2
Delete settings.json
PVDoriginal Jan 12, 2026
7dbb4ff
fixed typo and changed std to core
PVDoriginal Jan 12, 2026
7a2c844
Update sprite_material.rs
PVDoriginal Jan 12, 2026
4398858
anchor fix + minor hashing improvement
PVDoriginal Jan 14, 2026
3f856ae
changed std to core
PVDoriginal Jan 14, 2026
8fe74a5
removed unused struct
PVDoriginal Jan 14, 2026
12d2a59
sprite_mesh examples
PVDoriginal Jan 15, 2026
4a11679
example metadata
PVDoriginal Jan 15, 2026
64db84e
Update README.md
PVDoriginal Jan 15, 2026
0a0ae1c
Apply suggestions from code review
PVDoriginal Jan 15, 2026
73e677b
doc alignment
PVDoriginal Jan 15, 2026
86b47de
added backticks :)
PVDoriginal Jan 15, 2026
6b82d15
Create sprite_mesh.md
PVDoriginal Jan 15, 2026
6d539af
Update sprite_mesh.md
PVDoriginal Jan 15, 2026
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
24 changes: 24 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3350,6 +3350,18 @@ description = "Displays many animated sprites in a grid arrangement with slight
category = "Stress Tests"
wasm = true

[[example]]
name = "many_animated_sprite_meshes"
path = "examples/stress_tests/many_animated_sprite_meshes.rs"
# Causes an ICE on docs.rs
doc-scrape-examples = false

[package.metadata.example.many_animated_sprite_meshes]
name = "Many Animated Sprite Meshes"
description = "Displays many animated sprite meshes in a grid arrangement with slight offsets to their animation timers. Used for performance testing."
category = "Stress Tests"
wasm = false

[[example]]
name = "many_buttons"
path = "examples/stress_tests/many_buttons.rs"
Expand Down Expand Up @@ -3470,6 +3482,18 @@ description = "Displays many sprites in a grid arrangement! Used for performance
category = "Stress Tests"
wasm = true

[[example]]
name = "many_sprite_meshes"
path = "examples/stress_tests/many_sprite_meshes.rs"
# Causes an ICE on docs.rs
doc-scrape-examples = false

[package.metadata.example.many_sprite_meshes]
name = "Many Sprite Meshes"
description = "Displays many sprite meshes in a grid arrangement! Used for performance testing. Use `--colored` to enable color tinted sprites."
category = "Stress Tests"
wasm = false

[[example]]
name = "many_text2d"
path = "examples/stress_tests/many_text2d.rs"
Expand Down
77 changes: 75 additions & 2 deletions crates/bevy_sprite/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ extern crate alloc;
#[cfg(feature = "bevy_picking")]
mod picking_backend;
mod sprite;
mod sprite_mesh;
#[cfg(feature = "bevy_text")]
mod text2d;
mod texture_slice;
Expand All @@ -32,6 +33,7 @@ pub mod prelude {
#[doc(hidden)]
pub use crate::{
sprite::{Sprite, SpriteImageMode},
sprite_mesh::SpriteMesh,
texture_slice::{BorderRect, SliceScaleMode, TextureSlice, TextureSlicer},
SpriteScalingMode,
};
Expand All @@ -47,6 +49,7 @@ use bevy_mesh::{Mesh, Mesh2d};
#[cfg(feature = "bevy_picking")]
pub use picking_backend::*;
pub use sprite::*;
pub use sprite_mesh::*;
#[cfg(feature = "bevy_text")]
pub use text2d::*;
pub use texture_slice::*;
Expand Down Expand Up @@ -76,7 +79,9 @@ impl Plugin for SpritePlugin {
}
app.add_systems(
PostUpdate,
calculate_bounds_2d.in_set(VisibilitySystems::CalculateBounds),
(calculate_bounds_2d, calculate_bounds_2d_sprite_mesh)
.chain()
.in_set(VisibilitySystems::CalculateBounds),
);

#[cfg(feature = "bevy_text")]
Expand Down Expand Up @@ -115,6 +120,7 @@ pub fn calculate_bounds_2d(
Without<Aabb>,
Without<NoFrustumCulling>,
Without<NoAutoAabb>,
Without<SpriteMesh>, // temporary before merging SpriteMesh into Sprite,
),
>,
mut update_mesh_aabb: Query<
Expand All @@ -123,7 +129,8 @@ pub fn calculate_bounds_2d(
Or<(AssetChanged<Mesh2d>, Changed<Mesh2d>)>,
Without<NoFrustumCulling>,
Without<NoAutoAabb>,
Without<Sprite>, // disjoint mutable query
Without<SpriteMesh>, // temporary before merging SpriteMesh into Sprite,
Without<Sprite>, // disjoint mutable query
),
>,
new_sprite_aabb: Query<
Expand Down Expand Up @@ -202,6 +209,72 @@ pub fn calculate_bounds_2d(
});
}

// Temporarily added this to calculate aabb for sprite meshes.
// Will eventually be merged with Sprite in the system above.
//
// NOTE: this is separate from Mesh2d because sprites change their size
// inside the vertex shader which isn't recognized by calculate_aabb().
fn calculate_bounds_2d_sprite_mesh(
mut commands: Commands,
images: Res<Assets<Image>>,
atlases: Res<Assets<TextureAtlasLayout>>,
new_sprite_aabb: Query<
(Entity, &SpriteMesh, &Anchor),
(
Without<Aabb>,
Without<NoFrustumCulling>,
Without<NoAutoAabb>,
),
>,
mut update_sprite_aabb: Query<
(&SpriteMesh, &mut Aabb, &Anchor),
(
Or<(Changed<SpriteMesh>, Changed<Anchor>)>,
Without<NoFrustumCulling>,
Without<NoAutoAabb>,
),
>,
) {
// Sprite helper
let sprite_size = |sprite: &SpriteMesh| -> Option<Vec2> {
sprite
.custom_size
.or_else(|| sprite.rect.map(|rect| rect.size()))
.or_else(|| match &sprite.texture_atlas {
// We default to the texture size for regular sprites
None => images.get(&sprite.image).map(Image::size_f32),
// We default to the drawn rect for atlas sprites
Some(atlas) => atlas
.texture_rect(&atlases)
.map(|rect| rect.size().as_vec2()),
})
};

// New sprites require inserting a component
for (size, (entity, anchor)) in new_sprite_aabb
.iter()
.filter_map(|(entity, sprite, anchor)| sprite_size(sprite).zip(Some((entity, anchor))))
{
let aabb = Aabb {
center: (-anchor.as_vec() * size).extend(0.0).into(),
half_extents: (0.5 * size).extend(0.0).into(),
};
commands.entity(entity).try_insert(aabb);
}

// Updated sprites can take the fast path with parallel component mutation
update_sprite_aabb
.par_iter_mut()
.for_each(|(sprite, mut aabb, anchor)| {
if let Some(size) = sprite_size(sprite) {
aabb.set_if_neq(Aabb {
center: (-anchor.as_vec() * size).extend(0.0).into(),
half_extents: (0.5 * size).extend(0.0).into(),
});
}
});
}

#[cfg(test)]
mod test {
use super::*;
Expand Down
10 changes: 9 additions & 1 deletion crates/bevy_sprite/src/sprite.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,11 @@ use bevy_derive::{Deref, DerefMut};
use bevy_ecs::{component::Component, reflect::ReflectComponent};
use bevy_image::{Image, TextureAtlas, TextureAtlasLayout};
use bevy_math::{Rect, UVec2, Vec2};
use bevy_reflect::{std_traits::ReflectDefault, Reflect};
use bevy_reflect::{std_traits::ReflectDefault, PartialReflect, Reflect};
use bevy_transform::components::Transform;

use crate::TextureSlicer;
use core::hash::Hash;

/// Describes a sprite to be rendered to a 2D camera
#[derive(Component, Debug, Default, Clone, Reflect)]
Expand Down Expand Up @@ -254,6 +255,13 @@ pub enum SpriteScalingMode {
#[doc(alias = "pivot")]
pub struct Anchor(pub Vec2);

impl Eq for Anchor {}
impl Hash for Anchor {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.0.reflect_hash().hash(state);
}
}

impl Anchor {
pub const BOTTOM_LEFT: Self = Self(Vec2::new(-0.5, -0.5));
pub const BOTTOM_CENTER: Self = Self(Vec2::new(0.0, -0.5));
Expand Down
201 changes: 201 additions & 0 deletions crates/bevy_sprite/src/sprite_mesh.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,201 @@
use bevy_asset::{Assets, Handle};
use bevy_camera::visibility::{Visibility, VisibilityClass};
use bevy_color::Color;
use bevy_ecs::{component::Component, reflect::ReflectComponent};
use bevy_image::{Image, TextureAtlas, TextureAtlasLayout};
use bevy_math::{Rect, UVec2, Vec2};
use bevy_reflect::{std_traits::ReflectDefault, PartialReflect, Reflect};
use bevy_transform::components::Transform;

use crate::{Anchor, SpriteImageMode};

/// This is a carbon copy of [`Sprite`](crate::sprite::Sprite) that uses the
/// Mesh backend instead of the Sprite backend.
///
/// The only API difference is the added [`alpha mode`](SpriteMesh::alpha_mode).
#[derive(Component, Debug, Default, Clone, Reflect, PartialEq)]
#[require(Transform, Visibility, VisibilityClass, Anchor)]
#[reflect(Component, Default, Debug, Clone)]
pub struct SpriteMesh {
/// The image used to render the sprite
pub image: Handle<Image>,
/// The (optional) texture atlas used to render the sprite
pub texture_atlas: Option<TextureAtlas>,
/// The sprite's color tint
pub color: Color,
/// Flip the sprite along the `X` axis
pub flip_x: bool,
/// Flip the sprite along the `Y` axis
pub flip_y: bool,
/// An optional custom size for the sprite that will be used when rendering, instead of the size
/// of the sprite's image
pub custom_size: Option<Vec2>,
/// An optional rectangle representing the region of the sprite's image to render, instead of rendering
/// the full image. This is an easy one-off alternative to using a [`TextureAtlas`].
///
/// When used with a [`TextureAtlas`], the rect
/// is offset by the atlas's minimal (top-left) corner position.
pub rect: Option<Rect>,
/// How the sprite's image will be scaled.
pub image_mode: SpriteImageMode,
/// The sprite's alpha mode, defaulting to `Mask(0.5)`.
/// If you wish to render a sprite with translucent pixels,
/// set it to `Blend` instead (significantly worse for performance).
pub alpha_mode: SpriteAlphaMode,
}

impl core::hash::Hash for SpriteMesh {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.image.hash(state);
self.texture_atlas.hash(state);
self.color.reflect_hash().hash(state);
self.custom_size.reflect_hash().hash(state);
self.flip_x.hash(state);
self.flip_y.hash(state);
}
}

impl Eq for SpriteMesh {}

// NOTE: The SpriteImageMode, SpriteScalingMode and Anchor are imported from the sprite module.

impl SpriteMesh {
/// Create a Sprite with a custom size
pub fn sized(custom_size: Vec2) -> Self {
SpriteMesh {
custom_size: Some(custom_size),
..Default::default()
}
}

/// Create a sprite from an image
pub fn from_image(image: Handle<Image>) -> Self {
Self {
image,
..Default::default()
}
}

/// Create a sprite from an image, with an associated texture atlas
pub fn from_atlas_image(image: Handle<Image>, atlas: TextureAtlas) -> Self {
Self {
image,
texture_atlas: Some(atlas),
..Default::default()
}
}

/// Create a sprite from a solid color
pub fn from_color(color: impl Into<Color>, size: Vec2) -> Self {
Self {
color: color.into(),
custom_size: Some(size),
..Default::default()
}
}

/// Computes the pixel point where `point_relative_to_sprite` is sampled
/// from in this sprite. `point_relative_to_sprite` must be in the sprite's
/// local frame. Returns an Ok if the point is inside the bounds of the
/// sprite (not just the image), and returns an Err otherwise.
pub fn compute_pixel_space_point(
&self,
point_relative_to_sprite: Vec2,
anchor: Anchor,
images: &Assets<Image>,
texture_atlases: &Assets<TextureAtlasLayout>,
) -> Result<Vec2, Vec2> {
let image_size = images
.get(&self.image)
.map(Image::size)
.unwrap_or(UVec2::ONE);

let atlas_rect = self
.texture_atlas
.as_ref()
.and_then(|s| s.texture_rect(texture_atlases))
.map(|r| r.as_rect());
let texture_rect = match (atlas_rect, self.rect) {
(None, None) => Rect::new(0.0, 0.0, image_size.x as f32, image_size.y as f32),
(None, Some(sprite_rect)) => sprite_rect,
(Some(atlas_rect), None) => atlas_rect,
(Some(atlas_rect), Some(mut sprite_rect)) => {
// Make the sprite rect relative to the atlas rect.
sprite_rect.min += atlas_rect.min;
sprite_rect.max += atlas_rect.min;
sprite_rect
}
};

let sprite_size = self.custom_size.unwrap_or_else(|| texture_rect.size());
let sprite_center = -anchor.as_vec() * sprite_size;

let mut point_relative_to_sprite_center = point_relative_to_sprite - sprite_center;

if self.flip_x {
point_relative_to_sprite_center.x *= -1.0;
}
// Texture coordinates start at the top left, whereas world coordinates start at the bottom
// left. So flip by default, and then don't flip if `flip_y` is set.
if !self.flip_y {
point_relative_to_sprite_center.y *= -1.0;
}

if sprite_size.x == 0.0 || sprite_size.y == 0.0 {
return Err(point_relative_to_sprite_center);
}

let sprite_to_texture_ratio = {
let texture_size = texture_rect.size();
Vec2::new(
texture_size.x / sprite_size.x,
texture_size.y / sprite_size.y,
)
};

let point_relative_to_texture =
point_relative_to_sprite_center * sprite_to_texture_ratio + texture_rect.center();

// TODO: Support `SpriteImageMode`.

if texture_rect.contains(point_relative_to_texture) {
Ok(point_relative_to_texture)
} else {
Err(point_relative_to_texture)
}
}
}

// This is different from AlphaMode2d in bevy_sprite_render because that crate depends on this one,
// so using it would've been caused a circular dependency. An option would be to move the Enum here
// but it uses a bevy_render dependency in its documentation, and I wanted to avoid bringing that
// dependency to this crate.

// NOTE: If this is ever replaced by AlphaMode2d, make a custom Default impl for Sprite,
// because AlphaMode2d defaults to Opaque, but the sprite's alpha mode is most commonly Mask(0.5)

#[derive(Debug, Reflect, Copy, Clone, PartialEq)]
#[reflect(Default, Debug, Clone)]
pub enum SpriteAlphaMode {
/// Base color alpha values are overridden to be fully opaque (1.0).
Opaque,
/// Reduce transparency to fully opaque or fully transparent
/// based on a threshold.
///
/// Compares the base color alpha value to the specified threshold.
/// If the value is below the threshold,
/// considers the color to be fully transparent (alpha is set to 0.0).
/// If it is equal to or above the threshold,
/// considers the color to be fully opaque (alpha is set to 1.0).
Mask(f32),
/// The base color alpha value defines the opacity of the color.
/// Standard alpha-blending is used to blend the fragment's color
/// with the color behind it.
Blend,
}

impl Default for SpriteAlphaMode {
fn default() -> Self {
Self::Mask(0.5)
}
}
Loading