diff --git a/crates/bevy_render/src/texture/image_texture_loader.rs b/crates/bevy_render/src/texture/image_texture_loader.rs index 79fdceb9b0c84..7d6635c585882 100644 --- a/crates/bevy_render/src/texture/image_texture_loader.rs +++ b/crates/bevy_render/src/texture/image_texture_loader.rs @@ -16,8 +16,6 @@ impl AssetLoader for ImageTextureLoader { load_context: &'a mut LoadContext, ) -> BoxedFuture<'a, Result<()>> { Box::pin(async move { - use bevy_core::AsBytes; - // Find the image type we expect. A file with the extension "png" should // probably load as a PNG. @@ -41,118 +39,7 @@ impl AssetLoader for ImageTextureLoader { let dyn_img = image::load_from_memory_with_format(bytes, img_format)?; - let width; - let height; - - let data: Vec; - let format: TextureFormat; - - match dyn_img { - image::DynamicImage::ImageLuma8(i) => { - width = i.width(); - height = i.height(); - format = TextureFormat::R8Unorm; - - data = i.into_raw(); - } - image::DynamicImage::ImageLumaA8(i) => { - width = i.width(); - height = i.height(); - format = TextureFormat::Rg8Unorm; - - data = i.into_raw(); - } - image::DynamicImage::ImageRgb8(i) => { - let i = image::DynamicImage::ImageRgb8(i).into_rgba8(); - width = i.width(); - height = i.height(); - format = TextureFormat::Rgba8UnormSrgb; - - data = i.into_raw(); - } - image::DynamicImage::ImageRgba8(i) => { - width = i.width(); - height = i.height(); - format = TextureFormat::Rgba8UnormSrgb; - - data = i.into_raw(); - } - image::DynamicImage::ImageBgr8(i) => { - let i = image::DynamicImage::ImageBgr8(i).into_bgra8(); - - width = i.width(); - height = i.height(); - format = TextureFormat::Bgra8UnormSrgb; - - data = i.into_raw(); - } - image::DynamicImage::ImageBgra8(i) => { - width = i.width(); - height = i.height(); - format = TextureFormat::Bgra8UnormSrgb; - - data = i.into_raw(); - } - image::DynamicImage::ImageLuma16(i) => { - width = i.width(); - height = i.height(); - format = TextureFormat::R16Uint; - - let raw_data = i.into_raw(); - - data = raw_data.as_slice().as_bytes().to_owned(); - } - image::DynamicImage::ImageLumaA16(i) => { - width = i.width(); - height = i.height(); - format = TextureFormat::Rg16Uint; - - let raw_data = i.into_raw(); - - data = raw_data.as_slice().as_bytes().to_owned(); - } - - image::DynamicImage::ImageRgb16(image) => { - width = image.width(); - height = image.height(); - format = TextureFormat::Rgba16Uint; - - let mut local_data = - Vec::with_capacity(width as usize * height as usize * format.pixel_size()); - - for pixel in image.into_raw().chunks_exact(3) { - // TODO unsafe_get in release builds? - let r = pixel[0]; - let g = pixel[1]; - let b = pixel[2]; - let a = u16::max_value(); - - local_data.extend_from_slice(&r.to_ne_bytes()); - local_data.extend_from_slice(&g.to_ne_bytes()); - local_data.extend_from_slice(&b.to_ne_bytes()); - local_data.extend_from_slice(&a.to_ne_bytes()); - } - - data = local_data; - } - image::DynamicImage::ImageRgba16(i) => { - width = i.width(); - height = i.height(); - format = TextureFormat::Rgba16Uint; - - let raw_data = i.into_raw(); - - data = raw_data.as_slice().as_bytes().to_owned(); - } - } - - let texture = Texture::new( - Extent3d::new(width, height, 1), - TextureDimension::D2, - data, - format, - ); - load_context.set_default_asset(LoadedAsset::new(texture)); + load_context.set_default_asset(LoadedAsset::new(image_to_texture(dyn_img))); Ok(()) }) } @@ -173,3 +60,152 @@ mod tests { } } } + +/// Helper method to convert a `DynamicImage` to a `Texture` +pub(crate) fn image_to_texture(dyn_img: image::DynamicImage) -> Texture { + use bevy_core::AsBytes; + + let width; + let height; + + let data: Vec; + let format: TextureFormat; + + match dyn_img { + image::DynamicImage::ImageLuma8(i) => { + width = i.width(); + height = i.height(); + format = TextureFormat::R8Unorm; + + data = i.into_raw(); + } + image::DynamicImage::ImageLumaA8(i) => { + width = i.width(); + height = i.height(); + format = TextureFormat::Rg8Unorm; + + data = i.into_raw(); + } + image::DynamicImage::ImageRgb8(i) => { + let i = image::DynamicImage::ImageRgb8(i).into_rgba8(); + width = i.width(); + height = i.height(); + format = TextureFormat::Rgba8UnormSrgb; + + data = i.into_raw(); + } + image::DynamicImage::ImageRgba8(i) => { + width = i.width(); + height = i.height(); + format = TextureFormat::Rgba8UnormSrgb; + + data = i.into_raw(); + } + image::DynamicImage::ImageBgr8(i) => { + let i = image::DynamicImage::ImageBgr8(i).into_bgra8(); + + width = i.width(); + height = i.height(); + format = TextureFormat::Bgra8UnormSrgb; + + data = i.into_raw(); + } + image::DynamicImage::ImageBgra8(i) => { + width = i.width(); + height = i.height(); + format = TextureFormat::Bgra8UnormSrgb; + + data = i.into_raw(); + } + image::DynamicImage::ImageLuma16(i) => { + width = i.width(); + height = i.height(); + format = TextureFormat::R16Uint; + + let raw_data = i.into_raw(); + + data = raw_data.as_slice().as_bytes().to_owned(); + } + image::DynamicImage::ImageLumaA16(i) => { + width = i.width(); + height = i.height(); + format = TextureFormat::Rg16Uint; + + let raw_data = i.into_raw(); + + data = raw_data.as_slice().as_bytes().to_owned(); + } + + image::DynamicImage::ImageRgb16(image) => { + width = image.width(); + height = image.height(); + format = TextureFormat::Rgba16Uint; + + let mut local_data = + Vec::with_capacity(width as usize * height as usize * format.pixel_size()); + + for pixel in image.into_raw().chunks_exact(3) { + // TODO unsafe_get in release builds? + let r = pixel[0]; + let g = pixel[1]; + let b = pixel[2]; + let a = u16::max_value(); + + local_data.extend_from_slice(&r.to_ne_bytes()); + local_data.extend_from_slice(&g.to_ne_bytes()); + local_data.extend_from_slice(&b.to_ne_bytes()); + local_data.extend_from_slice(&a.to_ne_bytes()); + } + + data = local_data; + } + image::DynamicImage::ImageRgba16(i) => { + width = i.width(); + height = i.height(); + format = TextureFormat::Rgba16Uint; + + let raw_data = i.into_raw(); + + data = raw_data.as_slice().as_bytes().to_owned(); + } + } + + Texture::new( + Extent3d::new(width, height, 1), + TextureDimension::D2, + data, + format, + ) +} + +/// Helper method to convert a `Texture` to a `DynamicImage`. Not all `Texture` formats are +/// covered, it will return `None` if the format is not supported +pub(crate) fn texture_to_image(texture: &Texture) -> Option { + match texture.format { + TextureFormat::R8Unorm => image::ImageBuffer::from_raw( + texture.size.width, + texture.size.height, + texture.data.clone(), + ) + .map(image::DynamicImage::ImageLuma8), + TextureFormat::Rg8Unorm => image::ImageBuffer::from_raw( + texture.size.width, + texture.size.height, + texture.data.clone(), + ) + .map(image::DynamicImage::ImageLumaA8), + TextureFormat::Rgba8UnormSrgb => image::ImageBuffer::from_raw( + texture.size.width, + texture.size.height, + texture.data.clone(), + ) + .map(image::DynamicImage::ImageRgba8), + TextureFormat::Bgra8UnormSrgb => image::ImageBuffer::from_raw( + texture.size.width, + texture.size.height, + texture.data.clone(), + ) + .map(image::DynamicImage::ImageBgra8), + _ => None, + } +} diff --git a/crates/bevy_render/src/texture/texture.rs b/crates/bevy_render/src/texture/texture.rs index dcadaf0d3d5be..4093b38141241 100644 --- a/crates/bevy_render/src/texture/texture.rs +++ b/crates/bevy_render/src/texture/texture.rs @@ -125,6 +125,37 @@ impl Texture { }); } + /// Convert a texture from a format to another + /// Only a few formats are supported as input and output: + /// - `TextureFormat::R8Unorm` + /// - `TextureFormat::Rg8Unorm` + /// - `TextureFormat::Rgba8UnormSrgb` + /// - `TextureFormat::Bgra8UnormSrgb` + #[cfg(any( + feature = "png", + feature = "dds", + feature = "tga", + feature = "jpeg", + feature = "bmp" + ))] + pub fn convert(&self, new_format: TextureFormat) -> Option { + super::texture_to_image(self) + .and_then(|img| match new_format { + TextureFormat::R8Unorm => Some(image::DynamicImage::ImageLuma8(img.into_luma8())), + TextureFormat::Rg8Unorm => { + Some(image::DynamicImage::ImageLumaA8(img.into_luma_alpha8())) + } + TextureFormat::Rgba8UnormSrgb => { + Some(image::DynamicImage::ImageRgba8(img.into_rgba8())) + } + TextureFormat::Bgra8UnormSrgb => { + Some(image::DynamicImage::ImageBgra8(img.into_bgra8())) + } + _ => None, + }) + .map(super::image_to_texture) + } + pub fn texture_resource_system( render_resource_context: Res>, textures: Res>, diff --git a/crates/bevy_sprite/Cargo.toml b/crates/bevy_sprite/Cargo.toml index 57f35d7ec3273..2ae632f368ac7 100644 --- a/crates/bevy_sprite/Cargo.toml +++ b/crates/bevy_sprite/Cargo.toml @@ -18,6 +18,7 @@ bevy_app = { path = "../bevy_app", version = "0.4.0" } bevy_asset = { path = "../bevy_asset", version = "0.4.0" } bevy_core = { path = "../bevy_core", version = "0.4.0" } bevy_ecs = { path = "../bevy_ecs", version = "0.4.0" } +bevy_log = { path = "../bevy_log", version = "0.4.0" } bevy_math = { path = "../bevy_math", version = "0.4.0" } bevy_reflect = { path = "../bevy_reflect", version = "0.4.0", features = ["bevy"] } bevy_render = { path = "../bevy_render", version = "0.4.0" } diff --git a/crates/bevy_sprite/src/texture_atlas_builder.rs b/crates/bevy_sprite/src/texture_atlas_builder.rs index 2be6d0058e04d..7957c52759f9e 100644 --- a/crates/bevy_sprite/src/texture_atlas_builder.rs +++ b/crates/bevy_sprite/src/texture_atlas_builder.rs @@ -1,5 +1,6 @@ use crate::{Rect, TextureAtlas}; use bevy_asset::{Assets, Handle}; +use bevy_log::{debug, error, warn}; use bevy_math::Vec2; use bevy_render::texture::{Extent3d, Texture, TextureDimension, TextureFormat}; use bevy_utils::HashMap; @@ -13,6 +14,8 @@ use thiserror::Error; pub enum TextureAtlasBuilderError { #[error("could not pack textures into an atlas within the given bounds")] NotEnoughSpace, + #[error("added a texture with the wrong format in an atlas")] + WrongFormat, } #[derive(Debug)] @@ -26,6 +29,10 @@ pub struct TextureAtlasBuilder { initial_size: Vec2, /// The absolute maximum size of the texture atlas in pixels. max_size: Vec2, + /// The texture format for the textures that will be loaded in the atlas. + format: TextureFormat, + /// Enable automatic format conversion for textures if they are not in the atlas format. + auto_format_conversion: bool, } impl Default for TextureAtlasBuilder { @@ -34,6 +41,8 @@ impl Default for TextureAtlasBuilder { rects_to_place: GroupedRectsToPlace::new(), initial_size: Vec2::new(256., 256.), max_size: Vec2::new(2048., 2048.), + format: TextureFormat::Rgba8UnormSrgb, + auto_format_conversion: true, } } } @@ -53,6 +62,18 @@ impl TextureAtlasBuilder { self } + /// Sets the texture format for textures in the atlas. + pub fn format(mut self, format: TextureFormat) -> Self { + self.format = format; + self + } + + /// Control whether the added texture should be converted to the atlas format, if different. + pub fn auto_format_conversion(mut self, auto_format_conversion: bool) -> Self { + self.auto_format_conversion = auto_format_conversion; + self + } + /// Adds a texture to be copied to the texture atlas. pub fn add_texture(&mut self, texture_handle: Handle, texture: &Texture) { self.rects_to_place.push_rect( @@ -62,8 +83,7 @@ impl TextureAtlasBuilder { ) } - fn copy_texture( - &mut self, + fn copy_texture_to_atlas( atlas_texture: &mut Texture, texture: &Texture, packed_location: &PackedLocation, @@ -85,6 +105,28 @@ impl TextureAtlasBuilder { } } + fn copy_converted_texture( + &self, + atlas_texture: &mut Texture, + texture: &Texture, + packed_location: &PackedLocation, + ) { + if self.format == texture.format { + Self::copy_texture_to_atlas(atlas_texture, texture, packed_location); + } else if let Some(converted_texture) = texture.convert(self.format) { + debug!( + "Converting texture from '{:?}' to '{:?}'", + texture.format, self.format + ); + Self::copy_texture_to_atlas(atlas_texture, &converted_texture, packed_location); + } else { + error!( + "Error converting texture from '{:?}' to '{:?}', ignoring", + texture.format, self.format + ); + } + } + /// Consumes the builder and returns a result with a new texture atlas. /// /// Internally it copies all rectangles from the textures and copies them @@ -97,7 +139,7 @@ impl TextureAtlasBuilder { /// If there is not enough space in the atlas texture, an error will /// be returned. It is then recommended to make a larger sprite sheet. pub fn finish( - mut self, + self, textures: &mut Assets, ) -> Result { let initial_width = self.initial_size.x as u32; @@ -130,7 +172,7 @@ impl TextureAtlasBuilder { Extent3d::new(current_width, current_height, 1), TextureDimension::D2, &[0, 0, 0, 0], - TextureFormat::Rgba8UnormSrgb, + self.format, ); Some(rect_placements) } @@ -160,7 +202,14 @@ impl TextureAtlasBuilder { ); texture_handles.insert(texture_handle.clone_weak(), texture_rects.len()); texture_rects.push(Rect { min, max }); - self.copy_texture(&mut atlas_texture, texture, packed_location); + if texture.format != self.format && !self.auto_format_conversion { + warn!( + "Loading a texture of format '{:?}' in an atlas with format '{:?}'", + texture.format, self.format + ); + return Err(TextureAtlasBuilderError::WrongFormat); + } + self.copy_converted_texture(&mut atlas_texture, texture, packed_location); } Ok(TextureAtlas { size: atlas_texture.size.as_vec3().truncate(),