diff --git a/crates/bevy_image/src/compressed_image_saver.rs b/crates/bevy_image/src/compressed_image_saver.rs index c2adc2c029116..0ff9f2a5de999 100644 --- a/crates/bevy_image/src/compressed_image_saver.rs +++ b/crates/bevy_image/src/compressed_image_saver.rs @@ -1,6 +1,7 @@ use crate::{Image, ImageFormat, ImageFormatSetting, ImageLoader, ImageLoaderSettings}; use bevy_asset::saver::{AssetSaver, SavedAsset}; +use core::num::NonZero; use futures_lite::AsyncWriteExt; use thiserror::Error; @@ -64,11 +65,14 @@ impl AssetSaver for CompressedImageSaver { }; writer.write_all(&compressed_basis_data).await?; + + let layers = NonZero::new(image.texture_descriptor.size.depth_or_array_layers); Ok(ImageLoaderSettings { format: ImageFormatSetting::Format(ImageFormat::Basis), is_srgb, sampler: image.sampler.clone(), asset_usage: image.asset_usage, + layers, }) } } diff --git a/crates/bevy_image/src/image.rs b/crates/bevy_image/src/image.rs index eb0b9d1884c0a..aed512524d4d0 100644 --- a/crates/bevy_image/src/image.rs +++ b/crates/bevy_image/src/image.rs @@ -1042,21 +1042,20 @@ impl Image { } } - /// Changes the `size`, asserting that the total number of data elements (pixels) remains the - /// same. - /// - /// # Panics - /// Panics if the `new_size` does not have the same volume as to old one. - pub fn reinterpret_size(&mut self, new_size: Extent3d) { - assert_eq!( - new_size.volume(), - self.texture_descriptor.size.volume(), - "Incompatible sizes: old = {:?} new = {:?}", - self.texture_descriptor.size, - new_size - ); + /// Changes the `size` if the total number of data elements (pixels) remains the same. + pub fn reinterpret_size( + &mut self, + new_size: Extent3d, + ) -> Result<(), TextureReinterpretationError> { + if new_size.volume() != self.texture_descriptor.size.volume() { + return Err(TextureReinterpretationError::IncompatibleSizes { + old: self.texture_descriptor.size, + new: new_size, + }); + } self.texture_descriptor.size = new_size; + Ok(()) } /// Resizes the image to the new size, keeping the pixel data intact, anchored at the top-left. @@ -1106,21 +1105,31 @@ impl Image { /// Takes a 2D image containing vertically stacked images of the same size, and reinterprets /// it as a 2D array texture, where each of the stacked images becomes one layer of the /// array. This is primarily for use with the `texture2DArray` shader uniform type. - /// - /// # Panics - /// Panics if the texture is not 2D, has more than one layers or is not evenly dividable into - /// the `layers`. - pub fn reinterpret_stacked_2d_as_array(&mut self, layers: u32) { + pub fn reinterpret_stacked_2d_as_array( + &mut self, + layers: u32, + ) -> Result<(), TextureReinterpretationError> { // Must be a stacked image, and the height must be divisible by layers. - assert_eq!(self.texture_descriptor.dimension, TextureDimension::D2); - assert_eq!(self.texture_descriptor.size.depth_or_array_layers, 1); - assert_eq!(self.height() % layers, 0); + if self.texture_descriptor.dimension != TextureDimension::D2 { + return Err(TextureReinterpretationError::WrongDimension); + } + if self.texture_descriptor.size.depth_or_array_layers != 1 { + return Err(TextureReinterpretationError::InvalidLayerCount); + } + if self.height() % layers != 0 { + return Err(TextureReinterpretationError::HeightNotDivisableByLayers { + height: self.height(), + layers, + }); + } self.reinterpret_size(Extent3d { width: self.width(), height: self.height() / layers, depth_or_array_layers: layers, - }); + })?; + + Ok(()) } /// Convert a texture from a format to another. Only a few formats are @@ -1719,6 +1728,19 @@ pub enum TranscodeFormat { Rgb8, } +/// An error that occurs when reinterpreting the image. +#[derive(Error, Debug)] +pub enum TextureReinterpretationError { + #[error("incompatible sizes: old = {old:?} new = {new:?}")] + IncompatibleSizes { old: Extent3d, new: Extent3d }, + #[error("must be a 2d image")] + WrongDimension, + #[error("must not already be a layered image")] + InvalidLayerCount, + #[error("can not evenly divide height = {height} by layers = {layers}")] + HeightNotDivisableByLayers { height: u32, layers: u32 }, +} + /// An error that occurs when accessing specific pixels in a texture. #[derive(Error, Debug)] pub enum TextureAccessError { diff --git a/crates/bevy_image/src/image_loader.rs b/crates/bevy_image/src/image_loader.rs index fe086db674a4e..0b93cdbd35fcc 100644 --- a/crates/bevy_image/src/image_loader.rs +++ b/crates/bevy_image/src/image_loader.rs @@ -1,4 +1,9 @@ -use crate::image::{Image, ImageFormat, ImageType, TextureError}; +use core::num::NonZero; + +use crate::{ + image::{Image, ImageFormat, ImageType, TextureError}, + TextureReinterpretationError, +}; use bevy_asset::{io::Reader, AssetLoader, LoadContext, RenderAssetUsages}; use thiserror::Error; @@ -111,6 +116,9 @@ pub struct ImageLoaderSettings { /// Where the asset will be used - see the docs on /// [`RenderAssetUsages`] for details. pub asset_usage: RenderAssetUsages, + /// If the image should be loaded as a stacked 2d array + /// image with the given number of layers + pub layers: Option>, } impl Default for ImageLoaderSettings { @@ -120,6 +128,7 @@ impl Default for ImageLoaderSettings { is_srgb: true, sampler: ImageSampler::Default, asset_usage: RenderAssetUsages::default(), + layers: Default::default(), } } } @@ -134,6 +143,9 @@ pub enum ImageLoaderError { /// An error occurred while trying to decode the image bytes. #[error("Could not load texture file: {0}")] FileTexture(#[from] FileTextureError), + /// An error occurred while trying to reinterpret the image (e.g. loading as a stacked 2d array). + #[error("Could not reinterpret image: {0}")] + ReinterpretationError(#[from] TextureReinterpretationError), } impl AssetLoader for ImageLoader { @@ -168,7 +180,8 @@ impl AssetLoader for ImageLoader { )?) } }; - Ok(Image::from_buffer( + + let mut image = Image::from_buffer( &bytes, image_type, self.supported_compressed_formats, @@ -179,7 +192,13 @@ impl AssetLoader for ImageLoader { .map_err(|err| FileTextureError { error: err, path: format!("{}", load_context.path().display()), - })?) + })?; + + if let Some(layers) = settings.layers { + image.reinterpret_stacked_2d_as_array(layers.into())?; + } + + Ok(image) } fn extensions(&self) -> &[&str] { diff --git a/examples/2d/tilemap_chunk.rs b/examples/2d/tilemap_chunk.rs index a7e30ee64eb44..79a4b08cb0801 100644 --- a/examples/2d/tilemap_chunk.rs +++ b/examples/2d/tilemap_chunk.rs @@ -1,6 +1,9 @@ //! Shows a tilemap chunk rendered with a single draw call. +use core::num::NonZero; + use bevy::{ + image::ImageLoaderSettings, prelude::*, sprite::{TileData, TilemapChunk, TilemapChunkTileData}, }; @@ -11,7 +14,7 @@ fn main() { App::new() .add_plugins(DefaultPlugins.set(ImagePlugin::default_nearest())) .add_systems(Startup, setup) - .add_systems(Update, (update_tileset_image, update_tilemap)) + .add_systems(Update, update_tilemap) .run(); } @@ -39,12 +42,18 @@ fn setup(mut commands: Commands, assets: Res) { }) .collect(); + let layers = 4; commands.spawn(( TilemapChunk { chunk_size, tile_display_size, - tileset: assets.load("textures/array_texture.png"), - ..default() + tileset: assets.load_with_settings( + "textures/array_texture.png", + move |settings: &mut ImageLoaderSettings| { + settings.layers = NonZero::new(layers); + }, + ), + ..Default::default() }, TilemapChunkTileData(tile_data), UpdateTimer(Timer::from_seconds(0.1, TimerMode::Repeating)), @@ -55,20 +64,6 @@ fn setup(mut commands: Commands, assets: Res) { commands.insert_resource(SeededRng(rng)); } -fn update_tileset_image( - chunk_query: Single<&TilemapChunk>, - mut events: EventReader>, - mut images: ResMut>, -) { - let chunk = *chunk_query; - for event in events.read() { - if event.is_loaded_with_dependencies(chunk.tileset.id()) { - let image = images.get_mut(&chunk.tileset).unwrap(); - image.reinterpret_stacked_2d_as_array(4); - } - } -} - fn update_tilemap( time: Res