Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement images with layout for TextureAtlasBuilder #13302

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 50 additions & 1 deletion crates/bevy_render/src/texture/image.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ use crate::{
render_asset::{PrepareAssetError, RenderAsset, RenderAssetUsages},
render_resource::{Sampler, Texture, TextureView},
renderer::{RenderDevice, RenderQueue},
texture::BevyDefault,
texture::{image_texture_conversion::IntoDynamicImageError, BevyDefault},
};
use bevy_asset::Asset;
use bevy_derive::{Deref, DerefMut};
use bevy_ecs::system::{lifetimeless::SRes, Resource, SystemParamItem};
use bevy_math::{AspectRatio, UVec2, Vec2};
use bevy_reflect::prelude::*;
use image::{DynamicImage, GenericImage};
use serde::{Deserialize, Serialize};
use std::hash::Hash;
use thiserror::Error;
Expand Down Expand Up @@ -622,6 +623,45 @@ impl Image {
});
}

/// Attempts to create a sub-image defined by a rectangle.
///
/// # Arguments
///
/// * `x`: The x-coordinate of the top-left corner of the region.
/// * `y`: The y-coordinate of the top-left corner of the region.
/// * `width`: The width of the region.
/// * `height`: The height of the region.
///
/// # Errors
///
/// * [`SubImageError::OutOfBounds`] if the rectangle region is outside the image dimensions.
/// * [`SubImageError::DynamicImageError`] if the [`TextureFormat`] is unsupported by [`Image::try_into_dynamic`].
pub fn try_sub_image(
&self,
x: u32,
y: u32,
width: u32,
height: u32,
) -> Result<Image, SubImageError> {
// Out of bounds check to avoid a panic in image::sub_image.
if (x as u64 + width as u64 > self.width() as u64)
|| (y as u64 + height as u64 > self.height() as u64)
{
return Err(SubImageError::OutOfBounds);
}
let dyn_image = DynamicImage::from(
self.clone()
.try_into_dynamic()?
.sub_image(x, y, width, height)
.to_image(),
);
Ok(Self::from_dynamic(
dyn_image,
self.texture_descriptor.format.is_srgb(),
self.asset_usage,
))
}

/// Convert a texture from a format to another. Only a few formats are
/// supported as input and output:
/// - `TextureFormat::R8Unorm`
Expand Down Expand Up @@ -763,6 +803,15 @@ pub enum TextureError {
IncompleteCubemap,
}

/// Error that occurs when creating a sub-image from an existing image.
#[derive(Error, Debug)]
pub enum SubImageError {
#[error("out of bounds subimage")]
OutOfBounds,
#[error("failed to transform into dynamic image")]
DynamicImageError(#[from] IntoDynamicImageError),
}

/// The type of a raw image buffer.
#[derive(Debug)]
pub enum ImageType<'a> {
Expand Down
29 changes: 22 additions & 7 deletions crates/bevy_sprite/src/texture_atlas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ pub struct TextureAtlasLayout {
/// This field is set by [`TextureAtlasBuilder`].
///
/// [`TextureAtlasBuilder`]: crate::TextureAtlasBuilder
pub(crate) texture_handles: Option<HashMap<AssetId<Image>, usize>>,
pub(crate) texture_handles: Option<HashMap<AssetId<Image>, Vec<usize>>>,
}

/// Component used to draw a specific section of a texture.
Expand Down Expand Up @@ -136,16 +136,31 @@ impl TextureAtlasLayout {
self.textures.is_empty()
}

/// Retrieves the texture *section* index of the given `texture` handle.
/// Creates a layout of the given `texture` handle.
///
/// This requires the layout to have been built using a [`TextureAtlasBuilder`]
///
/// [`TextureAtlasBuilder`]: crate::TextureAtlasBuilder
pub fn get_texture_index(&self, texture: impl Into<AssetId<Image>>) -> Option<usize> {
let id = texture.into();
self.texture_handles
.as_ref()
.and_then(|texture_handles| texture_handles.get(&id).cloned())
pub fn sub_layout(&self, texture: impl Into<AssetId<Image>>) -> Option<TextureAtlasLayout> {
let mut layout = TextureAtlasLayout::new_empty(self.size);
for &index in self.get_texture_index(texture.into())? {
layout.textures.push(*self.textures.get(index)?);
}
Some(layout)
}

/// Retrieves the texture *section* indices of the given `texture` handle.
///
/// This requires the layout to have been built using a [`TextureAtlasBuilder`]
///
/// [`TextureAtlasBuilder`]: crate::TextureAtlasBuilder
pub fn get_texture_index(&self, texture: impl Into<AssetId<Image>>) -> Option<&[usize]> {
Some(
self.texture_handles
.as_ref()?
.get(&texture.into())?
.as_slice(),
)
}
}

Expand Down
41 changes: 38 additions & 3 deletions crates/bevy_sprite/src/texture_atlas_builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ use rectangle_pack::{
contains_smallest_box, pack_rects, volume_heuristic, GroupedRectsToPlace, PackedLocation,
RectToInsert, TargetBin,
};
use std::borrow::Cow;
use thiserror::Error;

use crate::TextureAtlasLayout;
Expand All @@ -29,7 +30,7 @@ pub enum TextureAtlasBuilderError {
/// sprites.
pub struct TextureAtlasBuilder<'a> {
/// Collection of texture's asset id (optional) and image data to be packed into an atlas
textures_to_place: Vec<(Option<AssetId<Image>>, &'a Image)>,
textures_to_place: Vec<(Option<AssetId<Image>>, Cow<'a, Image>)>,
/// The initial atlas size in pixels.
initial_size: UVec2,
/// The absolute maximum size of the texture atlas in pixels.
Expand Down Expand Up @@ -87,7 +88,38 @@ impl<'a> TextureAtlasBuilder<'a> {
/// Optionally an asset id can be passed that can later be used with the texture layout to retrieve the index of this texture.
/// The insertion order will reflect the index of the added texture in the finished texture atlas.
pub fn add_texture(&mut self, image_id: Option<AssetId<Image>>, texture: &'a Image) {
self.textures_to_place.push((image_id, texture));
self.textures_to_place
.push((image_id, Cow::Borrowed(texture)));
}

/// Adds textures from the provided layout to the texture atlas builder.
///
/// This method takes an optional image ID, a reference to the image, and a texture atlas layout as input.
/// It iterates over each rectangle defined in the layout and attempts to extract sub-images from the provided image based on the layout.
/// The sub-images are then added to the textures to place in the texture atlas builder.
///
/// If the layout contains rectangles that are outside the bounds of the provided image, those sub-images will be skipped.
///
/// *Note*: Texture format needs to be supported by [`Image::try_into_dynamic`].
pub fn add_texture_with_layout(
&mut self,
image_id: Option<AssetId<Image>>,
texture: &'a Image,
layout: TextureAtlasLayout,
) {
for rect in &layout.textures {
let top_left_corner = (rect.min.x.min(rect.max.x), rect.min.y.min(rect.max.y));
let Ok(image) = texture.try_sub_image(
top_left_corner.0,
top_left_corner.1,
rect.width(),
rect.height(),
) else {
warn!("TextureAtlasBuilder: Invalid input layout, sub-image will be ignored");
continue;
};
self.textures_to_place.push((image_id, Cow::Owned(image)));
}
}

/// Sets the amount of padding in pixels to add between the textures in the texture atlas.
Expand Down Expand Up @@ -263,7 +295,10 @@ impl<'a> TextureAtlasBuilder<'a> {
let max =
min + UVec2::new(packed_location.width(), packed_location.height()) - self.padding;
if let Some(image_id) = image_id {
texture_ids.insert(*image_id, index);
texture_ids
.entry(*image_id)
.or_insert(Vec::new())
.push(index);
}
texture_rects.push(URect { min, max });
if texture.texture_descriptor.format != self.format && !self.auto_format_conversion {
Expand Down
3 changes: 1 addition & 2 deletions examples/2d/texture_atlas.rs
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ fn setup(
mut textures: ResMut<Assets<Image>>,
) {
let loaded_folder = loaded_folders.get(&rpg_sprite_handles.0).unwrap();

// create texture atlases with different padding and sampling

let (texture_atlas_linear, linear_texture) = create_texture_atlas(
Expand Down Expand Up @@ -183,7 +182,7 @@ fn setup(
create_sprite_from_atlas(
&mut commands,
(x, base_y, 0.0),
vendor_index,
vendor_index[0],
atlas_handle,
image_handle,
);
Expand Down