diff --git a/crates/bevy_image/src/compressed_image_saver.rs b/crates/bevy_image/src/compressed_image_saver.rs index c2adc2c029116..411e18f6d95c1 100644 --- a/crates/bevy_image/src/compressed_image_saver.rs +++ b/crates/bevy_image/src/compressed_image_saver.rs @@ -69,6 +69,7 @@ impl AssetSaver for CompressedImageSaver { is_srgb, sampler: image.sampler.clone(), asset_usage: image.asset_usage, + view_dimension: None, }) } } diff --git a/crates/bevy_image/src/image.rs b/crates/bevy_image/src/image.rs index 9a6fcc63b8a7e..be5064ae0ef46 100644 --- a/crates/bevy_image/src/image.rs +++ b/crates/bevy_image/src/image.rs @@ -22,7 +22,7 @@ use thiserror::Error; use wgpu_types::{ AddressMode, CompareFunction, Extent3d, Features, FilterMode, SamplerBorderColor, SamplerDescriptor, TextureDataOrder, TextureDescriptor, TextureDimension, TextureFormat, - TextureUsages, TextureViewDescriptor, + TextureUsages, TextureViewDescriptor, TextureViewDimension, }; /// Trait used to provide default values for Bevy-external types that @@ -717,6 +717,38 @@ impl ImageSamplerDescriptor { } } +/// Dimensions of a particular texture view. +/// +/// This type mirrors [`TextureViewDimension`]. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub enum ImageTextureViewDimension { + /// A one dimensional texture. `texture_1d` in WGSL and `texture1D` in GLSL. + D1, + /// A two dimensional texture. `texture_2d` in WGSL and `texture2D` in GLSL. + D2, + /// A two dimensional array texture. `texture_2d_array` in WGSL and `texture2DArray` in GLSL. + D2Array(u32), + /// A cubemap texture. `texture_cube` in WGSL and `textureCube` in GLSL. + Cube, + /// A cubemap array texture. `texture_cube_array` in WGSL and `textureCubeArray` in GLSL. + CubeArray(u32), + /// A three dimensional texture. `texture_3d` in WGSL and `texture3D` in GLSL. + D3, +} + +impl From for TextureViewDimension { + fn from(value: ImageTextureViewDimension) -> Self { + match value { + ImageTextureViewDimension::D1 => TextureViewDimension::D1, + ImageTextureViewDimension::D2 => TextureViewDimension::D2, + ImageTextureViewDimension::D2Array(_) => TextureViewDimension::D2Array, + ImageTextureViewDimension::Cube => TextureViewDimension::Cube, + ImageTextureViewDimension::CubeArray(_) => TextureViewDimension::CubeArray, + ImageTextureViewDimension::D3 => TextureViewDimension::D3, + } + } +} + impl From for AddressMode { fn from(value: ImageAddressMode) -> Self { match value { @@ -1063,21 +1095,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. @@ -1128,21 +1159,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::HeightNotDivisibleByLayers { + 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 @@ -1741,6 +1782,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}")] + HeightNotDivisibleByLayers { 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..f45e7bc40b608 100644 --- a/crates/bevy_image/src/image_loader.rs +++ b/crates/bevy_image/src/image_loader.rs @@ -1,6 +1,11 @@ -use crate::image::{Image, ImageFormat, ImageType, TextureError}; +use crate::{ + image::{Image, ImageFormat, ImageType, TextureError}, + ImageTextureViewDimension, TextureReinterpretationError, +}; use bevy_asset::{io::Reader, AssetLoader, LoadContext, RenderAssetUsages}; +use bevy_utils::default; use thiserror::Error; +use wgpu_types::TextureViewDescriptor; use super::{CompressedImageFormats, ImageSampler}; use serde::{Deserialize, Serialize}; @@ -111,6 +116,9 @@ pub struct ImageLoaderSettings { /// Where the asset will be used - see the docs on /// [`RenderAssetUsages`] for details. pub asset_usage: RenderAssetUsages, + /// Dimension of this image's texture view. + /// None, lets the loader decide what the dimensions are from the image (if supported). + pub view_dimension: Option, } impl Default for ImageLoaderSettings { @@ -120,6 +128,7 @@ impl Default for ImageLoaderSettings { is_srgb: true, sampler: ImageSampler::Default, asset_usage: RenderAssetUsages::default(), + view_dimension: None, } } } @@ -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,7 @@ impl AssetLoader for ImageLoader { )?) } }; - Ok(Image::from_buffer( + let mut image = Image::from_buffer( &bytes, image_type, self.supported_compressed_formats, @@ -179,7 +191,31 @@ impl AssetLoader for ImageLoader { .map_err(|err| FileTextureError { error: err, path: format!("{}", load_context.path().display()), - })?) + })?; + + if let Some(view_dimension) = &settings.view_dimension { + match view_dimension { + ImageTextureViewDimension::D2Array(layers) => { + image.reinterpret_stacked_2d_as_array(*layers)?; + } + ImageTextureViewDimension::Cube => { + image.reinterpret_stacked_2d_as_array(image.height() / image.width())?; + } + ImageTextureViewDimension::CubeArray(layers) => { + image.reinterpret_stacked_2d_as_array( + image.height() / image.width() * *layers, + )?; + } + _ => {} + } + + image.texture_view_descriptor = Some(TextureViewDescriptor { + dimension: Some(view_dimension.clone().into()), + ..default() + }); + } + + Ok(image) } fn extensions(&self) -> &[&str] { diff --git a/examples/2d/tilemap_chunk.rs b/examples/2d/tilemap_chunk.rs index ac2df4e26e577..f05f9af9d8a3f 100644 --- a/examples/2d/tilemap_chunk.rs +++ b/examples/2d/tilemap_chunk.rs @@ -1,6 +1,7 @@ //! Shows a tilemap chunk rendered with a single draw call. use bevy::{ + image::{ImageLoaderSettings, ImageTextureViewDimension}, prelude::*, sprite_render::{TileData, TilemapChunk, TilemapChunkTileData}, }; @@ -11,7 +12,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(); } @@ -43,7 +44,12 @@ fn setup(mut commands: Commands, assets: Res) { TilemapChunk { chunk_size, tile_display_size, - tileset: assets.load("textures/array_texture.png"), + tileset: assets.load_with_settings( + "textures/array_texture.png", + |settings: &mut ImageLoaderSettings| { + settings.view_dimension = Some(ImageTextureViewDimension::D2Array(4)); + }, + ), ..default() }, TilemapChunkTileData(tile_data), @@ -55,20 +61,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