diff --git a/crates/bevy_image/src/image.rs b/crates/bevy_image/src/image.rs index 8f383d7759f1e..2845536078d3e 100644 --- a/crates/bevy_image/src/image.rs +++ b/crates/bevy_image/src/image.rs @@ -1397,6 +1397,9 @@ impl Image { layers: u32, ) -> Result<(), TextureReinterpretationError> { // Must be a stacked image, and the height must be divisible by layers. + if layers < 2 { + return Err(TextureReinterpretationError::NotEnoughLayers); + } if self.texture_descriptor.dimension != TextureDimension::D2 { return Err(TextureReinterpretationError::WrongDimension); } @@ -1419,6 +1422,108 @@ impl Image { Ok(()) } + /// Returns a newly constructed 2D image using the same properties as &self from a grid of tiles of the specified size, + /// The new image is constructed in a vertical stack of tiles to be used as a 2D array texture. + /// + /// This is primarily for preparing grid based tilesets. + /// + /// # Errors + /// Returns [`TextureReinterpretationError`] if the texture is not 2D, has more than one layers + /// or is not evenly dividable by `size_in_tiles`. + pub fn create_stacked_array_from_2d_grid( + &self, + rows: u32, + columns: u32, + ) -> Result { + // In a texture 2d array, there must be at least 2 textures or else a render validation + // error will be thrown. + if rows * columns < 2 { + return Err(TextureReinterpretationError::NotEnoughLayers); + } + // Must be a grid image, and the image height and width must be divisible by the rows and columns. + 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().is_multiple_of(rows) { + return Err( + TextureReinterpretationError::GridHeightNotDivisibleByTileHeight { + height: self.height(), + tile_count_y: rows, + }, + ); + } + if !self.width().is_multiple_of(columns) { + return Err( + TextureReinterpretationError::GridWidthNotDivisibleByTileWidth { + width: self.width(), + tile_count_x: columns, + }, + ); + } + + let tile_width = self.width() / columns; + let tile_height = self.height() / rows; + let tiles_x = columns as usize; + let tiles_y = rows as usize; + let image_width = self.width() as usize; + let total_tiles = tiles_x * tiles_y; + + let new_data = match &self.data { + Some(pixels) => { + let mut new_data: Vec = Vec::with_capacity(pixels.len()); + + let pixel_size = self + .texture_descriptor + .format + .pixel_size() + .map_err(|_| TextureReinterpretationError::InvalidTextureFormat)?; + + // Iterate tiles in row-major order (left-to-right, top-to-bottom). + let tile_height_usize = tile_height as usize; + let tile_width_usize = tile_width as usize; + + for ty in 0..tiles_y { + for tx in 0..tiles_x { + for row in 0..tile_height_usize { + let src_row = ty * tile_height_usize + row; + let src_col = tx * tile_width_usize; + let src_start = (src_row * image_width + src_col) * pixel_size; + let src_end = src_start + tile_width_usize * pixel_size; + new_data.extend_from_slice(&pixels[src_start..src_end]); + } + } + } + + Some(new_data) + } + None => None, + }; + + // Transform the grid of tiles into a single vertical stack of tiles. + + let new_image = Image { + data: new_data, + data_order: self.data_order, + texture_descriptor: TextureDescriptor { + size: Extent3d { + width: tile_width, + height: tile_height, + depth_or_array_layers: total_tiles as u32, + }, + ..self.texture_descriptor.clone() + }, + sampler: self.sampler.clone(), + texture_view_descriptor: self.texture_view_descriptor.clone(), + asset_usage: self.asset_usage, + copy_on_resize: self.copy_on_resize, + }; + + Ok(new_image) + } + /// Convert a texture from a format to another. Only a few formats are /// supported as input and output: /// - `TextureFormat::R8Unorm` @@ -2074,6 +2179,10 @@ pub enum TextureReinterpretationError { /// The image was expected to be 2d. #[error("must be a 2d image")] WrongDimension, + /// In a texture 2d array, there needs to be at least 2 layers or else a render validation error + /// will be throw. + #[error("Rows * Columns must be > 1")] + NotEnoughLayers, /// The image was expected to have a single layer. #[error("must not already be a layered image")] InvalidLayerCount, @@ -2085,6 +2194,25 @@ pub enum TextureReinterpretationError { /// The desired number of image layers. layers: u32, }, + /// The grid height is not divisible by the number of tiles in the height. + #[error("can not evenly divide grid with height = {height} by tiles = {tile_count_y}")] + GridHeightNotDivisibleByTileHeight { + /// The total image height in pixels. + height: u32, + /// The desired number of image layers. + tile_count_y: u32, + }, + /// The grid width is not divisible by the number of tiles in the width. + #[error("can not evenly divide grid with width = {width} by tiles = {tile_count_x}")] + GridWidthNotDivisibleByTileWidth { + /// The total image height in pixels. + width: u32, + /// The desired number of image layers. + tile_count_x: u32, + }, + /// The texture format is not supported. + #[error("Cannot process texture in its current format. Is it compressed?")] + InvalidTextureFormat, } /// An error that occurs when accessing specific pixels in a texture. @@ -2574,6 +2702,111 @@ mod test { )); } + #[test] + fn create_array_from_2d_grid() { + // 2x2 pixel image + let image = Image::new_fill( + Extent3d { + width: 6, + height: 6, + depth_or_array_layers: 1, + }, + TextureDimension::D2, + &[0; 4], + TextureFormat::Rgba8Snorm, + RenderAssetUsages::all(), + ); + + assert!(image.texture_descriptor.size.depth_or_array_layers == 1); + + let new_array_image = image.create_stacked_array_from_2d_grid(2, 2).unwrap(); + + // 2x2 pixel image with 1px tiles should convert to a 2d array of 4 layers + assert!( + new_array_image + .texture_descriptor + .size + .depth_or_array_layers + == 2 * 2 + ); + assert!( + new_array_image.data.unwrap().len() + == pixel_count(new_array_image.texture_descriptor.size) + * new_array_image + .texture_descriptor + .format + .pixel_size() + .unwrap() + ); + + // 9x2 pixel image with 3x2px tiles should convert to array of 3 layers + let image = Image::new_fill( + Extent3d { + width: 9, + height: 2, + depth_or_array_layers: 1, + }, + TextureDimension::D2, + &[0; 4], + TextureFormat::Rgba8Snorm, + RenderAssetUsages::all(), + ); + + assert!(image.texture_descriptor.size.depth_or_array_layers == 1); + + let new_array_image = image.create_stacked_array_from_2d_grid(1, 3).unwrap(); + + assert!( + new_array_image + .texture_descriptor + .size + .depth_or_array_layers + == 3 + ); + assert!( + new_array_image.data.unwrap().len() + == pixel_count(new_array_image.texture_descriptor.size) + * new_array_image + .texture_descriptor + .format + .pixel_size() + .unwrap() + ); + + let image = Image::new_fill( + Extent3d { + width: 2, + height: 1, + depth_or_array_layers: 1, + }, + TextureDimension::D2, + &[0; 4], + TextureFormat::Rgba8Snorm, + RenderAssetUsages::all(), + ); + + assert!(image.texture_descriptor.size.depth_or_array_layers == 1); + + let new_array_image = image.create_stacked_array_from_2d_grid(1, 2).unwrap(); + + assert!( + new_array_image + .texture_descriptor + .size + .depth_or_array_layers + == 2 + ); + assert!( + new_array_image.data.unwrap().len() + == pixel_count(new_array_image.texture_descriptor.size) + * new_array_image + .texture_descriptor + .format + .pixel_size() + .unwrap() + ); + } + #[test] fn image_clear() { let mut image = Image::new_fill( diff --git a/crates/bevy_image/src/image_loader.rs b/crates/bevy_image/src/image_loader.rs index 79442a0a82e74..38a538f673bf7 100644 --- a/crates/bevy_image/src/image_loader.rs +++ b/crates/bevy_image/src/image_loader.rs @@ -113,6 +113,20 @@ pub enum ImageArrayLayout { /// The height of a single image in the stack. pixels: u32, }, + /// Interpret the image as a grid of images, with **n** columns and rows. + GridCount { + /// The number of columns in the grid. + columns: u32, + /// The number of rows in the grid. + rows: u32, + }, + /// Interpret the image as a grid of images where each tile is **n** pixels wide/tall. + GridSize { + /// The width of a single tile in pixels. + tile_width_pixels: u32, + /// The height of a single tile in pixels. + tile_height_pixels: u32, + }, } /// Settings for loading an [`Image`] using an [`ImageLoader`]. @@ -229,14 +243,28 @@ impl AssetLoader for ImageLoader { } if let Some(array_layout) = settings.array_layout { - let layers = match array_layout { - ImageArrayLayout::RowCount { rows } => rows, - ImageArrayLayout::RowHeight { pixels } => image.height() / pixels, + let image = match array_layout { + ImageArrayLayout::RowCount { rows } => { + image.reinterpret_stacked_2d_as_array(rows)?; + image + } + ImageArrayLayout::RowHeight { pixels } => { + image.reinterpret_stacked_2d_as_array(image.height() / pixels)?; + image + } + ImageArrayLayout::GridCount { columns, rows } => { + image.create_stacked_array_from_2d_grid(rows, columns)? + } + ImageArrayLayout::GridSize { + tile_width_pixels, + tile_height_pixels, + } => image.create_stacked_array_from_2d_grid( + image.height() / tile_height_pixels, + image.width() / tile_width_pixels, + )?, }; - - image.reinterpret_stacked_2d_as_array(layers)?; + return Ok(image); } - Ok(image) }